musa-dsl 0.49.2 → 0.49.4
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/docs/subsystems/generative.md +20 -0
- data/docs/subsystems/midi.md +16 -0
- data/docs/subsystems/sequencer.md +48 -0
- data/docs/subsystems/transcription.md +7 -6
- data/docs/subsystems/transport.md +37 -3
- data/docs/vocabulary.md +3 -3
- data/lib/musa-dsl/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdf7189dfdc5b4f566f8727a90eae99f2025c46de642bd9e29af0d7c2ab53673
|
|
4
|
+
data.tar.gz: 4f3ecff2f2178e393a446c7ab4ac13c6f8aff89cbe6b178cf0ef333d84b376d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a472d7b75153cdeac08f67bfc81668dd713491587e6b185a72a4abea6acdd1a87c156af964acb435876f423fd34a2b8e077df41695b4693327d53675d1da900
|
|
7
|
+
data.tar.gz: fe000da956e7dd4b1a86ddfc3c3bb72604be4da365c4e6f5cd0d96bea25169ab406d4ee6cf5f350ce8526ac76f910bb650c1e33949d513545e512bf7604bb0c7
|
|
@@ -82,6 +82,26 @@ markov = Musa::Markov::Markov.new(
|
|
|
82
82
|
melody_pitches = markov.to_a
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Replacing the table while the chain is running
|
|
86
|
+
|
|
87
|
+
`transitions=` swaps the table on a live chain.
|
|
88
|
+
**The new table has to cover every state the
|
|
89
|
+
chain can currently be in, not only the states it wants to produce.** The chain
|
|
90
|
+
keeps its current state across the swap, and if the new table has no entry for it:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
RuntimeError: No transition defined for <STATE>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That raise happens on every call from then on, and under a sequencer it is easy
|
|
97
|
+
to miss entirely: the scheduled block's exception is recorded and the sequencer
|
|
98
|
+
carries on, so the voice simply goes quiet while everything else keeps playing.
|
|
99
|
+
Nothing crashes and nothing warns.
|
|
100
|
+
|
|
101
|
+
The way out is to build the tables over the **union** of the states any of them
|
|
102
|
+
can reach, giving each one an entry for every state even if some of those entries
|
|
103
|
+
only lead back out of it.
|
|
104
|
+
|
|
85
105
|
## Variatio
|
|
86
106
|
|
|
87
107
|
Generates all combinations of parameter variations using Cartesian product. Useful for creating comprehensive parameter sweeps, exploring all possibilities of a musical motif, or generating exhaustive harmonic permutations.
|
data/docs/subsystems/midi.md
CHANGED
|
@@ -82,6 +82,22 @@ voices.fast_forward = true
|
|
|
82
82
|
voices.fast_forward = false # Resume audible output
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
## A pitch on a channel is a boolean, not a counter
|
|
86
|
+
|
|
87
|
+
Two overlapping notes of the same pitch on the same voice are **one pitch
|
|
88
|
+
sounding**. `MIDIVoice` reference-counts per pitch: it emits a NoteOn for each,
|
|
89
|
+
and a single NoteOff when the last `NoteControl` is released. That is correct —
|
|
90
|
+
MIDI has no way to express "two of the same note on one channel" — but it breaks
|
|
91
|
+
two things people write.
|
|
92
|
+
|
|
93
|
+
**Counting NoteOn's will not find hanging notes.** `NoteOn - NoteOff > 0` is the normal
|
|
94
|
+
state of any piece with overlaps, not a leak. What answers the question is
|
|
95
|
+
whether any pitch still holds controls:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
hanging = voice.active_pitches.select { |_pitch, state| !state[:note_controls].empty? }
|
|
99
|
+
```
|
|
100
|
+
|
|
85
101
|
## MIDIRecorder - MIDI Event Recording
|
|
86
102
|
|
|
87
103
|
**MIDIRecorder** captures raw MIDI bytes alongside sequencer position timestamps and converts them into structured note events. Useful for recording phrases from external MIDI controllers synchronized with the sequencer timeline.
|
|
@@ -231,6 +231,30 @@ All scheduling methods (`every`, `play`, `move`, `play_timed`) pass parameters t
|
|
|
231
231
|
|
|
232
232
|
**Important**: keyword parameters (like `control:`) must be declared as **keyword arguments** in the block signature (`|control:|`), not as positional arguments (`|control|`).
|
|
233
233
|
|
|
234
|
+
**And the one that bites: a parameter with no value arrives as `nil`, which
|
|
235
|
+
overrides the Ruby default you wrote.** `SmartProcBinder` supplies every
|
|
236
|
+
declared parameter, so `nil` is passed rather than the parameter being left out
|
|
237
|
+
— and a default only fires when an argument is *absent*, never when it is `nil`.
|
|
238
|
+
This matters most in `launch`, where the recursion looks like it will seed
|
|
239
|
+
itself:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
# WRONG — rep is nil on the first call, not 0
|
|
243
|
+
control.after { launch :section }
|
|
244
|
+
on :section do |rep = 0|
|
|
245
|
+
launch :section, rep + 1 # NoMethodError: undefined method '+' for nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# RIGHT — pass the starting value explicitly
|
|
249
|
+
control.after { launch :section, 0 }
|
|
250
|
+
on :section do |rep = 0| # the default now only documents the intent
|
|
251
|
+
launch :section, rep + 1
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The rule is the same wherever a block declares a parameter the caller may not
|
|
256
|
+
supply: give it a value, and treat the Ruby default as documentation.
|
|
257
|
+
|
|
234
258
|
### Parameters available per method
|
|
235
259
|
|
|
236
260
|
| Method | Positional params | Keyword params |
|
|
@@ -292,6 +316,30 @@ Three things the table cannot say and the result does:
|
|
|
292
316
|
- `started_ago:` is an **array**, not a number: one entry per value that was
|
|
293
317
|
already sounding when this one arrived, empty when nothing was.
|
|
294
318
|
|
|
319
|
+
## A block that raises does not stop the piece
|
|
320
|
+
|
|
321
|
+
Every scheduled block runs inside a rescue: an exception is written to the
|
|
322
|
+
sequencer's logger and the sequencer carries on with the next tick. Nothing
|
|
323
|
+
propagates to whoever called `run`, and nothing appears on stdout unless the
|
|
324
|
+
logger is being watched.
|
|
325
|
+
|
|
326
|
+
That is the right behaviour for a piece playing live — one broken voice should
|
|
327
|
+
not take the other five with it — but it has a consequence worth knowing before
|
|
328
|
+
it happens to you: **a voice can fall silent for the rest of the piece while
|
|
329
|
+
everything reports success.** The piece runs to completion, the verification
|
|
330
|
+
passes, and the only symptom is silence where there should be a line.
|
|
331
|
+
|
|
332
|
+
If a voice goes quiet with no error, this is the first thing to check. An
|
|
333
|
+
offline verification can catch it by intercepting `logger.error` on the
|
|
334
|
+
transport's logger and failing when anything arrives:
|
|
335
|
+
|
|
336
|
+
```text
|
|
337
|
+
errors = []
|
|
338
|
+
transport.logger.define_singleton_method(:error) { |*args, &b| errors << (b ? b.call : args.first) }
|
|
339
|
+
# ... run the piece ...
|
|
340
|
+
raise "a scheduled block failed: #{errors.first}" unless errors.empty?
|
|
341
|
+
```
|
|
342
|
+
|
|
295
343
|
## Play Modes
|
|
296
344
|
|
|
297
345
|
`play` supports three modes that determine how series elements are scheduled. The default mode is `:wait`.
|
|
@@ -31,7 +31,7 @@ require 'musa-dsl'
|
|
|
31
31
|
|
|
32
32
|
using Musa::Extension::Neumas
|
|
33
33
|
|
|
34
|
-
# Neuma notation with ornaments: trill (
|
|
34
|
+
# Neuma notation with ornaments: trill (tr) and mordent (mor)
|
|
35
35
|
neumas = "(0 1 mf) (+2 1 tr) (+4 1 mor) (+5 1)"
|
|
36
36
|
|
|
37
37
|
# Create scale and decoder
|
|
@@ -67,11 +67,12 @@ end
|
|
|
67
67
|
# Pitch: 79, Duration: 1/4, Velocity: 80 # G5 (no ornament)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
**Supported ornaments
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
70
|
+
**Supported ornaments**:
|
|
71
|
+
|
|
72
|
+
- `tr` - Trill (rapid alternation with upper note)
|
|
73
|
+
- `mor` - Mordent (quick alternation with adjacent note)
|
|
74
|
+
- `turn` - Turn (four-note figure)
|
|
75
|
+
- `st` - Staccato (shortened duration)
|
|
75
76
|
|
|
76
77
|
## MusicXML with Ornament Symbols
|
|
77
78
|
|
|
@@ -220,15 +220,49 @@ clock = Musa::Clock::InputMidiClock.new(midi_input)
|
|
|
220
220
|
# Create transport
|
|
221
221
|
transport = Musa::Transport::Transport.new(clock, 4, 24)
|
|
222
222
|
|
|
223
|
-
# Schedule events
|
|
224
|
-
transport.
|
|
225
|
-
|
|
223
|
+
# Schedule events INSIDE on_start, not before transport.start. See below.
|
|
224
|
+
transport.on_start do
|
|
225
|
+
transport.sequencer.at 1 do
|
|
226
|
+
puts "Synchronized start at bar 1!"
|
|
227
|
+
end
|
|
226
228
|
end
|
|
227
229
|
|
|
228
230
|
# Start and wait for MIDI Clock Start message
|
|
229
231
|
transport.start
|
|
230
232
|
```
|
|
231
233
|
|
|
234
|
+
### Under a DAW's clock, schedule from `on_start` or `before_begin`
|
|
235
|
+
|
|
236
|
+
Anything scheduled before `transport.start` can be gone before the first note
|
|
237
|
+
sounds, and the reason is that pressing Play does not always send a Start.
|
|
238
|
+
|
|
239
|
+
A DAW with Song Position Pointer enabled cannot say "play from here" in one
|
|
240
|
+
message: `Start` means "from the beginning" and carries no position. So it sends
|
|
241
|
+
three -- `Stop`, `Song Position Pointer`, `Continue` -- and that first `Stop` is
|
|
242
|
+
a real stop. A stop resets the sequencer, which discards every `at`, `every` and
|
|
243
|
+
`play` registered on it. The transport then starts and runs an empty sequencer.
|
|
244
|
+
|
|
245
|
+
With Song Position Pointer switched off the same piece plays normally: Play
|
|
246
|
+
sends a plain `Start`, there is no stop, and nothing is reset.
|
|
247
|
+
|
|
248
|
+
Whether it bites depends on how the three messages arrive. They are sent within
|
|
249
|
+
a millisecond of each other, and when a single read returns all three the
|
|
250
|
+
transport repositions without stopping -- nothing is reset and a piece that
|
|
251
|
+
schedules early works. When they arrive apart, it does not.
|
|
252
|
+
|
|
253
|
+
That grouping is decided by the platform and by whatever else the machine is
|
|
254
|
+
doing at that moment, so **a piece written this way can work for years and then
|
|
255
|
+
stop working without anything in it having changed**: a different operating
|
|
256
|
+
system, a busier machine, a Ruby that schedules its threads differently. Working
|
|
257
|
+
today is not evidence that the rule does not apply.
|
|
258
|
+
|
|
259
|
+
**The symptom is silence, not an error**, and it is indistinguishable at a glance
|
|
260
|
+
from a transport that never started. If a piece runs mute under a DAW, read the
|
|
261
|
+
sequencer's position while it runs: advancing position with no notes is this.
|
|
262
|
+
|
|
263
|
+
`before_begin` and `on_start` run after that reset, which is what makes them the
|
|
264
|
+
right place.
|
|
265
|
+
|
|
232
266
|
## The lifecycle, run
|
|
233
267
|
|
|
234
268
|
```ruby
|
data/docs/vocabulary.md
CHANGED
|
@@ -27,7 +27,7 @@ missing from the documents, and that is where to add it.
|
|
|
27
27
|
|
|
28
28
|
## midi
|
|
29
29
|
|
|
30
|
-
`MIDIRecorder` · `MIDIVoices` · `channel` · `duration` · `note` · `note_off` · `pitch` · `position` · `raw` · `record` · `transcription` · `velocity` · `velocity_off` · `voice` · `voices`
|
|
30
|
+
`MIDIRecorder` · `MIDIVoice` · `MIDIVoices` · `channel` · `duration` · `note` · `note_off` · `pitch` · `position` · `raw` · `record` · `transcription` · `velocity` · `velocity_off` · `voice` · `voices`
|
|
31
31
|
|
|
32
32
|
## music
|
|
33
33
|
|
|
@@ -47,7 +47,7 @@ missing from the documents, and that is where to add it.
|
|
|
47
47
|
|
|
48
48
|
## sequencer
|
|
49
49
|
|
|
50
|
-
`Sequencer` · `after` · `at` · `duration` · `every` · `move` · `next_value` · `note_duration` · `now` · `on_stop` · `play` · `play_timed` · `sequencer` · `stop` · `time` · `wait` · `with`
|
|
50
|
+
`Sequencer` · `SmartProcBinder` · `after` · `at` · `duration` · `error` · `every` · `launch` · `logger` · `move` · `next_value` · `note_duration` · `now` · `on_stop` · `play` · `play_timed` · `run` · `sequencer` · `stop` · `time` · `wait` · `with`
|
|
51
51
|
|
|
52
52
|
## series
|
|
53
53
|
|
|
@@ -59,4 +59,4 @@ missing from the documents, and that is where to add it.
|
|
|
59
59
|
|
|
60
60
|
## transport
|
|
61
61
|
|
|
62
|
-
`Clock` · `DummyClock` · `ExternalTickClock` · `InputMidiClock` · `TimerClock` · `Transport` · `after_stop` · `before_begin` · `change_position_to` · `n` · `on_change_position` · `on_start` · `on_stop` · `run` · `sequencer` · `start` · `stop` · `terminate` · `tick`
|
|
62
|
+
`Clock` · `DummyClock` · `ExternalTickClock` · `InputMidiClock` · `TimerClock` · `Transport` · `after_stop` · `at` · `before_begin` · `change_position_to` · `every` · `n` · `on_change_position` · `on_start` · `on_stop` · `play` · `run` · `sequencer` · `start` · `stop` · `terminate` · `tick`
|
data/lib/musa-dsl/version.rb
CHANGED