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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -4,15 +4,150 @@ require_relative 'gdv'
4
4
  require_relative 'helper'
5
5
 
6
6
  module Musa::Datasets
7
+ # MIDI-style musical events with absolute pitches.
8
+ #
9
+ # PDV (Pitch/Duration/Velocity) represents musical events using MIDI-like
10
+ # absolute pitch numbers. Extends {AbsD} for duration support.
11
+ #
12
+ # ## Purpose
13
+ #
14
+ # PDV is the MIDI representation layer of the dataset framework:
15
+ #
16
+ # - Uses absolute MIDI pitch numbers (0-127)
17
+ # - Uses MIDI velocity values (0-127)
18
+ # - Direct mapping to MIDI messages
19
+ # - Machine-oriented (not human-readable)
20
+ #
21
+ # Contrast with {GDV} which uses score notation (scale degrees, dynamics).
22
+ #
23
+ # ## Natural Keys
24
+ #
25
+ # - **:pitch**: MIDI pitch number (0-127) or :silence for rests
26
+ # - **:velocity**: MIDI velocity (0-127)
27
+ # - **:duration**: Event duration (from {AbsD})
28
+ # - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
29
+ #
30
+ # ## Conversions
31
+ #
32
+ # ### To GDV (Score Notation)
33
+ #
34
+ # Converts MIDI pitches to scale degrees using a scale reference:
35
+ #
36
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
37
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
38
+ # gdv = pdv.to_gdv(scale)
39
+ # # => { grade: 0, octave: 0, duration: 1.0, velocity: 0 }
40
+ #
41
+ # - **Pitch → Grade**: Finds closest scale degree
42
+ # - **Chromatic notes**: Represented as grade + sharps
43
+ # - **Velocity**: Maps MIDI 0-127 to dynamics -5 to +4 (ppp to fff)
44
+ #
45
+ # ### Velocity Mapping
46
+ #
47
+ # MIDI velocities are mapped to musical dynamics:
48
+ #
49
+ # MIDI 1-1 → velocity -5 (ppp)
50
+ # MIDI 2-8 → velocity -4 (pp)
51
+ # MIDI 9-16 → velocity -3 (p)
52
+ # MIDI 17-33 → velocity -2 (mp)
53
+ # MIDI 34-48 → velocity -1 (mf-)
54
+ # MIDI 49-64 → velocity 0 (mf)
55
+ # MIDI 65-80 → velocity +1 (f)
56
+ # MIDI 81-96 → velocity +2 (ff)
57
+ # MIDI 97-112 → velocity +3 (fff-)
58
+ # MIDI 113-127 → velocity +4 (fff)
59
+ #
60
+ # ## Base Duration
61
+ #
62
+ # The `base_duration` attribute defines the unit for duration values,
63
+ # typically 1/4r (quarter note).
64
+ #
65
+ # @example Basic MIDI event
66
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PDV)
67
+ # pdv.base_duration = 1/4r
68
+ # # C4 (middle C) for 1 beat at mf dynamics
69
+ #
70
+ # @example Silence (rest)
71
+ # pdv = { pitch: :silence, duration: 1.0 }.extend(PDV)
72
+ # # Rest for 1 beat
73
+ #
74
+ # @example With articulation
75
+ # pdv = {
76
+ # pitch: 64,
77
+ # duration: 1.0,
78
+ # note_duration: 0.5, # Staccato
79
+ # velocity: 80
80
+ # }.extend(PDV)
81
+ #
82
+ # @example Convert to score notation
83
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
84
+ # pdv.base_duration = 1/4r
85
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
86
+ # gdv = pdv.to_gdv(scale)
87
+ # # => { grade: 0, octave: 0, duration: 1.0, velocity: 0 }
88
+ #
89
+ # @example Chromatic pitch
90
+ # pdv = { pitch: 61, duration: 1.0, velocity: 64 }.extend(PDV)
91
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
92
+ # gdv = pdv.to_gdv(scale)
93
+ # # => { grade: 0, octave: 0, sharps: 1, duration: 1.0, velocity: 0 }
94
+ # # C# represented as C (grade 0) + 1 sharp
95
+ #
96
+ # @example Preserve additional keys
97
+ # pdv = {
98
+ # pitch: 60,
99
+ # duration: 1.0,
100
+ # velocity: 64,
101
+ # custom_key: :value
102
+ # }.extend(PDV)
103
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
104
+ # gdv = pdv.to_gdv(scale)
105
+ # # custom_key copied to GDV (not a natural key)
106
+ #
107
+ # @see GDV Score-style representation
108
+ # @see AbsD Absolute duration events
109
+ # @see Helper String formatting utilities
7
110
  module PDV
8
111
  include AbsD
9
112
 
10
113
  include Helper
11
114
 
115
+ # Natural keys for MIDI events.
116
+ # @return [Array<Symbol>]
12
117
  NaturalKeys = (NaturalKeys + [:pitch, :velocity]).freeze
13
118
 
119
+ # Base duration for time calculations.
120
+ # @return [Rational]
14
121
  attr_accessor :base_duration
15
122
 
123
+ # Converts to GDV (score notation).
124
+ #
125
+ # Translates MIDI representation to score notation using a scale:
126
+ # - MIDI pitch → scale degree (grade + octave + sharps)
127
+ # - MIDI velocity → dynamics (-5 to +4)
128
+ # - Duration values copied
129
+ # - Additional keys preserved
130
+ #
131
+ # @param scale [Musa::Scales::Scale] reference scale for pitch conversion
132
+ #
133
+ # @return [GDV] score notation dataset
134
+ #
135
+ # @example Basic conversion
136
+ # pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
137
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
138
+ # gdv = pdv.to_gdv(scale)
139
+ #
140
+ # @example Chromatic note
141
+ # pdv = { pitch: 61, duration: 1.0 }.extend(PDV)
142
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
143
+ # gdv = pdv.to_gdv(scale)
144
+ # # => { grade: 0, octave: 0, sharps: 1, duration: 1.0 }
145
+ #
146
+ # @example Silence
147
+ # pdv = { pitch: :silence, duration: 1.0 }.extend(PDV)
148
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
149
+ # gdv = pdv.to_gdv(scale)
150
+ # # => { grade: :silence, duration: 1.0 }
16
151
  def to_gdv(scale)
17
152
  gdv = {}.extend GDV
18
153
  gdv.base_duration = @base_duration
@@ -39,7 +174,7 @@ module Musa::Datasets
39
174
  if self[:velocity]
40
175
  # ppp = 16 ... fff = 127
41
176
  # TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
42
- gdv[:velocity] = [1..1, 2..8, 9..16, 17..33, 34..49, 49..64, 65..80, 81..96, 97..112, 113..127].index { |r| r.cover? self[:velocity] } - 5
177
+ gdv[:velocity] = [1..1, 2..8, 9..16, 17..33, 34..48, 49..64, 65..80, 81..96, 97..112, 113..127].index { |r| r.cover? self[:velocity] } - 5
43
178
  end
44
179
 
45
180
  (keys - NaturalKeys).each { |k| gdv[k] = self[k] }
@@ -4,31 +4,161 @@ require_relative 'score'
4
4
  require_relative '../sequencer'
5
5
 
6
6
  module Musa::Datasets
7
+ # Parameter segments for continuous changes between multidimensional points.
8
+ #
9
+ # PS (Parameter Segment) represents a continuous change from one point
10
+ # to another over a duration. Extends {AbsD} for duration support.
11
+ #
12
+ # ## Purpose
13
+ #
14
+ # PS is used to represent:
15
+ #
16
+ # - **Glissandi**: Continuous pitch slides (portamento)
17
+ # - **Parameter sweeps**: Gradual changes in any sonic parameter
18
+ # - **Interpolations**: Smooth transitions between multidimensional states
19
+ #
20
+ # Unlike discrete events that jump from one value to another, PS represents
21
+ # the continuous path between values.
22
+ #
23
+ # ## Natural Keys
24
+ #
25
+ # - **:from**: Starting value (number, array, or hash)
26
+ # - **:to**: Ending value (must match :from type and structure)
27
+ # - **:right_open**: Whether endpoint is included (true = open interval)
28
+ # - **:duration**: Duration of the change (from {AbsD})
29
+ # - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
30
+ #
31
+ # ## Value Types
32
+ #
33
+ # ### Single Values
34
+ #
35
+ # { from: 60, to: 72, duration: 1.0 }
36
+ # # Single value glissando
37
+ #
38
+ # ### Arrays (parallel interpolation)
39
+ #
40
+ # { from: [60, 64], to: [72, 76], duration: 1.0 }
41
+ # # Both values interpolate in parallel
42
+ # # Arrays must be same size
43
+ #
44
+ # ### Hashes (named parameters)
45
+ #
46
+ # { from: { pitch: 60, velocity: 64 },
47
+ # to: { pitch: 72, velocity: 80 },
48
+ # duration: 1.0 }
49
+ # # Multiple parameters interpolate together
50
+ # # Hashes must have same keys
51
+ #
52
+ # ## Right Open Intervals
53
+ #
54
+ # The :right_open flag determines if the ending value is reached:
55
+ #
56
+ # - **false** (closed): Interpolation reaches :to value
57
+ # - **true** (open): Interpolation stops just before :to value
58
+ #
59
+ # This is important for consecutive segments where you don't want
60
+ # discontinuities at the boundaries.
61
+ #
62
+ # @example Basic parameter segment (pitch glissando)
63
+ # ps = { from: 60, to: 72, duration: 2.0 }.extend(Musa::Datasets::PS)
64
+ # # Continuous slide from C4 to C5 over 2 beats
65
+ #
66
+ # @example Parallel interpolation (multidimensional)
67
+ # ps = {
68
+ # from: [60, 64], # C4 and E4
69
+ # to: [72, 76], # C5 and E5
70
+ # duration: 1.0
71
+ # }.extend(PS)
72
+ # # Both parameters move in parallel
73
+ #
74
+ # @example Multiple parameters (sonic gesture)
75
+ # ps = {
76
+ # from: { pitch: 60, velocity: 64, pan: -1.0 },
77
+ # to: { pitch: 72, velocity: 80, pan: 1.0 },
78
+ # duration: 2.0
79
+ # }.extend(PS)
80
+ # # Pitch, velocity, and pan all change smoothly
81
+ #
82
+ # @example Right open interval
83
+ # ps1 = { from: 60, to: 64, duration: 1.0, right_open: true }.extend(PS)
84
+ # ps2 = { from: 64, to: 67, duration: 1.0, right_open: false }.extend(PS)
85
+ # # ps1 stops just before 64, ps2 starts at 64 - no discontinuity
86
+ #
87
+ # @example Created from P point series
88
+ # p = [60, 4, 64, 8, 67].extend(P)
89
+ # serie = p.to_ps_serie
90
+ # ps1 = serie.next_value
91
+ # # => { from: 60, to: 64, duration: 1.0, right_open: true }
92
+ #
93
+ # @see AbsD Parent absolute duration module
94
+ # @see P Point series (source of PS)
95
+ # @see Helper String formatting utilities
7
96
  module PS
8
97
  include AbsD
9
98
 
10
99
  include Helper
11
100
 
101
+ # Natural keys including segment endpoints.
102
+ # @return [Array<Symbol>]
12
103
  NaturalKeys = (NaturalKeys + [:from, :to, :right_open]).freeze
13
104
 
105
+ # Base duration for time calculations.
106
+ # @return [Rational]
14
107
  attr_accessor :base_duration
15
108
 
109
+ # Converts to Neuma notation string.
110
+ #
111
+ # @return [String] Neuma notation
112
+ # @todo Not yet implemented
16
113
  def to_neuma
17
- # TODO ???????
114
+ raise NotImplementedError, 'PS to_neuma conversion is not yet implemented'
18
115
  end
19
116
 
117
+ # Converts to PDV (Pitch/Duration/Velocity).
118
+ #
119
+ # @return [PDV] PDV dataset
120
+ # @todo Not yet implemented
20
121
  def to_pdv
21
- # TODO ??????
122
+ raise NotImplementedError, 'PS to_pdv conversion is not yet implemented'
22
123
  end
23
124
 
125
+ # Converts to GDV (Grade/Duration/Velocity).
126
+ #
127
+ # @return [GDV] GDV dataset
128
+ # @todo Not yet implemented
24
129
  def to_gdv
25
- # TODO ?????
130
+ raise NotImplementedError, 'PS to_gdv conversion is not yet implemented'
26
131
  end
27
132
 
133
+ # Converts to absolute indexed format.
134
+ #
135
+ # @return [AbsI] indexed dataset
136
+ # @todo Not yet implemented
28
137
  def to_absI
29
- # TODO ?????
138
+ raise NotImplementedError, 'PS to_absI conversion is not yet implemented'
30
139
  end
31
140
 
141
+ # Validates PS structure.
142
+ #
143
+ # Checks that:
144
+ # - :from and :to have compatible types
145
+ # - Arrays have same size
146
+ # - Hashes have same keys
147
+ # - Duration is positive numeric
148
+ #
149
+ # @return [Boolean] true if valid
150
+ #
151
+ # @example Valid array segment
152
+ # ps = { from: [60, 64], to: [72, 76], duration: 1.0 }.extend(PS)
153
+ # ps.valid? # => true
154
+ #
155
+ # @example Invalid - mismatched array sizes
156
+ # ps = { from: [60, 64], to: [72], duration: 1.0 }.extend(PS)
157
+ # ps.valid? # => false
158
+ #
159
+ # @example Invalid - mismatched hash keys
160
+ # ps = { from: { a: 1 }, to: { b: 2 }, duration: 1.0 }.extend(PS)
161
+ # ps.valid? # => false
32
162
  def valid?
33
163
  case self[:from]
34
164
  when Array
@@ -1,11 +1,71 @@
1
1
  module Musa::Datasets
2
2
  class Score
3
+ # Query extensions for Score result sets.
4
+ #
5
+ # Queriable provides mixins that extend query results from Score methods
6
+ # with convenient filtering, grouping, and sorting capabilities.
7
+ #
8
+ # Two result types are supported:
9
+ # - **Time slot queries**: Direct event arrays from {Score#at}
10
+ # - **Interval queries**: Result hashes from {Score#between} and {Score#changes_between}
11
+ #
12
+ # These modules are applied automatically to query results and provide
13
+ # chainable query methods for further filtering.
14
+ #
15
+ # @see Score Score class using these modules
3
16
  module Queriable
17
+ # Query methods for time slot arrays.
18
+ #
19
+ # QueryableByTimeSlot extends Arrays returned by {Score#at} with query methods.
20
+ # Each event in the array is a dataset (hash) with musical attributes.
21
+ #
22
+ # Methods access attributes directly on events.
23
+ #
24
+ # @example Group events by pitch
25
+ # events = score.at(0r) # Returns array extended with QueryableByTimeSlot
26
+ # by_pitch = events.group_by_attribute(:pitch)
27
+ # # => { 60 => [event1, event2], 64 => [event3] }
28
+ #
29
+ # @example Select events with attribute
30
+ # staccato = events.select_by_attribute(:staccato)
31
+ # # Returns events where :staccato is not nil
32
+ #
33
+ # @example Select by value
34
+ # forte = events.select_by_attribute(:velocity, 1)
35
+ # # Returns events where velocity == 1
36
+ #
37
+ # @api private
4
38
  module QueryableByTimeSlot
39
+ # Groups events by attribute value.
40
+ #
41
+ # @param attribute [Symbol] attribute to group by
42
+ #
43
+ # @return [Hash{Object => Array}] grouped events, values extended with QueryableByTimeSlot
44
+ #
45
+ # @example Group by grade
46
+ # by_grade = events.group_by_attribute(:grade)
47
+ # # => { 0 => [events with grade 0], 2 => [events with grade 2] }
5
48
  def group_by_attribute(attribute)
6
49
  group_by { |e| e[attribute] }.transform_values! { |e| e.extend(QueryableByTimeSlot) }
7
50
  end
8
51
 
52
+ # Selects events by attribute presence or value.
53
+ #
54
+ # Without value: selects events where attribute is not nil.
55
+ # With value: selects events where attribute equals value.
56
+ #
57
+ # @param attribute [Symbol] attribute to filter by
58
+ # @param value [Object, nil] optional value to match
59
+ #
60
+ # @return [Array] filtered events, extended with QueryableByTimeSlot
61
+ #
62
+ # @example Select with attribute present
63
+ # events.select_by_attribute(:staccato)
64
+ # # Events where :staccato is not nil
65
+ #
66
+ # @example Select by specific value
67
+ # events.select_by_attribute(:pitch, 60)
68
+ # # Events where pitch == 60
9
69
  def select_by_attribute(attribute, value = nil)
10
70
  if value.nil?
11
71
  select { |e| !e[attribute].nil? }
@@ -14,6 +74,17 @@ module Musa::Datasets
14
74
  end.extend(QueryableByTimeSlot)
15
75
  end
16
76
 
77
+ # Sorts events by attribute value.
78
+ #
79
+ # First filters to events with the attribute, then sorts by its value.
80
+ #
81
+ # @param attribute [Symbol] attribute to sort by
82
+ #
83
+ # @return [Array] sorted events, extended with QueryableByTimeSlot
84
+ #
85
+ # @example Sort by pitch
86
+ # sorted = events.sort_by_attribute(:pitch)
87
+ # # Events sorted by ascending pitch
17
88
  def sort_by_attribute(attribute)
18
89
  select_by_attribute(attribute).sort_by { |e| e[attribute] }.extend(QueryableByTimeSlot)
19
90
  end
@@ -21,11 +92,57 @@ module Musa::Datasets
21
92
 
22
93
  private_constant :QueryableByTimeSlot
23
94
 
95
+ # Query methods for interval query results.
96
+ #
97
+ # QueryableByDataset extends Arrays returned by {Score#between} and
98
+ # {Score#changes_between} with query methods. Each element is a hash
99
+ # containing timing info and a :dataset key with the event.
100
+ #
101
+ # Methods access attributes through the :dataset key.
102
+ #
103
+ # @example Interval query result structure
104
+ # results = score.between(0r, 4r)
105
+ # # Each result: { start: ..., finish: ..., dataset: event, ... }
106
+ #
107
+ # @example Group by pitch
108
+ # by_pitch = results.group_by_attribute(:pitch)
109
+ # # Groups by event[:dataset][:pitch]
110
+ #
111
+ # @example Select with custom condition
112
+ # high = results.subset { |event| event[:pitch] > 60 }
113
+ #
114
+ # @api private
24
115
  module QueryableByDataset
116
+ # Groups results by dataset attribute value.
117
+ #
118
+ # @param attribute [Symbol] dataset attribute to group by
119
+ #
120
+ # @return [Hash{Object => Array}] grouped results, values extended with QueryableByDataset
121
+ #
122
+ # @example Group by velocity
123
+ # by_velocity = results.group_by_attribute(:velocity)
124
+ # # => { 0 => [results with velocity 0], 1 => [results with velocity 1] }
25
125
  def group_by_attribute(attribute)
26
126
  group_by { |e| e[:dataset][attribute] }.transform_values! { |e| e.extend(QueryableByDataset) }
27
127
  end
28
128
 
129
+ # Selects results by dataset attribute presence or value.
130
+ #
131
+ # Without value: selects where dataset attribute is not nil.
132
+ # With value: selects where dataset attribute equals value.
133
+ #
134
+ # @param attribute [Symbol] dataset attribute to filter by
135
+ # @param value [Object, nil] optional value to match
136
+ #
137
+ # @return [Array] filtered results, extended with QueryableByDataset
138
+ #
139
+ # @example Select with attribute
140
+ # results.select_by_attribute(:staccato)
141
+ # # Where dataset[:staccato] is not nil
142
+ #
143
+ # @example Select by value
144
+ # results.select_by_attribute(:grade, 0)
145
+ # # Where dataset[:grade] == 0
29
146
  def select_by_attribute(attribute, value = nil)
30
147
  if value.nil?
31
148
  select { |e| !e[:dataset][attribute].nil? }
@@ -34,11 +151,36 @@ module Musa::Datasets
34
151
  end.extend(QueryableByDataset)
35
152
  end
36
153
 
154
+ # Filters results by custom condition on dataset.
155
+ #
156
+ # @yieldparam dataset [Hash] event dataset
157
+ # @yieldreturn [Boolean] true to include result
158
+ #
159
+ # @return [Array] filtered results, extended with QueryableByDataset
160
+ #
161
+ # @raise [ArgumentError] if no block given
162
+ #
163
+ # @example Filter by pitch range
164
+ # results.subset { |event| event[:pitch] > 60 && event[:pitch] < 72 }
165
+ #
166
+ # @example Filter by multiple conditions
167
+ # results.subset { |event| event[:grade] == 0 && event[:velocity] > 0 }
37
168
  def subset
38
169
  raise ArgumentError, "subset needs a block with the inclusion condition on the dataset" unless block_given?
39
170
  select { |e| yield e[:dataset] }.extend(QueryableByDataset)
40
171
  end
41
172
 
173
+ # Sorts results by dataset attribute value.
174
+ #
175
+ # First filters to results with the attribute, then sorts by its value.
176
+ #
177
+ # @param attribute [Symbol] dataset attribute to sort by
178
+ #
179
+ # @return [Array] sorted results, extended with QueryableByDataset
180
+ #
181
+ # @example Sort by start time within interval
182
+ # sorted = results.sort_by_attribute(:pitch)
183
+ # # Results sorted by ascending pitch
42
184
  def sort_by_attribute(attribute)
43
185
  select_by_attribute(attribute).sort_by { |e| e[:dataset][attribute] }.extend(QueryableByDataset)
44
186
  end
@@ -46,4 +188,4 @@ module Musa::Datasets
46
188
 
47
189
  private_constant :QueryableByDataset
48
190
  end
49
- end; end
191
+ end; end
@@ -4,7 +4,111 @@ require_relative '../../series'
4
4
  module Musa
5
5
  module Datasets
6
6
  class Score
7
+ # Real-time rendering of scores on sequencers.
8
+ #
9
+ # Render provides the {#render} method for playing back scores on a
10
+ # {Musa::Sequencer::Sequencer}. Events are scheduled at their score times
11
+ # relative to the sequencer's current position.
12
+ #
13
+ # ## Time Calculation
14
+ #
15
+ # Score times are 1-based (first beat is 1), but sequencer waits are
16
+ # 0-based. The conversion is:
17
+ #
18
+ # effective_wait = score_time - 1
19
+ #
20
+ # So score time 1 becomes wait 0 (immediate), time 2 becomes wait 1, etc.
21
+ #
22
+ # ## Nested Scores
23
+ #
24
+ # Scores can contain other scores. When a nested score is encountered,
25
+ # it's rendered recursively at the appropriate time.
26
+ #
27
+ # @example Basic rendering
28
+ # score = Musa::Datasets::Score.new
29
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
30
+ # score.at(2r, add: { pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
31
+ #
32
+ # seq = Musa::Sequencer::Sequencer.new(4, 24)
33
+ # score.render(on: seq) do |event|
34
+ # puts "Play #{event[:pitch]} at #{seq.position}"
35
+ # end
36
+ # seq.run
37
+ #
38
+ # @example Nested scores
39
+ # inner = Musa::Datasets::Score.new
40
+ # inner.at(1r, add: { pitch: 67 }.extend(Musa::Datasets::AbsD))
41
+ # inner.at(2r, add: { pitch: 69 }.extend(Musa::Datasets::AbsD))
42
+ #
43
+ # outer = Musa::Datasets::Score.new
44
+ # outer.at(1r, add: { pitch: 60 }.extend(Musa::Datasets::AbsD))
45
+ # outer.at(2r, add: inner) # Nested score
46
+ # # inner plays at sequencer time 2r
47
+ #
48
+ # @see Musa::Sequencer::Sequencer Sequencer for playback
49
+ # @see Score#at Adding events to scores
7
50
  module Render
51
+ # Renders score on sequencer.
52
+ #
53
+ # Schedules all events in the score on the sequencer, calling the block
54
+ # for each event at its scheduled time. Score times are converted to
55
+ # sequencer wait times (score_time - 1).
56
+ #
57
+ # Supports nested scores recursively.
58
+ #
59
+ # @param on [Musa::Sequencer::Sequencer] sequencer to render on
60
+ #
61
+ # @yieldparam event [Abs] each event to process
62
+ # Block is called at the scheduled time with the event dataset
63
+ #
64
+ # @return [nil]
65
+ #
66
+ # @raise [ArgumentError] if element is not Abs or Score
67
+ #
68
+ # @example MIDI output
69
+ # require 'midi-communications'
70
+ #
71
+ # score = Musa::Datasets::Score.new
72
+ # score.at(1r, add: { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PDV))
73
+ #
74
+ # midi_out = MIDICommunications::Output.gets
75
+ # sequencer = Musa::Sequencer::Sequencer.new(4, 24)
76
+ #
77
+ # score.render(on: sequencer) do |event|
78
+ # if event[:pitch]
79
+ # midi_out.puts(0x90, event[:pitch], event[:velocity] || 64)
80
+ # sequencer.at event[:duration] do
81
+ # midi_out.puts(0x80, event[:pitch], event[:velocity] || 64)
82
+ # end
83
+ # end
84
+ # end
85
+ #
86
+ # sequencer.run
87
+ #
88
+ # @example Console output
89
+ # score = Musa::Datasets::Score.new
90
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
91
+ #
92
+ # seq = Musa::Sequencer::Sequencer.new(4, 24)
93
+ # score.render(on: seq) do |event|
94
+ # puts "Time #{seq.position}: #{event.inspect}"
95
+ # end
96
+ # seq.run
97
+ #
98
+ # @example Nested score rendering
99
+ # inner = Musa::Datasets::Score.new
100
+ # inner.at(1r, add: { pitch: 67 }.extend(Musa::Datasets::PDV))
101
+ #
102
+ # outer = Musa::Datasets::Score.new
103
+ # outer.at(1r, add: { pitch: 60 }.extend(Musa::Datasets::PDV))
104
+ # outer.at(2r, add: inner)
105
+ #
106
+ # seq = Musa::Sequencer::Sequencer.new(4, 24)
107
+ # outer.render(on: seq) do |event|
108
+ # puts "Event: #{event[:pitch]}"
109
+ # end
110
+ # seq.run
111
+ # # Inner scores automatically rendered at their scheduled times
8
112
  def render(on:, &block)
9
113
  @score.keys.each do |score_at|
10
114
  effective_wait = score_at - 1r
@@ -32,4 +136,4 @@ module Musa
32
136
  end
33
137
  end
34
138
  end
35
- end
139
+ end