musalce-server 0.5.1 → 0.8.1

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.
@@ -2,7 +2,19 @@ require_relative 'tracks'
2
2
 
3
3
  module MusaLCEServer
4
4
  module Bitwig
5
+ # Manages Bitwig controller scripts and their MIDI channels.
6
+ #
7
+ # Controllers in Bitwig represent hardware MIDI devices configured
8
+ # through the MusaLCE controller extension. Each controller has
9
+ # 16 channels that can be named and mapped to tracks.
10
+ #
11
+ # @api private
5
12
  class Controllers
13
+ # Creates a new controllers manager.
14
+ #
15
+ # @param midi_devices [MIDIDevices] the MIDI devices manager
16
+ # @param clock [Musa::Clock::InputMidiClock] the MIDI clock
17
+ # @param logger [Logger] the logger
6
18
  def initialize(midi_devices, clock:, logger:)
7
19
  @midi_devices = midi_devices
8
20
  @clock = clock
@@ -11,8 +23,14 @@ module MusaLCEServer
11
23
  @tracks = Tracks.new(logger: logger)
12
24
  end
13
25
 
26
+ # @!attribute [r] tracks
27
+ # @return [Tracks] the tracks collection
14
28
  attr_reader :tracks
15
29
 
30
+ # Registers or updates the list of available controllers.
31
+ #
32
+ # @param controllers [Array<String>] controller names from Bitwig
33
+ # @return [void]
16
34
  def register_controllers(controllers)
17
35
  to_delete = @controllers.keys - controllers
18
36
 
@@ -31,6 +49,12 @@ module MusaLCEServer
31
49
  end
32
50
  end
33
51
 
52
+ # Registers a controller with its port and clock settings.
53
+ #
54
+ # @param name [String] the controller name
55
+ # @param port_name [String] the MIDI port name
56
+ # @param is_clock [Boolean] whether this controller provides MIDI clock
57
+ # @return [void]
34
58
  def register_controller(name:, port_name:, is_clock:)
35
59
  controller = @controllers[name]
36
60
  controller.port_name = port_name
@@ -38,6 +62,13 @@ module MusaLCEServer
38
62
  @logger.info "Controller #{name} defined with port_name #{port_name} clock #{is_clock}"
39
63
  end
40
64
 
65
+ # Updates a controller's name and settings.
66
+ #
67
+ # @param old_name [String] the current controller name
68
+ # @param new_name [String] the new controller name
69
+ # @param port_name [String] the MIDI port name
70
+ # @param is_clock [Boolean] whether this controller provides MIDI clock
71
+ # @return [void]
41
72
  def update_controller(old_name:, new_name:, port_name:, is_clock:)
42
73
  controller = @controllers.delete(old_name)
43
74
  @controllers[new_name] = controller
@@ -49,6 +80,11 @@ module MusaLCEServer
49
80
  @logger.info "Controller #{old_name} updated as #{new_name} with port_name #{port_name} clock #{is_clock}"
50
81
  end
51
82
 
83
+ # Registers channel names for a controller.
84
+ #
85
+ # @param controller_name [String] the controller name
86
+ # @param channels [Array<String>] channel names (up to 16)
87
+ # @return [void]
52
88
  def register_channels(controller_name:, channels:)
53
89
  controller = @controllers[controller_name]
54
90
 
@@ -60,7 +96,20 @@ module MusaLCEServer
60
96
  end
61
97
  end
62
98
 
99
+ # Represents a MIDI controller in Bitwig.
100
+ #
101
+ # A controller corresponds to a hardware MIDI device with 16 channels
102
+ # that can be routed to tracks.
103
+ #
104
+ # @api private
63
105
  class Controller
106
+ # Creates a new controller.
107
+ #
108
+ # @param name [String] the controller name
109
+ # @param midi_devices [MIDIDevices] the MIDI devices manager
110
+ # @param clock [Musa::Clock::InputMidiClock] the MIDI clock
111
+ # @param tracks [Tracks] the tracks collection
112
+ # @param logger [Logger] the logger
64
113
  def initialize(name, midi_devices, clock, tracks, logger:)
65
114
  @midi_devices = midi_devices
66
115
  @clock = clock
@@ -73,15 +122,34 @@ module MusaLCEServer
73
122
  @channels = Array.new(16) { |channel| Channel.new(self, tracks, channel, logger: logger) }
74
123
  end
75
124
 
125
+ # @!attribute port_name
126
+ # @return [String] the MIDI port name
76
127
  attr_accessor :port_name
128
+
129
+ # @!attribute [r] name
130
+ # @return [String] the controller name
131
+ # @!attribute [r] midi_device
132
+ # @return [MIDIDevice, nil] the associated MIDI device
133
+ # @!attribute [r] channels
134
+ # @return [Array<Channel>] the 16 MIDI channels
135
+ # @!attribute [r] is_clock
136
+ # @return [Boolean] whether this controller provides MIDI clock
77
137
  attr_reader :name, :midi_device, :channels, :is_clock
78
138
 
139
+ # Sets whether this controller provides MIDI clock.
140
+ #
141
+ # @param new_is_clock [Boolean] the clock setting
142
+ # @return [void]
79
143
  def is_clock=(new_is_clock)
80
144
  # TODO when new_is_clock is false look if another controller is true, else leave clock input as nil (the user has not selected any clock!)
81
145
  @is_clock = new_is_clock
82
146
  update_clock
83
147
  end
84
148
 
149
+ # Sets the controller name and finds the associated MIDI device.
150
+ #
151
+ # @param new_name [String] the controller name
152
+ # @return [void]
85
153
  def name=(new_name)
86
154
  @name = new_name
87
155
  @midi_device = @midi_devices.find(@name)
@@ -100,7 +168,18 @@ module MusaLCEServer
100
168
  end
101
169
  end
102
170
 
171
+ # Represents a MIDI channel on a controller.
172
+ #
173
+ # Each channel can be named and mapped to a track for MIDI output.
174
+ #
175
+ # @api private
103
176
  class Channel
177
+ # Creates a new channel.
178
+ #
179
+ # @param controller [Controller] the parent controller
180
+ # @param tracks [Tracks] the tracks collection
181
+ # @param channel_number [Integer] the MIDI channel (0-15)
182
+ # @param logger [Logger] the logger
104
183
  def initialize(controller, tracks, channel_number, logger:)
105
184
  @controller = controller
106
185
  @tracks = tracks
@@ -108,8 +187,16 @@ module MusaLCEServer
108
187
  @logger = logger
109
188
  end
110
189
 
190
+ # @!attribute [r] channel_number
191
+ # @return [Integer] the MIDI channel number (0-15)
192
+ # @!attribute [r] name
193
+ # @return [String, nil] the channel name
111
194
  attr_reader :channel_number, :name
112
195
 
196
+ # Sets the channel name and associates it with a track.
197
+ #
198
+ # @param new_name [String] the channel name
199
+ # @return [void]
113
200
  def name=(new_name)
114
201
  @tracks[@name]&._forget_channel
115
202
  @name = new_name
@@ -117,10 +204,16 @@ module MusaLCEServer
117
204
  @tracks[@name]._channel = self
118
205
  end
119
206
 
207
+ # Returns the MIDI voice for this channel.
208
+ #
209
+ # @return [Musa::MIDIVoices::MIDIVoice] the MIDI voice for output
120
210
  def output
121
211
  @controller.midi_device.channels[@channel_number]
122
212
  end
123
213
 
214
+ # Returns a string representation of this channel.
215
+ #
216
+ # @return [String] description including channel number, name, port, and controller
124
217
  def to_s
125
218
  "<Channel #{@channel_number} '#{@name}' on port '#{@controller.port_name}' (controller '#{@controller.name}')>"
126
219
  end
@@ -2,7 +2,21 @@ require_relative '../daw'
2
2
 
3
3
  module MusaLCEServer
4
4
  module Bitwig
5
+ # OSC message handler for Bitwig Studio.
6
+ #
7
+ # Handles communication with the MusaLCE for Bitwig controller extension,
8
+ # processing incoming OSC messages for controller and channel registration,
9
+ # and sending transport commands.
10
+ #
11
+ # @api private
5
12
  class Handler < ::MusaLCEServer::Handler
13
+ # Creates a new Bitwig handler.
14
+ #
15
+ # @param osc_server [OSC::EMServer] the OSC server for receiving messages
16
+ # @param osc_client [OSC::Client] the OSC client for sending messages
17
+ # @param controllers [Controllers] the controllers manager
18
+ # @param sequencer [Musa::Sequencer::Sequencer] the sequencer instance
19
+ # @param logger [Logger] the logger
6
20
  def initialize(osc_server, osc_client, controllers, sequencer, logger:)
7
21
  super()
8
22
 
@@ -44,36 +58,52 @@ module MusaLCEServer
44
58
  end
45
59
  end
46
60
 
61
+ # Requests synchronization of controllers and channels from Bitwig.
62
+ # @return [void]
47
63
  def sync
48
64
  @logger.info 'Asking sync'
49
65
  send_osc '/musalce4bitwig/sync'
50
66
  end
51
67
 
68
+ # Sends play command to Bitwig.
69
+ # @return [void]
52
70
  def play
53
71
  @logger.info 'Asking play'
54
72
  send_osc '/musalce4bitwig/play'
55
73
  end
56
74
 
75
+ # Sends stop command to Bitwig.
76
+ # @return [void]
57
77
  def stop
58
78
  @logger.info 'Asking stop'
59
79
  send_osc '/musalce4bitwig/stop'
60
80
  end
61
81
 
82
+ # Sends continue command to Bitwig.
83
+ # @return [void]
62
84
  def continue
63
85
  @logger.info 'Asking continue'
64
86
  send_osc '/musalce4bitwig/continue'
65
87
  end
66
88
 
89
+ # Moves playhead to specified position.
90
+ #
91
+ # @param position [Numeric] the bar number (1-based)
92
+ # @return [void]
67
93
  def goto(position)
68
94
  @logger.info "Asking goto #{position}"
69
95
  send_osc '/musalce4bitwig/goto', OSC::OSCDouble64.new(((position - 1) * @sequencer.beats_per_bar).to_f)
70
96
  end
71
97
 
98
+ # Sends record command to Bitwig.
99
+ # @return [void]
72
100
  def record
73
101
  @logger.info 'Asking record'
74
102
  send_osc '/musalce4bitwig/record'
75
103
  end
76
104
 
105
+ # Sends panic to all tracks.
106
+ # @return [void]
77
107
  def panic!
78
108
  @controllers.tracks.each(:panic!)
79
109
  end
data/lib/bitwig/tracks.rb CHANGED
@@ -2,18 +2,35 @@ require 'musa-dsl/core-ext/dynamic-proxy'
2
2
 
3
3
  module MusaLCEServer
4
4
  module Bitwig
5
+ # Collection of tracks for Bitwig.
6
+ #
7
+ # Tracks are created dynamically based on channel names received
8
+ # from the Bitwig controller extension.
9
+ #
10
+ # @api private
5
11
  class Tracks
6
12
  include Enumerable
7
13
 
14
+ # Creates a new tracks collection.
15
+ #
16
+ # @param logger [Logger] the logger
8
17
  def initialize(logger:)
9
18
  @logger = logger
10
19
  @tracks = {}
11
20
  end
12
21
 
22
+ # Creates a new track with the given name.
23
+ #
24
+ # @param name [String] the track name
25
+ # @return [Track] the created track
13
26
  def create(name)
14
27
  @tracks[name] = Track.new(name, logger: @logger)
15
28
  end
16
29
 
30
+ # Iterates over all tracks.
31
+ #
32
+ # @yield [Track] each track
33
+ # @return [Enumerator] if no block given
17
34
  def each(&block)
18
35
  if block_given?
19
36
  @tracks.values.each(&block)
@@ -22,16 +39,33 @@ module MusaLCEServer
22
39
  end
23
40
  end
24
41
 
42
+ # Retrieves a track by name.
43
+ #
44
+ # @param name [String] the track name
45
+ # @return [Track, nil] the track or nil if not found
25
46
  def [](name)
26
47
  @tracks[name]
27
48
  end
28
49
 
50
+ # Sets a track by name.
51
+ #
52
+ # @param name [String] the track name
53
+ # @param track [Track] the track
54
+ # @return [Track] the track
29
55
  def []=(name, track)
30
56
  @tracks[name] = track
31
57
  end
32
58
  end
33
59
 
60
+ # Represents a track in Bitwig with dynamic MIDI output.
61
+ #
62
+ # Uses DynamicProxy to allow the output to be reassigned
63
+ # when channel mappings change.
34
64
  class Track
65
+ # Creates a new track.
66
+ #
67
+ # @param name [String] the track name
68
+ # @param logger [Logger] the logger
35
69
  def initialize(name, logger:)
36
70
  @name = name
37
71
  @logger = logger
@@ -39,17 +73,29 @@ module MusaLCEServer
39
73
  @output = Musa::Extension::DynamicProxy::DynamicProxy.new
40
74
  end
41
75
 
76
+ # @!attribute [r] name
77
+ # @return [String] the track name
42
78
  attr_reader :name
43
79
 
80
+ # Disconnects the current channel from this track.
81
+ # @api private
82
+ # @return [void]
44
83
  def _forget_channel
45
84
  @output.receiver = nil
46
85
  end
47
86
 
87
+ # Sets the channel for this track.
88
+ # @api private
89
+ # @param new_channel [Channel] the channel to assign
90
+ # @return [void]
48
91
  def _channel=(new_channel)
49
92
  @logger.info "Assigning #{new_channel} to track '#{@name}'"
50
93
  @output.receiver = new_channel.output
51
94
  end
52
95
 
96
+ # Returns the MIDI output for this track.
97
+ #
98
+ # @return [Musa::Extension::DynamicProxy::DynamicProxy] proxy to the MIDI voice
53
99
  def out
54
100
  @output
55
101
  end
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