musa-dsl 0.40.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/Gemfile +0 -1
  4. data/README.md +15 -1
  5. data/docs/README.md +1 -0
  6. data/docs/subsystems/datasets.md +75 -0
  7. data/docs/subsystems/generative.md +92 -6
  8. data/docs/subsystems/music.md +349 -19
  9. data/docs/subsystems/transport.md +26 -0
  10. data/lib/musa-dsl/datasets/dataset.rb +2 -0
  11. data/lib/musa-dsl/datasets/gdv.rb +3 -3
  12. data/lib/musa-dsl/datasets/p.rb +1 -1
  13. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +4 -2
  14. data/lib/musa-dsl/datasets/score.rb +3 -1
  15. data/lib/musa-dsl/generative/darwin.rb +36 -1
  16. data/lib/musa-dsl/generative/generative-grammar.rb +31 -1
  17. data/lib/musa-dsl/generative/markov.rb +3 -1
  18. data/lib/musa-dsl/generative/rules.rb +54 -0
  19. data/lib/musa-dsl/generative/variatio.rb +69 -0
  20. data/lib/musa-dsl/midi/midi-recorder.rb +4 -0
  21. data/lib/musa-dsl/midi/midi-voices.rb +13 -1
  22. data/lib/musa-dsl/music/chord-definition.rb +7 -5
  23. data/lib/musa-dsl/music/chord-definitions.rb +37 -0
  24. data/lib/musa-dsl/music/chords.rb +88 -21
  25. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -521
  26. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
  27. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
  28. data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
  29. data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
  30. data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
  31. data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
  32. data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
  33. data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
  34. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
  35. data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
  36. data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
  37. data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
  38. data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
  39. data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
  40. data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
  41. data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
  42. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
  43. data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
  44. data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
  45. data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
  46. data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
  47. data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
  48. data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
  49. data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
  50. data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
  51. data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
  52. data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
  53. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
  54. data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
  55. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
  56. data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
  57. data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
  58. data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
  59. data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
  60. data/lib/musa-dsl/music/scales.rb +606 -67
  61. data/lib/musa-dsl/musicxml/builder/note.rb +31 -92
  62. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +33 -94
  63. data/lib/musa-dsl/musicxml/builder/rest.rb +30 -91
  64. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +31 -91
  65. data/lib/musa-dsl/neumas/array-to-neumas.rb +1 -1
  66. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +2 -2
  67. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +367 -3
  68. data/lib/musa-dsl/series/base-series.rb +250 -240
  69. data/lib/musa-dsl/series/buffer-serie.rb +16 -5
  70. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +29 -3
  71. data/lib/musa-dsl/series/main-serie-constructors.rb +19 -15
  72. data/lib/musa-dsl/series/main-serie-operations.rb +74 -29
  73. data/lib/musa-dsl/series/proxy-serie.rb +5 -1
  74. data/lib/musa-dsl/series/quantizer-serie.rb +16 -2
  75. data/lib/musa-dsl/series/queue-serie.rb +15 -1
  76. data/lib/musa-dsl/series/series-composer.rb +5 -2
  77. data/lib/musa-dsl/series/timed-serie.rb +8 -4
  78. data/lib/musa-dsl/transport/timer-clock.rb +4 -2
  79. data/lib/musa-dsl/transport/timer.rb +27 -4
  80. data/lib/musa-dsl/version.rb +1 -1
  81. data/musa-dsl.gemspec +18 -15
  82. metadata +85 -22
@@ -20,10 +20,54 @@ The **Scales** module provides hierarchical access to musical scales with multip
20
20
  - `:et12` (EquallyTempered12ToneScaleSystem): 12-tone equal temperament (default)
21
21
 
22
22
  **Available scale kinds in et12:**
23
- - `:major` - Major scale (Ionian mode)
24
- - `:minor` - Natural minor scale (Aeolian mode)
25
- - `:minor_harmonic` - Harmonic minor scale (raised 7th degree)
26
- - `:chromatic` - Chromatic scale (all 12 semitones)
23
+
24
+ *Core scales:*
25
+ - `:major` - Major scale (Ionian mode) - 7 notes
26
+ - `:minor` - Natural minor scale (Aeolian mode) - 7 notes
27
+ - `:minor_harmonic` - Harmonic minor scale (raised 7th) - 7 notes
28
+ - `:major_harmonic` - Harmonic major scale (lowered 6th) - 7 notes
29
+ - `:chromatic` - Chromatic scale (all 12 semitones) - 12 notes
30
+
31
+ *Greek/church modes:*
32
+ - `:dorian` - Dorian mode (minor with major 6th) - 7 notes
33
+ - `:phrygian` - Phrygian mode (minor with minor 2nd) - 7 notes
34
+ - `:lydian` - Lydian mode (major with augmented 4th) - 7 notes
35
+ - `:mixolydian` - Mixolydian mode (major with minor 7th) - 7 notes
36
+ - `:locrian` - Locrian mode (diminished 5th and minor 2nd) - 7 notes
37
+
38
+ *Pentatonic scales:*
39
+ - `:pentatonic_major` - Major pentatonic (no 4th or 7th) - 5 notes
40
+ - `:pentatonic_minor` - Minor pentatonic (no 2nd or 6th) - 5 notes
41
+
42
+ *Blues scales:*
43
+ - `:blues` - Blues scale (minor pentatonic + b5 blue note) - 6 notes
44
+ - `:blues_major` - Major blues scale (major pentatonic + b3 blue note) - 6 notes
45
+
46
+ *Symmetric scales:*
47
+ - `:whole_tone` - Whole tone scale (all whole steps) - 6 notes
48
+ - `:diminished_hw` - Diminished half-whole (octatonic) - 8 notes
49
+ - `:diminished_wh` - Diminished whole-half (dominant diminished) - 8 notes
50
+
51
+ *Melodic minor modes:*
52
+ - `:minor_melodic` - Melodic minor (jazz minor) - 7 notes
53
+ - `:dorian_b2` - Dorian b2 / Phrygian #6 - 7 notes
54
+ - `:lydian_augmented` - Lydian augmented (#4, #5) - 7 notes
55
+ - `:lydian_dominant` - Lydian dominant / Bartók scale (#4, b7) - 7 notes
56
+ - `:mixolydian_b6` - Mixolydian b6 / Hindu scale - 7 notes
57
+ - `:locrian_sharp2` - Locrian #2 / Half-diminished scale - 7 notes
58
+ - `:altered` - Altered / Super Locrian (all tensions altered) - 7 notes
59
+
60
+ *Ethnic/exotic scales:*
61
+ - `:double_harmonic` - Double harmonic / Byzantine (two augmented 2nds) - 7 notes
62
+ - `:hungarian_minor` - Hungarian minor / Gypsy minor (#4, raised 7th) - 7 notes
63
+ - `:phrygian_dominant` - Phrygian dominant / Spanish Phrygian (b2, major 3rd) - 7 notes
64
+ - `:neapolitan_minor` - Neapolitan minor (harmonic minor with b2) - 7 notes
65
+ - `:neapolitan_major` - Neapolitan major (melodic minor with b2) - 7 notes
66
+
67
+ *Bebop scales:*
68
+ - `:bebop_dominant` - Bebop dominant (Mixolydian + major 7th passing) - 8 notes
69
+ - `:bebop_major` - Bebop major (major + #5 passing) - 8 notes
70
+ - `:bebop_minor` - Bebop minor (Dorian + major 7th passing) - 8 notes
27
71
 
28
72
  ## Chords System
29
73
 
@@ -79,8 +123,8 @@ c_major[:V] # => Dominant (G)
79
123
  pitch = c_major.tonic.pitch # => 60
80
124
 
81
125
  # Navigate with octaves
82
- note = c_major[2].octave(1) # E in octave 1
83
- pitch_with_octave = note.pitch # => 76
126
+ note = c_major[2].at_octave(1) # E transposed up 1 octave
127
+ pitch_with_octave = note.pitch # => 76
84
128
 
85
129
  # Chromatic operations - sharp and flat
86
130
  c_sharp = c_major.tonic.sharp # => C# (chromatic, +1 semitone)
@@ -93,6 +137,49 @@ third_up = c_major.tonic.sharp(4) # => E (+4 semitones = major third)
93
137
  # Frequency calculation
94
138
  frequency = c_major.tonic.frequency # => 261.63 Hz (middle C at A=440)
95
139
 
140
+ # Greek modes (church modes)
141
+ d_dorian = tuning.dorian[62] # D Dorian (minor with major 6th)
142
+ e_phrygian = tuning.phrygian[64] # E Phrygian (minor with minor 2nd)
143
+ f_lydian = tuning.lydian[65] # F Lydian (major with augmented 4th)
144
+ g_mixolydian = tuning.mixolydian[67] # G Mixolydian (major with minor 7th)
145
+ b_locrian = tuning.locrian[71] # B Locrian (diminished 5th)
146
+
147
+ # Access notes in Greek modes by function
148
+ d_dorian.tonic.pitch # => 62 (D)
149
+ d_dorian[:vi].pitch # => 71 (B - the major 6th characteristic of Dorian)
150
+
151
+ e_phrygian[:ii].pitch # => 65 (F - the minor 2nd characteristic of Phrygian)
152
+
153
+ f_lydian[:IV].pitch # => 71 (B - the augmented 4th characteristic of Lydian)
154
+
155
+ g_mixolydian[:VII].pitch # => 77 (F - the minor 7th characteristic of Mixolydian)
156
+
157
+ b_locrian[:v].pitch # => 77 (F - the diminished 5th characteristic of Locrian)
158
+
159
+ # Pentatonic and blues scales
160
+ c_pent_maj = tuning.pentatonic_major[60] # C major pentatonic
161
+ a_pent_min = tuning.pentatonic_minor[69] # A minor pentatonic
162
+ a_blues = tuning.blues[69] # A blues scale
163
+ a_blues[:blue].pitch # => 75 (Eb - the blue note)
164
+
165
+ # Symmetric scales
166
+ c_whole = tuning.whole_tone[60] # C whole tone
167
+ c_dim_hw = tuning.diminished_hw[60] # C diminished (half-whole)
168
+ c_dim_wh = tuning.diminished_wh[60] # C diminished (whole-half)
169
+
170
+ # Melodic minor modes
171
+ c_mel_min = tuning.minor_melodic[60] # C melodic minor
172
+ g_altered = tuning.altered[67] # G altered (for G7alt chords)
173
+ f_lyd_dom = tuning.lydian_dominant[65] # F lydian dominant (F7#11)
174
+
175
+ # Ethnic scales
176
+ e_phry_dom = tuning.phrygian_dominant[64] # E Phrygian dominant (flamenco)
177
+ a_hung_min = tuning.hungarian_minor[69] # A Hungarian minor
178
+
179
+ # Bebop scales (8 notes for smooth eighth-note lines)
180
+ g_bebop = tuning.bebop_dominant[67] # G bebop dominant
181
+ g_bebop[7].pitch # => 78 (F# - the chromatic passing tone)
182
+
96
183
  # Create chords from scale degrees
97
184
  i_chord = c_major.tonic.chord # C major triad [C, E, G]
98
185
  ii_chord = c_major.supertonic.chord # D minor triad [D, F, A]
@@ -124,15 +211,258 @@ seventh_chord = i_chord.with_size(:seventh) # C major 7th
124
211
  ninth_chord = i_chord.with_size(:ninth) # C major 9th
125
212
 
126
213
  # Voicing modifications - move specific tones to different octaves
127
- voiced = i_chord.move(root: -1, fifth: 1) # Root down, fifth up
214
+ voiced = i_chord.with_move(root: -1, fifth: 1) # Root down, fifth up
215
+ i_chord.move # => { root: -1, fifth: 1 } (current settings)
128
216
 
129
217
  # Duplicate tones in other octaves
130
- doubled = i_chord.duplicate(root: -2, third: [-1, 1]) # Root 2 down, third 1 down and 1 up
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)
131
220
 
132
221
  # Transpose entire chord
133
222
  lower = i_chord.octave(-1) # Move chord down one octave
134
223
  ```
135
224
 
225
+ ### Chord-Scale Navigation
226
+
227
+ MusaDSL provides methods to explore the relationship between chords and scales,
228
+ enabling harmonic analysis and discovery of functional contexts.
229
+
230
+ #### Checking if a Scale Contains a Chord
231
+
232
+ ```ruby
233
+ c_major = Scales.et12[440.0].major[60]
234
+ g7 = c_major.dominant.chord :seventh
235
+
236
+ c_major.contains_chord?(g7) # => true
237
+ c_major.degree_of_chord(g7) # => 4 (V degree, 0-based)
238
+
239
+ # Non-diatonic chords return false/nil
240
+ cm = c_major.tonic.chord.with_quality(:minor) # C minor (Eb not in C major)
241
+ c_major.contains_chord?(cm) # => false
242
+ c_major.degree_of_chord(cm) # => nil
243
+ ```
244
+
245
+ #### Creating a Chord in a Different Scale Context
246
+
247
+ ```ruby
248
+ # Get the same chord but with a different scale as context
249
+ g_mixolydian = Scales.et12[440.0].mixolydian[67]
250
+ g7_in_mixolydian = g7.as_chord_in_scale(g_mixolydian)
251
+ g7_in_mixolydian.scale # => G Mixolydian scale
252
+ g_mixolydian.degree_of_chord(g7_in_mixolydian) # => 0 (I degree)
253
+ ```
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
+
272
+ #### Finding Scales That Contain a Chord
273
+
274
+ ```ruby
275
+ g_triad = c_major.dominant.chord # G-B-D
276
+
277
+ # Search in diatonic scales
278
+ g_triad.search_in_scales(family: :diatonic)
279
+ # => [Chord in C major (V), Chord in G major (I), Chord in D major (IV), ...]
280
+
281
+ # Search using metadata filters
282
+ g_triad.search_in_scales(family: :greek_modes, brightness: -1..1)
283
+
284
+ # Search in all scale types
285
+ g_triad.search_in_scales
286
+
287
+ # Each result has its scale context
288
+ g_triad.search_in_scales(family: :diatonic).each do |chord|
289
+ scale = chord.scale
290
+ degree = scale.degree_of_chord(chord)
291
+ puts "#{scale.kind.class.id} rooted on #{scale.root_pitch}: degree #{degree}"
292
+ end
293
+ # Output:
294
+ # major rooted on 60: degree 4
295
+ # major rooted on 67: degree 0
296
+ # major rooted on 62: degree 3
297
+ ```
298
+
299
+ #### Low-Level Navigation Methods
300
+
301
+ ```ruby
302
+ tuning = Scales.et12[440.0]
303
+
304
+ # Search at ScaleKind level
305
+ tuning.major.find_chord_in_scales(g_triad)
306
+
307
+ # Search at ScaleSystemTuning level with metadata filters
308
+ tuning.search_chord_in_scales(g7, family: :diatonic, roots: 60..71)
309
+ ```
310
+
311
+ ### Scale Kind Metadata
312
+
313
+ Scale kinds in MusaDSL have a three-layer metadata system that provides
314
+ both automatic structural information and extensible custom properties.
315
+
316
+ #### Metadata Layers
317
+
318
+ 1. **Intrinsic metadata**: Automatically derived from scale structure
319
+ - `id`: Scale kind identifier
320
+ - `grades`: Number of scale degrees
321
+ - `pitches`: Array of pitch offsets from root
322
+ - `intervals`: Intervals between consecutive degrees
323
+ - `has_leading_tone`: Whether scale has pitch 11 (semitone below octave)
324
+ - `has_tritone`: Whether scale contains tritone interval (pitch 6)
325
+ - `symmetric`: Type of symmetry if any (`:equal`, `:palindrome`, `:repeating`)
326
+
327
+ 2. **Base metadata**: Defined by musa-dsl library
328
+ - `family`: Scale family (`:diatonic`, `:greek_modes`, `:pentatonic`, etc.)
329
+ - `brightness`: Relative brightness (-3 to +3, major = 0)
330
+ - `character`: Array of descriptive tags
331
+ - `parent`: Parent scale and degree for modes (e.g., `{ scale: :major, degree: 2 }`)
332
+
333
+ 3. **Custom metadata**: Added by users at runtime
334
+
335
+ #### Accessing Metadata
336
+
337
+ ```ruby
338
+ tuning = Scales.et12[440.0]
339
+ major_class = tuning.major.class
340
+
341
+ # Get combined metadata (intrinsic < base < custom)
342
+ major_class.metadata
343
+ # => { id: :major, grades: 7, pitches: [0,2,4,5,7,9,11], family: :diatonic, ... }
344
+
345
+ # Get specific layer
346
+ major_class.intrinsic_metadata # Structure-derived only
347
+ major_class.base_metadata # Library-defined only
348
+ major_class.custom_metadata # User-added only
349
+
350
+ # Query helpers
351
+ major_class.has_metadata?(:family) # => true
352
+ major_class.has_metadata?(:family, :diatonic) # => true
353
+ major_class.has_metadata?(:character, :bright) # => true (array inclusion)
354
+ major_class.metadata_value(:brightness) # => 0
355
+ ```
356
+
357
+ #### Extending Metadata
358
+
359
+ Users can add custom metadata to any scale kind:
360
+
361
+ ```ruby
362
+ # Extend a specific scale kind class
363
+ tuning.dorian.class.extend_metadata(
364
+ my_mood: :nostalgic,
365
+ suitable_for: [:jazz, :fusion],
366
+ personal_rating: 5
367
+ )
368
+
369
+ # Or use the convenience method with scale ID
370
+ Scales.extend_metadata(:dorian, my_mood: :nostalgic)
371
+
372
+ # Multiple calls merge metadata
373
+ Scales.extend_metadata(:phrygian, mood: :dark)
374
+ Scales.extend_metadata(:phrygian, origin: :spanish) # Merges with previous
375
+
376
+ # Reset custom metadata if needed
377
+ tuning.dorian.class.reset_custom_metadata
378
+ ```
379
+
380
+ #### Custom Metadata Overrides
381
+
382
+ Custom metadata takes precedence over base metadata:
383
+
384
+ ```ruby
385
+ # Override library-defined family
386
+ Scales.extend_metadata(:dorian, family: :my_custom_category)
387
+ tuning.dorian.class.metadata[:family] # => :my_custom_category
388
+ ```
389
+
390
+ #### Brightness Scale Reference
391
+
392
+ | Value | Meaning | Examples |
393
+ |-------|---------|----------|
394
+ | +3 | Very bright | Lydian augmented |
395
+ | +2 | Bright | Lydian |
396
+ | +1 | Slightly bright | Mixolydian, Lydian dominant |
397
+ | 0 | Neutral (reference) | Major (Ionian) |
398
+ | -1 | Slightly dark | Dorian |
399
+ | -2 | Dark | Minor harmonic, Phrygian |
400
+ | -3 | Very dark | Locrian, Natural minor |
401
+
402
+ #### Scale Families
403
+
404
+ - `:diatonic` - Major, minor natural, minor harmonic
405
+ - `:greek_modes` - Dorian, Phrygian, Lydian, Mixolydian, Locrian
406
+ - `:melodic_minor_modes` - Melodic minor and its modes
407
+ - `:pentatonic` - Pentatonic major/minor
408
+ - `:blues` - Blues scales
409
+ - `:bebop` - Bebop scales
410
+ - `:symmetric` - Whole tone, diminished
411
+ - `:ethnic` - Hungarian, Spanish, Neapolitan, etc.
412
+ - `:chromatic` - Chromatic scale
413
+
414
+ ### Searching Scale Kinds
415
+
416
+ The `scale_kinds` method on ScaleSystemTuning allows searching and filtering
417
+ scale kinds by their metadata properties.
418
+
419
+ #### Basic Usage
420
+
421
+ ```ruby
422
+ tuning = Scales.et12[440.0]
423
+
424
+ # Get all scale kinds
425
+ tuning.scale_kinds
426
+ # => [major_kind, minor_kind, dorian_kind, ...]
427
+
428
+ # Filter by family
429
+ tuning.scale_kinds(family: :diatonic)
430
+ # => [major_kind, minor_kind, minor_harmonic_kind, harmonic_major_kind]
431
+
432
+ # Filter by brightness range
433
+ tuning.scale_kinds(brightness: -1..1)
434
+
435
+ # Filter by character
436
+ tuning.scale_kinds(character: :jazz)
437
+ ```
438
+
439
+ #### Custom Filtering with Blocks
440
+
441
+ Use a block for complex filtering based on any metadata:
442
+
443
+ ```ruby
444
+ # Scales with leading tone
445
+ tuning.scale_kinds { |klass| klass.intrinsic_metadata[:has_leading_tone] }
446
+
447
+ # Combine criteria and block
448
+ tuning.scale_kinds(family: :greek_modes) { |klass| klass.metadata[:brightness]&.negative? }
449
+ # => [dorian_kind, phrygian_kind, locrian_kind]
450
+ ```
451
+
452
+ #### Integration with Chord Search
453
+
454
+ The `search_chord_in_scales` method uses the same metadata filtering:
455
+
456
+ ```ruby
457
+ g7 = tuning.major[60].dominant.chord(:seventh)
458
+
459
+ # Find G7 in diatonic scales
460
+ tuning.search_chord_in_scales(g7, family: :diatonic)
461
+
462
+ # Find G7 in scales with specific brightness
463
+ tuning.search_chord_in_scales(g7, brightness: -1..1)
464
+ ```
465
+
136
466
  ## Defining Custom Scale Systems, Scale Kinds, and Chord Definitions
137
467
 
138
468
  The framework is extensible, allowing users to define custom tuning systems, scale types, and chord structures:
@@ -173,19 +503,19 @@ require 'musa-dsl'
173
503
  include Musa::Scales
174
504
  include Musa::Chords
175
505
 
176
- # Example 1: Define a custom pentatonic scale kind for the 12-tone system
177
- class PentatonicMajorScaleKind < ScaleKind
506
+ # Example 1: Define a custom Hirajoshi scale (Japanese pentatonic)
507
+ class HirajoshiScaleKind < ScaleKind
178
508
  class << self
179
509
  def id
180
- :pentatonic_major
510
+ :hirajoshi
181
511
  end
182
512
 
183
513
  def pitches
184
514
  [{ functions: [:I, :_1, :tonic], pitch: 0 },
185
515
  { functions: [:II, :_2], pitch: 2 },
186
- { functions: [:III, :_3], pitch: 4 },
187
- { functions: [:V, :_5], pitch: 7 },
188
- { functions: [:VI, :_6], pitch: 9 }]
516
+ { functions: [:III, :_3], pitch: 3 },
517
+ { functions: [:V, :_4], pitch: 7 },
518
+ { functions: [:VI, :_5], pitch: 8 }]
189
519
  end
190
520
 
191
521
  def grades
@@ -195,14 +525,14 @@ class PentatonicMajorScaleKind < ScaleKind
195
525
  end
196
526
 
197
527
  # Register the new scale kind with the 12-tone system
198
- Scales.et12.register(PentatonicMajorScaleKind)
528
+ Scales.et12.register(HirajoshiScaleKind)
199
529
 
200
530
  # Use the new scale kind
201
531
  tuning = Scales.default_system.default_tuning
202
- c_pentatonic = tuning[:pentatonic_major][60] # C pentatonic major
203
- puts c_pentatonic[0].pitch # => 60 (C)
204
- puts c_pentatonic[1].pitch # => 62 (D)
205
- puts c_pentatonic[2].pitch # => 64 (E)
532
+ c_hirajoshi = tuning[:hirajoshi][60] # C Hirajoshi
533
+ puts c_hirajoshi[0].pitch # => 60 (C)
534
+ puts c_hirajoshi[1].pitch # => 62 (D)
535
+ puts c_hirajoshi[2].pitch # => 63 (Eb)
206
536
 
207
537
  # Example 2: Register a custom chord definition (sus4)
208
538
  Musa::Chords::ChordDefinition.register :sus4,
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Musical dataset framework for MusaDSL.
2
4
  #
3
5
  # The Datasets module provides a comprehensive framework for representing and transforming
@@ -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).octave(self[:octave] || 0).pitch
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]].octave(self[:octave]).wide_grade -
403
- scale[previous[:grade]].octave(previous[:octave]).wide_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
@@ -206,7 +206,7 @@ module Musa::Datasets
206
206
  #
207
207
  # @api private
208
208
  class PtoTimedSerie
209
- include Musa::Series::Serie.base
209
+ include Musa::Series::Serie::Base
210
210
 
211
211
  # Creates new timed serie adapter.
212
212
  #
@@ -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 = 1r) # TODO remove (unused because of bad strategy to time groups)
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 = 1r) # TODO remove (unused because of bad strategy to time groups)
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 [](key)
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
@@ -104,6 +104,8 @@ module Musa
104
104
  # measures { |obj| dimension :value, obj[:score] }
105
105
  # weight value: 1.0
106
106
  # end
107
+ #
108
+ # @return [void]
107
109
  def initialize(&block)
108
110
  raise ArgumentError, 'block is needed' unless block
109
111
 
@@ -175,6 +177,14 @@ module Musa
175
177
  measured_objects.collect { |measured_object| measured_object[:object] }
176
178
  end
177
179
 
180
+ # Compares two measures by their weighted fitness.
181
+ #
182
+ # @param measure_a [Measure] first measure to compare
183
+ # @param measure_b [Measure] second measure to compare
184
+ #
185
+ # @return [Integer] comparison result (-1, 0, 1)
186
+ #
187
+ # @api private
178
188
  def evaluate_weights(measure_a, measure_b)
179
189
  measure_b.evaluate_weight(@weights) <=> measure_a.evaluate_weight(@weights)
180
190
  end
@@ -189,6 +199,7 @@ module Musa
189
199
  # @return [Hash] weight assignments
190
200
  attr_reader :_measures, :_weights
191
201
 
202
+ # @return [void]
192
203
  # @api private
193
204
  def initialize(&block)
194
205
  @_weights = {}
@@ -198,6 +209,9 @@ module Musa
198
209
  # Defines measures evaluation block.
199
210
  #
200
211
  # @yield [object] measures DSL block
212
+ #
213
+ # @return [Proc] the measures block
214
+ #
201
215
  # @api private
202
216
  def measures(&block)
203
217
  @_measures = block
@@ -205,7 +219,10 @@ module Musa
205
219
 
206
220
  # Assigns weights to features/dimensions.
207
221
  #
208
- # @param feature_or_dimension_weights [Hash] name => weight pairs
222
+ # @param feature_or_dimension_weights [Hash{Symbol => Numeric}] name => weight pairs
223
+ #
224
+ # @return [void]
225
+ #
209
226
  # @api private
210
227
  def weight(**feature_or_dimension_weights)
211
228
  feature_or_dimension_weights.each do |name, value|
@@ -220,6 +237,7 @@ module Musa
220
237
  class MeasuresEvalContext
221
238
  include Musa::Extension::With
222
239
 
240
+ # @return [void]
223
241
  # @api private
224
242
  def initialize
225
243
  @_features = {}
@@ -238,6 +256,9 @@ module Musa
238
256
  # Marks object as having a feature.
239
257
  #
240
258
  # @param feature_name [Symbol] feature identifier
259
+ #
260
+ # @return [void]
261
+ #
241
262
  # @api private
242
263
  def feature(feature_name)
243
264
  @_features[feature_name] = true
@@ -247,6 +268,9 @@ module Musa
247
268
  #
248
269
  # @param dimension_name [Symbol] dimension identifier
249
270
  # @param value [Numeric] measured value
271
+ #
272
+ # @return [void]
273
+ #
250
274
  # @api private
251
275
  def dimension(dimension_name, value)
252
276
  @_dimensions[dimension_name] = value
@@ -254,6 +278,8 @@ module Musa
254
278
 
255
279
  # Marks object as non-viable (to be excluded).
256
280
  #
281
+ # @return [void]
282
+ #
257
283
  # @api private
258
284
  def die
259
285
  @_died = true
@@ -280,6 +306,12 @@ module Musa
280
306
  class Measure
281
307
  attr_reader :features, :dimensions, :normalized_dimensions
282
308
 
309
+ # @param features [Hash{Symbol => Boolean}] feature flags
310
+ # @param dimensions [Hash{Symbol => Numeric}] dimension values
311
+ # @param died [Boolean] viability status
312
+ #
313
+ # @return [void]
314
+ #
283
315
  # @api private
284
316
  def initialize(features, dimensions, died)
285
317
  @features = features
@@ -319,6 +351,9 @@ module Musa
319
351
  total
320
352
  end
321
353
 
354
+ # Returns string representation of measure.
355
+ #
356
+ # @return [String] formatted measure description
322
357
  def inspect
323
358
  "Measure features=#{@features.collect { |k, _v| k }} dimensions=#{@normalized_dimensions.collect { |k, v| [k, [@dimensions[k].round(5), v.round(2)]] }.to_h}"
324
359
  end