musa-dsl 0.41.0 → 0.42.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 +1 -0
- data/README.md +15 -1
- data/docs/README.md +1 -0
- data/docs/subsystems/datasets.md +75 -0
- data/docs/subsystems/generative.md +92 -6
- data/docs/subsystems/music.md +33 -14
- data/docs/subsystems/transport.md +26 -0
- data/lib/musa-dsl/datasets/dataset.rb +2 -0
- data/lib/musa-dsl/datasets/gdv.rb +3 -3
- data/lib/musa-dsl/datasets/p.rb +1 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +4 -2
- data/lib/musa-dsl/datasets/score.rb +3 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +3 -1
- data/lib/musa-dsl/generative/markov.rb +1 -1
- data/lib/musa-dsl/midi/midi-voices.rb +3 -1
- data/lib/musa-dsl/music/chord-definition.rb +7 -5
- data/lib/musa-dsl/music/chord-definitions.rb +37 -0
- data/lib/musa-dsl/music/chords.rb +69 -47
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +1 -1
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +1 -1
- data/lib/musa-dsl/music/scales.rb +219 -107
- data/lib/musa-dsl/musicxml/builder/note.rb +31 -92
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +33 -94
- data/lib/musa-dsl/musicxml/builder/rest.rb +30 -91
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +31 -91
- data/lib/musa-dsl/neumas/array-to-neumas.rb +1 -1
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +2 -2
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +367 -3
- data/lib/musa-dsl/series/base-series.rb +250 -240
- data/lib/musa-dsl/series/buffer-serie.rb +10 -5
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +6 -3
- data/lib/musa-dsl/series/main-serie-constructors.rb +19 -15
- data/lib/musa-dsl/series/main-serie-operations.rb +74 -29
- data/lib/musa-dsl/series/proxy-serie.rb +5 -1
- data/lib/musa-dsl/series/quantizer-serie.rb +4 -2
- data/lib/musa-dsl/series/queue-serie.rb +2 -1
- data/lib/musa-dsl/series/series-composer.rb +5 -2
- data/lib/musa-dsl/series/timed-serie.rb +8 -4
- data/lib/musa-dsl/transport/timer-clock.rb +4 -2
- data/lib/musa-dsl/transport/timer.rb +27 -4
- data/lib/musa-dsl/version.rb +1 -2
- data/musa-dsl.gemspec +0 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f157001c0d45494eda872c34d4d73c161b686da78f0192868637873845ca62e
|
|
4
|
+
data.tar.gz: 2d93034d622985b67ae894d0cf7f4a5acdba787b4453fd6dbad08aa88d096866
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4dc00ab7d48971758d26bf982e8b61771fe81c26bd9e7cdedba97e457d3c97ff8286f7e53689712c97bb10674fe5f59261ee1f1186a1f20dbda87b62a16aa858
|
|
7
|
+
data.tar.gz: 9da464c4a8cfce824392e1c37b4987b84bde21c59924da5daf34b97b46c183d70d06afff212ab0de7d5b45971c7267aab5260730509b21522f12c49ab5474d46
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -58,6 +58,20 @@ Detailed tutorial showing the Neuma notation system for composing melodies with
|
|
|
58
58
|
|
|
59
59
|
**📖 [Complete Tutorial](docs/getting-started/tutorial.md)**
|
|
60
60
|
|
|
61
|
+
## Demo Projects
|
|
62
|
+
|
|
63
|
+
A collection of 22+ working demo projects covering the full spectrum of Musa DSL capabilities:
|
|
64
|
+
|
|
65
|
+
- **Basic concepts**: Setup, series, neumas, canon
|
|
66
|
+
- **Generative tools**: Markov chains, Variatio, Darwin, Grammar, Rules, Matrix
|
|
67
|
+
- **DAW integration**: MIDI sync, live coding, clock modes
|
|
68
|
+
- **External protocols**: OSC with SuperCollider and Max/MSP
|
|
69
|
+
- **Advanced patterns**: Event architecture, parameter automation, multi-phase compositions
|
|
70
|
+
|
|
71
|
+
Each demo is a complete, runnable project with documentation explaining the concepts demonstrated.
|
|
72
|
+
|
|
73
|
+
**📦 [musadsl-demo Repository](https://github.com/javier-sy/musadsl-demo)**
|
|
74
|
+
|
|
61
75
|
## System Architecture
|
|
62
76
|
|
|
63
77
|
MusaDSL is a comprehensive ecosystem consisting of a core framework (musa-dsl) and associated projects for communication, development, and integration.
|
|
@@ -230,4 +244,4 @@ Ruby refinements and metaprogramming utilities: Arrayfy, Hashify, ExplodeRanges,
|
|
|
230
244
|
|
|
231
245
|
## License
|
|
232
246
|
|
|
233
|
-
[Musa-DSL](https://github.com/javier-sy/musa-dsl) Copyright (c) 2016-
|
|
247
|
+
[Musa-DSL](https://github.com/javier-sy/musa-dsl) Copyright (c) 2016-2026 [Javier Sánchez Yeste](https://yeste.studio), licensed under LGPL 3.0 License
|
data/docs/README.md
CHANGED
|
@@ -52,6 +52,7 @@ For users extending the DSL or integrating deeply:
|
|
|
52
52
|
2. Explore [Music](subsystems/music.md) (scales & chords)
|
|
53
53
|
3. Try [Generative](subsystems/generative.md) algorithms
|
|
54
54
|
4. Use [MusicXML Builder](subsystems/musicxml-builder.md) for scores
|
|
55
|
+
5. Explore [Demo Projects](https://github.com/javier-sy/musadsl-demo) - 22+ working examples
|
|
55
56
|
|
|
56
57
|
### Live Coding?
|
|
57
58
|
1. Set up [REPL](subsystems/repl.md)
|
data/docs/subsystems/datasets.md
CHANGED
|
@@ -52,6 +52,51 @@ E (base event)
|
|
|
52
52
|
- Glissandi and parameter sweeps (PS)
|
|
53
53
|
- Dynamics changes and other evolving parameters
|
|
54
54
|
|
|
55
|
+
## Duration Types in AbsD
|
|
56
|
+
|
|
57
|
+
AbsD defines three duration concepts that enable complex rhythmic structures:
|
|
58
|
+
|
|
59
|
+
| Field | Purpose | Default |
|
|
60
|
+
|-------|---------|---------|
|
|
61
|
+
| `:duration` | Total event process time | Required |
|
|
62
|
+
| `:note_duration` | Actual sound length (staccato/legato) | = duration |
|
|
63
|
+
| `:forward_duration` | Time until next event starts | = duration |
|
|
64
|
+
|
|
65
|
+
**Examples:**
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
include Musa::Datasets
|
|
69
|
+
|
|
70
|
+
# Normal note - all durations equal
|
|
71
|
+
{ pitch: 60, duration: 1.0 }.extend(AbsD)
|
|
72
|
+
|
|
73
|
+
# Staccato - sounds shorter than it "lasts"
|
|
74
|
+
{ pitch: 60, duration: 1.0, note_duration: 0.5 }.extend(AbsD)
|
|
75
|
+
|
|
76
|
+
# Chord (simultaneous notes) - next event starts immediately
|
|
77
|
+
{ pitch: 60, duration: 1.0, forward_duration: 0 }.extend(AbsD)
|
|
78
|
+
|
|
79
|
+
# Overlap (legato) - next note starts before this one ends
|
|
80
|
+
{ pitch: 60, duration: 1.0, forward_duration: 0.8 }.extend(AbsD)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Understanding DeltaD:**
|
|
84
|
+
|
|
85
|
+
DeltaD is the delta-encoding counterpart to AbsD. While AbsD stores absolute durations, DeltaD stores changes relative to a previous event. If duration hasn't changed from the previous event, it can be omitted for compression (as shown in GDV → GDVd conversions).
|
|
86
|
+
|
|
87
|
+
## Natural Keys
|
|
88
|
+
|
|
89
|
+
Each dataset type defines "natural keys" - semantically meaningful fields for that type:
|
|
90
|
+
|
|
91
|
+
| Module | Natural Keys |
|
|
92
|
+
|--------|--------------|
|
|
93
|
+
| **E** | `[]` (none) |
|
|
94
|
+
| **AbsD** | `[:duration, :note_duration, :forward_duration]` |
|
|
95
|
+
| **PDV** | AbsD keys + `[:pitch, :velocity]` |
|
|
96
|
+
| **GDV** | AbsD keys + `[:grade, :sharps, :octave, :velocity, :silence]` |
|
|
97
|
+
|
|
98
|
+
Custom keys (any not in NaturalKeys) are preserved through conversions, enabling you to attach composition-specific metadata that travels with your musical data.
|
|
99
|
+
|
|
55
100
|
## Extensibility
|
|
56
101
|
|
|
57
102
|
All datasets support custom parameters beyond their natural keys:
|
|
@@ -102,6 +147,36 @@ gdv.is_a?(Abs) # => true (GDV includes Abs)
|
|
|
102
147
|
gdv.is_a?(AbsD) # => true (GDV includes AbsD for duration)
|
|
103
148
|
```
|
|
104
149
|
|
|
150
|
+
## Using AbsD for Containers
|
|
151
|
+
|
|
152
|
+
AbsD is not just for notes - use it for any time-spanning structure that needs a duration. This is useful for chord progression steps, sections, or any compositional element that occupies time but isn't itself a single note.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
include Musa::Datasets
|
|
156
|
+
|
|
157
|
+
scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
158
|
+
|
|
159
|
+
# A chord progression step (not a note, but has duration)
|
|
160
|
+
step = {
|
|
161
|
+
duration: 2, # How long this step lasts
|
|
162
|
+
symbol: :I, # Chord symbol for reference
|
|
163
|
+
bass: { grade: 0, octave: -1, duration: 2, velocity: 0 }.extend(GDV),
|
|
164
|
+
chord: scale[:I].chord, # Chord object
|
|
165
|
+
chord_duration: 7/4r,
|
|
166
|
+
chord_velocity: -1
|
|
167
|
+
}.extend(AbsD)
|
|
168
|
+
|
|
169
|
+
# Now the sequencer can use step.duration for timing
|
|
170
|
+
# while the step contains all the musical information needed to render it
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**When to use AbsD directly:**
|
|
174
|
+
|
|
175
|
+
- Containers that hold multiple notes (chords, arpeggios)
|
|
176
|
+
- Section markers with timing information
|
|
177
|
+
- Harmonic rhythm steps in a progression
|
|
178
|
+
- Any structure where you need `:duration` but PDV/GDV semantics don't apply
|
|
179
|
+
|
|
105
180
|
## Dataset Conversions
|
|
106
181
|
|
|
107
182
|
Datasets provide rich conversion capabilities for transforming between different representations, each optimized for specific compositional tasks:
|
|
@@ -80,18 +80,30 @@ limited_chords = variatio.on(root: [60, 64])
|
|
|
80
80
|
|
|
81
81
|
Production system with growth and pruning rules (similar to L-systems). Generates tree structures by applying sequential growth rules to create branches and validation rules to prune invalid paths. Useful for harmonic progressions with voice leading rules, melodic variations with contour constraints, or rhythmic patterns following metric rules.
|
|
82
82
|
|
|
83
|
+
### Execution Model
|
|
84
|
+
|
|
85
|
+
**Critical concept: Each `grow` rule = 1 level of depth in the tree.**
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Seed → GrowRule1 → GrowRule2 → GrowRule3 → ... → Result
|
|
89
|
+
↓ ↓ ↓
|
|
90
|
+
branches branches branches
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Rules are processed **sequentially**: the first grow rule applies to the seed, the second grow rule applies to all results from the first, and so on. If you define N grow rules, you get N levels of transformation.
|
|
94
|
+
|
|
83
95
|
**Constructor parameters:**
|
|
84
96
|
- `&block` - DSL block defining grow rules, cut rules, and end condition
|
|
85
97
|
|
|
86
98
|
**DSL methods:**
|
|
87
99
|
- `grow(name, &block)` - Define growth rule that generates new branches
|
|
88
|
-
- Block receives: `|object, history, **params|`
|
|
100
|
+
- Block receives: `|object|` or `|object, history|` or `|object, history, **params|`
|
|
89
101
|
- Use `branch(new_object)` to create new possibilities
|
|
90
102
|
- `cut(reason, &block)` - Define pruning rule to eliminate invalid paths
|
|
91
|
-
- Block receives: `|object, history, **params|`
|
|
103
|
+
- Block receives: `|object|` or `|object, history|` or `|object, history, **params|`
|
|
92
104
|
- Use `prune` to reject current branch
|
|
93
105
|
- `ended_when(&block)` - Define end condition to mark complete branches
|
|
94
|
-
- Block receives: `|object, history, **params|`
|
|
106
|
+
- Block receives: `|object|` or `|object, history|` or `|object, history, **params|`
|
|
95
107
|
- Return `true` to mark branch as complete
|
|
96
108
|
|
|
97
109
|
**Execution methods:**
|
|
@@ -103,23 +115,83 @@ Production system with growth and pruning rules (similar to L-systems). Generate
|
|
|
103
115
|
- `combinations` - Returns array of all valid complete paths through tree
|
|
104
116
|
- `fish` - Returns array of all valid endpoint objects
|
|
105
117
|
|
|
118
|
+
### Important: The `history` Parameter
|
|
119
|
+
|
|
120
|
+
**Warning:** When using a single seed (the typical case), the `history` parameter is **always an empty array `[]`** in all grow rules. This is because `history` contains the path of *confirmed* objects from previous seeds when using multiple seeds.
|
|
121
|
+
|
|
122
|
+
**Do NOT rely on `history` for tracking sequence length with a single seed:**
|
|
123
|
+
```ruby
|
|
124
|
+
# THIS DOES NOT WORK as expected with a single seed:
|
|
125
|
+
ended_when do |pitch, history|
|
|
126
|
+
history.size == 7 # Always false! history is []
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Recommended Pattern: Cumulative State
|
|
131
|
+
|
|
132
|
+
The recommended approach is to make your **object accumulate state** rather than relying on `history`:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
require 'musa-dsl'
|
|
136
|
+
|
|
137
|
+
# Generate 8-note melodies with interval constraints
|
|
138
|
+
rules = Musa::Rules::Rules.new do
|
|
139
|
+
# Each grow rule adds ONE note. For 8 notes, we need 7 grow rules (seed + 7).
|
|
140
|
+
7.times do
|
|
141
|
+
grow 'add next note' do |melody, max_interval:|
|
|
142
|
+
last_note = melody.last
|
|
143
|
+
(-max_interval..max_interval).each do |interval|
|
|
144
|
+
next if interval == 0 # Skip unisons
|
|
145
|
+
new_note = last_note + interval
|
|
146
|
+
branch melody + [new_note] if new_note.between?(48, 84) # Range limit
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Cut rule: no repeated notes
|
|
152
|
+
cut 'no repetition' do |melody|
|
|
153
|
+
prune if melody.size >= 2 && melody[-1] == melody[-2]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# End condition checks the OBJECT size, not history
|
|
157
|
+
ended_when do |melody|
|
|
158
|
+
melody.size == 8
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Seed is [[60]] - double array to prevent automatic flattening
|
|
163
|
+
tree = rules.apply([[60]], max_interval: 4)
|
|
164
|
+
|
|
165
|
+
# Extract all valid 8-note melodies
|
|
166
|
+
melodies = tree.combinations.map(&:last)
|
|
167
|
+
puts "Generated #{melodies.size} valid melodies"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Key points:**
|
|
171
|
+
- Use `N.times { grow }` to create N levels of depth
|
|
172
|
+
- The object (`melody`) accumulates state as an array
|
|
173
|
+
- Check `object.size` in `ended_when`, not `history.size`
|
|
174
|
+
- Seed with `[[value]]` (double array) to prevent Ruby's `arrayfy` from flattening
|
|
175
|
+
|
|
176
|
+
### Basic Example: Chord Voicings
|
|
177
|
+
|
|
106
178
|
```ruby
|
|
107
179
|
require 'musa-dsl'
|
|
108
180
|
|
|
109
181
|
# Build chord voicings by adding notes sequentially
|
|
110
182
|
rules = Musa::Rules::Rules.new do
|
|
111
|
-
# Step 1: Choose root note
|
|
183
|
+
# Step 1: Choose root note (1st grow rule = 1st level)
|
|
112
184
|
grow 'add root' do |seed|
|
|
113
185
|
[60, 64, 67].each { |root| branch [root] } # C, E, G
|
|
114
186
|
end
|
|
115
187
|
|
|
116
|
-
# Step 2: Add third (
|
|
188
|
+
# Step 2: Add third (2nd grow rule = 2nd level)
|
|
117
189
|
grow 'add third' do |chord|
|
|
118
190
|
branch chord + [chord[0] + 4] # Major third
|
|
119
191
|
branch chord + [chord[0] + 3] # Minor third
|
|
120
192
|
end
|
|
121
193
|
|
|
122
|
-
# Step 3: Add fifth
|
|
194
|
+
# Step 3: Add fifth (3rd grow rule = 3rd level)
|
|
123
195
|
grow 'add fifth' do |chord|
|
|
124
196
|
branch chord + [chord[0] + 7]
|
|
125
197
|
end
|
|
@@ -149,6 +221,20 @@ voicings = combinations.map { |path| path.last }
|
|
|
149
221
|
tree_with_params = rules.apply(0, max_interval: 7)
|
|
150
222
|
```
|
|
151
223
|
|
|
224
|
+
### Multiple Seeds (Advanced)
|
|
225
|
+
|
|
226
|
+
When passing an array of seeds `[s1, s2, s3]`, they are processed **sequentially and chained**: each seed starts from the valid endpoints of the previous seed's tree. In this case, `history` contains the confirmed path from previous seeds.
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# With multiple seeds, history IS populated:
|
|
230
|
+
rules.apply([seed1, seed2, seed3])
|
|
231
|
+
# seed1 processes → fish valid endpoints
|
|
232
|
+
# seed2 starts from each endpoint, history contains seed1's path
|
|
233
|
+
# seed3 starts from seed2's endpoints, history contains seed1+seed2's paths
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This is useful for multi-phase generation where each phase has different rules, but for most use cases the single-seed cumulative state pattern is simpler and recommended.
|
|
237
|
+
|
|
152
238
|
## Generative Grammar
|
|
153
239
|
|
|
154
240
|
Formal grammars with combinatorial generation using operators. Useful for generating melodic patterns with rhythmic constraints, harmonic progressions, or variations of musical motifs.
|
data/docs/subsystems/music.md
CHANGED
|
@@ -123,8 +123,8 @@ c_major[:V] # => Dominant (G)
|
|
|
123
123
|
pitch = c_major.tonic.pitch # => 60
|
|
124
124
|
|
|
125
125
|
# Navigate with octaves
|
|
126
|
-
note = c_major[2].
|
|
127
|
-
pitch_with_octave = note.pitch
|
|
126
|
+
note = c_major[2].at_octave(1) # E transposed up 1 octave
|
|
127
|
+
pitch_with_octave = note.pitch # => 76
|
|
128
128
|
|
|
129
129
|
# Chromatic operations - sharp and flat
|
|
130
130
|
c_sharp = c_major.tonic.sharp # => C# (chromatic, +1 semitone)
|
|
@@ -211,10 +211,12 @@ seventh_chord = i_chord.with_size(:seventh) # C major 7th
|
|
|
211
211
|
ninth_chord = i_chord.with_size(:ninth) # C major 9th
|
|
212
212
|
|
|
213
213
|
# Voicing modifications - move specific tones to different octaves
|
|
214
|
-
voiced = i_chord.
|
|
214
|
+
voiced = i_chord.with_move(root: -1, fifth: 1) # Root down, fifth up
|
|
215
|
+
i_chord.move # => { root: -1, fifth: 1 } (current settings)
|
|
215
216
|
|
|
216
217
|
# Duplicate tones in other octaves
|
|
217
|
-
doubled = i_chord.
|
|
218
|
+
doubled = i_chord.with_duplicate(root: -2, third: [-1, 1]) # Root 2 down, third 1 down and 1 up
|
|
219
|
+
i_chord.duplicate # => { root: -2, third: [-1, 1] } (current settings)
|
|
218
220
|
|
|
219
221
|
# Transpose entire chord
|
|
220
222
|
lower = i_chord.octave(-1) # Move chord down one octave
|
|
@@ -245,28 +247,45 @@ c_major.degree_of_chord(cm) # => nil
|
|
|
245
247
|
```ruby
|
|
246
248
|
# Get the same chord but with a different scale as context
|
|
247
249
|
g_mixolydian = Scales.et12[440.0].mixolydian[67]
|
|
248
|
-
g7_in_mixolydian =
|
|
250
|
+
g7_in_mixolydian = g7.as_chord_in_scale(g_mixolydian)
|
|
249
251
|
g7_in_mixolydian.scale # => G Mixolydian scale
|
|
250
252
|
g_mixolydian.degree_of_chord(g7_in_mixolydian) # => 0 (I degree)
|
|
251
253
|
```
|
|
252
254
|
|
|
255
|
+
#### Creating Chords from Scale Degrees
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
# Create chords directly from scale degrees using Scale#chord_on
|
|
259
|
+
i_chord = scale.chord_on(0) # Tonic triad
|
|
260
|
+
v7 = scale.chord_on(:dominant, :seventh) # V7
|
|
261
|
+
iv_maj7 = scale.chord_on(:IV, :seventh, :major) # IVmaj7
|
|
262
|
+
|
|
263
|
+
# Equivalent to using the note method then chord:
|
|
264
|
+
scale[0].chord # Same as scale.chord_on(0)
|
|
265
|
+
scale[:dominant].chord(:seventh) # Same as scale.chord_on(:dominant, :seventh)
|
|
266
|
+
|
|
267
|
+
# With voicing parameters
|
|
268
|
+
scale.chord_on(:I, :seventh, move: {root: -1})
|
|
269
|
+
scale.chord_on(0, :triad, duplicate: {root: 1})
|
|
270
|
+
```
|
|
271
|
+
|
|
253
272
|
#### Finding Scales That Contain a Chord
|
|
254
273
|
|
|
255
274
|
```ruby
|
|
256
275
|
g_triad = c_major.dominant.chord # G-B-D
|
|
257
276
|
|
|
258
277
|
# Search in diatonic scales
|
|
259
|
-
g_triad.
|
|
278
|
+
g_triad.search_in_scales(family: :diatonic)
|
|
260
279
|
# => [Chord in C major (V), Chord in G major (I), Chord in D major (IV), ...]
|
|
261
280
|
|
|
262
281
|
# Search using metadata filters
|
|
263
|
-
g_triad.
|
|
282
|
+
g_triad.search_in_scales(family: :greek_modes, brightness: -1..1)
|
|
264
283
|
|
|
265
284
|
# Search in all scale types
|
|
266
|
-
g_triad.
|
|
285
|
+
g_triad.search_in_scales
|
|
267
286
|
|
|
268
287
|
# Each result has its scale context
|
|
269
|
-
g_triad.
|
|
288
|
+
g_triad.search_in_scales(family: :diatonic).each do |chord|
|
|
270
289
|
scale = chord.scale
|
|
271
290
|
degree = scale.degree_of_chord(chord)
|
|
272
291
|
puts "#{scale.kind.class.id} rooted on #{scale.root_pitch}: degree #{degree}"
|
|
@@ -283,10 +302,10 @@ end
|
|
|
283
302
|
tuning = Scales.et12[440.0]
|
|
284
303
|
|
|
285
304
|
# Search at ScaleKind level
|
|
286
|
-
tuning.major.
|
|
305
|
+
tuning.major.find_chord_in_scales(g_triad)
|
|
287
306
|
|
|
288
307
|
# Search at ScaleSystemTuning level with metadata filters
|
|
289
|
-
tuning.
|
|
308
|
+
tuning.search_chord_in_scales(g7, family: :diatonic, roots: 60..71)
|
|
290
309
|
```
|
|
291
310
|
|
|
292
311
|
### Scale Kind Metadata
|
|
@@ -432,16 +451,16 @@ tuning.scale_kinds(family: :greek_modes) { |klass| klass.metadata[:brightness]&.
|
|
|
432
451
|
|
|
433
452
|
#### Integration with Chord Search
|
|
434
453
|
|
|
435
|
-
The `
|
|
454
|
+
The `search_chord_in_scales` method uses the same metadata filtering:
|
|
436
455
|
|
|
437
456
|
```ruby
|
|
438
457
|
g7 = tuning.major[60].dominant.chord(:seventh)
|
|
439
458
|
|
|
440
459
|
# Find G7 in diatonic scales
|
|
441
|
-
tuning.
|
|
460
|
+
tuning.search_chord_in_scales(g7, family: :diatonic)
|
|
442
461
|
|
|
443
462
|
# Find G7 in scales with specific brightness
|
|
444
|
-
tuning.
|
|
463
|
+
tuning.search_chord_in_scales(g7, brightness: -1..1)
|
|
445
464
|
```
|
|
446
465
|
|
|
447
466
|
## Defining Custom Scale Systems, Scale Kinds, and Chord Definitions
|
|
@@ -87,6 +87,32 @@ dummy_clock = Musa::Clock::DummyClock.new(100)
|
|
|
87
87
|
4. **on_change_position** - Run when position jumps/seeks
|
|
88
88
|
5. **after_stop** - Run when transport stops
|
|
89
89
|
|
|
90
|
+
### Clean Shutdown
|
|
91
|
+
|
|
92
|
+
To cleanly terminate a transport using TimerClock:
|
|
93
|
+
|
|
94
|
+
1. Call `transport.stop` from within a scheduled event
|
|
95
|
+
2. This calls `clock.terminate` internally
|
|
96
|
+
3. The clock's run loop exits
|
|
97
|
+
4. `transport.start` returns
|
|
98
|
+
5. Your code continues after `transport.start` for cleanup
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Example: Self-terminating composition
|
|
102
|
+
transport.sequencer.at 10 do
|
|
103
|
+
puts "Composition finished"
|
|
104
|
+
transport.stop # This will cause transport.start to return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
transport.start # Blocks until transport.stop is called
|
|
108
|
+
puts "Cleanup..." # Executes after stop
|
|
109
|
+
output.close
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Note:** For `InputMidiClock`, stop/start cycles are controlled by the DAW
|
|
113
|
+
and don't terminate the process. The `terminate` method can be used explicitly
|
|
114
|
+
if you need the run loop to exit.
|
|
115
|
+
|
|
90
116
|
**Key methods:**
|
|
91
117
|
- `start` - Start playback (blocks while running)
|
|
92
118
|
- `stop` - Stop playback
|
|
@@ -204,7 +204,7 @@ module Musa::Datasets
|
|
|
204
204
|
pdv[:pitch] = if self[:silence]
|
|
205
205
|
:silence
|
|
206
206
|
else
|
|
207
|
-
scale[self[:grade]].sharp(self[:sharps] || 0).
|
|
207
|
+
scale[self[:grade]].sharp(self[:sharps] || 0).at_octave(self[:octave] || 0).pitch
|
|
208
208
|
end
|
|
209
209
|
end
|
|
210
210
|
|
|
@@ -399,8 +399,8 @@ module Musa::Datasets
|
|
|
399
399
|
(self[:sharps] || 0) != (previous[:sharps] || 0)
|
|
400
400
|
|
|
401
401
|
gdvd[:delta_grade] =
|
|
402
|
-
scale[self[:grade]].
|
|
403
|
-
scale[previous[:grade]].
|
|
402
|
+
scale[self[:grade]].at_octave(self[:octave]).wide_grade -
|
|
403
|
+
scale[previous[:grade]].at_octave(previous[:octave]).wide_grade
|
|
404
404
|
|
|
405
405
|
gdvd[:delta_sharps] = (self[:sharps] || 0) - (previous[:sharps] || 0)
|
|
406
406
|
end
|
data/lib/musa-dsl/datasets/p.rb
CHANGED
|
@@ -42,7 +42,8 @@ module Musa::Datasets::Score::ToMXML
|
|
|
42
42
|
# @note This method is experimental and currently unused. See TODO comment.
|
|
43
43
|
#
|
|
44
44
|
# @api private
|
|
45
|
-
def initialize(element, bar, bar_size =
|
|
45
|
+
def initialize(element, bar, bar_size = Rational(1))
|
|
46
|
+
# TODO remove (unused because of bad strategy to time groups)
|
|
46
47
|
@continue_from_previous_bar = element[:start] < bar
|
|
47
48
|
@continue_to_next_bar = element[:finish] >= bar + bar_size
|
|
48
49
|
|
|
@@ -99,7 +100,8 @@ module Musa::Datasets::Score::ToMXML
|
|
|
99
100
|
#
|
|
100
101
|
# @api private
|
|
101
102
|
# @todo Complete or remove this experimental method
|
|
102
|
-
def time_and_tuplet_optimize(elements, bar, bar_size =
|
|
103
|
+
def time_and_tuplet_optimize(elements, bar, bar_size = Rational(1))
|
|
104
|
+
# TODO remove (unused because of bad strategy to time groups)
|
|
103
105
|
decompositions = elements.collect { |pdv| ElementDurationDecomposition.new(pdv, bar, bar_size) }
|
|
104
106
|
|
|
105
107
|
denominators = decompositions.collect { |g| g.duration_decomposition.collect { |d| d.to_r.denominator } }.flatten.uniq
|
|
@@ -143,12 +143,14 @@ module Musa::Datasets
|
|
|
143
143
|
# @return [Object, nil] attribute value
|
|
144
144
|
#
|
|
145
145
|
# @api private
|
|
146
|
-
def
|
|
146
|
+
def get(key)
|
|
147
147
|
if NaturalKeys.include?(key) && self.respond_to?(key)
|
|
148
148
|
self.send(key)
|
|
149
149
|
end
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
+
alias_method :[], :get
|
|
153
|
+
|
|
152
154
|
# Returns latest finish time of all events.
|
|
153
155
|
#
|
|
154
156
|
# @return [Rational, nil] latest finish time, or nil if score is empty
|
|
@@ -369,10 +369,12 @@ module Musa
|
|
|
369
369
|
# @param index [Integer] option index
|
|
370
370
|
#
|
|
371
371
|
# @return [Node] option converted to node
|
|
372
|
-
def
|
|
372
|
+
def get(index)
|
|
373
373
|
options[index].to_serie.to_node
|
|
374
374
|
end
|
|
375
375
|
|
|
376
|
+
alias_method :[], :get
|
|
377
|
+
|
|
376
378
|
# Converts options to series.
|
|
377
379
|
#
|
|
378
380
|
# Generates options and converts them to a {Musa::Series::Serie}.
|
|
@@ -97,7 +97,7 @@ module Musa
|
|
|
97
97
|
# Implements {Musa::Series::Serie} interface for integration with series operations.
|
|
98
98
|
class Markov
|
|
99
99
|
# TODO: adapt to series prototyping
|
|
100
|
-
include Musa::Series::Serie
|
|
100
|
+
include Musa::Series::Serie::Base
|
|
101
101
|
|
|
102
102
|
# Creates Markov chain generator.
|
|
103
103
|
#
|
|
@@ -385,10 +385,12 @@ module Musa
|
|
|
385
385
|
end
|
|
386
386
|
|
|
387
387
|
# @return [Integer, nil] last value assigned to the controller.
|
|
388
|
-
def
|
|
388
|
+
def get(controller_number_or_symbol)
|
|
389
389
|
@controller[number_of(controller_number_or_symbol)]
|
|
390
390
|
end
|
|
391
391
|
|
|
392
|
+
alias_method :[], :get
|
|
393
|
+
|
|
392
394
|
# Resolves a controller reference to its MIDI CC number.
|
|
393
395
|
#
|
|
394
396
|
# @param controller_number_or_symbol [Integer, Symbol] CC number or alias.
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require 'set'
|
|
2
|
-
|
|
3
1
|
module Musa
|
|
4
2
|
# Chord construction and manipulation framework.
|
|
5
3
|
#
|
|
@@ -115,10 +113,14 @@ module Musa
|
|
|
115
113
|
# @example
|
|
116
114
|
# ChordDefinition[:maj] # => <ChordDefinition :maj>
|
|
117
115
|
# ChordDefinition[:min7] # => <ChordDefinition :min7>
|
|
118
|
-
def self.
|
|
116
|
+
def self.get(name)
|
|
119
117
|
@definitions[name]
|
|
120
118
|
end
|
|
121
119
|
|
|
120
|
+
class << self
|
|
121
|
+
alias_method :[], :get
|
|
122
|
+
end
|
|
123
|
+
|
|
122
124
|
# Registers a new chord definition.
|
|
123
125
|
#
|
|
124
126
|
# Creates and registers a chord definition with specified intervals and features.
|
|
@@ -150,7 +152,7 @@ module Musa
|
|
|
150
152
|
definition.features.each { |k, v| @features_by_value[v] = k }
|
|
151
153
|
|
|
152
154
|
@feature_keys ||= Set[]
|
|
153
|
-
features.
|
|
155
|
+
features.each_key { |feature_name| @feature_keys << feature_name }
|
|
154
156
|
|
|
155
157
|
self
|
|
156
158
|
end
|
|
@@ -267,7 +269,7 @@ module Musa
|
|
|
267
269
|
|
|
268
270
|
# Checks if chord fits within a scale.
|
|
269
271
|
#
|
|
270
|
-
# @param scale [Scale] scale to check against
|
|
272
|
+
# @param scale [Scales::Scale] scale to check against
|
|
271
273
|
# @param chord_root_pitch [Integer] chord root pitch
|
|
272
274
|
# @return [Boolean] true if all chord notes are in scale
|
|
273
275
|
#
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
require_relative 'chord-definition'
|
|
2
2
|
|
|
3
|
+
# Standard chord definitions for Western harmony.
|
|
4
|
+
#
|
|
5
|
+
# This file registers the common chord types used in Western music theory,
|
|
6
|
+
# organized by size (number of notes) and quality (major, minor, etc.).
|
|
7
|
+
#
|
|
8
|
+
# ## Chord Categories
|
|
9
|
+
#
|
|
10
|
+
# ### Triads (3 notes)
|
|
11
|
+
# - **Major** (:maj) - Root, major 3rd, perfect 5th (0-4-7)
|
|
12
|
+
# - **Minor** (:min) - Root, minor 3rd, perfect 5th (0-3-7)
|
|
13
|
+
# - **Diminished** (:dim) - Root, minor 3rd, diminished 5th (0-3-6)
|
|
14
|
+
# - **Augmented** (:aug) - Root, major 3rd, augmented 5th (0-4-8)
|
|
15
|
+
#
|
|
16
|
+
# ### Seventh Chords (4 notes)
|
|
17
|
+
# - **Major 7th** (:maj7) - Major triad + major 7th (0-4-7-11)
|
|
18
|
+
# - **Minor 7th** (:min7) - Minor triad + minor 7th (0-3-7-10)
|
|
19
|
+
# - **Dominant 7th** (:dom7) - Major triad + minor 7th (0-4-7-10)
|
|
20
|
+
#
|
|
21
|
+
# ### Extended Chords (5+ notes)
|
|
22
|
+
# - **9th chords**: :maj9, :min9, :dom9
|
|
23
|
+
# - **11th chords**: :maj11, :min11
|
|
24
|
+
# - **13th chords**: :maj13, :min13
|
|
25
|
+
#
|
|
26
|
+
# ## Usage
|
|
27
|
+
#
|
|
28
|
+
# Chords are accessed via scale notes using the {Musa::Scales::NoteInScale#chord} method:
|
|
29
|
+
#
|
|
30
|
+
# scale = Scales.et12[440.0].major[60]
|
|
31
|
+
# scale.tonic.chord # Major triad (default)
|
|
32
|
+
# scale.tonic.chord :seventh # Major 7th
|
|
33
|
+
# scale.dominant.chord :seventh # Dominant 7th
|
|
34
|
+
# scale.tonic.chord quality: :minor # Minor triad
|
|
35
|
+
#
|
|
36
|
+
# @see Musa::Chords::ChordDefinition.register How chords are registered
|
|
37
|
+
# @see Musa::Chords::Chord How to build and use chords
|
|
38
|
+
# @see Musa::Scales::NoteInScale#chord Building chords from scale notes
|
|
39
|
+
|
|
3
40
|
# TODO trasladar los acordes de https://en.wikipedia.org/wiki/Chord_notation
|
|
4
41
|
|
|
5
42
|
# TRIADS
|