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.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/Gemfile +0 -1
- 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 +544 -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 +215 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
- data/lib/musa-dsl/generative/markov.rb +135 -3
- data/lib/musa-dsl/generative/rules.rb +312 -4
- data/lib/musa-dsl/generative/variatio.rb +286 -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 +113 -2
- data/lib/musa-dsl/midi/midi-voices.rb +275 -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 +353 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +1384 -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 +54 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -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 +57 -7
- data/lib/musa-dsl/series/queue-serie.rb +78 -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 +2 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +25 -18
- 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 = []
|