musalce-server 0.4.10

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'musalce-server'
4
+
5
+ MusaLCEServer.run(ARGV[0])
@@ -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
@@ -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
@@ -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