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/bitwig/bitwig.rb
CHANGED
|
@@ -4,8 +4,24 @@ require_relative 'handler'
|
|
|
4
4
|
require_relative 'controllers'
|
|
5
5
|
|
|
6
6
|
module MusaLCEServer
|
|
7
|
+
# Bitwig Studio integration module.
|
|
8
|
+
#
|
|
9
|
+
# Provides support for live coding with Bitwig Studio 5+ through
|
|
10
|
+
# the MusaLCE for Bitwig controller extension.
|
|
11
|
+
#
|
|
12
|
+
# @see https://github.com/javier-sy/MusaLCEforBitwig Controller extension
|
|
7
13
|
module Bitwig
|
|
14
|
+
# DAW controller for Bitwig Studio.
|
|
15
|
+
#
|
|
16
|
+
# Implements the {Daw} interface for Bitwig Studio, providing
|
|
17
|
+
# transport control, track management, and MIDI routing through
|
|
18
|
+
# the MusaLCE for Bitwig controller extension.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# # Started via MusaLCEServer.run('bitwig')
|
|
22
|
+
# daw.track('Bass').out.note(60, velocity: 100, duration: 1)
|
|
8
23
|
class Bitwig < Daw
|
|
24
|
+
# @api private
|
|
9
25
|
def daw_initialize(midi_devices:, clock:, osc_server:, osc_client:, logger:)
|
|
10
26
|
super
|
|
11
27
|
|
|
@@ -17,6 +33,11 @@ module MusaLCEServer
|
|
|
17
33
|
return controllers.tracks, handler
|
|
18
34
|
end
|
|
19
35
|
|
|
36
|
+
# Retrieves a track by name.
|
|
37
|
+
#
|
|
38
|
+
# @param name [String] the track name as configured in Bitwig
|
|
39
|
+
# @param all [Boolean] if true, returns array; otherwise returns single track
|
|
40
|
+
# @return [Track, Array<Track>] the track or array containing the track
|
|
20
41
|
def track(name, all: false)
|
|
21
42
|
if all
|
|
22
43
|
[@tracks[name]]
|
|
@@ -25,26 +46,37 @@ module MusaLCEServer
|
|
|
25
46
|
end
|
|
26
47
|
end
|
|
27
48
|
|
|
49
|
+
# Starts playback in Bitwig.
|
|
50
|
+
# @return [void]
|
|
28
51
|
def play
|
|
29
52
|
@handler.play
|
|
30
53
|
super
|
|
31
54
|
end
|
|
32
55
|
|
|
56
|
+
# Stops playback in Bitwig.
|
|
57
|
+
# @return [void]
|
|
33
58
|
def stop
|
|
34
59
|
@handler.stop
|
|
35
60
|
super
|
|
36
61
|
end
|
|
37
62
|
|
|
63
|
+
# Continues playback from current position.
|
|
64
|
+
# @return [void]
|
|
38
65
|
def continue
|
|
39
66
|
@handler.continue
|
|
40
67
|
super
|
|
41
68
|
end
|
|
42
69
|
|
|
70
|
+
# Moves playhead to specified bar position.
|
|
71
|
+
# @param position [Numeric] the bar number (1-based)
|
|
72
|
+
# @return [void]
|
|
43
73
|
def goto(position)
|
|
44
74
|
@handler.goto(position)
|
|
45
75
|
super
|
|
46
76
|
end
|
|
47
77
|
|
|
78
|
+
# Starts recording in Bitwig.
|
|
79
|
+
# @return [void]
|
|
48
80
|
def record
|
|
49
81
|
@handler.record
|
|
50
82
|
super
|
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
|