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.
- 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 +307 -10
- data/docs/architecture.md +178 -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/bitwig/controllers.rb
CHANGED
|
@@ -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
|
data/lib/bitwig/handler.rb
CHANGED
|
@@ -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
|
-
|
|
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
|