musalce-server 0.4.10 → 0.8.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.
data/lib/daw.rb CHANGED
@@ -3,16 +3,59 @@ require 'midi-communications'
3
3
  require_relative 'midi-devices'
4
4
 
5
5
  module MusaLCEServer
6
+ # @return [Surface, nil] active control surface, set during {Daw}
7
+ # initialization. Exposed to the DSL as +surface+ in
8
+ # {MusaLCE_Context}; mutating its controls (e.g.
9
+ # +surface[:foo].enabled = true+) emits OSC state outbound to
10
+ # the DAW extension and on to Pulso Bridge / Stream Deck.
11
+ class << self
12
+ attr_accessor :surface
13
+ end
14
+
15
+ # Base class for DAW (Digital Audio Workstation) controllers.
16
+ #
17
+ # This class provides the common infrastructure for communicating with
18
+ # DAWs like Ableton Live and Bitwig Studio. It manages:
19
+ # - OSC server/client for communication with DAW controller extensions
20
+ # - MIDI clock synchronization
21
+ # - Musa-DSL sequencer integration
22
+ # - MIDI device management
23
+ # - Transport controls (play, stop, record, etc.)
24
+ #
25
+ # Subclasses must implement {#daw_initialize} to set up DAW-specific
26
+ # handlers and track management.
27
+ #
28
+ # @abstract Subclass and implement {#daw_initialize} and {#track}
29
+ #
30
+ # @see Bitwig::Bitwig Bitwig Studio implementation
31
+ # @see Live::Live Ableton Live implementation
6
32
  class Daw
33
+ # Registers a DAW driver class for a given identifier.
34
+ #
35
+ # @param daw_id [Symbol] the DAW identifier (:bitwig or :live)
36
+ # @param daw_class [Class] the DAW controller class to register
37
+ # @return [void]
38
+ #
39
+ # @example
40
+ # Daw.register(:bitwig, Bitwig::Bitwig)
7
41
  def self.register(daw_id, daw_class)
8
42
  @@daws ||= {}
9
43
  @@daws[daw_id] = daw_class
10
44
  end
11
45
 
46
+ # Creates and returns a new DAW controller instance for the given identifier.
47
+ #
48
+ # @param daw_id [Symbol] the DAW identifier (:bitwig or :live)
49
+ # @return [Daw] a new instance of the registered DAW controller
12
50
  def self.daw_controller_for(daw_id)
13
51
  @@daws[daw_id].new
14
52
  end
15
53
 
54
+ # Creates a new DAW controller instance.
55
+ #
56
+ # Sets up OSC server (port 11011) and client (port 10001),
57
+ # initializes the Musa-DSL sequencer, MIDI clock, and transport.
58
+ # Calls {#daw_initialize} for DAW-specific setup.
16
59
  def initialize
17
60
  osc_server = OSC::EMServer.new(11_011)
18
61
  osc_client = OSC::Client.new('localhost', 10_001)
@@ -22,70 +65,172 @@ module MusaLCEServer
22
65
  @sequencer = Musa::Sequencer::Sequencer.new 4, 24, dsl_context_class: MusaLCE_Context, do_log: true
23
66
 
24
67
  @clock = Musa::Clock::InputMidiClock.new do_log: true, logger: @sequencer.logger
25
- transport = Musa::Transport::Transport.new @clock, sequencer: @sequencer
68
+ @transport = Musa::Transport::Transport.new @clock, sequencer: @sequencer
26
69
 
27
- transport.after_stop do
70
+ @transport.after_stop do
28
71
  sequencer.reset
29
72
  end
30
73
 
31
74
  @midi_devices = MIDIDevices.new(@sequencer)
32
75
 
76
+ # Build the surface side: inbound +/musalce/surface/*+
77
+ # messages land on the EM reactor thread, are enqueued by the
78
+ # bridge, then drained on the sequencer tick thread via
79
+ # +before_tick+ — guaranteeing that inventory mutations and
80
+ # user-defined trigger handlers can safely use any DSL method
81
+ # (+play+, +at+, +launch+, …). Drainer latency is one tick
82
+ # (≈20 ms at 120 BPM / 24 PPQN).
83
+ surface_bridge = SurfaceBridge.new(osc_client, @sequencer, logger: @sequencer.logger)
84
+ @surface = Surface.new(bridge: surface_bridge, logger: @sequencer.logger)
85
+ surface_bridge.surface = @surface
86
+ surface_bridge.register_inbound(osc_server)
87
+ MusaLCEServer.surface = @surface
88
+
89
+ @sequencer.before_tick do |_position|
90
+ surface_bridge.drain
91
+ end
92
+
33
93
  @tracks, @handler = daw_initialize(midi_devices: @midi_devices, clock: @clock, osc_server: osc_server, osc_client: osc_client, logger: @sequencer.logger)
34
94
 
35
95
  @handler.version
36
96
  @handler.sync
37
97
 
38
- Thread.new { transport.start }
98
+ # Ask Pulso Bridge (through the DAW extension) to dump its
99
+ # current inventory. The reply re-establishes
100
+ # +@surface.controls+ from scratch and triggers a full state
101
+ # re-emit. Sent after +@handler.sync+ so the DAW extension is
102
+ # known to be alive.
103
+ surface_bridge.request_sync
104
+
105
+ Thread.new { @transport.start }
39
106
  end
40
107
 
41
- attr_reader :clock, :sequencer, :tracks
108
+ # @!attribute [r] clock
109
+ # @return [Musa::Clock::InputMidiClock] the MIDI clock for synchronization
110
+ # @!attribute [r] sequencer
111
+ # @return [Musa::Sequencer::Sequencer] the Musa-DSL sequencer instance
112
+ # @!attribute [r] transport
113
+ # @return [Musa::Transport::Transport] the transport driving the
114
+ # sequencer. Exposed so REPL users can register callbacks
115
+ # ({Musa::Transport::Transport#on_start},
116
+ # {Musa::Transport::Transport#after_stop},
117
+ # {Musa::Transport::Transport#before_begin}) that survive across
118
+ # DAW Stop/Play cycles — useful for re-installing +on :event+
119
+ # handlers, +every+ loops or +at+ schedules that are wiped by
120
+ # the built-in +after_stop { sequencer.reset }+ callback.
121
+ # @!attribute [r] tracks
122
+ # @return [Object] the DAW-specific tracks collection
123
+ # @!attribute [r] surface
124
+ # @return [Surface] the control surface (Stream Deck etc.)
125
+ attr_reader :clock, :sequencer, :transport, :tracks, :surface
42
126
 
127
+ # DAW-specific initialization hook.
128
+ #
129
+ # Subclasses must implement this method to set up their specific
130
+ # handlers and track management.
131
+ #
132
+ # @param midi_devices [MIDIDevices] the MIDI devices manager
133
+ # @param clock [Musa::Clock::InputMidiClock] the MIDI clock
134
+ # @param osc_server [OSC::EMServer] the OSC server for receiving messages
135
+ # @param osc_client [OSC::Client] the OSC client for sending messages
136
+ # @param logger [Logger] the logger instance
137
+ # @return [Array(Object, Handler)] tuple of [tracks, handler]
138
+ # @api private
43
139
  protected def daw_initialize(midi_devices:, clock:, osc_server:, osc_client:, logger:); end
44
140
 
141
+ # Retrieves a track by name.
142
+ #
143
+ # @param name [String] the track name
144
+ # @param all [Boolean] if true, returns all matching tracks; otherwise returns first match
145
+ # @return [Object, Array<Object>] the track(s) matching the name
146
+ # @raise [NotImplementedError] must be implemented by subclasses
147
+ # @abstract
45
148
  def track(name, all: false)
46
149
  raise NotImplementedError
47
150
  end
48
151
 
152
+ # Starts playback in the DAW.
153
+ # @return [void]
49
154
  def play; end
50
155
 
156
+ # Stops playback in the DAW.
157
+ # @return [void]
51
158
  def stop; end
52
159
 
160
+ # Continues playback from current position.
161
+ # @return [void]
53
162
  def continue; end
54
163
 
164
+ # Moves playhead to specified position.
165
+ # @param position [Numeric] the bar position to go to
166
+ # @return [void]
55
167
  def goto(position); end
56
168
 
169
+ # Starts recording in the DAW.
170
+ # @return [void]
57
171
  def record; end
58
172
 
173
+ # Sends All Notes Off to all tracks.
174
+ #
175
+ # Use this to stop stuck notes after errors or interruptions.
176
+ # @return [void]
59
177
  def panic!
60
178
  @tracks.each do |track|
61
179
  track.out.all_notes_off
62
180
  end
63
181
  end
64
182
 
183
+ # Requests track synchronization from the DAW.
184
+ # @return [void]
65
185
  def sync
66
186
  @handler.sync
67
187
  end
68
188
 
189
+ # Requests the DAW controller extension to reload.
190
+ # @return [void]
69
191
  def reload
70
192
  @handler.reload
71
193
  end
72
194
  end
73
195
 
196
+ # Base class for DAW-specific OSC message handlers.
197
+ #
198
+ # Handles communication with DAW controller extensions via OSC.
199
+ # Subclasses implement DAW-specific message handling.
200
+ #
201
+ # @abstract
202
+ # @api private
74
203
  class Handler
204
+ # Requests the DAW controller extension to reload its configuration.
205
+ # @return [void]
75
206
  def reload
76
207
  @logger.info 'Asking controller reset and reload'
77
208
  send_osc '/reload'
78
209
  end
79
210
 
211
+ # Sends the server version to the DAW controller extension.
212
+ # @return [void]
80
213
  def version
81
214
  @logger.info "Sending version #{VERSION}"
82
215
  send_osc '/version', VERSION
83
216
  end
84
217
 
218
+ # Sends panic (All Notes Off) to all tracks.
219
+ # @return [void]
220
+ # @raise [NotImplementedError] must be implemented by subclasses
221
+ # @abstract
85
222
  def panic!
86
223
  raise NotImplementedError
87
224
  end
88
225
 
226
+ # Sends an OSC message to the DAW controller.
227
+ #
228
+ # Includes retry logic for connection refused errors.
229
+ #
230
+ # @param message [String] the OSC address pattern
231
+ # @param args [Array] optional arguments to send
232
+ # @return [void]
233
+ # @api private
89
234
  private def send_osc(message, *args)
90
235
  counter = 0
91
236
  begin
@@ -98,17 +243,61 @@ module MusaLCEServer
98
243
  end
99
244
  end
100
245
 
246
+ # DSL context for the MusaLCE REPL environment.
247
+ #
248
+ # Extends the Musa-DSL sequencer context with REPL customization
249
+ # capabilities, allowing users to import additional modules and
250
+ # access the binding for evaluation.
251
+ #
252
+ # @api private
101
253
  class MusaLCE_Context < Musa::Sequencer::Sequencer::DSLContext
102
254
  include Musa::REPL::CustomizableDSLContext
103
255
 
256
+ # Returns the binding for this context.
257
+ #
258
+ # Used by the REPL for evaluating user code.
259
+ #
260
+ # @return [Binding] the context binding
104
261
  def binder
105
262
  @__binder ||= binding
106
263
  end
107
264
 
265
+ # Imports modules into this context.
266
+ #
267
+ # Allows users to extend the REPL environment with additional
268
+ # functionality by including modules.
269
+ #
270
+ # @param modules [Array<Module>] modules to include
271
+ # @return [void]
272
+ #
273
+ # @example
274
+ # import(MyHelperModule, AnotherModule)
108
275
  def import(*modules)
109
276
  modules.each do |m|
110
277
  self.class.include(m)
111
278
  end
112
279
  end
280
+
281
+ # @return [Surface] the active control surface (Stream Deck and
282
+ # similar hardware reached via Pulso Bridge).
283
+ #
284
+ # The surface holds typed controls keyed by event name. Controls
285
+ # appear in the inventory once Pulso Bridge advertises them over
286
+ # OSC; before that, +surface[:event]+ returns +nil+.
287
+ #
288
+ # @example single-line state write via {Control#set}
289
+ # on :launch_chorus do |payload|
290
+ # launch :chorus_section
291
+ # surface[:launch_chorus]&.set(enabled: true, message: "Chorus on")
292
+ # end
293
+ #
294
+ # @example reading state to toggle
295
+ # on :master_mute do
296
+ # surface[:master_mute]&.toggle!
297
+ # end
298
+ def surface
299
+ MusaLCEServer.surface or
300
+ raise 'No Surface available (DAW not initialized)'
301
+ end
113
302
  end
114
303
  end
data/lib/live/handler.rb CHANGED
@@ -2,7 +2,19 @@ require_relative '../daw'
2
2
 
3
3
  module MusaLCEServer
4
4
  module Live
5
+ # OSC message handler for Ableton Live.
6
+ #
7
+ # Handles communication with the MusaLCE for Live MIDI Remote Script,
8
+ # processing incoming OSC messages for track registration and routing.
9
+ #
10
+ # @api private
5
11
  class Handler < ::MusaLCEServer::Handler
12
+ # Creates a new Live handler.
13
+ #
14
+ # @param osc_server [OSC::EMServer] the OSC server for receiving messages
15
+ # @param osc_client [OSC::Client] the OSC client for sending messages
16
+ # @param tracks [Tracks] the tracks manager
17
+ # @param logger [Logger] the logger
6
18
  def initialize(osc_server, osc_client, tracks, logger:)
7
19
  super()
8
20
 
@@ -47,6 +59,8 @@ module MusaLCEServer
47
59
  end
48
60
  end
49
61
 
62
+ # Requests track information from Live.
63
+ # @return [void]
50
64
  def sync
51
65
  send_osc '/musalce4live/tracks'
52
66
  end
data/lib/live/live.rb CHANGED
@@ -4,8 +4,27 @@ require_relative 'handler'
4
4
  require_relative 'tracks'
5
5
 
6
6
  module MusaLCEServer
7
+ # Ableton Live integration module.
8
+ #
9
+ # Provides support for live coding with Ableton Live 11+ through
10
+ # the MusaLCE for Live MIDI Remote Script.
11
+ #
12
+ # @see https://github.com/javier-sy/MusaLCEforLive MIDI Remote Script
7
13
  module Live
14
+ # DAW controller for Ableton Live.
15
+ #
16
+ # Implements the {Daw} interface for Ableton Live, providing
17
+ # track management and MIDI routing through the MusaLCE for Live
18
+ # MIDI Remote Script.
19
+ #
20
+ # @note Transport controls (play, stop, etc.) are not implemented
21
+ # for Live as the MIDI Remote Script API doesn't support them.
22
+ #
23
+ # @example
24
+ # # Started via MusaLCEServer.run('live')
25
+ # daw.track('Bass').out.note(60, velocity: 100, duration: 1)
8
26
  class Live < Daw
27
+ # @api private
9
28
  def daw_initialize(midi_devices:, clock:, osc_server:, osc_client:, logger:)
10
29
  super
11
30
  tracks = Tracks.new(midi_devices, logger: logger)
@@ -16,6 +35,13 @@ module MusaLCEServer
16
35
  return tracks, handler
17
36
  end
18
37
 
38
+ # Retrieves track(s) by name.
39
+ #
40
+ # Unlike Bitwig, Live can have multiple tracks with the same name.
41
+ #
42
+ # @param name [String] the track name
43
+ # @param all [Boolean] if true, returns all matching tracks; otherwise returns first match
44
+ # @return [Track, Array<Track>] the track(s) matching the name
19
45
  def track(name, all: false)
20
46
  if all
21
47
  @tracks.find_by_name(name)
@@ -24,6 +50,16 @@ module MusaLCEServer
24
50
  end
25
51
  end
26
52
 
53
+ # Sets the MIDI device to use for clock synchronization.
54
+ #
55
+ # @param midi_device_name [String] default device name to search for
56
+ # @param manufacturer [String, nil] optional manufacturer filter
57
+ # @param model [String, nil] optional model filter
58
+ # @param name [String, nil] optional name filter (overrides midi_device_name)
59
+ # @return [void]
60
+ #
61
+ # @example
62
+ # daw.midi_sync('IAC Driver Bus 1')
27
63
  def midi_sync(midi_device_name, manufacturer: nil, model: nil, name: nil)
28
64
  name ||= midi_device_name
29
65
 
data/lib/live/tracks.rb CHANGED
@@ -2,7 +2,17 @@ require 'musa-dsl/core-ext/dynamic-proxy'
2
2
 
3
3
  module MusaLCEServer
4
4
  module Live
5
+ # Represents a track in Ableton Live.
6
+ #
7
+ # Tracks in Live are identified by their internal ID and can have
8
+ # MIDI input routing configured. The output is dynamically proxied
9
+ # to allow routing changes without recreating the track object.
5
10
  class Track
11
+ # Creates a new track.
12
+ #
13
+ # @param id [Integer] the Live track ID
14
+ # @param midi_devices [MIDIDevices] the MIDI devices manager
15
+ # @param logger [Logger] the logger
6
16
  def initialize(id, midi_devices, logger:)
7
17
  @id = id
8
18
  @midi_devices = midi_devices
@@ -11,35 +21,49 @@ module MusaLCEServer
11
21
  @output = Musa::Extension::DynamicProxy::DynamicProxy.new
12
22
  end
13
23
 
24
+ # @!attribute [r] id
25
+ # @return [Integer] the Live track ID
26
+ # @!attribute [r] name
27
+ # @return [String, nil] the track name
14
28
  attr_reader :id, :name
15
29
 
30
+ # Returns the MIDI output for this track.
31
+ #
32
+ # @return [Musa::Extension::DynamicProxy::DynamicProxy] proxy to the MIDI voice
16
33
  def out
17
34
  @output
18
35
  end
19
36
 
37
+ # @api private
20
38
  def _update_name(value)
21
39
  @name = value
22
40
  @logger.info "track #{@id} assigned name #{@name}"
23
41
  end
24
42
 
43
+ # @api private
25
44
  def _update_has_midi_input(value);
26
45
  @has_midi_input = value == 1;
27
46
  end
47
+ # @api private
28
48
  def _update_has_midi_output(value);
29
49
  @has_midi_output = value == 1;
30
50
  end
51
+ # @api private
31
52
  def _update_has_audio_input(value);
32
53
  @has_audio_input = value == 1;
33
54
  end
55
+ # @api private
34
56
  def _update_has_audio_output(value);
35
57
  @has_audio_output = value == 1;
36
58
  end
37
59
 
60
+ # @api private
38
61
  def _update_current_input_routing(value)
39
62
  @current_input_routing = value
40
63
  _update_current_input_sub_routing(@current_input_sub_routing)
41
64
  end
42
65
 
66
+ # @api private
43
67
  def _update_current_input_sub_routing(value)
44
68
  @current_input_sub_routing = value
45
69
 
@@ -59,24 +83,40 @@ module MusaLCEServer
59
83
  @output.receiver = effective_midi_voice
60
84
  end
61
85
 
86
+ # @api private
62
87
  def _update_current_output_routing(value);
63
88
  @current_output_routing = value
64
89
  end
65
90
 
91
+ # @api private
66
92
  def _update_current_output_sub_routing(value);
67
93
  @current_output_sub_routing = value
68
94
  end
69
95
  end
70
96
 
97
+ # Collection of tracks for Ableton Live.
98
+ #
99
+ # Manages track registration and lookup, automatically creating
100
+ # and updating tracks based on OSC messages from the MIDI Remote Script.
101
+ #
102
+ # @api private
71
103
  class Tracks
72
104
  include Enumerable
73
105
 
106
+ # Creates a new tracks collection.
107
+ #
108
+ # @param midi_devices [MIDIDevices] the MIDI devices manager
109
+ # @param logger [Logger] the logger
74
110
  def initialize(midi_devices, logger:)
75
111
  @midi_devices = midi_devices
76
112
  @logger = logger
77
113
  @tracks = {}
78
114
  end
79
115
 
116
+ # Processes a batch of track data, creating, updating, and deleting tracks.
117
+ #
118
+ # @param tracks_data [Array<Array>] array of track data arrays
119
+ # @return [void]
80
120
  def grant_registry_collection(tracks_data)
81
121
  tracks_to_delete = Set[*@tracks.keys]
82
122
 
@@ -91,6 +131,19 @@ module MusaLCEServer
91
131
  end
92
132
  end
93
133
 
134
+ # Registers or updates a track with the provided data.
135
+ #
136
+ # @param id [Integer] the track ID
137
+ # @param name [String, nil] the track name
138
+ # @param has_midi_input [Integer, nil] 1 if track has MIDI input
139
+ # @param has_midi_output [Integer, nil] 1 if track has MIDI output
140
+ # @param has_audio_input [Integer, nil] 1 if track has audio input
141
+ # @param has_audio_output [Integer, nil] 1 if track has audio output
142
+ # @param current_input_routing [String, nil] input routing device name
143
+ # @param current_input_sub_routing [String, nil] input sub-routing (channel)
144
+ # @param current_output_routing [String, nil] output routing device name
145
+ # @param current_output_sub_routing [String, nil] output sub-routing
146
+ # @return [void]
94
147
  def grant_registry(id, name = nil,
95
148
  has_midi_input = nil, has_midi_output = nil,
96
149
  has_audio_input = nil, has_audio_output = nil,
@@ -115,6 +168,10 @@ module MusaLCEServer
115
168
  track._update_current_output_sub_routing(current_output_sub_routing) if current_output_sub_routing
116
169
  end
117
170
 
171
+ # Iterates over all tracks.
172
+ #
173
+ # @yield [Track] each track
174
+ # @return [Enumerator] if no block given
118
175
  def each(&block)
119
176
  if block_given?
120
177
  @tracks.values.each(&block)
@@ -123,12 +180,20 @@ module MusaLCEServer
123
180
  end
124
181
  end
125
182
 
183
+ # Retrieves a track by ID.
184
+ #
185
+ # @param id [Integer] the track ID
186
+ # @return [Track, nil] the track or nil if not found
126
187
  def [](id)
127
188
  @tracks[id]
128
189
  end
129
190
 
130
- # TODO adaptar a contrato y semántica de Bitwig (en bitwig sólo hay un track con un nombre determinado, el id no existe)
131
-
191
+ # Finds all tracks with the given name.
192
+ #
193
+ # @param name [String] the track name
194
+ # @return [Array<Track>] matching tracks
195
+ #
196
+ # @todo Adapt to Bitwig semantics where track names are unique
132
197
  def find_by_name(name)
133
198
  @tracks.values.select { |_| _.name == name }
134
199
  end
data/lib/midi-devices.rb CHANGED
@@ -3,9 +3,22 @@ require 'midi-communications'
3
3
  require 'musa-dsl/midi/midi-voices'
4
4
 
5
5
  module MusaLCEServer
6
+ # Manages available MIDI output devices.
7
+ #
8
+ # Provides enumeration and lookup of MIDI devices, automatically
9
+ # synchronizing with the system's available devices.
10
+ #
11
+ # @example Iterating over devices
12
+ # midi_devices.each { |device| puts device.name }
13
+ #
14
+ # @example Finding a device by name suffix
15
+ # device = midi_devices.find('IAC Driver Bus 1')
6
16
  class MIDIDevices
7
17
  include Enumerable
8
18
 
19
+ # Creates a new MIDI devices manager.
20
+ #
21
+ # @param sequencer [Musa::Sequencer::Sequencer] the sequencer for MIDI voice management
9
22
  def initialize(sequencer)
10
23
  @sequencer = sequencer
11
24
  @low_level_devices = {}
@@ -13,6 +26,11 @@ module MusaLCEServer
13
26
  sync
14
27
  end
15
28
 
29
+ # Synchronizes the device list with system MIDI devices.
30
+ #
31
+ # Adds newly connected devices and removes disconnected ones.
32
+ #
33
+ # @return [void]
16
34
  def sync
17
35
  names = @low_level_devices.keys
18
36
 
@@ -30,15 +48,32 @@ module MusaLCEServer
30
48
  end
31
49
  end
32
50
 
51
+ # Retrieves a device by exact name.
52
+ #
53
+ # @param name [String] the exact device name
54
+ # @return [MIDIDevice, nil] the device or nil if not found
33
55
  def [](name)
34
56
  @low_level_devices[name]
35
57
  end
36
58
 
59
+ # Finds a device by name suffix.
60
+ #
61
+ # Useful when device names include prefixes that vary by system.
62
+ #
63
+ # @param name [String] the name suffix to match
64
+ # @return [MIDIDevice, nil] the first matching device or nil
65
+ #
66
+ # @example
67
+ # device = midi_devices.find('Bus 1') # Matches 'IAC Driver Bus 1'
37
68
  def find(name)
38
69
  full_name = @low_level_devices.keys.find { |_| _.end_with?(name) }
39
70
  @low_level_devices[full_name]
40
71
  end
41
72
 
73
+ # Iterates over all MIDI devices.
74
+ #
75
+ # @yield [MIDIDevice] each device
76
+ # @return [Enumerator] if no block given
42
77
  def each(&block)
43
78
  if block_given?
44
79
  @low_level_devices.values.each(&block)
@@ -47,27 +82,49 @@ module MusaLCEServer
47
82
  end
48
83
  end
49
84
  end
50
-
85
+
86
+ # Wrapper for a MIDI output device with voice management.
87
+ #
88
+ # Provides access to individual MIDI channels as voices and
89
+ # panic functionality.
51
90
  class MIDIDevice
91
+ # Creates a new MIDI device wrapper.
92
+ #
93
+ # @param sequencer [Musa::Sequencer::Sequencer] the sequencer for voice management
94
+ # @param low_level_device [MIDICommunications::Output] the underlying MIDI device
52
95
  def initialize(sequencer, low_level_device)
53
96
  @low_level_device = low_level_device
54
97
  @voices = Musa::MIDIVoices::MIDIVoices.new(sequencer: sequencer, output: low_level_device, channels: 0..15, do_log: true)
55
98
  end
56
99
 
100
+ # @!attribute [r] low_level_device
101
+ # @return [MIDICommunications::Output] the underlying MIDI output device
57
102
  attr_reader :low_level_device
58
103
 
104
+ # Returns the device name.
105
+ #
106
+ # @return [String] the device name
59
107
  def name
60
108
  @low_level_device.name
61
109
  end
62
110
 
111
+ # Sends All Notes Off and reset to all channels.
112
+ #
113
+ # @return [void]
63
114
  def panic!
64
115
  @voices.panic reset: true
65
116
  end
66
117
 
118
+ # Returns the MIDI channels/voices for this device.
119
+ #
120
+ # @return [Array<Musa::MIDIVoices::MIDIVoice>] the 16 MIDI channels
67
121
  def channels
68
122
  @voices.voices
69
123
  end
70
124
 
125
+ # Returns the display name of the device.
126
+ #
127
+ # @return [String] the display name
71
128
  def to_s
72
129
  @low_level_device.display_name
73
130
  end