musa-dsl 0.30.2 → 0.41.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.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/Gemfile +0 -1
  6. data/README.md +227 -6
  7. data/docs/README.md +83 -0
  8. data/docs/api-reference.md +86 -0
  9. data/docs/getting-started/quick-start.md +93 -0
  10. data/docs/getting-started/tutorial.md +58 -0
  11. data/docs/subsystems/core-extensions.md +316 -0
  12. data/docs/subsystems/datasets.md +465 -0
  13. data/docs/subsystems/generative.md +290 -0
  14. data/docs/subsystems/matrix.md +63 -0
  15. data/docs/subsystems/midi.md +123 -0
  16. data/docs/subsystems/music.md +544 -0
  17. data/docs/subsystems/musicxml-builder.md +264 -0
  18. data/docs/subsystems/neumas.md +71 -0
  19. data/docs/subsystems/repl.md +135 -0
  20. data/docs/subsystems/sequencer.md +98 -0
  21. data/docs/subsystems/series.md +302 -0
  22. data/docs/subsystems/transcription.md +152 -0
  23. data/docs/subsystems/transport.md +177 -0
  24. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  25. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  26. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  27. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  28. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  29. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  30. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  31. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  32. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  33. data/lib/musa-dsl/core-ext/with.rb +114 -0
  34. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  35. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  36. data/lib/musa-dsl/datasets/e.rb +186 -2
  37. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  38. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  39. data/lib/musa-dsl/datasets/helper.rb +75 -0
  40. data/lib/musa-dsl/datasets/p.rb +177 -2
  41. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  42. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  43. data/lib/musa-dsl/datasets/ps.rb +134 -4
  44. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  45. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  48. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  49. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  50. data/lib/musa-dsl/datasets/score.rb +279 -0
  51. data/lib/musa-dsl/datasets/v.rb +88 -0
  52. data/lib/musa-dsl/generative/darwin.rb +215 -1
  53. data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
  54. data/lib/musa-dsl/generative/markov.rb +135 -3
  55. data/lib/musa-dsl/generative/rules.rb +312 -4
  56. data/lib/musa-dsl/generative/variatio.rb +286 -2
  57. data/lib/musa-dsl/logger/logger.rb +267 -2
  58. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  59. data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
  60. data/lib/musa-dsl/midi/midi-voices.rb +275 -4
  61. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  62. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  63. data/lib/musa-dsl/music/chords.rb +353 -2
  64. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
  65. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  66. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  67. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  68. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  69. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  70. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  71. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  72. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  73. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  74. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  75. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  76. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  77. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  78. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  79. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  80. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  81. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  82. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  83. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  84. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  85. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  86. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  87. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  88. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  89. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  90. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  91. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  92. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  93. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  94. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  95. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  96. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  97. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  98. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  99. data/lib/musa-dsl/music/scales.rb +1384 -40
  100. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  101. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  102. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  103. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  104. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  105. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  106. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  107. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  108. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  109. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  110. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  111. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  112. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  113. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  114. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  115. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  116. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  117. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  118. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  119. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  120. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  121. data/lib/musa-dsl/repl/repl.rb +550 -0
  122. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  123. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  124. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  125. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  126. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  127. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  128. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  129. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  130. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  131. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  132. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  133. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  134. data/lib/musa-dsl/series/base-series.rb +843 -5
  135. data/lib/musa-dsl/series/buffer-serie.rb +54 -0
  136. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
  137. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  138. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  139. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  140. data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
  141. data/lib/musa-dsl/series/queue-serie.rb +78 -0
  142. data/lib/musa-dsl/series/series-composer.rb +701 -0
  143. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  144. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  145. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  146. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  147. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  148. data/lib/musa-dsl/transport/clock.rb +125 -0
  149. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  150. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  151. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  152. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  153. data/lib/musa-dsl/transport/timer.rb +83 -0
  154. data/lib/musa-dsl/transport/transport.rb +318 -0
  155. data/lib/musa-dsl/version.rb +2 -1
  156. data/lib/musa-dsl.rb +132 -25
  157. data/musa-dsl.gemspec +25 -18
  158. metadata +158 -16
@@ -0,0 +1,302 @@
1
+ # Series - Sequence Generators
2
+
3
+ Series are the fundamental building blocks for generating musical sequences. They provide functional operations for transforming pitches, rhythms, dynamics, and any musical parameter.
4
+
5
+ ## Basic Series Operations
6
+
7
+ ```ruby
8
+ require 'musa-dsl'
9
+ include Musa::Series
10
+
11
+ # S constructor: Create series from values
12
+ melody = S(0, 2, 4, 5, 7).repeat(2)
13
+ melody.i.to_a # => [0, 2, 4, 5, 7, 0, 2, 4, 5, 7]
14
+
15
+ # Transform with map
16
+ transposed = S(60, 64, 67).map { |n| n + 12 }
17
+ transposed.i.to_a # => [72, 76, 79]
18
+
19
+ # Filter with select
20
+ evens = S(1, 2, 3, 4, 5, 6).select { |n| n.even? }
21
+ evens.i.to_a # => [2, 4, 6]
22
+ ```
23
+
24
+ ## Combining Multiple Parameters
25
+
26
+ Use `.with` to combine pitches, durations, and velocities:
27
+
28
+ ```ruby
29
+ # Combine pitch, duration, and velocity
30
+ pitches = S(60, 64, 67, 72)
31
+ durations = S(1r, 1/2r, 1/2r, 1r)
32
+ velocities = S(96, 80, 90, 100)
33
+
34
+ notes = pitches.with(dur: durations, vel: velocities) do |p, dur:, vel:|
35
+ { pitch: p, duration: dur, velocity: vel }
36
+ end
37
+
38
+ notes.i.to_a
39
+ # => [{pitch: 60, duration: 1, velocity: 96},
40
+ # {pitch: 64, duration: 1/2, velocity: 80},
41
+ # {pitch: 67, duration: 1/2, velocity: 90},
42
+ # {pitch: 72, duration: 1, velocity: 100}]
43
+ ```
44
+
45
+ **Creating PDV with `H()` and `HC()`:**
46
+
47
+ When series have different lengths, use `H` (stops at shortest) or `HC` (cycles all series):
48
+
49
+ ```ruby
50
+ # Create PDV from series of different sizes
51
+ pitches = S(60, 62, 64, 65, 67) # 5 notes
52
+ durations = S(1r, 1/2r, 1/4r) # 3 durations
53
+ velocities = S(96, 80, 90, 100) # 4 velocities
54
+
55
+ # H: Stop when shortest series exhausts (3 notes - limited by durations)
56
+ notes = H(pitch: pitches, duration: durations, velocity: velocities)
57
+
58
+ notes.i.to_a
59
+ # => [{pitch: 60, duration: 1, velocity: 96},
60
+ # {pitch: 62, duration: 1/2, velocity: 80},
61
+ # {pitch: 64, duration: 1/4, velocity: 90}]
62
+
63
+ # HC: Continue cycling all series (cycles until common multiple)
64
+ notes_cycling = HC(pitch: pitches, duration: durations, velocity: velocities)
65
+ .max_size(7) # Limit output for readability
66
+
67
+ notes_cycling.i.to_a
68
+ # => [{pitch: 60, duration: 1, velocity: 96},
69
+ # {pitch: 62, duration: 1/2, velocity: 80},
70
+ # {pitch: 64, duration: 1/4, velocity: 90},
71
+ # {pitch: 65, duration: 1, velocity: 100},
72
+ # {pitch: 67, duration: 1/2, velocity: 96},
73
+ # {pitch: 60, duration: 1/4, velocity: 80},
74
+ # {pitch: 62, duration: 1, velocity: 90}]
75
+ ```
76
+
77
+ ## Merging Melodic Phrases
78
+
79
+ Use `MERGE` to concatenate multiple series:
80
+
81
+ ```ruby
82
+ # Build melody from phrases
83
+ phrase1 = S(60, 64, 67) # C major triad ascending
84
+ phrase2 = S(72, 69, 65) # Descending from octave
85
+ phrase3 = S(60, 62, 64) # Scale fragment
86
+
87
+ melody = MERGE(phrase1, phrase2, phrase3)
88
+ melody.i.to_a # => [60, 64, 67, 72, 69, 65, 60, 62, 64]
89
+
90
+ # Repeat merged structure
91
+ section = MERGE(S(1, 2, 3), S(4, 5, 6)).repeat(2)
92
+ section.i.to_a # => [1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
93
+ ```
94
+
95
+ ## Numeric Generators
96
+
97
+ ```ruby
98
+ # FOR: Numeric ranges
99
+ ascending = FOR(from: 0, to: 7, step: 1)
100
+ ascending.i.to_a # => [0, 1, 2, 3, 4, 5, 6, 7]
101
+
102
+ descending = FOR(from: 10, to: 0, step: 2)
103
+ descending.i.to_a # => [10, 8, 6, 4, 2, 0]
104
+
105
+ # FIBO: Fibonacci rhythmic proportions
106
+ rhythm = FIBO().max_size(8).map { |n| Rational(n, 16) }
107
+ rhythm.i.to_a
108
+ # => [1/16, 1/16, 1/8, 3/16, 5/16, 1/2, 13/16, 21/16]
109
+
110
+ # RND: Random melody with constraints
111
+ melody = RND(60, 62, 64, 65, 67, 69, 71, 72)
112
+ .max_size(16)
113
+ .remove { |note, history| note == history.last } # No consecutive repeats
114
+
115
+ # HARMO: Harmonic series (overtones)
116
+ harmonics = HARMO(error: 0.5).max_size(10)
117
+ harmonics.i.to_a # => [0, 12, 19, 24, 28, 31, 34, 36, 38, 40]
118
+ ```
119
+
120
+ ## Structural Transformations
121
+
122
+ ```ruby
123
+ # Reverse: Retrograde motion
124
+ melody = S(60, 64, 67, 72)
125
+ retrograde = melody.reverse
126
+ retrograde.i.to_a # => [72, 67, 64, 60]
127
+
128
+ # merge operation: Flatten serie of series
129
+ chunks = S(1, 2, 3, 4, 5, 6).cut(2) # Split into pairs (serie of series)
130
+
131
+ # Each chunk is a serie, use .merge to flatten
132
+ reconstructed = chunks.merge
133
+ reconstructed.i.to_a # => [1, 2, 3, 4, 5, 6]
134
+
135
+ # Chaining operations
136
+ result = S(60, 62, 64, 65, 67, 69, 71, 72)
137
+ .select { |n| n.even? } # Keep even pitches: [60, 62, 64, 72]
138
+ .map { |n| n + 12 } # Transpose up octave: [72, 74, 76, 84]
139
+ .reverse # Retrograde: [84, 76, 74, 72]
140
+ .repeat(2) # Repeat twice
141
+
142
+ result.i.to_a # => [84, 76, 74, 72, 84, 76, 74, 72]
143
+ ```
144
+
145
+ **Serie Constructors:**
146
+ - `S(...)` - Array serie
147
+ - `E(&block)` - Serie from evaluation block
148
+ - `H(k1: s1, k2: s2, ...)` - Hash serie from series (stops at shortest)
149
+ - `HC(k1: s1, k2: s2, ...)` - Hash combined (cycles all series)
150
+ - `A(s1, s2, ...)` - Array of series (stops at shortest)
151
+ - `AC(s1, s2, ...)` - Array combined (cycles all series)
152
+ - `FOR(from:, to:, step:)` - Numeric range generator
153
+ - `MERGE(s1, s2, ...)` - Concatenate series sequentially
154
+ - `RND(...)` - Random values (infinite)
155
+ - `RND1(...)` - Random single value (exhausts after one)
156
+ - `SIN(steps:, amplitude:, center:)` - Sinusoidal waveform
157
+ - `FIBO()` - Fibonacci sequence
158
+ - `HARMO(error:, extended:)` - Harmonic series (overtones)
159
+
160
+ **Serie Operations:**
161
+ - `.map(&block)` - Transform each value
162
+ - `.select(&block)`, `.remove(&block)` - Filter values
163
+ - `.with(*series, &block)` - Combine multiple series
164
+ - `.hashify(*keys)` - Convert array values to hash
165
+ - `.repeat(times)`, `.autorestart` - Repetition control
166
+ - `.reverse` - Reverse order
167
+ - `.randomize(random:)` - Randomize order
168
+ - `.merge`, `.flatten` - Flatten nested series
169
+ - `.cut(length)` - Split into chunks
170
+ - `.max_size(n)`, `.skip(n)` - Limit/offset control
171
+ - `.shift(n)` - Circular rotation (negative: rotate left, positive: rotate right)
172
+ - `.after(*series)` - Concatenate series
173
+ - `.switch(*series)`, `.multiplex(*series)` - Switch between series
174
+ - `.lock` - Lock/freeze values
175
+ - `.anticipate(&block)`, `.lazy(&block)` - Advanced evaluation
176
+
177
+ ## API Reference
178
+
179
+ **Complete API documentation:**
180
+ - [Musa::Series](https://rubydoc.info/gems/musa-dsl/Musa/Series) - Sequence generators and operations
181
+
182
+ **Source code:** `lib/musa-dsl/series/`
183
+
184
+ ## Specialized Series Types
185
+
186
+ Beyond basic operations, Series provides specialized types for advanced transformations and musical applications.
187
+
188
+ **BufferSerie** - Multiple Independent Readers:
189
+
190
+ Enables multiple "readers" to independently iterate over the same series source without interfering with each other. Essential for canonic structures (rounds, fugues), polyphonic playback from a single source, and multi-voice compositions.
191
+
192
+ ```ruby
193
+ require 'musa-dsl'
194
+ include Musa::Series
195
+
196
+ # Create buffered melody for canon
197
+ melody = S(60, 64, 67, 72, 76).buffered
198
+
199
+ # Create independent readers (voices)
200
+ voice1 = melody.buffer.i
201
+ voice2 = melody.buffer.i
202
+ voice3 = melody.buffer.i
203
+
204
+ # Each voice progresses independently
205
+ voice1.next_value # => 60
206
+ voice1.next_value # => 64
207
+
208
+ voice2.next_value # => 60 (independent of voice1)
209
+ voice3.next_value # => 60 (independent of others)
210
+
211
+ voice1.next_value # => 67
212
+ voice2.next_value # => 64
213
+
214
+ # Use in canon: play voice2 delayed by 2 beats, voice3 delayed by 4 beats
215
+ # Each voice reads the same melodic material at its own pace
216
+ ```
217
+
218
+ **QuantizerSerie** - Value Quantization:
219
+
220
+ Quantizes continuous time-value pairs to discrete steps. Useful for converting MIDI controller data to discrete values, snapping pitch bends to semitones, or generating stepped automation curves.
221
+
222
+ Two quantization modes:
223
+ - **Raw mode**: Rounds values to nearest step with configurable boundary inclusion
224
+ - **Predictive mode**: Predicts crossings of quantization boundaries for smooth transitions
225
+
226
+ ```ruby
227
+ require 'musa-dsl'
228
+ include Musa::Series
229
+
230
+ # Example 1: Quantize continuous pitch bend to semitones
231
+ pitch_bend = S({ time: 0r, value: 60.3 },
232
+ { time: 1r, value: 61.8 },
233
+ { time: 2r, value: 63.1 })
234
+
235
+ quantized = pitch_bend.quantize(step: 1) # Quantize to integer semitones
236
+
237
+ quantized.i.to_a
238
+ # => [{ time: 0r, value: 60, duration: 1r },
239
+ # { time: 1r, value: 62, duration: 1r }]
240
+
241
+ # Example 2: Predictive quantization for smooth crossings
242
+ continuous = S({ time: 0r, value: 0 }, { time: 4r, value: 10 })
243
+
244
+ predicted = continuous.quantize(step: 2, predictive: true)
245
+
246
+ predicted.i.to_a
247
+ # Generates crossing points at values 0, 2, 4, 6, 8, 10
248
+ # with precise timing for each boundary crossing
249
+ ```
250
+
251
+ **TimedSerie Operations** - Time-Based Merging:
252
+
253
+ Operations for series with explicit `:time` attributes. Enables multi-track MIDI sequencing, polyphonic event streams, and synchronized parameter automation.
254
+
255
+ ```ruby
256
+ require 'musa-dsl'
257
+ include Musa::Series
258
+
259
+ # Create independent melodic lines with timing
260
+ melody = S({ time: 0r, value: 60 },
261
+ { time: 1r, value: 64 },
262
+ { time: 2r, value: 67 })
263
+
264
+ bass = S({ time: 0r, value: 36 },
265
+ { time: 2r, value: 38 },
266
+ { time: 4r, value: 41 })
267
+
268
+ harmony = S({ time: 0r, value: 64 },
269
+ { time: 2r, value: 67 })
270
+
271
+ # Merge by time using TIMED_UNION (hash mode)
272
+ combined = TIMED_UNION(melody: melody, bass: bass, harmony: harmony)
273
+
274
+ combined.i.to_a
275
+ # => [{ time: 0r, value: { melody: 60, bass: 36, harmony: 64 } },
276
+ # { time: 1r, value: { melody: 64, bass: nil, harmony: nil } },
277
+ # { time: 2r, value: { melody: 67, bass: 38, harmony: 67 } },
278
+ # { time: 4r, value: { melody: nil, bass: 41, harmony: nil } }]
279
+
280
+ # Array mode for unnamed voices
281
+ voice1 = S({ time: 0r, value: 60 }, { time: 1r, value: 64 })
282
+ voice2 = S({ time: 0r, value: 48 }, { time: 1r, value: 52 })
283
+
284
+ merged = TIMED_UNION(voice1, voice2)
285
+
286
+ merged.i.to_a
287
+ # => [{ time: 0r, value: [60, 48] },
288
+ # { time: 1r, value: [64, 52] }]
289
+
290
+ # Flatten timed values
291
+ multi = S({ time: 0r, value: { soprano: 60, alto: 64 } })
292
+ flat = multi.flatten_timed.i.next_value
293
+ # => { soprano: { time: 0r, value: 60 },
294
+ # alto: { time: 0r, value: 64 } }
295
+
296
+ # Compact removes nil values
297
+ sparse = S({ time: 0r, value: [60, nil, 67] })
298
+ compact = sparse.compact_timed.i.to_a
299
+ # Removes entries where all values are nil
300
+ ```
301
+
302
+
@@ -0,0 +1,152 @@
1
+ # Transcription - MIDI & MusicXML Output
2
+
3
+ The Transcription system converts GDV events to MIDI (with ornament expansion) or MusicXML format (preserving ornaments as symbols).
4
+
5
+ ## MIDI with Ornament Expansion
6
+
7
+ ```ruby
8
+ require 'musa-dsl'
9
+
10
+ using Musa::Extension::Neumas
11
+
12
+ # Neuma notation with ornaments: trill (.tr) and mordent (.mor)
13
+ neumas = "(0 1 mf) (+2 1 tr) (+4 1 mor) (+5 1)"
14
+
15
+ # Create scale and decoder
16
+ scale = Musa::Scales::Scales.et12[440.0].major[60]
17
+ decoder = Musa::Neumas::Decoders::NeumaDecoder.new(scale, base_duration: 1/4r)
18
+
19
+ # Create MIDI transcriptor with ornament expansion
20
+ transcriptor = Musa::Transcription::Transcriptor.new(
21
+ Musa::Transcriptors::FromGDV::ToMIDI.transcription_set(duration_factor: 1/6r),
22
+ base_duration: 1/4r,
23
+ tick_duration: 1/96r
24
+ )
25
+
26
+ # Parse and expand ornaments to PDV
27
+ result = Musa::Neumalang::Neumalang.parse(neumas, decode_with: decoder)
28
+ .process_with { |gdv| transcriptor.transcript(gdv) }
29
+ .map { |gdv| gdv.to_pdv(scale) }
30
+ .to_a(recursive: true)
31
+
32
+ # View expanded notes (ornaments rendered as note sequences)
33
+ result.each do |pdv|
34
+ puts "Pitch: #{pdv[:pitch]}, Duration: #{pdv[:duration]}, Velocity: #{pdv[:velocity]}"
35
+ end
36
+ # => Pitch: 60, Duration: 1/4, Velocity: 80 # C4 (no ornament)
37
+ # Pitch: 65, Duration: 1/24, Velocity: 80 # Trill: F4 (upper neighbor)
38
+ # Pitch: 64, Duration: 1/24, Velocity: 80 # Trill: E4 (original)
39
+ # Pitch: 65, Duration: 1/24, Velocity: 80 # Trill: F4
40
+ # Pitch: 64, Duration: 1/24, Velocity: 80 # Trill: E4
41
+ # ... (trill alternation continues)
42
+ # Pitch: 71, Duration: 1/24, Velocity: 80 # Mordent: B4 (original)
43
+ # Pitch: 72, Duration: 1/24, Velocity: 80 # Mordent: C5 (neighbor)
44
+ # Pitch: 71, Duration: 1/6, Velocity: 80 # Mordent: B4 (return)
45
+ # Pitch: 79, Duration: 1/4, Velocity: 80 # G5 (no ornament)
46
+ ```
47
+
48
+ **Supported ornaments:**
49
+ - `.tr` - Trill (rapid alternation with upper note)
50
+ - `.mor` - Mordent (quick alternation with adjacent note)
51
+ - `.turn` - Turn (four-note figure)
52
+ - `.st` - Staccato (shortened duration)
53
+
54
+ ## MusicXML with Ornament Symbols
55
+
56
+ ```ruby
57
+ require 'musa-dsl'
58
+
59
+ using Musa::Extension::Neumas
60
+
61
+ # Same phrase as MIDI example (ornaments preserved as symbols)
62
+ neumas = "(0 1 mf) (+2 1 tr) (+4 1 mor) (+5 1)"
63
+
64
+ # Create scale and decoder
65
+ scale = Musa::Scales::Scales.et12[440.0].major[60]
66
+ decoder = Musa::Neumas::Decoders::NeumaDecoder.new(scale, base_duration: 1/4r)
67
+
68
+ # Create MusicXML transcriptor (preserves ornaments as symbols)
69
+ transcriptor = Musa::Transcription::Transcriptor.new(
70
+ Musa::Transcriptors::FromGDV::ToMusicXML.transcription_set,
71
+ base_duration: 1/4r,
72
+ tick_duration: 1/96r
73
+ )
74
+
75
+ # Parse and convert to GDV with preserved ornament markers
76
+ serie = Musa::Neumalang::Neumalang.parse(neumas, decode_with: decoder)
77
+ .process_with { |gdv| transcriptor.transcript(gdv) }
78
+
79
+ # Create Score and use sequencer to fill it
80
+ score = Musa::Datasets::Score.new
81
+ sequencer = Musa::Sequencer::Sequencer.new(4, 24)
82
+
83
+ sequencer.at 1 do
84
+ play serie, decoder: decoder, mode: :neumalang do |gdv|
85
+ pdv = gdv.to_pdv(scale)
86
+ score.at(position, add: pdv) # position is automatically tracked by sequencer
87
+ end
88
+ end
89
+
90
+ sequencer.run
91
+
92
+ # Convert to MusicXML
93
+ mxml = score.to_mxml(
94
+ 4, 24, # 4 beats per bar, 24 ticks per beat
95
+ bpm: 120,
96
+ title: 'Ornaments Example',
97
+ creators: { composer: 'MusaDSL' },
98
+ parts: { piano: { name: 'Piano', clefs: { g: 2 } } }
99
+ )
100
+
101
+ # Generated MusicXML (excerpt showing notes with ornaments):
102
+ puts mxml.to_xml.string
103
+ ```
104
+
105
+ **Generated MusicXML output (excerpt):**
106
+ ```xml
107
+ <note>
108
+ <pitch>
109
+ <step>C</step>
110
+ <octave>4</octave>
111
+ </pitch>
112
+ <duration>24</duration>
113
+ <type>quarter</type>
114
+ </note>
115
+ <note>
116
+ <pitch>
117
+ <step>E</step>
118
+ <octave>4</octave>
119
+ </pitch>
120
+ <duration>24</duration>
121
+ <type>quarter</type>
122
+ <notations>
123
+ <ornaments>
124
+ <trill-mark /> <!-- Trill preserved as notation symbol -->
125
+ </ornaments>
126
+ </notations>
127
+ </note>
128
+ <note>
129
+ <pitch>
130
+ <step>B</step>
131
+ <octave>4</octave>
132
+ </pitch>
133
+ <duration>24</duration>
134
+ <type>quarter</type>
135
+ <notations>
136
+ <ornaments>
137
+ <inverted-mordent /> <!-- Mordent preserved as notation symbol -->
138
+ </ornaments>
139
+ </notations>
140
+ </note>
141
+ ```
142
+
143
+ **Note:** Only 4 notes (vs 11 in MIDI) - ornaments preserved as notation symbols, not expanded
144
+
145
+ ## API Reference
146
+
147
+ **Complete API documentation:**
148
+ - [Musa::Transcription](https://rubydoc.info/gems/musa-dsl/Musa/Transcription) - Musical event transformation and ornament expansion
149
+
150
+ **Source code:** `lib/transcription/` and `lib/musicxml/`
151
+
152
+
@@ -0,0 +1,177 @@
1
+ # Transport - Timing & Clocks
2
+
3
+ Comprehensive timing infrastructure connecting clock sources to the sequencer. The transport system manages musical playback lifecycle, timing synchronization, and position control.
4
+
5
+ **Architecture:**
6
+ ```
7
+ Clock --ticks--> Transport --tick()--> Sequencer --events--> Music
8
+ ```
9
+
10
+ The system provides precise timing control with support for internal timers, MIDI clock synchronization, and manual control for testing and integration.
11
+
12
+ ## Clock - Timing Sources
13
+
14
+ **Clock** is the abstract base class for timing sources. All clocks generate regular ticks that drive the sequencer forward. Multiple clock implementations are available for different use cases.
15
+
16
+ ### Clock Activation Models
17
+
18
+ Clocks use two different activation models:
19
+
20
+ **Automatic Activation** (DummyClock):
21
+ - Begins generating ticks immediately when `transport.start` is called
22
+ - No external activation required
23
+ - Appropriate for testing, batch processing, simulations
24
+
25
+ **External Activation** (TimerClock, InputMidiClock, ExternalTickClock):
26
+ - Requires external signal/control to begin generating ticks
27
+ - `transport.start` blocks waiting for activation
28
+ - Appropriate for live coding, DAW sync, external control
29
+
30
+ ### Available Clock Types
31
+
32
+ **DummyClock** - Simplified clock for testing (automatic activation):
33
+ - Fast playback without real-time constraints
34
+ - Immediately begins generating ticks
35
+ - Useful for test suites or batch generation
36
+ - No external dependencies
37
+
38
+ **TimerClock** - Internal high-precision timer-based clock (external activation):
39
+ - Standalone compositions with internal timing
40
+ - Requires calling `clock.start()` from another thread
41
+ - Configurable BPM (tempo) and ticks per beat
42
+ - Can dynamically change tempo during playback
43
+ - Appropriate for live coding clients
44
+
45
+ **InputMidiClock** - Synchronized to external MIDI Clock messages (external activation):
46
+ - DAW-synchronized playback
47
+ - Waits for MIDI "Start" (0xFA) message to begin ticks
48
+ - Automatically follows external MIDI Clock Start/Stop/Continue
49
+ - Locked to external timing source
50
+
51
+ **ExternalTickClock** - Manually triggered ticks (external activation):
52
+ - Testing and debugging with precise control
53
+ - Integration with external systems (game engines, etc.)
54
+ - Call `clock.tick()` manually to generate each tick
55
+ - Frame-by-frame control
56
+
57
+ ```ruby
58
+ require 'musa-dsl'
59
+
60
+ # TimerClock - Internal timer-based timing
61
+ timer_clock = Musa::Clock::TimerClock.new(
62
+ bpm: 120, # Beats per minute
63
+ ticks_per_beat: 24 # Resolution
64
+ )
65
+
66
+ # InputMidiClock - Synchronized to external MIDI Clock
67
+ require 'midi-communications'
68
+ midi_input = MIDICommunications::Input.gets # Select MIDI input
69
+
70
+ midi_clock = Musa::Clock::InputMidiClock.new(midi_input)
71
+
72
+ # ExternalTickClock - Manual tick control
73
+ external_clock = Musa::Clock::ExternalTickClock.new
74
+
75
+ # DummyClock - For testing (100 ticks)
76
+ dummy_clock = Musa::Clock::DummyClock.new(100)
77
+ ```
78
+
79
+ ## Transport - Playback Lifecycle Manager
80
+
81
+ **Transport** connects a clock to a sequencer and manages the playback lifecycle. It provides methods for starting/stopping playback, seeking to different positions, and registering callbacks for lifecycle events.
82
+
83
+ **Lifecycle phases:**
84
+ 1. **before_begin** - Run once before first start (initialization)
85
+ 2. **on_start** - Run each time transport starts
86
+ 3. **Running** - Clock generates ticks → sequencer processes events
87
+ 4. **on_change_position** - Run when position jumps/seeks
88
+ 5. **after_stop** - Run when transport stops
89
+
90
+ **Key methods:**
91
+ - `start` - Start playback (blocks while running)
92
+ - `stop` - Stop playback
93
+ - `change_position_to(bars: n)` - Seek to position (in bars)
94
+
95
+ ```ruby
96
+ require 'musa-dsl'
97
+
98
+ # Create clock
99
+ clock = Musa::Clock::TimerClock.new(bpm: 120, ticks_per_beat: 24)
100
+
101
+ # Create transport
102
+ transport = Musa::Transport::Transport.new(
103
+ clock,
104
+ 4, # beats_per_bar (time signature numerator)
105
+ 24 # ticks_per_beat (resolution)
106
+ )
107
+
108
+ # Access sequencer through transport
109
+ sequencer = transport.sequencer
110
+
111
+ # Schedule events
112
+ sequencer.at 1 do
113
+ puts "Starting at bar 1!"
114
+ end
115
+
116
+ sequencer.at 4 do
117
+ puts "Reached bar 4"
118
+ transport.stop
119
+ end
120
+
121
+ # Register lifecycle callbacks
122
+ transport.before_begin do
123
+ puts "Initializing (runs once)..."
124
+ end
125
+
126
+ transport.on_start do
127
+ puts "Transport started!"
128
+ end
129
+
130
+ transport.after_stop do
131
+ puts "Transport stopped, cleaning up..."
132
+ end
133
+
134
+ # IMPORTANT: TimerClock requires external activation
135
+ # Start transport in background thread (it will block waiting)
136
+ thread = Thread.new { transport.start }
137
+ sleep 0.1 # Let transport initialize
138
+
139
+ # Activate clock from external control (e.g., live coding client)
140
+ clock.start # NOW ticks begin generating
141
+
142
+ # Wait for completion
143
+ thread.join
144
+
145
+ # Seeking example (in separate context)
146
+ # transport.change_position_to(bars: 2) # Jump to bar 2
147
+ ```
148
+
149
+ **Complete example with MIDI Clock synchronization:**
150
+
151
+ ```ruby
152
+ require 'musa-dsl'
153
+ require 'midi-communications'
154
+
155
+ # Setup MIDI-synchronized clock
156
+ midi_input = MIDICommunications::Input.gets
157
+ clock = Musa::Clock::InputMidiClock.new(midi_input)
158
+
159
+ # Create transport
160
+ transport = Musa::Transport::Transport.new(clock, 4, 24)
161
+
162
+ # Schedule events
163
+ transport.sequencer.at 1 do
164
+ puts "Synchronized start at bar 1!"
165
+ end
166
+
167
+ # Start and wait for MIDI Clock Start message
168
+ transport.start
169
+ ```
170
+
171
+ ## API Reference
172
+
173
+ **Complete API documentation:**
174
+ - [Musa::Transport](https://rubydoc.info/gems/musa-dsl/Musa/Transport) - Playback lifecycle management
175
+ - [Musa::Clock](https://rubydoc.info/gems/musa-dsl/Musa/Clock) - Timing sources and clock implementations
176
+
177
+ **Source code:** `lib/transport/`
@@ -1,6 +1,74 @@
1
+ require_relative 'extension'
2
+
1
3
  module Musa
2
4
  module Extension
5
+ # Refinement that expands Range objects within arrays into their constituent elements.
6
+ #
7
+ # This is particularly useful in musical contexts where arrays may contain both
8
+ # individual values and ranges (like MIDI note ranges or channel ranges), and you
9
+ # need to work with the fully expanded list.
10
+ #
11
+ # ## Use Cases
12
+ #
13
+ # - Expanding MIDI channel specifications: [0, 2..4, 7] → [0, 2, 3, 4, 7]
14
+ # - Expanding note ranges in chord definitions
15
+ # - Processing mixed literal and range values in musical parameters
16
+ # - Any scenario where ranges need to be flattened for iteration
17
+ #
18
+ # @example Basic usage
19
+ # using Musa::Extension::ExplodeRanges
20
+ #
21
+ # [1, 3..5, 8].explode_ranges
22
+ # # => [1, 3, 4, 5, 8]
23
+ #
24
+ # @example MIDI channels
25
+ # using Musa::Extension::ExplodeRanges
26
+ #
27
+ # channels = [0, 2..4, 7, 9..10]
28
+ # channels.explode_ranges
29
+ # # => [0, 2, 3, 4, 7, 9, 10]
30
+ #
31
+ # @example Mixed with other array methods
32
+ # using Musa::Extension::ExplodeRanges
33
+ #
34
+ # [1..3, 5, 7..9].explode_ranges.map { |n| n * 2 }
35
+ # # => [2, 4, 6, 10, 14, 16, 18]
36
+ #
37
+ # @see Musa::MIDIVoices::MIDIVoices#initialize Uses this for channel expansion
38
+ # @note This refinement must be activated with `using Musa::Extension::ExplodeRanges`
39
+ #
40
+ # ## Methods Added
41
+ #
42
+ # ### Array
43
+ # - {Array#explode_ranges} - Expands all Range objects in the array into their individual elements
3
44
  module ExplodeRanges
45
+ # @!method explode_ranges
46
+ # Expands all Range objects in the array into their individual elements.
47
+ #
48
+ # Iterates through the array and converts any Range objects to their
49
+ # constituent elements via `to_a`, leaving non-Range elements unchanged.
50
+ # The result is a new flat array.
51
+ #
52
+ # @note This method is added to Array via refinement. Requires `using Musa::Extension::ExplodeRanges`.
53
+ #
54
+ # @return [Array] new array with all ranges expanded.
55
+ #
56
+ # @example Empty ranges
57
+ # using Musa::Extension::ExplodeRanges
58
+ # [1, (5..4), 8].explode_ranges # (5..4) is empty
59
+ # # => [1, 8]
60
+ #
61
+ # @example Exclusive ranges
62
+ # using Musa::Extension::ExplodeRanges
63
+ # [1, (3...6), 9].explode_ranges
64
+ # # => [1, 3, 4, 5, 9]
65
+ #
66
+ # @example Nested arrays are NOT expanded recursively
67
+ # using Musa::Extension::ExplodeRanges
68
+ # [1, [2..4], 5].explode_ranges
69
+ # # => [1, [2..4], 5] # Inner range NOT expanded
70
+ class ::Array; end
71
+
4
72
  refine Array do
5
73
  def explode_ranges
6
74
  array = []