webmidi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/Rakefile +48 -0
- data/lib/webmidi/access.rb +170 -0
- data/lib/webmidi/callback_subscription.rb +26 -0
- data/lib/webmidi/clock.rb +129 -0
- data/lib/webmidi/configuration.rb +43 -0
- data/lib/webmidi/error.rb +26 -0
- data/lib/webmidi/message/base.rb +64 -0
- data/lib/webmidi/message/channel.rb +238 -0
- data/lib/webmidi/message/parser.rb +308 -0
- data/lib/webmidi/message/system.rb +162 -0
- data/lib/webmidi/message/ump.rb +675 -0
- data/lib/webmidi/message.rb +154 -0
- data/lib/webmidi/middleware/base.rb +16 -0
- data/lib/webmidi/middleware/channel_map.rb +36 -0
- data/lib/webmidi/middleware/filter.rb +22 -0
- data/lib/webmidi/middleware/logger.rb +17 -0
- data/lib/webmidi/middleware/note_range_filter.rb +34 -0
- data/lib/webmidi/middleware/panic.rb +73 -0
- data/lib/webmidi/middleware/pipeline.rb +19 -0
- data/lib/webmidi/middleware/recorder.rb +123 -0
- data/lib/webmidi/middleware/split_by_channel.rb +66 -0
- data/lib/webmidi/middleware/stack.rb +55 -0
- data/lib/webmidi/middleware/timing_gate.rb +58 -0
- data/lib/webmidi/middleware/transpose.rb +30 -0
- data/lib/webmidi/middleware/velocity_clamp.rb +37 -0
- data/lib/webmidi/middleware/velocity_scale.rb +55 -0
- data/lib/webmidi/middleware.rb +21 -0
- data/lib/webmidi/music/chord.rb +90 -0
- data/lib/webmidi/music/note.rb +102 -0
- data/lib/webmidi/music/rhythm.rb +92 -0
- data/lib/webmidi/music/scale.rb +85 -0
- data/lib/webmidi/music.rb +24 -0
- data/lib/webmidi/network/apple_midi.rb +189 -0
- data/lib/webmidi/network/osc.rb +205 -0
- data/lib/webmidi/network/rtp.rb +410 -0
- data/lib/webmidi/network.rb +10 -0
- data/lib/webmidi/port/base.rb +89 -0
- data/lib/webmidi/port/input.rb +158 -0
- data/lib/webmidi/port/map.rb +65 -0
- data/lib/webmidi/port/output.rb +208 -0
- data/lib/webmidi/port.rb +11 -0
- data/lib/webmidi/smf/event.rb +206 -0
- data/lib/webmidi/smf/reader.rb +237 -0
- data/lib/webmidi/smf/sequence.rb +135 -0
- data/lib/webmidi/smf/tempo_map.rb +107 -0
- data/lib/webmidi/smf/track.rb +130 -0
- data/lib/webmidi/smf/writer.rb +121 -0
- data/lib/webmidi/smf.rb +13 -0
- data/lib/webmidi/transport/adapter.rb +46 -0
- data/lib/webmidi/transport/base.rb +59 -0
- data/lib/webmidi/transport/device_info.rb +7 -0
- data/lib/webmidi/transport/null.rb +81 -0
- data/lib/webmidi/transport/virtual.rb +184 -0
- data/lib/webmidi/transport.rb +80 -0
- data/lib/webmidi/version.rb +5 -0
- data/lib/webmidi/virtual/loopback.rb +45 -0
- data/lib/webmidi/virtual/port.rb +48 -0
- data/lib/webmidi/virtual.rb +9 -0
- data/lib/webmidi.rb +19 -0
- data/webmidi.gemspec +32 -0
- metadata +108 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Webmidi
|
|
6
|
+
module SMF
|
|
7
|
+
module Writer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def write(sequence, path_or_io, **options)
|
|
11
|
+
binary = to_binary(sequence, **options)
|
|
12
|
+
if path_or_io.respond_to?(:write)
|
|
13
|
+
path_or_io.write(binary)
|
|
14
|
+
else
|
|
15
|
+
File.binwrite(path_or_io, binary)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_binary(sequence, running_status: false)
|
|
20
|
+
validate_sequence!(sequence)
|
|
21
|
+
out = StringIO.new(String.new(encoding: Encoding::ASCII_8BIT))
|
|
22
|
+
write_header(out, sequence)
|
|
23
|
+
sequence.each { |track| write_track(out, track, running_status: running_status) }
|
|
24
|
+
out.string
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write_header(out, sequence)
|
|
28
|
+
out.write("MThd")
|
|
29
|
+
write_uint32(out, 6)
|
|
30
|
+
write_uint16(out, sequence.format)
|
|
31
|
+
write_uint16(out, sequence.size)
|
|
32
|
+
write_uint16(out, sequence.ppqn)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def write_track(out, track, running_status: false)
|
|
36
|
+
track_data = StringIO.new(String.new(encoding: Encoding::ASCII_8BIT))
|
|
37
|
+
has_end_of_track = false
|
|
38
|
+
last_status = nil
|
|
39
|
+
|
|
40
|
+
track.each do |event|
|
|
41
|
+
write_vlq(track_data, event.delta_time)
|
|
42
|
+
|
|
43
|
+
case event
|
|
44
|
+
when MetaEvent
|
|
45
|
+
track_data.putc(0xFF)
|
|
46
|
+
track_data.putc(event.type)
|
|
47
|
+
write_vlq(track_data, event.data.size)
|
|
48
|
+
event.data.each { |b| track_data.putc(b) }
|
|
49
|
+
has_end_of_track = true if event.type == MetaEvent::META_TYPES[:end_of_track]
|
|
50
|
+
last_status = nil
|
|
51
|
+
when SysExEvent
|
|
52
|
+
data = (event.data.last == 0xF7) ? event.data : [*event.data, 0xF7]
|
|
53
|
+
track_data.putc(0xF0)
|
|
54
|
+
write_vlq(track_data, data.size)
|
|
55
|
+
data.each { |b| track_data.putc(b) }
|
|
56
|
+
last_status = nil
|
|
57
|
+
when MIDIEvent
|
|
58
|
+
bytes = event.to_bytes
|
|
59
|
+
if running_status && channel_status?(bytes[0]) && last_status == bytes[0]
|
|
60
|
+
bytes[1..].each { |b| track_data.putc(b) }
|
|
61
|
+
else
|
|
62
|
+
bytes.each { |b| track_data.putc(b) }
|
|
63
|
+
last_status = channel_status?(bytes[0]) ? bytes[0] : nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless has_end_of_track
|
|
69
|
+
write_vlq(track_data, 0)
|
|
70
|
+
track_data.putc(0xFF)
|
|
71
|
+
track_data.putc(0x2F)
|
|
72
|
+
write_vlq(track_data, 0)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
track_bytes = track_data.string
|
|
76
|
+
out.write("MTrk")
|
|
77
|
+
write_uint32(out, track_bytes.bytesize)
|
|
78
|
+
out.write(track_bytes)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def write_uint16(out, value)
|
|
82
|
+
out.putc((value >> 8) & 0xFF)
|
|
83
|
+
out.putc(value & 0xFF)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def write_uint32(out, value)
|
|
87
|
+
out.putc((value >> 24) & 0xFF)
|
|
88
|
+
out.putc((value >> 16) & 0xFF)
|
|
89
|
+
out.putc((value >> 8) & 0xFF)
|
|
90
|
+
out.putc(value & 0xFF)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def write_vlq(out, value)
|
|
94
|
+
unless value.is_a?(Integer) && value.between?(0, 0x0FFF_FFFF)
|
|
95
|
+
raise InvalidSMFError, "VLQ value must be between 0 and 0x0FFFFFFF, got #{value.inspect}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
bytes = [value & 0x7F]
|
|
99
|
+
value >>= 7
|
|
100
|
+
while value > 0
|
|
101
|
+
bytes.unshift((value & 0x7F) | 0x80)
|
|
102
|
+
value >>= 7
|
|
103
|
+
end
|
|
104
|
+
bytes.each { |b| out.putc(b) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_sequence!(sequence)
|
|
108
|
+
if sequence.format.zero? && sequence.size != 1
|
|
109
|
+
raise InvalidSMFError, "SMF format 0 must contain exactly one track"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def channel_status?(status)
|
|
114
|
+
status < 0xF0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private_class_method :write_header, :write_track, :write_uint16, :write_uint32, :write_vlq,
|
|
118
|
+
:validate_sequence!, :channel_status?
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/webmidi/smf.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "smf/event"
|
|
4
|
+
require_relative "smf/track"
|
|
5
|
+
require_relative "smf/tempo_map"
|
|
6
|
+
require_relative "smf/sequence"
|
|
7
|
+
require_relative "smf/reader"
|
|
8
|
+
require_relative "smf/writer"
|
|
9
|
+
|
|
10
|
+
module Webmidi
|
|
11
|
+
module SMF
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Webmidi
|
|
4
|
+
module Transport
|
|
5
|
+
module Adapter
|
|
6
|
+
REQUIRED_METHODS = %i[
|
|
7
|
+
available?
|
|
8
|
+
list_inputs
|
|
9
|
+
list_outputs
|
|
10
|
+
open_input
|
|
11
|
+
open_output
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def validate!(transport)
|
|
17
|
+
missing = REQUIRED_METHODS.reject { |method| transport.respond_to?(method) }
|
|
18
|
+
return transport if missing.empty?
|
|
19
|
+
|
|
20
|
+
raise TransportNotAvailableError,
|
|
21
|
+
"Transport adapter #{transport.inspect} is missing: #{missing.join(", ")}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def gem_name(name)
|
|
25
|
+
"webmidi-#{normalized_name(name).tr("_", "-")}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def require_path(name)
|
|
29
|
+
"webmidi/transport/#{normalized_name(name)}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def constant_name(name)
|
|
33
|
+
camel = normalized_name(name).split("_").map(&:capitalize).join
|
|
34
|
+
"Webmidi::Transport::#{camel}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def constantize(path)
|
|
38
|
+
path.split("::").reduce(Object) { |namespace, const_name| namespace.const_get(const_name) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def normalized_name(name)
|
|
42
|
+
name.to_s.tr("-", "_")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Webmidi
|
|
4
|
+
module Transport
|
|
5
|
+
class Base
|
|
6
|
+
def self.available?
|
|
7
|
+
false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.list_inputs
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.list_outputs
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.open_input(device_info)
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.open_output(device_info)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.create_virtual_input(name)
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.create_virtual_output(name)
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module InputHandle
|
|
36
|
+
def read(timeout: nil)
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_data(&block)
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def close
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module OutputHandle
|
|
50
|
+
def write(bytes)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def close
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Webmidi
|
|
6
|
+
module Transport
|
|
7
|
+
class Null < Base
|
|
8
|
+
def self.available?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.list_inputs
|
|
13
|
+
[]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.list_outputs
|
|
17
|
+
[]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.create_virtual_input(name)
|
|
21
|
+
info = DeviceInfo.new(id: generate_id("input"), name: name, manufacturer: "Null", version: "0")
|
|
22
|
+
NullInputHandle.new(info)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.create_virtual_output(name)
|
|
26
|
+
info = DeviceInfo.new(id: generate_id("output"), name: name, manufacturer: "Null", version: "0")
|
|
27
|
+
NullOutputHandle.new(info)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.generate_id(type)
|
|
31
|
+
"null-#{type}-#{SecureRandom.uuid}"
|
|
32
|
+
end
|
|
33
|
+
private_class_method :generate_id
|
|
34
|
+
|
|
35
|
+
class NullInputHandle
|
|
36
|
+
include InputHandle
|
|
37
|
+
|
|
38
|
+
attr_reader :device_info
|
|
39
|
+
|
|
40
|
+
def initialize(device_info)
|
|
41
|
+
@device_info = device_info
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read(timeout: nil)
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_data(&block)
|
|
49
|
+
# no-op
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def close
|
|
53
|
+
# no-op
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class NullOutputHandle
|
|
58
|
+
include OutputHandle
|
|
59
|
+
|
|
60
|
+
attr_reader :device_info
|
|
61
|
+
|
|
62
|
+
def initialize(device_info)
|
|
63
|
+
@device_info = device_info
|
|
64
|
+
@sent_messages = []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def write(bytes)
|
|
68
|
+
@sent_messages << bytes
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def sent_messages
|
|
72
|
+
@sent_messages.dup
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def close
|
|
76
|
+
# no-op
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Webmidi
|
|
6
|
+
module Transport
|
|
7
|
+
class Virtual < Base
|
|
8
|
+
@ports = {}
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
def self.available?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.list_inputs
|
|
16
|
+
@mutex.synchronize { @ports.values.select { |p| p.is_a?(VirtualInputHandle) && !p.closed? }.map(&:device_info) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.list_outputs
|
|
20
|
+
@mutex.synchronize { @ports.values.select { |p| p.is_a?(VirtualOutputHandle) && !p.closed? }.map(&:device_info) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.create_virtual_input(name)
|
|
24
|
+
info = DeviceInfo.new(id: generate_id, name: name, manufacturer: "Webmidi Virtual", version: "1.0")
|
|
25
|
+
handle = VirtualInputHandle.new(info, on_close: -> { unregister(info.id) })
|
|
26
|
+
@mutex.synchronize { @ports[info.id] = handle }
|
|
27
|
+
handle
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.create_virtual_output(name)
|
|
31
|
+
info = DeviceInfo.new(id: generate_id, name: name, manufacturer: "Webmidi Virtual", version: "1.0")
|
|
32
|
+
handle = VirtualOutputHandle.new(info, on_close: -> { unregister(info.id) })
|
|
33
|
+
@mutex.synchronize { @ports[info.id] = handle }
|
|
34
|
+
handle
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.open_input(device_info)
|
|
38
|
+
handle = find_handle(device_info.id, VirtualInputHandle)
|
|
39
|
+
return handle if handle
|
|
40
|
+
|
|
41
|
+
raise PortNotFoundError, "Virtual input not found: #{device_info.id}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.open_output(device_info)
|
|
45
|
+
handle = find_handle(device_info.id, VirtualOutputHandle)
|
|
46
|
+
return handle if handle
|
|
47
|
+
|
|
48
|
+
raise PortNotFoundError, "Virtual output not found: #{device_info.id}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.create_loopback(name)
|
|
52
|
+
input = create_virtual_input(name)
|
|
53
|
+
output = create_virtual_output(name)
|
|
54
|
+
output.connect(input)
|
|
55
|
+
[input, output]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.reset!
|
|
59
|
+
handles = @mutex.synchronize { @ports.values.dup }
|
|
60
|
+
handles.each(&:close)
|
|
61
|
+
@mutex.synchronize { @ports.clear }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.generate_id
|
|
65
|
+
"virtual-#{SecureRandom.uuid}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.unregister(id)
|
|
69
|
+
@mutex.synchronize { @ports.delete(id) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.find_handle(id, handle_class)
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
handle = @ports[id]
|
|
75
|
+
handle if handle.is_a?(handle_class) && !handle.closed?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
private_class_method :generate_id
|
|
79
|
+
private_class_method :find_handle
|
|
80
|
+
|
|
81
|
+
class VirtualInputHandle
|
|
82
|
+
include InputHandle
|
|
83
|
+
|
|
84
|
+
attr_reader :device_info
|
|
85
|
+
|
|
86
|
+
def initialize(device_info, on_close: nil)
|
|
87
|
+
@device_info = device_info
|
|
88
|
+
@on_close = on_close
|
|
89
|
+
@queue = Thread::Queue.new
|
|
90
|
+
@callbacks = []
|
|
91
|
+
@mutex = Mutex.new
|
|
92
|
+
@closed = false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def read(timeout: nil)
|
|
96
|
+
return nil if @closed
|
|
97
|
+
|
|
98
|
+
if timeout
|
|
99
|
+
@queue.pop(timeout: timeout)
|
|
100
|
+
else
|
|
101
|
+
begin
|
|
102
|
+
@queue.pop(true)
|
|
103
|
+
rescue
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def on_data(&block)
|
|
110
|
+
@mutex.synchronize { @callbacks << block }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def receive(bytes)
|
|
114
|
+
return if @closed
|
|
115
|
+
|
|
116
|
+
@queue.push(bytes)
|
|
117
|
+
@mutex.synchronize { @callbacks.dup }.each { |cb| cb.call(bytes) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def close
|
|
121
|
+
return if @closed
|
|
122
|
+
|
|
123
|
+
@closed = true
|
|
124
|
+
@queue.close
|
|
125
|
+
@on_close&.call
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def closed?
|
|
129
|
+
@closed
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class VirtualOutputHandle
|
|
134
|
+
include OutputHandle
|
|
135
|
+
|
|
136
|
+
attr_reader :device_info
|
|
137
|
+
|
|
138
|
+
def initialize(device_info, on_close: nil)
|
|
139
|
+
@device_info = device_info
|
|
140
|
+
@on_close = on_close
|
|
141
|
+
@connected_inputs = []
|
|
142
|
+
@mutex = Mutex.new
|
|
143
|
+
@closed = false
|
|
144
|
+
@sent_messages = []
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def write(bytes)
|
|
148
|
+
raise PortClosedError, "Port is closed" if @closed
|
|
149
|
+
|
|
150
|
+
connected_inputs = @mutex.synchronize do
|
|
151
|
+
@sent_messages << bytes
|
|
152
|
+
@connected_inputs.dup
|
|
153
|
+
end
|
|
154
|
+
connected_inputs.each { |input| input.receive(bytes) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def connect(input_handle)
|
|
158
|
+
@mutex.synchronize do
|
|
159
|
+
@connected_inputs << input_handle unless @connected_inputs.include?(input_handle)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def disconnect(input_handle)
|
|
164
|
+
@mutex.synchronize { @connected_inputs.delete(input_handle) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def sent_messages
|
|
168
|
+
@mutex.synchronize { @sent_messages.dup }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def close
|
|
172
|
+
return if @closed
|
|
173
|
+
|
|
174
|
+
@closed = true
|
|
175
|
+
@on_close&.call
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def closed?
|
|
179
|
+
@closed
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "transport/device_info"
|
|
4
|
+
require_relative "transport/base"
|
|
5
|
+
require_relative "transport/adapter"
|
|
6
|
+
require_relative "transport/virtual"
|
|
7
|
+
require_relative "transport/null"
|
|
8
|
+
|
|
9
|
+
module Webmidi
|
|
10
|
+
module Transport
|
|
11
|
+
@registered_transports = []
|
|
12
|
+
@registry_mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def register(transport)
|
|
17
|
+
Adapter.validate!(transport)
|
|
18
|
+
@registry_mutex.synchronize do
|
|
19
|
+
@registered_transports << transport unless @registered_transports.include?(transport)
|
|
20
|
+
end
|
|
21
|
+
transport
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def unregister(transport)
|
|
25
|
+
@registry_mutex.synchronize { @registered_transports.delete(transport) }
|
|
26
|
+
transport
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def registered
|
|
30
|
+
@registry_mutex.synchronize { @registered_transports.dup.freeze }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def load_adapter(name, require_path: Adapter.require_path(name), constant: Adapter.constant_name(name))
|
|
34
|
+
require require_path if require_path
|
|
35
|
+
register(Adapter.constantize(constant))
|
|
36
|
+
rescue LoadError => e
|
|
37
|
+
raise TransportNotAvailableError, "Could not load #{Adapter.gem_name(name)}: #{e.message}"
|
|
38
|
+
rescue NameError => e
|
|
39
|
+
raise TransportNotAvailableError, "Could not find transport adapter #{constant}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def auto_detect(transport: Webmidi.configuration.transport,
|
|
43
|
+
fallback_transport: Webmidi.configuration.fallback_transport,
|
|
44
|
+
candidates: default_candidates)
|
|
45
|
+
return resolve_transport!(transport) unless transport == :auto
|
|
46
|
+
|
|
47
|
+
detected = candidates.find { |candidate| candidate.respond_to?(:available?) && candidate.available? }
|
|
48
|
+
detected || resolve_transport!(fallback_transport)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_transport!(transport)
|
|
52
|
+
case transport
|
|
53
|
+
when :auto
|
|
54
|
+
auto_detect(transport: :auto, fallback_transport: :null)
|
|
55
|
+
when :virtual
|
|
56
|
+
available_transport!(Virtual)
|
|
57
|
+
when :null, nil
|
|
58
|
+
Null
|
|
59
|
+
else
|
|
60
|
+
unless transport.respond_to?(:available?)
|
|
61
|
+
raise TransportNotAvailableError, "Unknown transport: #{transport.inspect}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
available_transport!(transport)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def available_transport!(transport)
|
|
69
|
+
return transport unless transport.respond_to?(:available?) && !transport.available?
|
|
70
|
+
|
|
71
|
+
raise TransportNotAvailableError, "Transport is not available: #{transport}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_candidates
|
|
75
|
+
registered + [Virtual]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private_class_method :resolve_transport!, :available_transport!, :default_candidates
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Webmidi
|
|
4
|
+
module Virtual
|
|
5
|
+
class Loopback
|
|
6
|
+
attr_reader :input, :output
|
|
7
|
+
|
|
8
|
+
def self.create(name: "Loopback")
|
|
9
|
+
new(name: name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(name:)
|
|
13
|
+
@name = name
|
|
14
|
+
input_handle, output_handle = Transport::Virtual.create_loopback(name)
|
|
15
|
+
|
|
16
|
+
@input = Webmidi::Port::Input.new(
|
|
17
|
+
id: input_handle.device_info.id,
|
|
18
|
+
name: input_handle.device_info.name,
|
|
19
|
+
manufacturer: input_handle.device_info.manufacturer,
|
|
20
|
+
version: input_handle.device_info.version,
|
|
21
|
+
transport_handle: input_handle
|
|
22
|
+
)
|
|
23
|
+
@input.open
|
|
24
|
+
|
|
25
|
+
@output = Webmidi::Port::Output.new(
|
|
26
|
+
id: output_handle.device_info.id,
|
|
27
|
+
name: output_handle.device_info.name,
|
|
28
|
+
manufacturer: output_handle.device_info.manufacturer,
|
|
29
|
+
version: output_handle.device_info.version,
|
|
30
|
+
transport_handle: output_handle
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Wire up: when data comes in from transport, dispatch to input port
|
|
34
|
+
input_handle.on_data do |bytes|
|
|
35
|
+
@input.dispatch(bytes)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def close
|
|
40
|
+
@input&.disconnect
|
|
41
|
+
@output&.disconnect
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Webmidi
|
|
4
|
+
module Virtual
|
|
5
|
+
class Port
|
|
6
|
+
attr_reader :input, :output
|
|
7
|
+
|
|
8
|
+
def self.create(name:, direction: :bidirectional)
|
|
9
|
+
new(name: name, direction: direction)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(name:, direction: :bidirectional)
|
|
13
|
+
@name = name
|
|
14
|
+
@direction = direction
|
|
15
|
+
transport = Transport::Virtual
|
|
16
|
+
|
|
17
|
+
case direction
|
|
18
|
+
when :bidirectional, :input
|
|
19
|
+
input_handle = transport.create_virtual_input(name)
|
|
20
|
+
@input = Webmidi::Port::Input.new(
|
|
21
|
+
id: input_handle.device_info.id,
|
|
22
|
+
name: input_handle.device_info.name,
|
|
23
|
+
manufacturer: input_handle.device_info.manufacturer,
|
|
24
|
+
version: input_handle.device_info.version,
|
|
25
|
+
transport_handle: input_handle
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
case direction
|
|
30
|
+
when :bidirectional, :output
|
|
31
|
+
output_handle = transport.create_virtual_output(name)
|
|
32
|
+
@output = Webmidi::Port::Output.new(
|
|
33
|
+
id: output_handle.device_info.id,
|
|
34
|
+
name: output_handle.device_info.name,
|
|
35
|
+
manufacturer: output_handle.device_info.manufacturer,
|
|
36
|
+
version: output_handle.device_info.version,
|
|
37
|
+
transport_handle: output_handle
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def close
|
|
43
|
+
@input&.disconnect
|
|
44
|
+
@output&.disconnect
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|