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
@@ -3,7 +3,146 @@ require_relative '../neumalang'
3
3
 
4
4
  module Musa
5
5
  module Extension
6
+ # Array refinement for converting to neuma series.
7
+ #
8
+ # Adds methods to Array class for converting arrays of neuma elements (strings,
9
+ # neuma objects) into merged series. Enables convenient composition of multiple
10
+ # neuma sequences.
11
+ #
12
+ # ## Array to Neumas Conversion
13
+ #
14
+ # Arrays are converted using `MERGE` to create sequential series:
15
+ # ```ruby
16
+ # ["0 +2 +4", "+5 +7"].to_neumas
17
+ # # Equivalent to:
18
+ # MERGE("0 +2 +4".to_neumas, "+5 +7".to_neumas)
19
+ # ```
20
+ #
21
+ # ## Element Types
22
+ #
23
+ # Array elements can be:
24
+ # - **Strings**: Parsed as neuma notation
25
+ # - **Neuma::Serie**: Used directly
26
+ # - **Neuma::Parallel**: Wrapped in series
27
+ #
28
+ # ## Usage with Refinement
29
+ #
30
+ # This is a refinement - must be activated with `using`:
31
+ # ```ruby
32
+ # using Musa::Extension::Neumas
33
+ #
34
+ # phrases = [
35
+ # "0 +2 +4 +5", # First phrase
36
+ # "+7 +5 +4 +2", # Second phrase
37
+ # "0 -2 -4 -5" # Third phrase
38
+ # ].to_neumas
39
+ # ```
40
+ #
41
+ # ## Musical Applications
42
+ #
43
+ # - **Phrase composition**: Combine multiple musical phrases
44
+ # - **Section building**: Assemble larger structures from fragments
45
+ # - **Pattern sequencing**: Chain melodic/rhythmic patterns
46
+ # - **Mixed sources**: Combine string notation with existing neuma objects
47
+ #
48
+ # @example Sequential phrases
49
+ # using Musa::Extension::Neumas
50
+ #
51
+ # melody = [
52
+ # "0 +2 +4 +5", # Phrase A
53
+ # "+7 +5 +4 +2", # Phrase B
54
+ # "0 -2 -4 -5" # Phrase C
55
+ # ].to_neumas
56
+ #
57
+ # @example Mixed element types
58
+ # using Musa::Extension::Neumas
59
+ #
60
+ # intro = "0 +2 +4".to_neumas
61
+ # verse = "0 +2 +2 -1 0"
62
+ # chorus = "+7 +5 +7"
63
+ #
64
+ # song = [intro, verse, chorus].to_neumas
65
+ #
66
+ # @example Single element
67
+ # using Musa::Extension::Neumas
68
+ #
69
+ # # Single element returns converted element directly (not merged)
70
+ # single = ["0 +2 +4"].to_neumas
71
+ #
72
+ # Must be activated with `using Musa::Extension::Neumas`.
73
+ #
74
+ # ## Methods Added
75
+ #
76
+ # ### Array
77
+ # - {Array#to_neumas} - Converts array elements to merged neuma series
78
+ # - {Array#neumas} - Alias for to_neumas
79
+ # - {Array#n} - Short alias for to_neumas
80
+ #
81
+ # @see Musa::Extension::Neumas String refinement
82
+ #
83
+ # @api public
6
84
  module Neumas
85
+ # @!method to_neumas
86
+ # Converts array elements to merged neuma series.
87
+ #
88
+ # - Single element: Returns converted element directly
89
+ # - Multiple elements: Returns MERGE of all converted elements
90
+ #
91
+ # Each element is converted based on its type:
92
+ # - String → parsed as neuma notation
93
+ # - Neuma::Serie → used directly
94
+ # - Neuma::Parallel → wrapped in series
95
+ #
96
+ # @note This method is added to Array via refinement. Requires `using Musa::Extension::Neumas`.
97
+ #
98
+ # @return [Serie, Neuma] merged series or single neuma
99
+ #
100
+ # @raise [ArgumentError] if element type cannot be converted
101
+ #
102
+ # @example Convert string array
103
+ # using Musa::Extension::Neumas
104
+ #
105
+ # phrases = [
106
+ # "0 +2 +4",
107
+ # "+5 +7"
108
+ # ].to_neumas
109
+ # # Returns MERGE of two parsed series
110
+ #
111
+ # @example Mixed types
112
+ # using Musa::Extension::Neumas
113
+ #
114
+ # existing = "0 +2".to_neumas
115
+ # combined = [existing, "+4 +5"].to_neumas
116
+ #
117
+ # @example Single element
118
+ # using Musa::Extension::Neumas
119
+ #
120
+ # single = ["0 +2 +4"].to_neumas
121
+ # # Returns parsed series directly (not merged)
122
+ #
123
+ # @api public
124
+ class ::Array; end
125
+
126
+ # @!method neumas
127
+ # Alias for `to_neumas`.
128
+ #
129
+ # @note This method is added to Array via refinement. Requires `using Musa::Extension::Neumas`.
130
+ #
131
+ # @see Array#to_neumas
132
+ #
133
+ # @api public
134
+ class ::Array; end
135
+
136
+ # @!method n
137
+ # Short alias for `to_neumas`.
138
+ #
139
+ # @note This method is added to Array via refinement. Requires `using Musa::Extension::Neumas`.
140
+ #
141
+ # @see Array#to_neumas
142
+ #
143
+ # @api public
144
+ class ::Array; end
145
+
7
146
  refine Array do
8
147
  def to_neumas
9
148
  if length > 1
@@ -14,10 +153,20 @@ module Musa
14
153
  end
15
154
 
16
155
  alias_method :neumas, :to_neumas
156
+
17
157
  alias_method :n, :to_neumas
18
158
 
19
159
  private
20
160
 
161
+ # Converts element to neuma based on type.
162
+ #
163
+ # @param e [Object] element to convert
164
+ #
165
+ # @return [Serie] converted neuma serie
166
+ #
167
+ # @raise [ArgumentError] if type cannot be converted
168
+ #
169
+ # @api private
21
170
  def convert_to_neumas(e)
22
171
  case e
23
172
  when Musa::Neumas::Neuma::Serie then e
@@ -1,28 +1,216 @@
1
1
  require_relative 'neumas'
2
2
 
3
3
  module Musa::Neumas
4
+ # Neuma decoder infrastructure for converting neuma notation to musical events.
5
+ #
6
+ # Provides base classes for decoding neuma notation (a musical representation format)
7
+ # into GDV (Grade-Duration-Velocity) events. The decoder system supports differential
8
+ # decoding where each event is interpreted relative to the previous event.
9
+ #
10
+ # ## Architecture Overview
11
+ #
12
+ # ### Decoder Hierarchy
13
+ #
14
+ # ```
15
+ # ProtoDecoder (abstract)
16
+ # └── DifferentialDecoder (abstract)
17
+ # └── Decoder (stateful base)
18
+ # ├── NeumaDecoder (GDV output)
19
+ # └── NeumaDifferentialDecoder (GDVD output)
20
+ # ```
21
+ #
22
+ # ### Key Concepts
23
+ #
24
+ # 1. **Differential Decoding**: Each neuma is interpreted relative to previous state
25
+ #
26
+ # - Grade: `+2` means "2 steps up from last note"
27
+ # - Duration: `_2` means "double the base duration"
28
+ #
29
+ # 2. **Stateful Processing**: Decoders maintain `@last` state for differential interpretation
30
+ #
31
+ # 3. **Subcontexts**: Create independent decoder contexts for nested structures
32
+ #
33
+ # 4. **Transcription Integration**: Optional transcriptor for post-processing (ornaments, etc.)
34
+ #
35
+ # ## Processing Pipeline
36
+ #
37
+ # ```ruby
38
+ # Neuma Input → process() → apply() → Transcriptor → GDV Output
39
+ # ↓ ↓
40
+ # Prepare Apply to
41
+ # attributes last state
42
+ # ```
43
+ #
44
+ # ## Neuma Notation
45
+ #
46
+ # Neumas are text-based musical notation:
47
+ # ```ruby
48
+ # "0 +2 +2 -1" # Grade sequence (scale degrees)
49
+ # "_ _2 _/2" # Duration modifiers
50
+ # ".st .tr .mor" # Articulation/ornament modifiers
51
+ # "(+1_/4)+2_" # Appogiatura (grace note) + main note
52
+ # ```
53
+ #
54
+ # @example Basic usage
55
+ #
56
+ # Decoders are used by the neuma parsing system:
57
+ # ```ruby
58
+ # decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
59
+ # scale,
60
+ # base_duration: 1/4r,
61
+ # transcriptor: transcriptor
62
+ # )
63
+ #
64
+ # # Parse and decode neuma string
65
+ # neumas = "0 +2 +2 -1 0".to_neumas
66
+ # neumas.each { |neuma| decoder.decode(neuma) }
67
+ # ```
68
+ #
69
+ # @see Musa::Neumas Neuma notation system
70
+ # @see Musa::Datasets::GDV Absolute GDV format
71
+ # @see Musa::Datasets::GDVd Differential GDVD format
72
+ # @see Musa::Neumas::Decoders::NeumaDecoder
73
+ # @see Musa::Neumas::Decoders::NeumaDifferentialDecoder
74
+ # @see Musa::Transcription
75
+ #
76
+ # @api public
4
77
  module Decoders
78
+ # Abstract base decoder class.
79
+ #
80
+ # Defines the basic decoder interface. All decoders must implement:
81
+ #
82
+ # - `decode(element)` - Main decoding method
83
+ # - `subcontext` - Create independent decoder context
84
+ #
85
+ # ## Subcontexts
86
+ #
87
+ # Subcontexts allow creating independent decoder instances for nested
88
+ # structures (like grace notes) that need their own state tracking.
89
+ #
90
+ # @api public
5
91
  class ProtoDecoder
92
+ # Creates subcontext decoder.
93
+ #
94
+ # Returns independent decoder instance for nested decoding.
95
+ # Default implementation returns self (stateless).
96
+ #
97
+ # @return [ProtoDecoder] subcontext decoder instance
98
+ #
99
+ # @api public
6
100
  def subcontext
7
101
  self
8
102
  end
9
103
 
104
+ # Decodes element to musical event.
105
+ #
106
+ # Abstract method - must be implemented by subclasses.
107
+ #
108
+ # @param _element [Object] element to decode
109
+ #
110
+ # @return [Hash] decoded musical event
111
+ #
112
+ # @raise [NotImplementedError] if not overridden
113
+ #
114
+ # @api public
10
115
  def decode(_element)
11
116
  raise NotImplementedError
12
117
  end
13
118
  end
14
119
 
120
+ # Differential decoder base class.
121
+ #
122
+ # Adds `process` step to decoding pipeline for preparing/transforming
123
+ # input before final decoding. Useful for setting default values,
124
+ # normalizing formats, etc.
125
+ #
126
+ # ## Pipeline
127
+ #
128
+ # ```ruby
129
+ # input → process(input) → decode(processed)
130
+ # ```
131
+ #
132
+ # @api public
15
133
  class DifferentialDecoder < ProtoDecoder
134
+ # Decodes element after processing.
135
+ #
136
+ # Calls `process` to prepare element, then returns processed result.
137
+ #
138
+ # @param gdvd [Hash] GDVD (Grade-Duration-Velocity-Differential) attributes
139
+ #
140
+ # @return [Hash] processed attributes
141
+ #
142
+ # @api public
16
143
  def decode(gdvd)
17
144
  process gdvd
18
145
  end
19
146
 
147
+ # Processes/prepares attributes for decoding.
148
+ #
149
+ # Abstract method - must be implemented by subclasses to transform
150
+ # input attributes (set defaults, normalize, etc.).
151
+ #
152
+ # @param _gdvd [Hash] GDVD attributes
153
+ #
154
+ # @return [Hash] processed attributes
155
+ #
156
+ # @raise [NotImplementedError] if not overridden
157
+ #
158
+ # @api public
20
159
  def process(_gdvd)
21
160
  raise NotImplementedError
22
161
  end
23
162
  end
24
163
 
164
+ # Stateful decoder with differential interpretation and transcription.
165
+ #
166
+ # Maintains state (`@base`, `@last`) to interpret each neuma relative to
167
+ # the previous one. Supports optional transcriptor for post-processing
168
+ # (expanding ornaments, applying articulations, etc.).
169
+ #
170
+ # ## Differential Interpretation
171
+ #
172
+ # Each decoded event is interpreted relative to `@last`:
173
+ #
174
+ # - Grade changes: `+2` = last_grade + 2
175
+ # - Duration changes: `_2` = base_duration * 2
176
+ #
177
+ # After decoding, `@last` is updated for next event.
178
+ #
179
+ # ## Processing Pipeline
180
+ #
181
+ # ```ruby
182
+ # Input → process() → apply(on: @last) → update @last → transcriptor → Output
183
+ # ```
184
+ #
185
+ # @example Stateful decoding
186
+ # decoder = Musa::Neumas::Decoders::NeumaDifferentialDecoder.new(
187
+ # base_duration: 1/4r
188
+ # )
189
+ #
190
+ # # Create mock GDVD object
191
+ # gdvd1 = Object.new
192
+ # def gdvd1.clone; self; end
193
+ # def gdvd1.base_duration=(val); @bd = val; end
194
+ #
195
+ # result = decoder.decode(gdvd1)
196
+ # # Returns processed GDVD with base_duration set
197
+ #
198
+ # @api public
25
199
  class Decoder < DifferentialDecoder
200
+ # Creates stateful decoder.
201
+ #
202
+ # @param base [Hash] base/initial state for differential decoding
203
+ # @param transcriptor [Transcriptor, nil] optional transcriptor for post-processing
204
+ #
205
+ # @example Create decoder with base state
206
+ # base_state = { grade: 0, octave: 0, duration: 1/4r, velocity: 1 }
207
+ # decoder = Musa::Neumas::Decoders::Decoder.new(base_state)
208
+ #
209
+ # # Decoder maintains state
210
+ # decoder.base[:grade] # => 0
211
+ # decoder.base[:duration] # => 1/4r
212
+ #
213
+ # @api public
26
214
  def initialize(base, transcriptor: nil)
27
215
  @base = base
28
216
  @last = base.clone
@@ -30,18 +218,70 @@ module Musa::Neumas
30
218
  @transcriptor = transcriptor
31
219
  end
32
220
 
221
+ # Transcriptor for post-processing decoded events.
222
+ #
223
+ # @return [Transcriptor, nil] transcriptor instance or nil
224
+ #
225
+ # @api public
33
226
  attr_accessor :transcriptor
227
+
228
+ # Base state for decoder.
229
+ #
230
+ # @return [Hash] base state
231
+ #
232
+ # @api public
34
233
  attr_reader :base
35
234
 
235
+ # Sets base state and resets last state.
236
+ #
237
+ # @param base [Hash] new base state
238
+ #
239
+ # @api public
36
240
  def base=(base)
37
241
  @base = base
38
242
  @last = base.clone
39
243
  end
40
244
 
245
+ # Creates independent subcontext decoder.
246
+ #
247
+ # Returns new decoder with same base state but independent `@last` tracking.
248
+ # Used for nested structures like grace notes.
249
+ #
250
+ # @return [Decoder] independent decoder instance
251
+ #
252
+ # @api public
41
253
  def subcontext
42
254
  Decoder.new @base
43
255
  end
44
256
 
257
+ # Decodes attributes with differential interpretation and transcription.
258
+ #
259
+ # Pipeline:
260
+ # 1. Process attributes
261
+ # 2. Apply to last state
262
+ # 3. Update last state
263
+ # 4. Optional transcription
264
+ #
265
+ # @param attributes [Hash] neuma attributes to decode
266
+ #
267
+ # @return [Hash, Array<Hash>] decoded event(s), possibly transcribed
268
+ #
269
+ # @example Create decoder with transcriptor
270
+ # base_state = { grade: 0, octave: 0, duration: 1/4r, velocity: 1 }
271
+ #
272
+ # # Create mock transcriptor
273
+ # transcriptor = Object.new
274
+ # def transcriptor.transcript(gdv); [gdv, gdv.clone]; end
275
+ #
276
+ # decoder = Musa::Neumas::Decoders::Decoder.new(
277
+ # base_state,
278
+ # transcriptor: transcriptor
279
+ # )
280
+ #
281
+ # # Transcriptor can expand events (e.g., ornaments)
282
+ # decoder.transcriptor # => transcriptor object
283
+ #
284
+ # @api public
45
285
  def decode(attributes)
46
286
  result = apply process(attributes), on: @last
47
287
 
@@ -54,6 +294,19 @@ module Musa::Neumas
54
294
  end
55
295
  end
56
296
 
297
+ # Applies processed attributes to previous state.
298
+ #
299
+ # Abstract method - must be implemented by subclasses to define how
300
+ # differential attributes are applied to produce absolute values.
301
+ #
302
+ # @param _action [Hash] processed attributes
303
+ # @param on [Hash] previous state to apply attributes to
304
+ #
305
+ # @return [Hash] resulting absolute event
306
+ #
307
+ # @raise [NotImplementedError] if not overridden
308
+ #
309
+ # @api public
57
310
  def apply(_action, on:)
58
311
  raise NotImplementedError
59
312
  end
@@ -2,7 +2,96 @@ require_relative 'neuma-decoder'
2
2
 
3
3
  module Musa::Neumas
4
4
  module Decoders
5
+ # GDV neuma decoder for converting neumas to Grade-Duration-Velocity events.
6
+ #
7
+ # Converts neuma notation (GDVD - differential format) to GDV (absolute format)
8
+ # using scale information. This decoder is the primary way to transform text-based
9
+ # neuma notation into playable musical events.
10
+ #
11
+ # ## GDVD vs GDV
12
+ #
13
+ # - **GDVD** (differential): Relative changes `+2 _2` (up 2 steps, double duration)
14
+ # - **GDV** (absolute): Absolute values `{grade: 2, duration: 1/2r}` ready for playback
15
+ #
16
+ # ## Conversion Process
17
+ #
18
+ # ```ruby
19
+ # Neuma String → Parser → GDVD → NeumaDecoder → GDV → Transcriptor → MIDI/MusicXML
20
+ # "0 +2 +2 -1" ↓ ↓
21
+ # {grade_diff: +2} {grade: 2, duration: 1/4r}
22
+ # ```
23
+ #
24
+ # ## Scale Integration
25
+ #
26
+ # The decoder uses a scale to interpret grade values:
27
+ # ```ruby
28
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
29
+ # decoder = NeumaDecoder.new(scale)
30
+ #
31
+ # # Grade 2 in C major = E (C=0, D=1, E=2)
32
+ # ```
33
+ #
34
+ # ## Appogiatura Handling
35
+ #
36
+ # Grace notes (appogiatura) are processed recursively:
37
+ # ```ruby
38
+ # "(+1_/4)+2_" # Grace note +1 with duration 1/4, main note +2
39
+ # ```
40
+ #
41
+ # Both the grace note and main note are converted from GDVD to GDV.
42
+ #
43
+ # @example Using with transcriptor
44
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
45
+ #
46
+ # # Create mock transcriptor
47
+ # transcriptor = Object.new
48
+ # def transcriptor.transcript(gdv); [gdv]; end
49
+ #
50
+ # decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
51
+ # scale,
52
+ # base_duration: 1/4r,
53
+ # transcriptor: transcriptor
54
+ # )
55
+ #
56
+ # # Transcriptor will process decoded events
57
+ # decoder.transcriptor # => transcriptor object
58
+ #
59
+ # @see Musa::Neumas::Decoders::Decoder
60
+ # @see Musa::Scales
61
+ # @see Musa::Transcription
62
+ #
63
+ # @api public
5
64
  class NeumaDecoder < Decoder # to get a GDV
65
+ # Creates GDV neuma decoder.
66
+ #
67
+ # @param scale [Scale] scale for interpreting grade values
68
+ # @param base_duration [Rational, nil] base duration unit (default: 1/4)
69
+ # @param transcriptor [Transcriptor, nil] optional transcriptor for ornaments
70
+ # @param base [Hash, nil] initial state (auto-created if nil)
71
+ #
72
+ # @example Create decoder with scale
73
+ # scale = Object.new
74
+ # decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
75
+ # scale,
76
+ # base_duration: 1/4r
77
+ # )
78
+ #
79
+ # # Check initial state
80
+ # decoder.base[:grade] # => 0
81
+ # decoder.base[:duration] # => 1/4r
82
+ #
83
+ # @example Custom initial state
84
+ # scale = Object.new
85
+ # decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
86
+ # scale,
87
+ # base: { grade: 2, octave: 1, duration: 1/8r, velocity: 0.8 }
88
+ # )
89
+ #
90
+ # # Verify custom state
91
+ # decoder.base[:grade] # => 2
92
+ # decoder.base[:octave] # => 1
93
+ #
94
+ # @api public
6
95
  def initialize(scale, base_duration: nil, transcriptor: nil, base: nil)
7
96
  @base_duration = base_duration
8
97
  @base_duration ||= base[:duration] if base
@@ -15,8 +104,30 @@ module Musa::Neumas
15
104
  super base, transcriptor: transcriptor
16
105
  end
17
106
 
18
- attr_accessor :scale, :base_duration
19
-
107
+ # Scale for interpreting grade values.
108
+ #
109
+ # @return [Scale] scale object
110
+ #
111
+ # @api public
112
+ attr_accessor :scale
113
+
114
+ # Base duration unit for duration calculations.
115
+ #
116
+ # @return [Rational] base duration (e.g., 1/4 for quarter note)
117
+ #
118
+ # @api public
119
+ attr_accessor :base_duration
120
+
121
+ # Processes GDVD attributes before conversion.
122
+ #
123
+ # Sets base_duration on GDVD object for duration calculations.
124
+ # Handles appogiatura (grace note) modifiers recursively.
125
+ #
126
+ # @param gdvd [Hash] GDVD attributes
127
+ #
128
+ # @return [Hash] processed GDVD with base_duration set
129
+ #
130
+ # @api public
20
131
  def process(gdvd)
21
132
  gdvd = gdvd.clone
22
133
 
@@ -34,10 +145,34 @@ module Musa::Neumas
34
145
  gdvd
35
146
  end
36
147
 
148
+ # Creates independent subcontext decoder.
149
+ #
150
+ # Returns new decoder with current `@last` state as base, enabling
151
+ # independent processing of nested structures (grace notes, etc.).
152
+ #
153
+ # @return [NeumaDecoder] subcontext decoder with current state
154
+ #
155
+ # @api public
37
156
  def subcontext
38
157
  NeumaDecoder.new @scale, base_duration: @base_duration, transcriptor: @transcriptor, base: @last
39
158
  end
40
159
 
160
+ # Applies GDVD to previous state, producing absolute GDV.
161
+ #
162
+ # Converts differential GDVD to absolute GDV using scale. Processes
163
+ # appogiatura modifiers recursively.
164
+ #
165
+ # @param gdvd [Hash] processed GDVD attributes
166
+ # @param on [Hash] previous GDV state
167
+ #
168
+ # @return [Hash] absolute GDV event
169
+ #
170
+ # @example Convert differential to absolute
171
+ # # Previous: { grade: 0, duration: 1/4r }
172
+ # # GDVD: { grade_diff: +2, duration_factor: 2 }
173
+ # # Result: { grade: 2, duration: 1/2r, ... }
174
+ #
175
+ # @api public
41
176
  def apply(gdvd, on:)
42
177
  gdv = gdvd.to_gdv @scale, previous: on
43
178
 
@@ -47,6 +182,11 @@ module Musa::Neumas
47
182
  gdv
48
183
  end
49
184
 
185
+ # Returns debug representation.
186
+ #
187
+ # @return [String] debug string with last state
188
+ #
189
+ # @api public
50
190
  def inspect
51
191
  "GDV NeumaDecoder: @last = #{@last}"
52
192
  end