musa-dsl 0.30.2 → 0.40.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/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- metadata +87 -8
|
@@ -3,12 +3,110 @@ require_relative '../core-ext/inspect-nice'
|
|
|
3
3
|
require_relative '../sequencer'
|
|
4
4
|
|
|
5
5
|
module Musa
|
|
6
|
+
# Transport system connecting clocks to sequencers.
|
|
7
|
+
#
|
|
8
|
+
# The Transport module provides the infrastructure for managing musical playback,
|
|
9
|
+
# connecting timing sources (clocks) to the sequencer that executes musical events.
|
|
10
|
+
#
|
|
11
|
+
# @see Transport Main transport class
|
|
12
|
+
# @see Clock Clock implementations
|
|
13
|
+
# @see Sequencer Event scheduling
|
|
6
14
|
module Transport
|
|
15
|
+
# Main transport class connecting clocks to sequencers with lifecycle management.
|
|
16
|
+
#
|
|
17
|
+
# Transport acts as the bridge between a clock (timing source) and a sequencer
|
|
18
|
+
# (event scheduler). It manages the playback lifecycle, including initialization,
|
|
19
|
+
# start/stop, and position changes, with support for callbacks at each stage.
|
|
20
|
+
#
|
|
21
|
+
# ## Architecture
|
|
22
|
+
#
|
|
23
|
+
# Clock --ticks--> Transport --tick()--> Sequencer --events--> Music
|
|
24
|
+
#
|
|
25
|
+
# ## Lifecycle Phases
|
|
26
|
+
#
|
|
27
|
+
# 1. **before_begin**: Run once before first start (initialization)
|
|
28
|
+
# 2. **on_start**: Run each time transport starts
|
|
29
|
+
# 3. **Running**: Clock generates ticks → sequencer processes events
|
|
30
|
+
# 4. **on_position_change**: Run when position jumps/seeks
|
|
31
|
+
# 5. **after_stop**: Run when transport stops
|
|
32
|
+
#
|
|
33
|
+
# ## Position Management
|
|
34
|
+
#
|
|
35
|
+
# Transport handles three position formats:
|
|
36
|
+
#
|
|
37
|
+
# - **bars**: Musical position in bars (Rational)
|
|
38
|
+
# - **beats**: Position in beats
|
|
39
|
+
# - **midi_beats**: Position in MIDI beats (for MIDI Clock sync)
|
|
40
|
+
#
|
|
41
|
+
# ## Use Cases
|
|
42
|
+
#
|
|
43
|
+
# - Standalone compositions with internal timing
|
|
44
|
+
# - DAW-synchronized playback via MIDI Clock
|
|
45
|
+
# - Testing with dummy/external clocks
|
|
46
|
+
# - Live coding with dynamic tempo changes
|
|
47
|
+
#
|
|
48
|
+
# @example Basic setup with TimerClock
|
|
49
|
+
# clock = Musa::Clock::TimerClock.new(bpm: 120)
|
|
50
|
+
# transport = Musa::Transport::Transport.new(
|
|
51
|
+
# clock,
|
|
52
|
+
# beats_per_bar: 4,
|
|
53
|
+
# ticks_per_beat: 24
|
|
54
|
+
# )
|
|
55
|
+
#
|
|
56
|
+
# # Schedule events
|
|
57
|
+
# transport.sequencer.at 0 { puts "Start!" }
|
|
58
|
+
# transport.sequencer.at 4 { puts "Bar 4" }
|
|
59
|
+
#
|
|
60
|
+
# transport.start
|
|
61
|
+
#
|
|
62
|
+
# @example With lifecycle callbacks
|
|
63
|
+
# transport = Transport.new(clock) do |t|
|
|
64
|
+
# t.before_begin { puts "Initializing..." }
|
|
65
|
+
# t.on_start { puts "Started!" }
|
|
66
|
+
# t.after_stop { puts "Stopped, cleaning up..." }
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# @example MIDI Clock synchronization
|
|
70
|
+
# midi_input = MIDICommunications::Input.all.first
|
|
71
|
+
# clock = Musa::Clock::InputMidiClock.new(midi_input)
|
|
72
|
+
# transport = Transport.new(clock)
|
|
73
|
+
# transport.start # Waits for MIDI Clock Start
|
|
74
|
+
#
|
|
75
|
+
# @see Clock::TimerClock Internal timer-based clock
|
|
76
|
+
# @see Clock::InputMidiClock MIDI Clock synchronized
|
|
77
|
+
# @see Sequencer Event scheduling
|
|
7
78
|
class Transport
|
|
8
79
|
using Musa::Extension::InspectNice
|
|
9
80
|
|
|
81
|
+
# The sequencer instance managing event scheduling.
|
|
82
|
+
#
|
|
83
|
+
# @return [Sequencer::Sequencer] the sequencer
|
|
10
84
|
attr_reader :sequencer
|
|
11
85
|
|
|
86
|
+
# Creates a new transport connecting a clock to a sequencer.
|
|
87
|
+
#
|
|
88
|
+
# @param clock [Clock] timing source (TimerClock, InputMidiClock, etc.)
|
|
89
|
+
# @param beats_per_bar [Integer, nil] time signature numerator (default: 4)
|
|
90
|
+
# @param ticks_per_beat [Integer, nil] timing resolution (default: 24)
|
|
91
|
+
# @param offset [Rational, nil] time offset in bars (default: 0)
|
|
92
|
+
# @param sequencer [Sequencer, nil] existing sequencer (creates new if nil)
|
|
93
|
+
# @param before_begin [Proc, nil] callback run once before first start
|
|
94
|
+
# @param on_start [Proc, nil] callback run each time transport starts
|
|
95
|
+
# @param after_stop [Proc, nil] callback run when transport stops
|
|
96
|
+
# @param on_position_change [Proc, nil] callback for position changes
|
|
97
|
+
# @param logger [Logger, nil] logger instance
|
|
98
|
+
# @param do_log [Boolean, nil] enable logging
|
|
99
|
+
#
|
|
100
|
+
# @example With callback parameters
|
|
101
|
+
# Transport.new(clock, 4, 24,
|
|
102
|
+
# on_start: -> (seq) { puts "Started at #{seq.position}" }
|
|
103
|
+
# )
|
|
104
|
+
#
|
|
105
|
+
# @example With callback methods
|
|
106
|
+
# transport = Transport.new(clock, 4, 24)
|
|
107
|
+
# transport.before_begin { setup_instruments }
|
|
108
|
+
# transport.on_start { start_recording }
|
|
109
|
+
# transport.after_stop { save_recording }
|
|
12
110
|
def initialize(clock,
|
|
13
111
|
beats_per_bar = nil,
|
|
14
112
|
ticks_per_beat = nil,
|
|
@@ -57,24 +155,161 @@ module Musa
|
|
|
57
155
|
@clock.on_change_position do |bars: nil, beats: nil, midi_beats: nil|
|
|
58
156
|
change_position_to bars: bars, beats: beats, midi_beats: midi_beats
|
|
59
157
|
end
|
|
158
|
+
|
|
159
|
+
# TODO: Consider adding block/DSL support for cleaner initialization syntax.
|
|
160
|
+
#
|
|
161
|
+
# Future enhancement could yield a DSL context that provides direct access to
|
|
162
|
+
# transport methods without requiring the transport variable. This would enable:
|
|
163
|
+
#
|
|
164
|
+
# Transport.new(clock, 4, 24) do
|
|
165
|
+
# before_begin { setup_instruments }
|
|
166
|
+
# on_start { start_recording }
|
|
167
|
+
# after_stop { save_recording }
|
|
168
|
+
# end
|
|
169
|
+
#
|
|
170
|
+
# Instead of current approach:
|
|
171
|
+
#
|
|
172
|
+
# transport = Transport.new(clock, 4, 24)
|
|
173
|
+
# transport.before_begin { setup_instruments }
|
|
174
|
+
# transport.on_start { start_recording }
|
|
175
|
+
# transport.after_stop { save_recording }
|
|
176
|
+
#
|
|
177
|
+
# Implementation would require a DSL context object that delegates methods to
|
|
178
|
+
# the transport instance, similar to the sequencer DSL pattern.
|
|
60
179
|
end
|
|
61
180
|
|
|
181
|
+
# Registers a callback to run once before the first start.
|
|
182
|
+
#
|
|
183
|
+
# before_begin callbacks are run only once, before the very first start.
|
|
184
|
+
# They're ideal for one-time setup like loading samples or initializing state.
|
|
185
|
+
#
|
|
186
|
+
# After a stop, before_begin runs again before the next start.
|
|
187
|
+
#
|
|
188
|
+
# @yield [sequencer] Called before first start
|
|
189
|
+
# @yieldparam sequencer [Sequencer] the sequencer instance
|
|
190
|
+
# @return [void]
|
|
191
|
+
#
|
|
192
|
+
# @example
|
|
193
|
+
# transport.before_begin do |seq|
|
|
194
|
+
# puts "Initializing at position #{seq.position}"
|
|
195
|
+
# load_samples
|
|
196
|
+
# end
|
|
62
197
|
def before_begin(&block)
|
|
63
198
|
@before_begin << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
|
|
64
199
|
end
|
|
65
200
|
|
|
201
|
+
# Registers a callback to run each time the transport starts.
|
|
202
|
+
#
|
|
203
|
+
# on_start callbacks run every time {#start} is called, after before_begin.
|
|
204
|
+
#
|
|
205
|
+
# @yield [sequencer] Called on each start
|
|
206
|
+
# @yieldparam sequencer [Sequencer] the sequencer instance
|
|
207
|
+
# @return [void]
|
|
208
|
+
#
|
|
209
|
+
# @example
|
|
210
|
+
# transport.on_start do |seq|
|
|
211
|
+
# puts "Starting playback at #{seq.position}"
|
|
212
|
+
# end
|
|
66
213
|
def on_start(&block)
|
|
67
214
|
@on_start << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
|
|
68
215
|
end
|
|
69
216
|
|
|
217
|
+
# Registers a callback to run when the transport stops.
|
|
218
|
+
#
|
|
219
|
+
# after_stop callbacks run when the clock stops, before the sequencer is reset.
|
|
220
|
+
#
|
|
221
|
+
# @yield [sequencer] Called when stopping
|
|
222
|
+
# @yieldparam sequencer [Sequencer] the sequencer instance
|
|
223
|
+
# @return [void]
|
|
224
|
+
#
|
|
225
|
+
# @example
|
|
226
|
+
# transport.after_stop do |seq|
|
|
227
|
+
# puts "Stopped at position #{seq.position}"
|
|
228
|
+
# cleanup_resources
|
|
229
|
+
# end
|
|
70
230
|
def after_stop(&block)
|
|
71
231
|
@after_stop << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
|
|
72
232
|
end
|
|
73
233
|
|
|
234
|
+
# Registers a callback for position changes.
|
|
235
|
+
#
|
|
236
|
+
# Called when playback position changes non-linearly (seek/jump), typically
|
|
237
|
+
# from MIDI Song Position Pointer or manual position changes.
|
|
238
|
+
#
|
|
239
|
+
# @yield [sequencer] Called on position change
|
|
240
|
+
# @yieldparam sequencer [Sequencer] the sequencer instance
|
|
241
|
+
# @return [void]
|
|
242
|
+
#
|
|
243
|
+
# @example
|
|
244
|
+
# transport.on_change_position do |seq|
|
|
245
|
+
# puts "Position jumped to #{seq.position}"
|
|
246
|
+
# resync_external_devices
|
|
247
|
+
# end
|
|
74
248
|
def on_change_position(&block)
|
|
75
249
|
@on_change_position << Musa::Extension::SmartProcBinder::SmartProcBinder.new(block)
|
|
76
250
|
end
|
|
77
251
|
|
|
252
|
+
# Starts the transport and begins playback.
|
|
253
|
+
#
|
|
254
|
+
# Runs before_begin (if first start or after stop), then starts the clock's
|
|
255
|
+
# run loop. **Behavior depends on the clock type**:
|
|
256
|
+
#
|
|
257
|
+
# ## Clock Activation by Type
|
|
258
|
+
#
|
|
259
|
+
# **DummyClock** (automatic activation):
|
|
260
|
+
# - Starts generating ticks immediately
|
|
261
|
+
# - Blocks until tick count/condition completes
|
|
262
|
+
# - No external activation needed
|
|
263
|
+
#
|
|
264
|
+
# **TimerClock** (external activation required):
|
|
265
|
+
# - Blocks but remains paused until `clock.start` is called
|
|
266
|
+
# - Must call `clock.start()` from another thread to begin ticks
|
|
267
|
+
# - Typical pattern: `Thread.new { transport.start }` then `clock.start`
|
|
268
|
+
#
|
|
269
|
+
# **InputMidiClock** (MIDI activation required):
|
|
270
|
+
# - Blocks waiting for MIDI "Start" (0xFA) message
|
|
271
|
+
# - External DAW/device controls when ticks begin
|
|
272
|
+
# - Automatically starts when MIDI Start received
|
|
273
|
+
#
|
|
274
|
+
# **ExternalTickClock** (manual tick control):
|
|
275
|
+
# - Returns immediately (doesn't block)
|
|
276
|
+
# - Call `clock.tick()` manually to generate each tick
|
|
277
|
+
# - Complete control over timing from external system
|
|
278
|
+
#
|
|
279
|
+
# @return [void]
|
|
280
|
+
#
|
|
281
|
+
# @note Blocking behavior varies by clock type (see above)
|
|
282
|
+
# @note For TimerClock, must call clock.start from separate thread
|
|
283
|
+
#
|
|
284
|
+
# @example With DummyClock (automatic)
|
|
285
|
+
# clock = DummyClock.new(100)
|
|
286
|
+
# transport = Transport.new(clock, 4, 24)
|
|
287
|
+
# transport.start # Runs 100 ticks automatically, then returns
|
|
288
|
+
#
|
|
289
|
+
# @example With TimerClock (external activation)
|
|
290
|
+
# clock = TimerClock.new(bpm: 120)
|
|
291
|
+
# transport = Transport.new(clock, 4, 24)
|
|
292
|
+
#
|
|
293
|
+
# thread = Thread.new { transport.start } # Blocks waiting
|
|
294
|
+
# sleep 0.1
|
|
295
|
+
# clock.start # Activate from external control
|
|
296
|
+
# thread.join
|
|
297
|
+
#
|
|
298
|
+
# @example With InputMidiClock (MIDI activation)
|
|
299
|
+
# input = MIDICommunications::Input.all.first
|
|
300
|
+
# clock = InputMidiClock.new(input)
|
|
301
|
+
# transport = Transport.new(clock, 4, 24)
|
|
302
|
+
# transport.start # Blocks until MIDI Start received from DAW
|
|
303
|
+
#
|
|
304
|
+
# @example With ExternalTickClock (manual control)
|
|
305
|
+
# clock = ExternalTickClock.new
|
|
306
|
+
# transport = Transport.new(clock, 4, 24)
|
|
307
|
+
#
|
|
308
|
+
# thread = Thread.new { transport.start } # Returns immediately
|
|
309
|
+
# sleep 0.1
|
|
310
|
+
# 100.times { clock.tick } # Generate ticks manually
|
|
311
|
+
# transport.stop
|
|
312
|
+
# thread.join
|
|
78
313
|
def start
|
|
79
314
|
do_before_begin unless @before_begin_already_done
|
|
80
315
|
|
|
@@ -84,16 +319,73 @@ module Musa
|
|
|
84
319
|
end
|
|
85
320
|
end
|
|
86
321
|
|
|
322
|
+
# Changes the playback position (seek/jump).
|
|
323
|
+
#
|
|
324
|
+
# Handles position changes from various sources, converting between formats.
|
|
325
|
+
# **IMPORTANT**: Position changes trigger fast-forward, which executes all
|
|
326
|
+
# intermediate events between current and target positions.
|
|
327
|
+
#
|
|
328
|
+
# ## Fast-Forward Behavior
|
|
329
|
+
#
|
|
330
|
+
# When changing position forward:
|
|
331
|
+
# 1. Calls `sequencer.on_fast_forward` callbacks with `true` (entering fast-forward)
|
|
332
|
+
# 2. Ticks through all intermediate positions, **executing all scheduled events**
|
|
333
|
+
# 3. Calls `sequencer.on_fast_forward` callbacks with `false` (exiting fast-forward)
|
|
334
|
+
# 4. Calls `on_change_position` callbacks at the new position
|
|
335
|
+
#
|
|
336
|
+
# When changing position backward (requires stop):
|
|
337
|
+
# 1. Stops the transport (calls `after_stop` callbacks)
|
|
338
|
+
# 2. Resets the sequencer to initial state
|
|
339
|
+
# 3. Sets position to target
|
|
340
|
+
# 4. Restarts (calls `on_start` callbacks)
|
|
341
|
+
#
|
|
342
|
+
# ## Handling Intermediate Events
|
|
343
|
+
#
|
|
344
|
+
# During fast-forward, all scheduled events execute. To prevent unwanted sound
|
|
345
|
+
# output (e.g., MIDI notes), use `on_fast_forward` callbacks:
|
|
346
|
+
#
|
|
347
|
+
# - **MIDIVoices integration**: Set `midi_voices.fast_forward = true` during
|
|
348
|
+
# fast-forward to register note state internally without emitting MIDI messages
|
|
349
|
+
# - **Custom handlers**: Check fast-forward state in event handlers to skip
|
|
350
|
+
# audio/visual output during position jumps
|
|
351
|
+
#
|
|
352
|
+
# @param bars [Numeric, nil] target position in bars
|
|
353
|
+
# @param beats [Numeric, nil] offset in beats to add
|
|
354
|
+
# @param midi_beats [Integer, nil] offset in MIDI beats (for MIDI Clock)
|
|
355
|
+
#
|
|
356
|
+
# @return [void]
|
|
357
|
+
#
|
|
358
|
+
# @raise [ArgumentError] if no valid position specified
|
|
359
|
+
#
|
|
360
|
+
# @note Backward seeks trigger stop/restart cycle
|
|
361
|
+
# @note Calls on_change_position callbacks at new position
|
|
362
|
+
# @note Calls sequencer.on_fast_forward callbacks during forward seek
|
|
363
|
+
#
|
|
364
|
+
# @example Jump to bar 8 (fast-forwards through bars 1-7)
|
|
365
|
+
# transport.change_position_to(bars: 8)
|
|
366
|
+
#
|
|
367
|
+
# @example MIDI Song Position Pointer
|
|
368
|
+
# transport.change_position_to(midi_beats: 96) # Bar 4 in 4/4
|
|
369
|
+
#
|
|
370
|
+
# @example Preventing sound during fast-forward with MIDIVoices
|
|
371
|
+
# transport.sequencer.on_fast_forward do |is_starting|
|
|
372
|
+
# midi_voices.fast_forward = is_starting
|
|
373
|
+
# end
|
|
374
|
+
#
|
|
375
|
+
# # Now position changes won't produce audible MIDI output
|
|
376
|
+
# transport.change_position_to(bars: 10)
|
|
87
377
|
def change_position_to(bars: nil, beats: nil, midi_beats: nil)
|
|
88
378
|
logger.debug('Transport') do
|
|
89
379
|
"asked to change position to #{"#{bars} bars " if bars}#{"#{beats} beats " if beats}" \
|
|
90
380
|
"#{"#{midi_beats} midi beats " if midi_beats}"
|
|
91
381
|
end
|
|
92
382
|
|
|
383
|
+
# Calculate position from provided parameters
|
|
93
384
|
position = bars&.rationalize || 1r
|
|
94
385
|
position += Rational(midi_beats, 4 * @sequencer.beats_per_bar) if midi_beats
|
|
95
386
|
position += Rational(beats, @sequencer.beats_per_bar) if beats
|
|
96
387
|
|
|
388
|
+
# Adjust for sequencer offset and tick duration
|
|
97
389
|
position += @sequencer.offset
|
|
98
390
|
position -= @sequencer.tick_duration
|
|
99
391
|
|
|
@@ -103,6 +395,7 @@ module Musa
|
|
|
103
395
|
|
|
104
396
|
start_again_later = false
|
|
105
397
|
|
|
398
|
+
# Backward seek requires stop/restart to reinitialize state
|
|
106
399
|
if @sequencer.position > position
|
|
107
400
|
do_stop
|
|
108
401
|
start_again_later = true
|
|
@@ -110,6 +403,7 @@ module Musa
|
|
|
110
403
|
|
|
111
404
|
logger.debug('Transport') { "setting sequencer position #{position.inspect}" }
|
|
112
405
|
|
|
406
|
+
# Schedule position change callback at new position
|
|
113
407
|
@sequencer.raw_at position, force_first: true do
|
|
114
408
|
@on_change_position.each { |block| block.call @sequencer }
|
|
115
409
|
end
|
|
@@ -119,28 +413,51 @@ module Musa
|
|
|
119
413
|
do_on_start if start_again_later
|
|
120
414
|
end
|
|
121
415
|
|
|
416
|
+
# Stops the transport.
|
|
417
|
+
#
|
|
418
|
+
# Terminates the clock, which triggers the stop sequence (after_stop callbacks,
|
|
419
|
+
# sequencer reset, etc.)
|
|
420
|
+
#
|
|
421
|
+
# @return [void]
|
|
122
422
|
def stop
|
|
123
423
|
@clock.terminate
|
|
124
424
|
end
|
|
125
425
|
|
|
426
|
+
# Returns the transport's logger.
|
|
427
|
+
#
|
|
428
|
+
# Delegates to sequencer's logger.
|
|
429
|
+
#
|
|
430
|
+
# @return [Logger] the logger instance
|
|
126
431
|
def logger
|
|
127
432
|
@sequencer.logger
|
|
128
433
|
end
|
|
129
434
|
|
|
130
435
|
private
|
|
131
436
|
|
|
437
|
+
# Executes before_begin callbacks.
|
|
438
|
+
#
|
|
439
|
+
# @api private
|
|
132
440
|
def do_before_begin
|
|
133
441
|
logger.debug('Transport') { 'doing before_begin initialization...' } unless @before_begin.empty?
|
|
134
442
|
@before_begin.each { |block| block.call @sequencer }
|
|
135
443
|
logger.debug('Transport') { 'doing before_begin initialization... done' } unless @before_begin.empty?
|
|
136
444
|
end
|
|
137
445
|
|
|
446
|
+
# Executes on_start callbacks.
|
|
447
|
+
#
|
|
448
|
+
# @api private
|
|
138
449
|
def do_on_start
|
|
139
450
|
logger.debug('Transport') { 'starting...' } unless @on_start.empty?
|
|
140
451
|
@on_start.each { |block| block.call @sequencer }
|
|
141
452
|
logger.debug('Transport') { 'starting... done' } unless @on_start.empty?
|
|
142
453
|
end
|
|
143
454
|
|
|
455
|
+
# Executes the stop sequence.
|
|
456
|
+
#
|
|
457
|
+
# Runs after_stop callbacks, resets the sequencer, then runs before_begin
|
|
458
|
+
# callbacks (preparing for next start).
|
|
459
|
+
#
|
|
460
|
+
# @api private
|
|
144
461
|
def do_stop
|
|
145
462
|
logger.debug('Transport') { 'stopping...' } unless @after_stop.empty?
|
|
146
463
|
@after_stop.each { |block| block.call @sequencer }
|
|
@@ -150,6 +467,7 @@ module Musa
|
|
|
150
467
|
@sequencer.reset
|
|
151
468
|
logger.debug('Transport') { 'resetting sequencer... done' }
|
|
152
469
|
|
|
470
|
+
# Prepare for next start
|
|
153
471
|
do_before_begin
|
|
154
472
|
@before_begin_already_done = true
|
|
155
473
|
end
|
data/lib/musa-dsl/version.rb
CHANGED
data/lib/musa-dsl.rb
CHANGED
|
@@ -24,41 +24,148 @@ require_relative 'musa-dsl/music'
|
|
|
24
24
|
|
|
25
25
|
require_relative 'musa-dsl/generative'
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
# Musa-DSL: A Ruby framework and DSL for algorithmic sound and musical thinking and composition.
|
|
28
|
+
#
|
|
29
|
+
# Musa-DSL is a programming language DSL (Domain-Specific Language) based on Ruby designed for
|
|
30
|
+
# sonic and musical composition. It emphasizes the creation of complex temporal structures
|
|
31
|
+
# independently of the audio rendering engine, providing composers and developers with powerful
|
|
32
|
+
# tools for algorithmic composition, generative music, and musical notation.
|
|
33
|
+
#
|
|
34
|
+
# ## Who is it for?
|
|
35
|
+
#
|
|
36
|
+
# - Composers exploring algorithmic composition
|
|
37
|
+
# - Musicians interested in generative music systems
|
|
38
|
+
# - Developers building music applications
|
|
39
|
+
# - Researchers in computational musicology
|
|
40
|
+
# - Live coders and interactive music performers
|
|
41
|
+
#
|
|
42
|
+
# ## Key Features
|
|
43
|
+
#
|
|
44
|
+
# - **Advanced Sequencer** - Precise temporal control for complex polyrhythmic and polytemporal structures
|
|
45
|
+
# - **Transport & Timing** - Multiple clock sources (internal, MIDI, external) with microsecond precision
|
|
46
|
+
# - **Audio Engine Independent** - Works with any MIDI-capable, OSC-capable or any other output hardware or software system
|
|
47
|
+
# - **Series-Based Composition** - Flexible sequence generators for pitches, rhythms, dynamics, and any musical parameter
|
|
48
|
+
# - **Generative Tools** - Markov chains, combinatorial variations (Variatio), rule-based production systems (Rules), formal grammars (GenerativeGrammar), and genetic algorithms (Darwin)
|
|
49
|
+
# - **Matrix Operations** - Mathematical transformations for musical structures
|
|
50
|
+
# - **Scale System** - Comprehensive support for scales, tuning systems, and chord structures
|
|
51
|
+
# - **Neumalang Notation** - Intuitive text-based and customizable musical (or sound) notation
|
|
52
|
+
# - **Transcription System** - Convert musical gestures to MIDI and MusicXML with ornament transcription expansion
|
|
53
|
+
#
|
|
54
|
+
# ## Documentation
|
|
55
|
+
#
|
|
56
|
+
# For complete user manual, tutorials, examples, and detailed subsystem documentation,
|
|
57
|
+
# please visit the Musa-DSL project on GitHub:
|
|
58
|
+
#
|
|
59
|
+
# https://github.com/javier-sy/musa-dsl
|
|
60
|
+
#
|
|
61
|
+
# The README.md file contains comprehensive documentation including:
|
|
62
|
+
#
|
|
63
|
+
# - Quick Start guide
|
|
64
|
+
# - Complete tutorial
|
|
65
|
+
# - System architecture overview
|
|
66
|
+
# - Detailed documentation for all subsystems
|
|
67
|
+
# - Installation instructions
|
|
68
|
+
# - Usage examples
|
|
69
|
+
#
|
|
70
|
+
module Musa
|
|
71
|
+
# Convenience module that includes all Musa DSL components in a single namespace.
|
|
29
72
|
#
|
|
30
|
-
include Musa
|
|
73
|
+
# This module provides a convenient way to include all Musa DSL functionality
|
|
74
|
+
# into your code with a single `include Musa::All` statement, rather than
|
|
75
|
+
# including each component individually.
|
|
76
|
+
#
|
|
77
|
+
# ## Included Components
|
|
78
|
+
#
|
|
79
|
+
# ### Core Functionality
|
|
80
|
+
#
|
|
81
|
+
# - {Musa::Logger} - Logging utilities
|
|
82
|
+
# - {Musa::Clock} - Timing and clock management
|
|
83
|
+
# - {Musa::Transport} - Transport control (play, stop, tempo)
|
|
84
|
+
# - {Musa::Sequencer} - Event sequencing and scheduling
|
|
85
|
+
# - {Musa::Series} - Series operations and transformations
|
|
86
|
+
# - {Musa::Datasets} - Musical dataset management
|
|
87
|
+
# - {Musa::REPL} - Read-Eval-Print Loop for live coding
|
|
88
|
+
#
|
|
89
|
+
# ### Notation and Language
|
|
90
|
+
#
|
|
91
|
+
# - {Musa::Neumalang} - Neuma language parser
|
|
92
|
+
# - {Musa::Neumas} - Neuma notation system
|
|
93
|
+
#
|
|
94
|
+
# ### Musical Theory
|
|
95
|
+
#
|
|
96
|
+
# - {Musa::Scales} - Scale definitions and operations
|
|
97
|
+
# - {Musa::Chords} - Chord construction and manipulation
|
|
98
|
+
#
|
|
99
|
+
# ### Data Structures
|
|
100
|
+
#
|
|
101
|
+
# - {Musa::Matrix} - Matrix operations for musical data
|
|
102
|
+
#
|
|
103
|
+
# ### Generative Algorithms
|
|
104
|
+
#
|
|
105
|
+
# - {Musa::Darwin} - Evolutionary/genetic algorithms
|
|
106
|
+
# - {Musa::Markov} - Markov chain generation
|
|
107
|
+
# - {Musa::Rules} - Rule-based generative system
|
|
108
|
+
# - {Musa::Variatio} - Combinatorial variation generator
|
|
109
|
+
#
|
|
110
|
+
# ### Input/Output
|
|
111
|
+
#
|
|
112
|
+
# - {Musa::MIDIRecorder} - MIDI event recording
|
|
113
|
+
# - {Musa::MIDIVoices} - MIDI voice management
|
|
114
|
+
# - {Musa::MusicXML} - MusicXML score generation
|
|
115
|
+
# - {Musa::Transcription} - Event transcription system
|
|
116
|
+
# - {Musa::Transcriptors} - Transcriptor implementations
|
|
117
|
+
#
|
|
118
|
+
# ## Usage
|
|
119
|
+
#
|
|
120
|
+
# @example Include all Musa DSL components
|
|
121
|
+
# require 'musa-dsl'
|
|
122
|
+
# include Musa::All
|
|
123
|
+
#
|
|
124
|
+
# # Now you have access to all Musa DSL methods and classes
|
|
125
|
+
# score = S.with(pitches: [60, 62, 64, 65])
|
|
126
|
+
# sequencer = Sequencer.new
|
|
127
|
+
#
|
|
128
|
+
# @example Selective inclusion (alternative)
|
|
129
|
+
# # Instead of Musa::All, you can include only what you need:
|
|
130
|
+
# include Musa::Series
|
|
131
|
+
# include Musa::Sequencer
|
|
132
|
+
module All
|
|
133
|
+
# Core
|
|
134
|
+
#
|
|
135
|
+
include Musa::Logger
|
|
31
136
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
137
|
+
include Musa::Clock
|
|
138
|
+
include Musa::Transport
|
|
139
|
+
include Musa::Sequencer
|
|
35
140
|
|
|
36
|
-
|
|
37
|
-
|
|
141
|
+
include Musa::Series
|
|
142
|
+
include Musa::Datasets
|
|
38
143
|
|
|
39
|
-
|
|
40
|
-
|
|
144
|
+
include Musa::Neumalang
|
|
145
|
+
include Musa::Neumas
|
|
41
146
|
|
|
42
|
-
|
|
147
|
+
include Musa::Transcription
|
|
43
148
|
|
|
44
|
-
|
|
149
|
+
include Musa::REPL
|
|
45
150
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
151
|
+
# Extensions: ojo, el nombre extensions ya se usa para algunos paquetes de core-ext que funcionan con Refinements
|
|
152
|
+
#
|
|
153
|
+
include Musa::Scales
|
|
154
|
+
include Musa::Chords
|
|
50
155
|
|
|
51
|
-
|
|
156
|
+
# Note: Musa::Extension::Matrix is a refinement and cannot be included
|
|
157
|
+
# Use: using Musa::Extension::Matrix
|
|
52
158
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
159
|
+
include Musa::Darwin
|
|
160
|
+
include Musa::Markov
|
|
161
|
+
include Musa::Rules
|
|
162
|
+
include Musa::Variatio
|
|
57
163
|
|
|
58
|
-
|
|
59
|
-
|
|
164
|
+
include Musa::MIDIRecorder
|
|
165
|
+
include Musa::MIDIVoices
|
|
60
166
|
|
|
61
|
-
|
|
167
|
+
include Musa::MusicXML
|
|
62
168
|
|
|
63
|
-
|
|
169
|
+
include Musa::Transcriptors
|
|
170
|
+
end
|
|
64
171
|
end
|
data/musa-dsl.gemspec
CHANGED
|
@@ -12,15 +12,13 @@ Gem::Specification.new do |s|
|
|
|
12
12
|
s.homepage = 'https://github.com/javier-sy/musa-dsl'
|
|
13
13
|
s.license = 'LGPL-3.0-or-later'
|
|
14
14
|
|
|
15
|
-
s.required_ruby_version = '
|
|
15
|
+
s.required_ruby_version = '>= 3.4.7'
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# "changelog_uri" => ""
|
|
23
|
-
#}
|
|
17
|
+
s.metadata = {
|
|
18
|
+
'homepage_uri' => s.homepage,
|
|
19
|
+
'source_code_uri' => s.homepage,
|
|
20
|
+
'documentation_uri' => 'https://www.rubydoc.info/gems/musa-dsl'
|
|
21
|
+
}
|
|
24
22
|
|
|
25
23
|
s.add_runtime_dependency 'prime', '~> 0.1'
|
|
26
24
|
s.add_runtime_dependency 'matrix', '~> 0.4'
|
|
@@ -30,9 +28,13 @@ Gem::Specification.new do |s|
|
|
|
30
28
|
|
|
31
29
|
s.add_runtime_dependency 'citrus', '~> 3.0'
|
|
32
30
|
|
|
33
|
-
s.add_runtime_dependency 'midi-events', '~> 0.
|
|
34
|
-
s.add_runtime_dependency 'midi-parser', '~> 0.
|
|
31
|
+
s.add_runtime_dependency 'midi-events', '~> 0.7'
|
|
32
|
+
s.add_runtime_dependency 'midi-parser', '~> 0.5'
|
|
35
33
|
|
|
36
34
|
s.add_development_dependency 'descriptive-statistics', '~> 2.2'
|
|
37
35
|
s.add_development_dependency 'rspec', '~> 3'
|
|
36
|
+
s.add_development_dependency 'yard', '~> 0.9'
|
|
37
|
+
s.add_development_dependency 'redcarpet', '~> 3.6'
|
|
38
|
+
s.add_development_dependency 'webrick', '~> 1.8'
|
|
39
|
+
s.add_development_dependency 'rack', '~> 2.2'
|
|
38
40
|
end
|