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.
@@ -4,10 +4,43 @@ require 'osc-ruby'
4
4
  require 'osc-ruby/em_server'
5
5
 
6
6
  require_relative 'version'
7
+ require_relative 'surface'
8
+ require_relative 'surface-bridge'
7
9
  require_relative 'live/live'
8
10
  require_relative 'bitwig/bitwig'
9
11
 
12
+ # Musa Live Coding Environment Server.
13
+ #
14
+ # This module provides the main entry point for the MusaLCE server,
15
+ # which enables live coding with Ableton Live 11+ and Bitwig Studio 5+.
16
+ #
17
+ # The server provides:
18
+ # - OSC communication with DAW controller extensions
19
+ # - MIDI device management and routing
20
+ # - A REPL (Read-Eval-Print-Loop) for interactive live coding
21
+ # - Integration with Musa-DSL sequencer for music composition
22
+ #
23
+ # @example Starting the server for Bitwig Studio
24
+ # MusaLCEServer.run('bitwig')
25
+ #
26
+ # @example Starting the server for Ableton Live
27
+ # MusaLCEServer.run('live')
28
+ #
29
+ # @see Daw Base class for DAW controllers
30
+ # @see Bitwig::Bitwig Bitwig Studio driver
31
+ # @see Live::Live Ableton Live driver
10
32
  module MusaLCEServer
33
+ # Starts the MusaLCE server for the specified DAW.
34
+ #
35
+ # This method initializes the DAW controller, sets up the REPL environment,
36
+ # and starts the main server loop. The server runs until `shutdown` is called from the REPL.
37
+ #
38
+ # @param daw_name [String] the DAW to connect to ('bitwig' or 'live')
39
+ # @return [void]
40
+ # @raise [ArgumentError] if daw_name is nil or not a supported DAW
41
+ #
42
+ # @example
43
+ # MusaLCEServer.run('bitwig')
11
44
  def self.run(daw_name)
12
45
  raise ArgumentError, 'A daw must be specified. Options: \'bitwig\' or \'live\'' unless daw_name
13
46
  raise ArgumentError, "Incompatible DAW '#{daw_name}'. Options: 'bitwig' or 'live'" unless %w[bitwig live].include?(daw_name)
@@ -0,0 +1,171 @@
1
+ require 'osc-ruby'
2
+
3
+ module MusaLCEServer
4
+ # OSC bridge between the server-side {Surface} and the physical
5
+ # control surface (Stream Deck, …) reached through the chain
6
+ # MusaLCEServer ↔ MusaLCEforXXX ↔ Pulso Bridge ↔ plugin.
7
+ #
8
+ # Two responsibilities:
9
+ #
10
+ # 1. **Outbound emission** — translates {Surface} state changes
11
+ # and sync requests into +/musalce/surface/*+ OSC messages
12
+ # sent on the existing UDP client (port 10001, shared with
13
+ # {Daw}).
14
+ #
15
+ # 2. **Inbound dispatch** — receives +/musalce/surface/*+ messages
16
+ # on the OSC server (EM reactor thread), enqueues them, and
17
+ # drains the queue on the sequencer tick thread via
18
+ # {#drain}. This is critical: inventory mutations and trigger
19
+ # dispatch may invoke arbitrary user DSL code (+play+, +at+,
20
+ # +launch+, …) which must run on the sequencer thread.
21
+ #
22
+ # Wired up by {Daw#initialize}; expected to be the sole emitter
23
+ # of +/musalce/surface/*+ on the server side.
24
+ class SurfaceBridge
25
+ # @return [Surface] the surface this bridge dispatches inbound
26
+ # messages to; set during {Daw} initialization after both
27
+ # instances exist.
28
+ attr_accessor :surface
29
+
30
+ # @param osc_client [OSC::Client] outbound OSC client (shared
31
+ # with the active {Handler})
32
+ # @param sequencer [Musa::Sequencer::Sequencer] used to launch
33
+ # user-defined event handlers in response to surface triggers
34
+ # @param logger [Logger] the logger
35
+ def initialize(osc_client, sequencer, logger:)
36
+ @client = osc_client
37
+ @sequencer = sequencer
38
+ @logger = logger
39
+ @inbox = Queue.new
40
+ end
41
+
42
+ # Sends +/musalce/surface/sync_request+ outbound. Used on
43
+ # server startup to ask Pulso Bridge (via the DAW extension) to
44
+ # dump its current inventory. The reply arrives as a sequence
45
+ # of +inventory/begin+, +inventory/add+ ..., +inventory/end+.
46
+ # @return [void]
47
+ def request_sync
48
+ send_osc '/musalce/surface/sync_request'
49
+ end
50
+
51
+ # Sends +/musalce/surface/state/<prop>+ for a property change.
52
+ #
53
+ # One address per property so the Java relay and the surface
54
+ # plugin can dispatch on a fixed argument layout per address
55
+ # (event + N typed args). All values are serialized to strings
56
+ # on the wire — receivers parse them based on the address.
57
+ # Keeps the Java forwarder generic without inspecting typetags.
58
+ #
59
+ # @param event [Symbol] the event the control is bound to
60
+ # @param prop [Symbol] the property name (+:message+,
61
+ # +:enabled+, +:value+, +:range+, …)
62
+ # @param value [Array<Object>] one or more values for the
63
+ # property (e.g. one for +:message+, two for +:range+)
64
+ # @return [void]
65
+ def send_state(event:, prop:, value:)
66
+ args = value.map { |v| serialize_arg(v) }
67
+ send_osc "/musalce/surface/state/#{prop}", event.to_s, *args
68
+ end
69
+
70
+ # Registers all inbound +/musalce/surface/*+ handlers on the
71
+ # given OSC server. Each handler enqueues the message; actual
72
+ # processing happens on {#drain}.
73
+ #
74
+ # @param osc_server [OSC::EMServer]
75
+ # @return [void]
76
+ def register_inbound(osc_server)
77
+ osc_server.add_method('/musalce/surface/inventory/begin') do |_msg|
78
+ @inbox << [:inventory_begin]
79
+ end
80
+
81
+ osc_server.add_method('/musalce/surface/inventory/add') do |msg|
82
+ args = msg.to_a
83
+ @inbox << [:inventory_add, args[0], args[1]]
84
+ end
85
+
86
+ osc_server.add_method('/musalce/surface/inventory/remove') do |msg|
87
+ @inbox << [:inventory_remove, msg.to_a[0]]
88
+ end
89
+
90
+ osc_server.add_method('/musalce/surface/inventory/end') do |_msg|
91
+ @inbox << [:inventory_end]
92
+ end
93
+
94
+ osc_server.add_method('/musalce/surface/state_request') do |_msg|
95
+ @inbox << [:state_request]
96
+ end
97
+
98
+ osc_server.add_method('/musalce/surface/trigger') do |msg|
99
+ args = msg.to_a
100
+ @inbox << [:trigger, args[0], (args[1] || '').to_s]
101
+ end
102
+ end
103
+
104
+ # Drains the inbound queue. Called from the sequencer tick
105
+ # thread (via +before_tick+) so every dispatched action runs in
106
+ # a context where DSL methods like +launch+, +play+, +at+ are
107
+ # safe to invoke.
108
+ # @return [void]
109
+ # @api private
110
+ def drain
111
+ loop do
112
+ msg = @inbox.pop(true)
113
+ dispatch(msg)
114
+ end
115
+ rescue ThreadError
116
+ # Queue empty — done draining.
117
+ end
118
+
119
+ private def dispatch(msg)
120
+ kind = msg[0]
121
+ case kind
122
+ when :inventory_begin
123
+ @surface.begin_inventory
124
+ when :inventory_add
125
+ event, type = msg[1], msg[2]
126
+ if event.nil? || type.nil?
127
+ @logger.warn "/musalce/surface/inventory/add missing event or type (#{msg.inspect})"
128
+ else
129
+ @surface.add_control(event, type)
130
+ end
131
+ when :inventory_remove
132
+ event = msg[1]
133
+ @surface.remove_control(event) unless event.nil?
134
+ when :inventory_end
135
+ @surface.end_inventory
136
+ when :state_request
137
+ @surface.emit_full_state
138
+ when :trigger
139
+ event, payload = msg[1], msg[2]
140
+ if event.nil? || event.to_s.empty?
141
+ @logger.warn '/musalce/surface/trigger received without event'
142
+ elsif !@surface.known?(event)
143
+ @logger.warn "/musalce/surface/trigger for unknown event #{event.inspect}; ignoring"
144
+ else
145
+ @sequencer.launch(event.to_sym, payload)
146
+ end
147
+ end
148
+ rescue StandardError => e
149
+ @logger.error "Error dispatching surface message #{msg.inspect}: #{e.class}: #{e.message}"
150
+ end
151
+
152
+ # All values cross the wire as strings: this lets the Java
153
+ # relay forward generically without inspecting OSC typetags,
154
+ # and lets the plugin parse per-property. Receivers know how
155
+ # to interpret each value from the OSC address.
156
+ private def serialize_arg(v)
157
+ v.nil? ? '' : v.to_s
158
+ end
159
+
160
+ private def send_osc(address, *args)
161
+ counter = 0
162
+ begin
163
+ @client.send OSC::Message.new(address, *args)
164
+ rescue Errno::ECONNREFUSED
165
+ counter += 1
166
+ @logger.warn "Errno::ECONNREFUSED sending #{address} #{args}. Retrying... (#{counter})"
167
+ retry if counter < 3
168
+ end
169
+ end
170
+ end
171
+ end
data/lib/surface.rb ADDED
@@ -0,0 +1,369 @@
1
+ require 'set'
2
+
3
+ module MusaLCEServer
4
+ # Authoritative model of the physical control surface (Stream Deck
5
+ # buttons, encoders, …) as seen from the server.
6
+ #
7
+ # The surface is **the abstraction shared with hardware** but
8
+ # surface-agnostic: it knows only about named controls with a type
9
+ # and dynamic state. Each control is keyed by its **event name** — a
10
+ # Symbol (e.g. +:launch_chorus+) that doubles as the identifier used
11
+ # with the sequencer's +on+/+launch+ mechanism when the control
12
+ # fires. In MusaLCE the unit of interaction is always the *event*;
13
+ # the surface is the physical face of one or more events.
14
+ #
15
+ # Ownership of the two data axes:
16
+ #
17
+ # - **Inventory** (which controls exist and their type) flows
18
+ # inbound from Pulso Bridge through the DAW extension; the server
19
+ # trusts what it receives (Pulso validates type consistency
20
+ # across physical instances).
21
+ # - **State** (message, enabled, value, …) is owned by the server:
22
+ # the score writes to +surface[:event]+ and changes propagate
23
+ # outbound on +/musalce/surface/state/<prop>+.
24
+ #
25
+ # Inventory may arrive as a full dump (between +inventory/begin+
26
+ # and +inventory/end+, in which case events absent from the dump
27
+ # are purged at end) or as runtime deltas (+inventory/add+ /
28
+ # +inventory/remove+). Re-adding an event with the same type
29
+ # preserves its state; a type change replaces the control and
30
+ # resets state.
31
+ #
32
+ # All mutating methods are expected to run on the sequencer tick
33
+ # thread (inbound OSC messages are routed there by
34
+ # {SurfaceBridge}). Score code writing +surface[:event].xxx =+ ...
35
+ # also runs on that thread (inside +at+/+every+/+on+ blocks),
36
+ # which keeps access serial without explicit locking.
37
+ class Surface
38
+ # @param bridge [SurfaceBridge] the bridge used to emit state outbound
39
+ # @param logger [Logger] the logger
40
+ def initialize(bridge:, logger:)
41
+ @bridge = bridge
42
+ @logger = logger
43
+ @controls = {}
44
+ @pending_events = nil
45
+ end
46
+
47
+ # Returns the control for the given event, or +nil+ if unknown.
48
+ #
49
+ # A control becomes known once its inventory entry has been
50
+ # received from the surface. Score code that runs before that
51
+ # should use safe navigation (+surface[:foo]&.enabled = true+)
52
+ # or guard with {#known?}.
53
+ #
54
+ # @param event [Symbol, String]
55
+ # @return [Control, nil]
56
+ def [](event)
57
+ @controls[event.to_sym]
58
+ end
59
+
60
+ # @return [Array<Symbol>] all known control events
61
+ def events
62
+ @controls.keys
63
+ end
64
+
65
+ # @param event [Symbol, String]
66
+ # @return [Boolean] whether a control with this event is in the inventory
67
+ def known?(event)
68
+ @controls.key?(event.to_sym)
69
+ end
70
+
71
+ # Begins a full inventory dump. Events that are not re-added before
72
+ # {#end_inventory} are purged.
73
+ # @return [void]
74
+ # @api private
75
+ def begin_inventory
76
+ @pending_events = Set.new
77
+ end
78
+
79
+ # Registers (or refreshes) a control in the inventory.
80
+ #
81
+ # If a control with the same event and type already exists, its
82
+ # state is preserved. If the type differs, the existing control
83
+ # is replaced with a fresh instance (state reset).
84
+ #
85
+ # @param event [Symbol, String]
86
+ # @param type [Symbol, String] one of +:toggle+, +:trigger+, +:encoder+
87
+ # @return [Control] the (possibly new) control
88
+ # @api private
89
+ def add_control(event, type)
90
+ event = event.to_sym
91
+ type = type.to_sym
92
+ existing = @controls[event]
93
+
94
+ if existing && existing.class.type_name == type
95
+ ctrl = existing
96
+ else
97
+ ctrl = Control.create(type, event: event, surface: self)
98
+ @controls[event] = ctrl
99
+ @logger.info "Surface: added control #{event} (#{type})"
100
+ end
101
+
102
+ @pending_events << event if @pending_events
103
+ ctrl
104
+ end
105
+
106
+ # Removes a control from the inventory and drops its state.
107
+ # @param event [Symbol, String]
108
+ # @return [Control, nil] the removed control, or nil if unknown
109
+ # @api private
110
+ def remove_control(event)
111
+ event = event.to_sym
112
+ removed = @controls.delete(event)
113
+ @logger.info "Surface: removed control #{event}" if removed
114
+ removed
115
+ end
116
+
117
+ # Ends a full inventory dump. Any event present before the dump
118
+ # but not re-added between {#begin_inventory} and this call is
119
+ # purged. Re-emits all state so the surface re-syncs after the
120
+ # round-trip.
121
+ # @return [void]
122
+ # @api private
123
+ def end_inventory
124
+ if @pending_events
125
+ stale = @controls.keys - @pending_events.to_a
126
+ stale.each do |event|
127
+ @controls.delete(event)
128
+ @logger.info "Surface: purged stale control #{event}"
129
+ end
130
+ @pending_events = nil
131
+ end
132
+ emit_full_state
133
+ end
134
+
135
+ # Re-emits state for every known control. Used after an
136
+ # inventory dump or in response to a +state_request+ from the
137
+ # surface side.
138
+ # @return [void]
139
+ # @api private
140
+ def emit_full_state
141
+ @controls.each_value(&:emit_all_state)
142
+ end
143
+
144
+ # Called by a Control when one of its properties changes; relays
145
+ # to the bridge.
146
+ # @param event [Symbol]
147
+ # @param prop [Symbol]
148
+ # @param value [Array<Object>] OSC-serializable values
149
+ # @return [void]
150
+ # @api private
151
+ def emit_state(event, prop, *value)
152
+ @bridge.send_state(event: event, prop: prop, value: value)
153
+ end
154
+ end
155
+
156
+ # Abstract base for all controls on a {Surface}.
157
+ #
158
+ # Subclasses declare a {.type_name} matching the inventory string
159
+ # received from the surface, expose typed state accessors, and
160
+ # implement {#emit_all_state} to push their current state outbound.
161
+ #
162
+ # Setting a property emits exactly one OSC
163
+ # +/musalce/surface/state/<prop>+ message; the setter is therefore
164
+ # the canonical mutation point. Direct manipulation of instance
165
+ # variables bypasses emission.
166
+ class Control
167
+ # @return [Symbol] the event name this control is bound to
168
+ attr_reader :event
169
+
170
+ # @return [String, nil] the displayable message, +nil+ if unset
171
+ attr_reader :message
172
+
173
+ def initialize(event:, surface:)
174
+ @event = event
175
+ @surface = surface
176
+ @message = nil
177
+ end
178
+
179
+ # Sets the displayable message. Two-line text is allowed; the
180
+ # surface side is responsible for truncation/wrapping.
181
+ # @param value [String, nil]
182
+ # @return [void]
183
+ def message=(value)
184
+ @message = value
185
+ emit(:message, value.to_s)
186
+ end
187
+
188
+ # Sets multiple attributes in a single call. Each key must name a
189
+ # writable attribute of the receiver's type; unknown keys raise
190
+ # +ArgumentError+ so a typo can't silently no-op.
191
+ #
192
+ # Order of assignment follows the kwargs hash insertion order
193
+ # (Ruby >= 1.9 guarantees insertion-ordered iteration). Each
194
+ # assignment goes through the regular setter, so each property
195
+ # emits its own +/musalce/surface/state/<prop>+ message. This is
196
+ # intentional: the wire protocol is per-property, and the plugin
197
+ # merges deltas into the rendered state, so two adjacent
198
+ # +/state/<prop>+ messages render exactly the same as a single
199
+ # batched one would.
200
+ #
201
+ # @example Toggle
202
+ # surface[:launch_chorus].set(enabled: true, message: "Chorus on")
203
+ # @example Encoder
204
+ # surface[:cutoff].set(range: 0..127, value: 64, message: "Cutoff")
205
+ #
206
+ # @param attrs [Hash{Symbol => Object}]
207
+ # @raise [ArgumentError] if a key doesn't correspond to a writer
208
+ # @return [self] for chaining
209
+ def set(**attrs)
210
+ attrs.each do |key, value|
211
+ writer = :"#{key}="
212
+ unless respond_to?(writer)
213
+ raise ArgumentError,
214
+ "#{self.class.type_name} control has no '#{key}' attribute"
215
+ end
216
+ public_send(writer, value)
217
+ end
218
+ self
219
+ end
220
+
221
+ # Re-emits every state property of this control. Called by the
222
+ # surface during +state_request+ or after inventory dumps.
223
+ # @return [void]
224
+ # @api private
225
+ def emit_all_state
226
+ emit(:message, @message.to_s) unless @message.nil?
227
+ end
228
+
229
+ # @return [Symbol] the inventory type identifier
230
+ def self.type_name
231
+ raise NotImplementedError, "#{self} must implement .type_name"
232
+ end
233
+
234
+ # Instantiates the right subclass for the given type.
235
+ # @param type [Symbol]
236
+ # @return [Control]
237
+ # @raise [ArgumentError] if the type is unknown
238
+ # @api private
239
+ def self.create(type, **kwargs)
240
+ case type.to_sym
241
+ when :toggle then Toggle.new(**kwargs)
242
+ when :trigger then Trigger.new(**kwargs)
243
+ when :encoder then Encoder.new(**kwargs)
244
+ else raise ArgumentError, "Unknown control type: #{type.inspect}"
245
+ end
246
+ end
247
+
248
+ protected def emit(prop, *value)
249
+ @surface.emit_state(@event, prop, *value)
250
+ end
251
+ end
252
+
253
+ # A stateful on/off control with a three-valued enabled property:
254
+ # +true+ (on), +false+ (off available), +:inactive+ (control is
255
+ # known but currently not actionable, typically rendered dimmed).
256
+ class Toggle < Control
257
+ def self.type_name = :toggle
258
+
259
+ # @return [Boolean, Symbol] +true+, +false+, or +:inactive+
260
+ attr_reader :enabled
261
+
262
+ def initialize(**kwargs)
263
+ super
264
+ @enabled = :inactive
265
+ end
266
+
267
+ # Sets the enabled state. Accepts +true+, +false+, +:inactive+
268
+ # and their string equivalents.
269
+ # @param value [Boolean, Symbol, String]
270
+ # @raise [ArgumentError] on any other value
271
+ def enabled=(value)
272
+ @enabled = normalize_enabled(value)
273
+ emit(:enabled, @enabled.to_s)
274
+ end
275
+
276
+ # @return [Boolean] true iff +enabled+ is exactly +true+
277
+ def enabled?
278
+ @enabled == true
279
+ end
280
+
281
+ # @return [Boolean] true iff +enabled+ is +:inactive+
282
+ def inactive?
283
+ @enabled == :inactive
284
+ end
285
+
286
+ # Convenience: set to +true+.
287
+ def on! = (self.enabled = true)
288
+ # Convenience: set to +false+.
289
+ def off! = (self.enabled = false)
290
+ # Convenience: set to +:inactive+.
291
+ def inactive! = (self.enabled = :inactive)
292
+
293
+ # Toggles between +true+ and +false+. From +:inactive+ goes to
294
+ # +true+ (entering active service).
295
+ def toggle!
296
+ case @enabled
297
+ when true then off!
298
+ when false then on!
299
+ else on!
300
+ end
301
+ end
302
+
303
+ def emit_all_state
304
+ super
305
+ emit(:enabled, @enabled.to_s)
306
+ end
307
+
308
+ private def normalize_enabled(v)
309
+ case v
310
+ when true, :true, 'true' then true
311
+ when false, :false, 'false' then false
312
+ when :inactive, 'inactive', nil then :inactive
313
+ else
314
+ raise ArgumentError,
315
+ "enabled must be true, false or :inactive (got #{v.inspect})"
316
+ end
317
+ end
318
+ end
319
+
320
+ # A momentary, stateless control. Pressing it fires the
321
+ # corresponding sequencer event; the control itself carries no
322
+ # persistent on/off state beyond an optional {#message}.
323
+ class Trigger < Control
324
+ def self.type_name = :trigger
325
+ end
326
+
327
+ # An absolute-value rotary or fader control with an integer
328
+ # +value+ inside an inclusive +range+. Range defaults to
329
+ # +0..127+ (standard MIDI 7-bit).
330
+ class Encoder < Control
331
+ def self.type_name = :encoder
332
+
333
+ # @return [Integer]
334
+ attr_reader :value
335
+ # @return [Range]
336
+ attr_reader :range
337
+
338
+ def initialize(**kwargs)
339
+ super
340
+ @range = 0..127
341
+ @value = 0
342
+ end
343
+
344
+ # @param v [Integer, Numeric] clamped to {#range}
345
+ def value=(v)
346
+ @value = clamp_to_range(v.to_i)
347
+ emit(:value, @value)
348
+ end
349
+
350
+ # @param r [Range] inclusive integer range; +value+ is re-clamped
351
+ def range=(r)
352
+ raise ArgumentError, "range must be a Range (got #{r.inspect})" unless r.is_a?(Range)
353
+ @range = r
354
+ @value = clamp_to_range(@value)
355
+ emit(:range, r.min, r.max)
356
+ emit(:value, @value)
357
+ end
358
+
359
+ def emit_all_state
360
+ super
361
+ emit(:range, @range.min, @range.max)
362
+ emit(:value, @value)
363
+ end
364
+
365
+ private def clamp_to_range(v)
366
+ [[v, @range.min].max, @range.max].min
367
+ end
368
+ end
369
+ end
data/lib/version.rb CHANGED
@@ -1,3 +1,15 @@
1
+ # Musa Live Coding Environment Server.
2
+ #
3
+ # A Ruby server for live coding with Ableton Live and Bitwig Studio DAWs.
4
+ # Provides OSC communication, MIDI device management, and a REPL for
5
+ # interactive music composition using Musa-DSL.
6
+ #
7
+ # @see MusaLCEServer.run Entry point for starting the server
8
+ # @see MusaLCEServer::Daw Base class for DAW controllers
9
+ #
10
+ # @author Javier Sánchez Yeste
11
+ # @since 0.1.0
1
12
  module MusaLCEServer
2
- VERSION = '0.5.1'.freeze
13
+ # Current version of the musalce-server gem.
14
+ VERSION = '0.8.0'.freeze
3
15
  end
@@ -3,7 +3,7 @@ require_relative 'lib/version'
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'musalce-server'
5
5
  s.version = MusaLCEServer::VERSION
6
- s.date = '2025-08-23'
6
+ s.date = '2026-05-16'
7
7
  s.summary = 'A Musa DSL live coding environment for Ableton Live 11 and Bitwig Studio 5'
8
8
  s.description = 'This package implements the Server part of the Musa DSL Live Coding Environment for Ableton Live and Bitwig Studio'
9
9
  s.authors = ['Javier Sánchez Yeste']
@@ -15,20 +15,27 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.required_ruby_version = '>= 2.7'
17
17
 
18
- # TODO
19
- #s.metadata = {
20
- # "source_code_uri" => "https://",
21
- # "homepage_uri" => "",
22
- # "documentation_uri" => "",
23
- # "changelog_uri" => ""
24
- #}
18
+ s.metadata = {
19
+ 'homepage_uri' => s.homepage,
20
+ 'source_code_uri' => s.homepage,
21
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/musalce-server'
22
+ }
25
23
 
26
- s.add_runtime_dependency 'musa-dsl', '~> 0', '>= 0.26.0'
24
+ s.add_runtime_dependency 'musa-dsl', '~> 0.40'
27
25
 
28
- s.add_runtime_dependency 'midi-communications', '~> 0.6'
29
- s.add_runtime_dependency 'midi-events', '~> 0.6'
30
- s.add_runtime_dependency 'midi-parser', '~> 0.4'
26
+ s.add_runtime_dependency 'midi-communications', '~> 0.7'
27
+ s.add_runtime_dependency 'midi-events', '~> 0.7'
28
+ s.add_runtime_dependency 'midi-parser', '~> 0.5'
31
29
 
32
- #s.add_runtime_dependency 'eventmachine', '~> 1.2', '>= 1.2.7'
33
30
  s.add_runtime_dependency 'osc-ruby', '~> 1.1', '>= 1.1.5'
31
+ # EventMachine is an *optional* dep of osc-ruby (only needed when
32
+ # using OSC::EMServer, which we do in daw.rb to receive OSC from
33
+ # the DAW extension). osc-ruby doesn't declare it, so we must.
34
+ s.add_runtime_dependency 'eventmachine', '~> 1.2'
35
+
36
+ s.add_development_dependency 'rspec', '~> 3'
37
+
38
+ s.add_development_dependency 'yard', '~> 0.9'
39
+ s.add_development_dependency 'redcarpet', '~> 3.6'
40
+ s.add_development_dependency 'webrick', '~> 1.8'
34
41
  end