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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48983d8ced3dbbbe19f220bf441cf6c23e228f274ca79afb384c13fc22339768
4
- data.tar.gz: 2384a66db6951f33d884362b49385d1ccce38362327c086a90dbaf9820386c96
3
+ metadata.gz: cdf7189dfdc5b4f566f8727a90eae99f2025c46de642bd9e29af0d7c2ab53673
4
+ data.tar.gz: 4f3ecff2f2178e393a446c7ab4ac13c6f8aff89cbe6b178cf0ef333d84b376d7
5
5
  SHA512:
6
- metadata.gz: 5a3bea3424e5bbc09419f8e6f6f5cef241d9a834d4ca5a574f6675fa0ef613073f7e25bfd8185bdc53d4a9b37fa812cce09df88c61b4494af907bf4ded21946c
7
- data.tar.gz: 599a9e1281d38b72b42e36144aee91f69122f8a1cddadfe71d18b7cf8da63d4343f85f8d70d0410b0ace0c35d2d472765f300acaaafb353e865371f2f464a83a
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.
@@ -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 (.tr) and mordent (.mor)
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
- - `.tr` - Trill (rapid alternation with upper note)
72
- - `.mor` - Mordent (quick alternation with adjacent note)
73
- - `.turn` - Turn (four-note figure)
74
- - `.st` - Staccato (shortened duration)
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.sequencer.at 1 do
225
- puts "Synchronized start at bar 1!"
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`
@@ -1,3 +1,3 @@
1
1
  module Musa
2
- VERSION = '0.49.2'.freeze
2
+ VERSION = '0.49.4'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: musa-dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.49.2
4
+ version: 0.49.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Sánchez Yeste