musalce-server 0.4.10

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.
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