musalce-server 0.4.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +3 -0
- data/LICENSE +674 -0
- data/README.md +22 -0
- data/bin/musalce-server +5 -0
- data/lib/bitwig/bitwig.rb +57 -0
- data/lib/bitwig/controllers.rb +129 -0
- data/lib/bitwig/handler.rb +82 -0
- data/lib/bitwig/tracks.rb +58 -0
- data/lib/daw.rb +114 -0
- data/lib/live/handler.rb +55 -0
- data/lib/live/live.rb +40 -0
- data/lib/live/tracks.rb +142 -0
- data/lib/midi-devices.rb +76 -0
- data/lib/musalce-server.rb +57 -0
- data/musalce-server.gemspec +32 -0
- metadata +181 -0
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Musa Live Coding Environment (Server for Ableton Live and Bitwig)
|
2
|
+
|
3
|
+
Musa-DSL Live Coding Environment for Ableton Live and Bitwig Studio (Server part)
|
4
|
+
|
5
|
+
**TODO: complete README**
|
6
|
+
|
7
|
+
## Install
|
8
|
+
**TODO**
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
**TODO**
|
12
|
+
|
13
|
+
## Documentation
|
14
|
+
**TODO**
|
15
|
+
|
16
|
+
## Author
|
17
|
+
|
18
|
+
* [Javier Sánchez Yeste](https://github.com/javier-sy)
|
19
|
+
|
20
|
+
## License
|
21
|
+
|
22
|
+
[MusaLCE-Server](https://github.com/javier-sy/MusaLCE-Server) Copyright (c) 2021-2023 [Javier Sánchez Yeste](https://yeste.studio), licensed under GPL 3.0 License
|
data/bin/musalce-server
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative '../daw'
|
2
|
+
|
3
|
+
require_relative 'handler'
|
4
|
+
require_relative 'controllers'
|
5
|
+
|
6
|
+
module MusaLCEServer
|
7
|
+
module Bitwig
|
8
|
+
class Bitwig < Daw
|
9
|
+
def daw_initialize(midi_devices:, clock:, osc_server:, osc_client:, logger:)
|
10
|
+
super
|
11
|
+
|
12
|
+
controllers = Controllers.new(midi_devices, clock: clock, logger: logger)
|
13
|
+
handler = Handler.new(osc_server, osc_client, controllers, @sequencer, logger: logger)
|
14
|
+
|
15
|
+
logger.info('Loaded Bitwig Studio driver')
|
16
|
+
|
17
|
+
return controllers.tracks, handler
|
18
|
+
end
|
19
|
+
|
20
|
+
def track(name, all: false)
|
21
|
+
if all
|
22
|
+
[@tracks[name]]
|
23
|
+
else
|
24
|
+
@tracks[name]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def play
|
29
|
+
@handler.play
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
@handler.stop
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def continue
|
39
|
+
@handler.continue
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def goto(position)
|
44
|
+
@handler.goto(position)
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def record
|
49
|
+
@handler.record
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Daw.register :bitwig, Bitwig
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require_relative 'tracks'
|
2
|
+
|
3
|
+
module MusaLCEServer
|
4
|
+
module Bitwig
|
5
|
+
class Controllers
|
6
|
+
def initialize(midi_devices, clock:, logger:)
|
7
|
+
@midi_devices = midi_devices
|
8
|
+
@clock = clock
|
9
|
+
@logger = logger
|
10
|
+
@controllers = {}
|
11
|
+
@tracks = Tracks.new(logger: logger)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :tracks
|
15
|
+
|
16
|
+
def register_controllers(controllers)
|
17
|
+
to_delete = @controllers.keys - controllers
|
18
|
+
|
19
|
+
controllers.each do |controller_name|
|
20
|
+
if @controllers.key?(controller_name)
|
21
|
+
@logger.info "Controller #{controller_name} already exists"
|
22
|
+
else
|
23
|
+
@logger.info "Added controller #{controller_name}"
|
24
|
+
@controllers[controller_name] = Controller.new(controller_name, @midi_devices, @clock, @tracks, logger: @logger)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
to_delete.each do |controller_name|
|
29
|
+
@controllers.delete(controller_name)
|
30
|
+
@logger.info "Deleted controller #{controller_name}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_controller(name:, port_name:, is_clock:)
|
35
|
+
controller = @controllers[name]
|
36
|
+
controller.port_name = port_name
|
37
|
+
controller.is_clock = is_clock
|
38
|
+
@logger.info "Controller #{name} defined with port_name #{port_name} clock #{is_clock}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def update_controller(old_name:, new_name:, port_name:, is_clock:)
|
42
|
+
controller = @controllers.delete(old_name)
|
43
|
+
@controllers[new_name] = controller
|
44
|
+
controller.name = new_name
|
45
|
+
|
46
|
+
controller.port_name = port_name
|
47
|
+
controller.is_clock = is_clock
|
48
|
+
|
49
|
+
@logger.info "Controller #{old_name} updated as #{new_name} with port_name #{port_name} clock #{is_clock}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def register_channels(controller_name:, channels:)
|
53
|
+
controller = @controllers[controller_name]
|
54
|
+
|
55
|
+
@logger.info "Channels for controller #{controller_name} named #{channels}"
|
56
|
+
|
57
|
+
channels.each.with_index do |name, i|
|
58
|
+
controller.channels[i].name = name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Controller
|
64
|
+
def initialize(name, midi_devices, clock, tracks, logger:)
|
65
|
+
@midi_devices = midi_devices
|
66
|
+
@clock = clock
|
67
|
+
@tracks = tracks
|
68
|
+
@logger = logger
|
69
|
+
|
70
|
+
@midi_device = nil
|
71
|
+
|
72
|
+
self.name = name
|
73
|
+
@channels = Array.new(16) { |channel| Channel.new(self, tracks, channel, logger: logger) }
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_accessor :port_name
|
77
|
+
attr_reader :name, :midi_device, :channels, :is_clock
|
78
|
+
|
79
|
+
def is_clock=(new_is_clock)
|
80
|
+
# 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
|
+
@is_clock = new_is_clock
|
82
|
+
update_clock
|
83
|
+
end
|
84
|
+
|
85
|
+
def name=(new_name)
|
86
|
+
@name = new_name
|
87
|
+
@midi_device = @midi_devices.find(@name)
|
88
|
+
|
89
|
+
update_clock
|
90
|
+
|
91
|
+
if @midi_device
|
92
|
+
@logger.info "Found midi device #{@midi_device.to_s} for #{@name}"
|
93
|
+
else
|
94
|
+
@logger.warn "Not found midi device for #{@name}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private def update_clock
|
99
|
+
@clock.input = MIDICommunications::Input.find_by_name(@midi_device.low_level_device.name) if @is_clock
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class Channel
|
104
|
+
def initialize(controller, tracks, channel_number, logger:)
|
105
|
+
@controller = controller
|
106
|
+
@tracks = tracks
|
107
|
+
@channel_number = channel_number
|
108
|
+
@logger = logger
|
109
|
+
end
|
110
|
+
|
111
|
+
attr_reader :channel_number, :name
|
112
|
+
|
113
|
+
def name=(new_name)
|
114
|
+
@tracks[@name]&._forget_channel
|
115
|
+
@name = new_name
|
116
|
+
@tracks.create(@name)
|
117
|
+
@tracks[@name]._channel = self
|
118
|
+
end
|
119
|
+
|
120
|
+
def output
|
121
|
+
@controller.midi_device.channels[@channel_number]
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
"<Channel #{@channel_number} '#{@name}' on port '#{@controller.port_name}' (controller '#{@controller.name}')>"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative '../daw'
|
2
|
+
|
3
|
+
module MusaLCEServer
|
4
|
+
module Bitwig
|
5
|
+
class Handler < ::MusaLCEServer::Handler
|
6
|
+
def initialize(osc_server, osc_client, controllers, sequencer, logger:)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@server = osc_server
|
10
|
+
@client = osc_client
|
11
|
+
|
12
|
+
@controllers = controllers
|
13
|
+
@sequencer = sequencer
|
14
|
+
|
15
|
+
@logger = logger
|
16
|
+
|
17
|
+
@server.add_method '/hello' do |message|
|
18
|
+
@logger.info "Received /hello #{message.to_a}!"
|
19
|
+
version
|
20
|
+
sync
|
21
|
+
end
|
22
|
+
|
23
|
+
@server.add_method '/musalce4bitwig/controllers' do |message|
|
24
|
+
@logger.info("Received /musalce4bitwig/controllers #{message.to_a}")
|
25
|
+
@controllers.register_controllers(message.to_a)
|
26
|
+
end
|
27
|
+
|
28
|
+
@server.add_method '/musalce4bitwig/controller' do |message|
|
29
|
+
@logger.info("Received /musalce4bitwig/controller #{message.to_a}")
|
30
|
+
a = message.to_a
|
31
|
+
@controllers.register_controller(name: a[0], port_name: a[1], is_clock: a[2] == 1)
|
32
|
+
end
|
33
|
+
|
34
|
+
@server.add_method '/musalce4bitwig/controller/update' do |message|
|
35
|
+
@logger.info("Received /musalce4bitwig/controller/update #{message.to_a}")
|
36
|
+
a = message.to_a
|
37
|
+
@controllers.update_controller(old_name: a[0], new_name: a[1], port_name: a[2], is_clock: a[3] == 1)
|
38
|
+
end
|
39
|
+
|
40
|
+
@server.add_method '/musalce4bitwig/channels' do |message|
|
41
|
+
@logger.info("Received /musalce4bitwig/channels #{message.to_a}")
|
42
|
+
a = message.to_a
|
43
|
+
@controllers.register_channels(controller_name: a[0], channels: a[1..])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def sync
|
48
|
+
@logger.info 'Asking sync'
|
49
|
+
send_osc '/musalce4bitwig/sync'
|
50
|
+
end
|
51
|
+
|
52
|
+
def play
|
53
|
+
@logger.info 'Asking play'
|
54
|
+
send_osc '/musalce4bitwig/play'
|
55
|
+
end
|
56
|
+
|
57
|
+
def stop
|
58
|
+
@logger.info 'Asking stop'
|
59
|
+
send_osc '/musalce4bitwig/stop'
|
60
|
+
end
|
61
|
+
|
62
|
+
def continue
|
63
|
+
@logger.info 'Asking continue'
|
64
|
+
send_osc '/musalce4bitwig/continue'
|
65
|
+
end
|
66
|
+
|
67
|
+
def goto(position)
|
68
|
+
@logger.info "Asking goto #{position}"
|
69
|
+
send_osc '/musalce4bitwig/goto', OSC::OSCDouble64.new(((position - 1) * @sequencer.beats_per_bar).to_f)
|
70
|
+
end
|
71
|
+
|
72
|
+
def record
|
73
|
+
@logger.info 'Asking record'
|
74
|
+
send_osc '/musalce4bitwig/record'
|
75
|
+
end
|
76
|
+
|
77
|
+
def panic!
|
78
|
+
@controllers.tracks.each(:panic!)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'musa-dsl/core-ext/dynamic-proxy'
|
2
|
+
|
3
|
+
module MusaLCEServer
|
4
|
+
module Bitwig
|
5
|
+
class Tracks
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(logger:)
|
9
|
+
@logger = logger
|
10
|
+
@tracks = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def create(name)
|
14
|
+
@tracks[name] = Track.new(name, logger: @logger)
|
15
|
+
end
|
16
|
+
|
17
|
+
def each(&block)
|
18
|
+
if block_given?
|
19
|
+
@tracks.values.each(&block)
|
20
|
+
else
|
21
|
+
@tracks.values.each
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](name)
|
26
|
+
@tracks[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def []=(name, track)
|
30
|
+
@tracks[name] = track
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Track
|
35
|
+
def initialize(name, logger:)
|
36
|
+
@name = name
|
37
|
+
@logger = logger
|
38
|
+
|
39
|
+
@output = Musa::Extension::DynamicProxy::DynamicProxy.new
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :name
|
43
|
+
|
44
|
+
def _forget_channel
|
45
|
+
@output.receiver = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def _channel=(new_channel)
|
49
|
+
@logger.info "Assigning #{new_channel} to track '#{@name}'"
|
50
|
+
@output.receiver = new_channel.output
|
51
|
+
end
|
52
|
+
|
53
|
+
def out
|
54
|
+
@output
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/daw.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'midi-communications'
|
2
|
+
|
3
|
+
require_relative 'midi-devices'
|
4
|
+
|
5
|
+
module MusaLCEServer
|
6
|
+
class Daw
|
7
|
+
def self.register(daw_id, daw_class)
|
8
|
+
@@daws ||= {}
|
9
|
+
@@daws[daw_id] = daw_class
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.daw_controller_for(daw_id)
|
13
|
+
@@daws[daw_id].new
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
osc_server = OSC::EMServer.new(11_011)
|
18
|
+
osc_client = OSC::Client.new('localhost', 10_001)
|
19
|
+
|
20
|
+
Thread.new { osc_server.run }
|
21
|
+
|
22
|
+
@sequencer = Musa::Sequencer::Sequencer.new 4, 24, dsl_context_class: MusaLCE_Context, do_log: true
|
23
|
+
|
24
|
+
@clock = Musa::Clock::InputMidiClock.new do_log: true, logger: @sequencer.logger
|
25
|
+
transport = Musa::Transport::Transport.new @clock, sequencer: @sequencer
|
26
|
+
|
27
|
+
transport.after_stop do
|
28
|
+
sequencer.reset
|
29
|
+
end
|
30
|
+
|
31
|
+
@midi_devices = MIDIDevices.new(@sequencer)
|
32
|
+
|
33
|
+
@tracks, @handler = daw_initialize(midi_devices: @midi_devices, clock: @clock, osc_server: osc_server, osc_client: osc_client, logger: @sequencer.logger)
|
34
|
+
|
35
|
+
@handler.version
|
36
|
+
@handler.sync
|
37
|
+
|
38
|
+
Thread.new { transport.start }
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :clock, :sequencer, :tracks
|
42
|
+
|
43
|
+
protected def daw_initialize(midi_devices:, clock:, osc_server:, osc_client:, logger:); end
|
44
|
+
|
45
|
+
def track(name, all: false)
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
|
49
|
+
def play; end
|
50
|
+
|
51
|
+
def stop; end
|
52
|
+
|
53
|
+
def continue; end
|
54
|
+
|
55
|
+
def goto(position); end
|
56
|
+
|
57
|
+
def record; end
|
58
|
+
|
59
|
+
def panic!
|
60
|
+
@tracks.each do |track|
|
61
|
+
track.out.all_notes_off
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def sync
|
66
|
+
@handler.sync
|
67
|
+
end
|
68
|
+
|
69
|
+
def reload
|
70
|
+
@handler.reload
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Handler
|
75
|
+
def reload
|
76
|
+
@logger.info 'Asking controller reset and reload'
|
77
|
+
send_osc '/reload'
|
78
|
+
end
|
79
|
+
|
80
|
+
def version
|
81
|
+
@logger.info "Sending version #{VERSION}"
|
82
|
+
send_osc '/version', VERSION
|
83
|
+
end
|
84
|
+
|
85
|
+
def panic!
|
86
|
+
raise NotImplementedError
|
87
|
+
end
|
88
|
+
|
89
|
+
private def send_osc(message, *args)
|
90
|
+
counter = 0
|
91
|
+
begin
|
92
|
+
@client.send OSC::Message.new(message, *args)
|
93
|
+
rescue Errno::ECONNREFUSED
|
94
|
+
counter += 1
|
95
|
+
@logger.warn "Errno::ECONNREFUSED when sending message #{message} #{args}. Retrying... (#{counter})"
|
96
|
+
retry if counter < 3
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class MusaLCE_Context < Musa::Sequencer::Sequencer::DSLContext
|
102
|
+
include Musa::REPL::CustomizableDSLContext
|
103
|
+
|
104
|
+
def binder
|
105
|
+
@__binder ||= binding
|
106
|
+
end
|
107
|
+
|
108
|
+
def import(*modules)
|
109
|
+
modules.each do |m|
|
110
|
+
self.class.include(m)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/live/handler.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative '../daw'
|
2
|
+
|
3
|
+
module MusaLCEServer
|
4
|
+
module Live
|
5
|
+
class Handler < ::MusaLCEServer::Handler
|
6
|
+
def initialize(osc_server, osc_client, tracks, logger:)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@server = osc_server
|
10
|
+
@client = osc_client
|
11
|
+
|
12
|
+
@tracks = tracks
|
13
|
+
|
14
|
+
@logger = logger
|
15
|
+
|
16
|
+
@server.add_method '/hello' do |message|
|
17
|
+
@logger.info "Received /hello #{message.to_a}!"
|
18
|
+
sync
|
19
|
+
end
|
20
|
+
|
21
|
+
@server.add_method '/musalce4live/tracks' do |message|
|
22
|
+
@tracks.grant_registry_collection(message.to_a.each_slice(10).to_a)
|
23
|
+
end
|
24
|
+
|
25
|
+
@server.add_method '/musalce4live/track/name' do |message|
|
26
|
+
message.to_a.each_slice(2).to_a.each do |track_data|
|
27
|
+
@tracks.grant_registry(track_data[0], track_data[1])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
@server.add_method '/musalce4live/track/midi' do |message|
|
32
|
+
message.to_a.each_slice(3).to_a.each do |track_data|
|
33
|
+
@tracks.grant_registry(track_data[0], *([nil] * 1), *track_data[1..])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@server.add_method '/musalce4live/track/audio' do |message|
|
38
|
+
message.to_a.each_slice(3).to_a.each do |track_data|
|
39
|
+
@tracks.grant_registry(track_data[0], *([nil] * 3), *track_data[1..])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@server.add_method '/musalce4live/track/routings' do |message|
|
44
|
+
message.to_a.each_slice(5).to_a.each do |track_data|
|
45
|
+
@tracks.grant_registry(track_data[0], *([nil] * 5), *track_data[1..])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def sync
|
51
|
+
send_osc '/musalce4live/tracks'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/live/live.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative '../daw'
|
2
|
+
|
3
|
+
require_relative 'handler'
|
4
|
+
require_relative 'tracks'
|
5
|
+
|
6
|
+
module MusaLCEServer
|
7
|
+
module Live
|
8
|
+
class Live < Daw
|
9
|
+
def daw_initialize(midi_devices:, clock:, osc_server:, osc_client:, logger:)
|
10
|
+
super
|
11
|
+
tracks = Tracks.new(midi_devices, logger: logger)
|
12
|
+
handler = Handler.new(osc_server, osc_client, tracks, logger: logger)
|
13
|
+
|
14
|
+
logger.info('Loaded Ableton Live driver')
|
15
|
+
|
16
|
+
return tracks, handler
|
17
|
+
end
|
18
|
+
|
19
|
+
def track(name, all: false)
|
20
|
+
if all
|
21
|
+
@tracks.find_by_name(name)
|
22
|
+
else
|
23
|
+
@tracks.find_by_name(name).first
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def midi_sync(midi_device_name, manufacturer: nil, model: nil, name: nil)
|
28
|
+
name ||= midi_device_name
|
29
|
+
|
30
|
+
@clock.input = MIDICommunications::Input.all.find do |_|
|
31
|
+
(_.manufacturer == manufacturer || manufacturer.nil?) &&
|
32
|
+
(_.model == model || model.nil?) &&
|
33
|
+
(_.name == name || name.nil?)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Daw.register :live, Live
|
39
|
+
end
|
40
|
+
end
|
data/lib/live/tracks.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'musa-dsl/core-ext/dynamic-proxy'
|
2
|
+
|
3
|
+
module MusaLCEServer
|
4
|
+
module Live
|
5
|
+
class Track
|
6
|
+
def initialize(id, midi_devices, logger:)
|
7
|
+
@id = id
|
8
|
+
@midi_devices = midi_devices
|
9
|
+
@logger = logger
|
10
|
+
|
11
|
+
@output = Musa::Extension::DynamicProxy::DynamicProxy.new
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :id, :name
|
15
|
+
|
16
|
+
def out
|
17
|
+
@output
|
18
|
+
end
|
19
|
+
|
20
|
+
def _update_name(value)
|
21
|
+
@name = value
|
22
|
+
@logger.info "track #{@id} assigned name #{@name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def _update_has_midi_input(value);
|
26
|
+
@has_midi_input = value == 1;
|
27
|
+
end
|
28
|
+
def _update_has_midi_output(value);
|
29
|
+
@has_midi_output = value == 1;
|
30
|
+
end
|
31
|
+
def _update_has_audio_input(value);
|
32
|
+
@has_audio_input = value == 1;
|
33
|
+
end
|
34
|
+
def _update_has_audio_output(value);
|
35
|
+
@has_audio_output = value == 1;
|
36
|
+
end
|
37
|
+
|
38
|
+
def _update_current_input_routing(value)
|
39
|
+
@current_input_routing = value
|
40
|
+
_update_current_input_sub_routing(@current_input_sub_routing)
|
41
|
+
end
|
42
|
+
|
43
|
+
def _update_current_input_sub_routing(value)
|
44
|
+
@current_input_sub_routing = value
|
45
|
+
|
46
|
+
effective_midi_voice = nil
|
47
|
+
|
48
|
+
if @has_midi_input
|
49
|
+
device = @midi_devices.find(@current_input_routing)
|
50
|
+
|
51
|
+
if device
|
52
|
+
channel = /Ch\. (?<channel>\d+)/.match(@current_input_sub_routing)&.[](:channel)
|
53
|
+
effective_midi_voice = device.channels[channel.to_i - 1] if channel
|
54
|
+
|
55
|
+
@logger.info "track #{@id} assigned new input: device '#{device.name}' #{effective_midi_voice}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
@output.receiver = effective_midi_voice
|
60
|
+
end
|
61
|
+
|
62
|
+
def _update_current_output_routing(value);
|
63
|
+
@current_output_routing = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def _update_current_output_sub_routing(value);
|
67
|
+
@current_output_sub_routing = value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Tracks
|
72
|
+
include Enumerable
|
73
|
+
|
74
|
+
def initialize(midi_devices, logger:)
|
75
|
+
@midi_devices = midi_devices
|
76
|
+
@logger = logger
|
77
|
+
@tracks = {}
|
78
|
+
end
|
79
|
+
|
80
|
+
def grant_registry_collection(tracks_data)
|
81
|
+
tracks_to_delete = Set[*@tracks.keys]
|
82
|
+
|
83
|
+
tracks_data.each do |track_data|
|
84
|
+
grant_registry(*track_data)
|
85
|
+
tracks_to_delete.delete track_data[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
tracks_to_delete.each do |id|
|
89
|
+
@tracks.delete(id)
|
90
|
+
@logger.info "deleted track #{id}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def grant_registry(id, name = nil,
|
95
|
+
has_midi_input = nil, has_midi_output = nil,
|
96
|
+
has_audio_input = nil, has_audio_output = nil,
|
97
|
+
current_input_routing = nil, current_input_sub_routing = nil,
|
98
|
+
current_output_routing = nil, current_output_sub_routing = nil)
|
99
|
+
|
100
|
+
track = @tracks[id]
|
101
|
+
|
102
|
+
unless track
|
103
|
+
track = Track.new(id, @midi_devices, logger: @logger)
|
104
|
+
@tracks[id] = track
|
105
|
+
end
|
106
|
+
|
107
|
+
track._update_name(name) if name
|
108
|
+
track._update_has_midi_input(has_midi_input) if has_midi_input
|
109
|
+
track._update_has_midi_output(has_midi_output) if has_midi_output
|
110
|
+
track._update_has_audio_input(has_audio_input) if has_audio_input
|
111
|
+
track._update_has_audio_output(has_audio_output) if has_audio_output
|
112
|
+
track._update_current_input_routing(parse_device_name(current_input_routing)) if current_input_routing
|
113
|
+
track._update_current_input_sub_routing(current_input_sub_routing) if current_input_sub_routing
|
114
|
+
track._update_current_output_routing(parse_device_name(current_output_routing)) if current_output_routing
|
115
|
+
track._update_current_output_sub_routing(current_output_sub_routing) if current_output_sub_routing
|
116
|
+
end
|
117
|
+
|
118
|
+
def each(&block)
|
119
|
+
if block_given?
|
120
|
+
@tracks.values.each(&block)
|
121
|
+
else
|
122
|
+
@tracks.values.each
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def [](id)
|
127
|
+
@tracks[id]
|
128
|
+
end
|
129
|
+
|
130
|
+
# TODO adaptar a contrato y semántica de Bitwig (en bitwig sólo hay un track con un nombre determinado, el id no existe)
|
131
|
+
|
132
|
+
def find_by_name(name)
|
133
|
+
@tracks.values.select { |_| _.name == name }
|
134
|
+
end
|
135
|
+
|
136
|
+
private def parse_device_name(name)
|
137
|
+
match = name.match(/Driver IAC \((?<name>.+)\)/)
|
138
|
+
match ? match[:name] : name
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|