musalce-server 0.5.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/notify-plugin.yml +17 -0
- data/.gitignore +3 -0
- data/.version +6 -0
- data/.yardopts +6 -0
- data/README.md +317 -9
- data/docs/architecture.md +242 -0
- data/lib/bitwig/bitwig.rb +32 -0
- data/lib/bitwig/controllers.rb +93 -0
- data/lib/bitwig/handler.rb +30 -0
- data/lib/bitwig/tracks.rb +46 -0
- data/lib/daw.rb +193 -4
- data/lib/live/handler.rb +14 -0
- data/lib/live/live.rb +36 -0
- data/lib/live/tracks.rb +67 -2
- data/lib/midi-devices.rb +58 -1
- data/lib/musalce-server.rb +33 -0
- data/lib/surface-bridge.rb +171 -0
- data/lib/surface.rb +369 -0
- data/lib/version.rb +13 -1
- data/musalce-server.gemspec +20 -13
- metadata +90 -17
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|