musa-dsl 0.30.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- metadata +87 -8
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Sequencer - Temporal Engine
|
|
2
|
+
|
|
3
|
+
The Sequencer manages time-based event scheduling with microsecond precision, supporting complex polyrhythmic and polytemporal structures.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require 'musa-dsl'
|
|
7
|
+
include Musa::All
|
|
8
|
+
|
|
9
|
+
# Setup: Create clock and transport (for real-time execution)
|
|
10
|
+
clock = TimerClock.new(bpm: 120, ticks_per_beat: 24)
|
|
11
|
+
transport = Transport.new(clock, 4, 24) # 4 beats per bar, 24 ticks per beat
|
|
12
|
+
|
|
13
|
+
# Define series outside DSL block (Series constructors not available in DSL context)
|
|
14
|
+
melody = S({ note: 60, duration: 1/2r }, { note: 62, duration: 1/2r },
|
|
15
|
+
{ note: 64, duration: 1/2r }, { note: 65, duration: 1/2r },
|
|
16
|
+
{ note: 67, duration: 1/2r }, { note: 65, duration: 1/2r },
|
|
17
|
+
{ note: 64, duration: 1/2r }, { note: 62, duration: 1/2r })
|
|
18
|
+
|
|
19
|
+
# Program sequencer using DSL
|
|
20
|
+
transport.sequencer.with do
|
|
21
|
+
# Custom event handlers (on/launch)
|
|
22
|
+
on :section_change do |name|
|
|
23
|
+
puts "Section: #{name}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Immediate event (now)
|
|
27
|
+
now do
|
|
28
|
+
launch :section_change, "Start"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Absolute positioning (at): event at bar 1
|
|
32
|
+
at 1 do
|
|
33
|
+
puts "Bar 1: position #{position}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Relative positioning (wait): event 2 bars later
|
|
37
|
+
wait 2 do
|
|
38
|
+
puts "Bar 3: position #{position}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Play series (play): reproduces series with automatic timing
|
|
42
|
+
at 5 do
|
|
43
|
+
play melody do |note:, duration:, control:|
|
|
44
|
+
puts "Playing note: #{note}, duration: #{duration}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Recurring event (every) with stop control
|
|
49
|
+
beat_loop = nil
|
|
50
|
+
at 10 do
|
|
51
|
+
# Store control object to stop it later
|
|
52
|
+
beat_loop = every 2, duration: 10 do
|
|
53
|
+
puts "Beat at position #{position}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Stop the beat loop at bar 18
|
|
58
|
+
at 18 do
|
|
59
|
+
beat_loop.stop if beat_loop
|
|
60
|
+
puts "Beat loop stopped"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Animated value (move) from 0 to 10 over 4 bars
|
|
64
|
+
at 20 do
|
|
65
|
+
move from: 0, to: 10, duration: 4, every: 1/2r do |value|
|
|
66
|
+
puts "Value: #{value.round(2)}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Multi-parameter animation (move with hash)
|
|
71
|
+
at 25 do
|
|
72
|
+
move from: { pitch: 60, vel: 60 },
|
|
73
|
+
to: { pitch: 72, vel: 100 },
|
|
74
|
+
duration: 2,
|
|
75
|
+
every: 1/4r do |values|
|
|
76
|
+
puts "Pitch: #{values[:pitch].round}, Velocity: #{values[:vel].round}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Final event
|
|
81
|
+
at 30 do
|
|
82
|
+
launch :section_change, "End"
|
|
83
|
+
puts "Finished at position #{position}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Start real-time playback
|
|
88
|
+
transport.start
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
**Complete API documentation:**
|
|
94
|
+
- [Musa::Sequencer](https://rubydoc.info/gems/musa-dsl/Musa/Sequencer) - Main sequencer class and DSL
|
|
95
|
+
|
|
96
|
+
**Source code:** `lib/sequencer/`
|
|
97
|
+
|
|
98
|
+
|
|
@@ -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
|
+
|