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
@@ -5,18 +5,229 @@ require_relative '../neumas'
5
5
  require_relative '../datasets'
6
6
 
7
7
  module Musa
8
+ # Neumalang namespace for parser and related functionality.
9
+ #
10
+ # @api public
8
11
  module Neumalang
12
+ # Neumalang parser for parsing neuma text notation to structured objects.
13
+ #
14
+ # Neumalang is a domain-specific language (DSL) for expressing musical notation
15
+ # in a compact, text-based format. The parser uses Citrus (PEG parser framework)
16
+ # to parse neuma notation strings into structured Ruby objects.
17
+ #
18
+ # ## Architecture Overview
19
+ #
20
+ # ### Parser Framework
21
+ #
22
+ # The parser is built on **Citrus**, a Parsing Expression Grammar (PEG) framework:
23
+ #
24
+ # - Grammar defined in `.citrus` files (terminals, datatypes, neuma, vectors, process, neumalang)
25
+ # - Each grammar rule has a corresponding Ruby module for semantic actions
26
+ # - Modules transform parse tree into structured neuma objects
27
+ #
28
+ # ### Grammar Files
29
+ #
30
+ # 1. **terminals.citrus** - Basic tokens (numbers, names, symbols, whitespace)
31
+ # 2. **datatypes.citrus** - Data types (strings, numbers, symbols, vectors)
32
+ # 3. **neuma.citrus** - Neuma notation (grade, duration, velocity, modifiers)
33
+ # 4. **vectors.citrus** - Vector notation (V, PackedV for musical data)
34
+ # 5. **process.citrus** - Process notation (P for rhythmic processes)
35
+ # 6. **neumalang.citrus** - Top-level grammar combining all elements
36
+ #
37
+ # ## Parsing Pipeline
38
+ #
39
+ # ```ruby
40
+ # Text → Citrus Parser → Parse Tree → Semantic Modules → Neuma Objects → Series
41
+ # "0 +2 +2" ↓ ↓ ↓ ↓
42
+ # Grammar AST nodes Module.value() Structured hashes
43
+ # ```
44
+ #
45
+ # ## Neuma Object Structure
46
+ #
47
+ # Parsed neumas are hashes with `:kind` key indicating type:
48
+ #
49
+ # ### GDVD (Musical Event)
50
+ # ```ruby
51
+ # {
52
+ # kind: :gdvd,
53
+ # gdvd: {
54
+ # delta_grade: +2,
55
+ # factor_duration: 2,
56
+ # modifiers: { tr: true }
57
+ # }.extend(Musa::Datasets::GDVd)
58
+ # }.extend(Musa::Neumas::Neuma)
59
+ # ```
60
+ #
61
+ # ### Serie (Sequential)
62
+ # ```ruby
63
+ # {
64
+ # kind: :serie,
65
+ # serie: [neuma1, neuma2, ...]
66
+ # }.extend(Musa::Neumas::Neuma::Serie)
67
+ # ```
68
+ #
69
+ # ### Parallel (Polyphonic)
70
+ # ```ruby
71
+ # {
72
+ # kind: :parallel,
73
+ # parallel: [
74
+ # { kind: :serie, serie: [...] },
75
+ # { kind: :serie, serie: [...] }
76
+ # ]
77
+ # }.extend(Musa::Neumas::Neuma::Parallel)
78
+ # ```
79
+ #
80
+ # ### Commands & Variables
81
+ # ```ruby
82
+ # { kind: :command, command: proc { ... } }
83
+ # { kind: :use_variable, use_variable: :@variable_name }
84
+ # { kind: :assign_to, assign_to: [:@var1], assign_value: ... }
85
+ # ```
86
+ #
87
+ # ### Values
88
+ # ```ruby
89
+ # { kind: :value, value: 42 }
90
+ # { kind: :value, value: :symbol }
91
+ # { kind: :value, value: "string" }
92
+ # ```
93
+ #
94
+ # ### Vectors & Processes
95
+ # ```ruby
96
+ # { kind: :v, v: [1, 2, 3].extend(Musa::Datasets::V) }
97
+ # { kind: :packed_v, packed_v: {a: 1, b: 2}.extend(Musa::Datasets::PackedV) }
98
+ # { kind: :p, p: [values...].extend(Musa::Datasets::P) }
99
+ # ```
100
+ #
101
+ # ## Neumalang Syntax Features
102
+ #
103
+ # - **Grade notation**: `0`, `+2`, `-1`, `^2` (octave up), `v1` (octave down)
104
+ # - **Duration notation**: `_`, `_2`, `_/2`, `_3/2` (dotted), `_.` (dots)
105
+ # - **Velocity notation**: `p`, `pp`, `mp`, `mf`, `f`, `ff`, `fff`
106
+ # - **Modifiers**: `.tr`, `.mor`, `.turn`, `.st`, `.b` (ornaments/articulations)
107
+ # - **Appogiatura**: `(+1_/4)+2_` (grace note before main note)
108
+ # - **Parallel**: `[0 +2 +4 | +7 +5 +7]` (multiple voices)
109
+ # - **Vectors**: `<1 2 3>` (V), `<a: 1 b: 2>` (PackedV)
110
+ # - **Process**: `<< 1 _ _ 2 _ >>` (rhythmic process)
111
+ # - **Commands**: `{ ruby code }` (embedded Ruby)
112
+ # - **Variables**: `@variable`, `@var = value`
113
+ # - **Events**: `event_name(params)`, `event_name(key: value)`
114
+ #
115
+ # ## Usage
116
+ #
117
+ # ```ruby
118
+ # # Parse string
119
+ # neumas = Musa::Neumalang::Neumalang.parse("0 +2 +2 -1 0")
120
+ #
121
+ # # Parse with decoder (converts GDVD to GDV)
122
+ # decoder = NeumaDecoder.new(scale)
123
+ # gdvs = Musa::Neumalang::Neumalang.parse(
124
+ # "0 +2 +2 -1 0",
125
+ # decode_with: decoder
126
+ # )
127
+ #
128
+ # # Parse file
129
+ # neumas = Musa::Neumalang::Neumalang.parse_file("melody.neuma")
130
+ #
131
+ # # Debug parsing
132
+ # Musa::Neumalang::Neumalang.parse(
133
+ # "0 +2 +2 -1 0",
134
+ # debug: true # Dumps parse tree
135
+ # )
136
+ # ```
137
+ #
138
+ # ## Integration
139
+ #
140
+ # - **Series**: Parsed neumas generate Series for sequential playback
141
+ # - **Neumas**: Output extends Neuma modules for structural operations
142
+ # - **Datasets**: GDVd, V, PackedV, P extensions for musical data
143
+ # - **Decoders**: Optional decode_with parameter for immediate GDV conversion
144
+ #
145
+ # @example Basic parsing (simple melody)
146
+ # neumas = Musa::Neumalang::Neumalang.parse("(0) (+2) (+2) (-1) (0)")
147
+ # # Returns serie of GDVD neuma objects
148
+ #
149
+ # # Access the series
150
+ # neumas.i.to_a.size # => 5
151
+ # neumas.i.to_a[0][:gdvd][:abs_grade] # => 0
152
+ # neumas.i.to_a[1][:gdvd][:delta_grade] # => 2
153
+ #
154
+ # @example With decoder
155
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
156
+ # decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
157
+ # scale,
158
+ # base_duration: 1/4r
159
+ # )
160
+ #
161
+ # gdvs = Musa::Neumalang::Neumalang.parse(
162
+ # "(0) (+2) (+2) (-1) (0)",
163
+ # decode_with: decoder
164
+ # )
165
+ # # Returns serie of GDV events
166
+ #
167
+ # @example Complex notation
168
+ # neumas = Musa::Neumalang::Neumalang.parse(
169
+ # "(0) (+2 .tr) (+4 _) (+5 _2) ((^1 _/4) +7 _) (+5) (+4) (+2) (0)"
170
+ # )
171
+ #
172
+ # @example Parallel voices
173
+ # neumas = Musa::Neumalang::Neumalang.parse(
174
+ # "[(0) (+2) (+4) | (+7) (+5) (+7)]"
175
+ # )
176
+ #
177
+ # @example With variables and commands
178
+ # neumas = Musa::Neumalang::Neumalang.parse(
179
+ # "@melody = (0) (+2) (+2) (-1) (0)
180
+ # @melody { |gdv| gdv[:duration] *= 2 }"
181
+ # )
182
+ #
183
+ # @see Musa::Neumas
184
+ # @see Musa::Neumas::Decoders::NeumaDecoder
185
+ # @see Musa::Datasets
186
+ # @see https://github.com/mjackson/citrus Citrus parser framework
187
+ #
188
+ # @api public
9
189
  module Neumalang
190
+ # Parser namespace containing grammar and semantic modules.
191
+ #
192
+ # @api public
10
193
  module Parser
194
+ # Grammar namespace where Citrus loads grammar rules.
195
+ #
196
+ # Populated by Citrus when loading .citrus files.
197
+ # Contains Grammar class with parse methods.
198
+ #
199
+ # @api private
11
200
  module Grammar; end
12
201
 
202
+ # Semantic action for top-level sentences (sequence of expressions).
203
+ #
204
+ # Transforms parsed expressions into Serie structure.
205
+ # Used for main neuma sequences like `"0 +2 +4"`.
206
+ #
207
+ # @api private
13
208
  module Sentences
209
+ # Builds serie from expressions.
210
+ #
211
+ # @return [Hash] serie structure with kind :serie
212
+ #
213
+ # @api private
14
214
  def value
15
215
  Musa::Series::Constructors.S(*captures(:expression).collect(&:value)).extend(Musa::Neumas::Neuma::Serie)
16
216
  end
17
217
  end
18
218
 
219
+ # Semantic action for parallel structure (voices separated by `|`).
220
+ #
221
+ # Transforms bracketed parallel notation into Parallel structure.
222
+ # Used for polyphonic notation like `"[0 +2 | +7 +5]"`.
223
+ #
224
+ # @api private
19
225
  module BracketedBarSentences
226
+ # Builds parallel structure from bar-separated series.
227
+ #
228
+ # @return [Hash] parallel structure with kind :parallel
229
+ #
230
+ # @api private
20
231
  def value
21
232
  { kind: :parallel,
22
233
  parallel: [{ kind: :serie,
@@ -26,21 +237,54 @@ module Musa
26
237
  end
27
238
  end
28
239
 
240
+ # Semantic action for bracketed sentences (grouping).
241
+ #
242
+ # Transforms bracketed expressions into serie structure.
243
+ # Used for grouping like `"[0 +2 +4]"` (without bars).
244
+ #
245
+ # @api private
29
246
  module BracketedSentences
247
+ # Builds serie from bracketed sentences.
248
+ #
249
+ # @return [Hash] serie structure with kind :serie
250
+ #
251
+ # @api private
30
252
  def value
31
253
  { kind: :serie,
32
254
  serie: Musa::Series::Constructors.S(*capture(:sentences).value) }.extend Musa::Neumas::Neuma
33
255
  end
34
256
  end
35
257
 
258
+ # Semantic action for command references.
259
+ #
260
+ # Transforms referenced braced commands (Ruby code blocks).
261
+ # Used when command is referenced elsewhere.
262
+ #
263
+ # @api private
36
264
  module ReferencedBracedCommand
265
+ # Builds command reference structure.
266
+ #
267
+ # @return [Hash] command reference with kind :command_reference
268
+ #
269
+ # @api private
37
270
  def value
38
271
  { kind: :command_reference,
39
272
  command: capture(:braced_command).value }.extend Musa::Neumas::Neuma
40
273
  end
41
274
  end
42
275
 
276
+ # Semantic action for variable assignment.
277
+ #
278
+ # Transforms variable assignment notation like `"@melody = 0 +2 +4"`.
279
+ # Supports multiple variables: `"@var1 @var2 = expression"`.
280
+ #
281
+ # @api private
43
282
  module VariableAssign
283
+ # Builds variable assignment structure.
284
+ #
285
+ # @return [Hash] assignment with kind :assign_to, variable names, and value
286
+ #
287
+ # @api private
44
288
  def value
45
289
  { kind: :assign_to,
46
290
  assign_to: captures(:use_variable).collect { |c| c.value[:use_variable] },
@@ -49,7 +293,18 @@ module Musa
49
293
  end
50
294
  end
51
295
 
296
+ # Semantic action for event notation.
297
+ #
298
+ # Transforms event calls like `"event_name(params)"` or `"event_name(key: value)"`.
299
+ # Events can trigger external actions during playback.
300
+ #
301
+ # @api private
52
302
  module Event
303
+ # Builds event structure with name and parameters.
304
+ #
305
+ # @return [Hash] event with kind :event, name, and optional parameters
306
+ #
307
+ # @api private
53
308
  def value
54
309
  { kind: :event,
55
310
  event: capture(:name).value.to_sym
@@ -57,7 +312,18 @@ module Musa
57
312
  end
58
313
  end
59
314
 
315
+ # Semantic action for function/event parameters.
316
+ #
317
+ # Parses parameter lists for events and commands.
318
+ # Separates positional parameters, keyword parameters, and proc parameters.
319
+ #
320
+ # @api private
60
321
  module Parameters
322
+ # Builds parameters structure.
323
+ #
324
+ # @return [Hash] parameters with :value_parameters, :key_parameters, :proc_parameter
325
+ #
326
+ # @api private
61
327
  def value
62
328
  value_parameters = []
63
329
  key_value_parameters = {}
@@ -80,7 +346,17 @@ module Musa
80
346
  end
81
347
  end
82
348
 
349
+ # Semantic action for code blocks (proc parameters).
350
+ #
351
+ # Transforms braced commands or variable references into proc parameters.
352
+ #
353
+ # @api private
83
354
  module Codeblock
355
+ # Builds codeblock structure.
356
+ #
357
+ # @return [Hash] codeblock with command or variable reference
358
+ #
359
+ # @api private
84
360
  def value
85
361
  { codeblock:
86
362
  capture(:braced_command)&.value ||
@@ -89,7 +365,17 @@ module Musa
89
365
  end
90
366
  end
91
367
 
368
+ # Semantic action for individual parameter.
369
+ #
370
+ # Transforms either key-value parameter or positional parameter.
371
+ #
372
+ # @api private
92
373
  module Parameter
374
+ # Builds parameter structure.
375
+ #
376
+ # @return [Hash] parameter as :key_value or :value
377
+ #
378
+ # @api private
93
379
  def value
94
380
  if capture(:key_value_parameter)
95
381
  { key_value: capture(:key_value_parameter).value }
@@ -99,7 +385,18 @@ module Musa
99
385
  end
100
386
  end
101
387
 
388
+ # Semantic action for braced Ruby commands.
389
+ #
390
+ # Transforms `{ ruby code }` into executable proc.
391
+ # Evaluates Ruby code string to create proc object.
392
+ #
393
+ # @api private
102
394
  module BracedCommand
395
+ # Builds command structure with evaluated proc.
396
+ #
397
+ # @return [Hash] command with kind :command and executable proc
398
+ #
399
+ # @api private
103
400
  def value
104
401
  { kind: :command,
105
402
  command: eval("proc { #{capture(:complex_command).value.strip} }")
@@ -107,7 +404,18 @@ module Musa
107
404
  end
108
405
  end
109
406
 
407
+ # Semantic action for method call chains.
408
+ #
409
+ # Transforms method call chains like `"object.method1.method2"`.
410
+ # Enables fluent interface notation in Neumalang.
411
+ #
412
+ # @api private
110
413
  module CallMethodsExpression
414
+ # Builds method call chain structure.
415
+ #
416
+ # @return [Hash] call_methods with kind :call_methods, methods array, and target object
417
+ #
418
+ # @api private
111
419
  def value
112
420
  { kind: :call_methods,
113
421
  call_methods: captures(:method_call).collect(&:value),
@@ -115,14 +423,58 @@ module Musa
115
423
  end
116
424
  end
117
425
 
426
+ # Semantic action for variable usage.
427
+ #
428
+ # Transforms variable references like `"@variable"`.
429
+ # Converts name to symbol with @ prefix.
430
+ #
431
+ # @api private
118
432
  module UseVariable
433
+ # Builds variable reference structure.
434
+ #
435
+ # @return [Hash] variable reference with kind :use_variable and symbol name
436
+ #
437
+ # @api private
119
438
  def value
120
439
  { kind: :use_variable,
121
440
  use_variable: "@#{capture(:name).value}".to_sym }.extend Musa::Neumas::Neuma
122
441
  end
123
442
  end
124
443
 
444
+ # Semantic action for neuma notation (core musical event).
445
+ #
446
+ # Transforms neuma notation like `"+2_2.tr"` into GDVD structure.
447
+ # Combines grade, octave, duration, velocity, and modifiers.
448
+ #
449
+ # This is the most important module - it builds the musical events
450
+ # that form the core of Neumalang notation.
451
+ #
452
+ # @api private
125
453
  module NeumaAsAttributes
454
+ # Builds GDVD structure from neuma attributes.
455
+ #
456
+ # Merges:
457
+ #
458
+ # - Grade attributes (delta_grade, abs_grade, etc.)
459
+ # - Octave attributes (delta_octave, abs_octave)
460
+ # - Duration attributes (delta_duration, abs_duration, factor_duration)
461
+ # - Velocity attributes (delta_velocity, abs_velocity)
462
+ # - Modifiers (ornaments, articulations)
463
+ #
464
+ # @return [Hash] GDVD neuma with kind :gdvd
465
+ #
466
+ # @example Parse result
467
+ # # "+2_2.tr" becomes:
468
+ # {
469
+ # kind: :gdvd,
470
+ # gdvd: {
471
+ # delta_grade: 2,
472
+ # factor_duration: 2,
473
+ # modifiers: { tr: true }
474
+ # }.extend(Musa::Datasets::GDVd)
475
+ # }
476
+ #
477
+ # @api private
126
478
  def value
127
479
  h = {}.extend Musa::Datasets::GDVd
128
480
 
@@ -138,31 +490,86 @@ module Musa
138
490
  end
139
491
  end
140
492
 
493
+ # Semantic action for packed vector notation.
494
+ #
495
+ # Transforms packed vector notation `"<a: 1 b: 2>"` into PackedV structure.
496
+ # PackedV is hash-based vector with named components.
497
+ #
498
+ # @api private
141
499
  module PackedVector
500
+ # Builds packed vector structure.
501
+ #
502
+ # @return [Hash] packed_v with kind :packed_v
503
+ #
504
+ # @api private
142
505
  def value
143
506
  { kind: :packed_v, packed_v: capture(:raw_packed_vector).value }.extend(Musa::Neumas::Neuma)
144
507
  end
145
508
  end
146
509
 
510
+ # Semantic action for raw packed vector data.
511
+ #
512
+ # Extracts key-value pairs from packed vector notation.
513
+ #
514
+ # @api private
147
515
  module RawPackedVector
516
+ # Builds PackedV hash.
517
+ #
518
+ # @return [Hash] PackedV extended hash
519
+ #
520
+ # @api private
148
521
  def value
149
522
  captures(:key_value).collect(&:value).to_h.extend(Musa::Datasets::PackedV)
150
523
  end
151
524
  end
152
525
 
526
+ # Semantic action for vector notation.
527
+ #
528
+ # Transforms vector notation `"<1 2 3>"` into V structure.
529
+ # V is array-based vector for musical data.
530
+ #
531
+ # @api private
153
532
  module Vector
533
+ # Builds vector structure.
534
+ #
535
+ # @return [Hash] v with kind :v
536
+ #
537
+ # @api private
154
538
  def value
155
539
  { kind: :v, v: capture(:raw_vector).value }.extend(Musa::Neumas::Neuma)
156
540
  end
157
541
  end
158
542
 
543
+ # Semantic action for raw vector data.
544
+ #
545
+ # Extracts numeric values from vector notation.
546
+ #
547
+ # @api private
159
548
  module RawVector
549
+ # Builds V array.
550
+ #
551
+ # @return [Array] V extended array
552
+ #
553
+ # @api private
160
554
  def value
161
555
  captures(:raw_number).collect(&:value).extend(Musa::Datasets::V)
162
556
  end
163
557
  end
164
558
 
559
+ # Semantic action for process notation (rhythmic process).
560
+ #
561
+ # Transforms process notation `"<< 1 _ _ 2 _ >>"` into P structure.
562
+ # P represents rhythmic/temporal processes with values and rests.
563
+ #
564
+ # @api private
165
565
  module ProcessOfVectors
566
+ # Builds process structure.
567
+ #
568
+ # Interleaves durations and rests: [first, duration1, rest1, duration2, rest2, ...]
569
+ #
570
+ # @return [Hash] p with kind :p
571
+ #
572
+ # @api private
166
573
  def value
167
574
  durations_rest = []
168
575
  i = 0
@@ -179,7 +586,18 @@ module Musa
179
586
  end
180
587
  end
181
588
 
589
+ # Semantic action for differential grade (pitch) attribute.
590
+ #
591
+ # Parses relative pitch changes: `"+2"`, `"-1"`, `"+2#"` (with sharp), `"+I"` (interval).
592
+ # Used for scale-degree-based melodic motion.
593
+ #
594
+ # @api private
182
595
  module DeltaGradeAttribute
596
+ # Builds delta grade structure.
597
+ #
598
+ # @return [Hash] delta_grade, delta_sharps, delta_interval attributes
599
+ #
600
+ # @api private
183
601
  def value
184
602
 
185
603
  value = {}
@@ -196,7 +614,18 @@ module Musa
196
614
  end
197
615
  end
198
616
 
617
+ # Semantic action for absolute grade (pitch) attribute.
618
+ #
619
+ # Parses absolute pitch: `"0"` (tonic), `"2#"` (2nd degree sharp), `"I"` (interval name).
620
+ # Used for establishing absolute pitch positions.
621
+ #
622
+ # @api private
199
623
  module AbsGradeAttribute
624
+ # Builds absolute grade structure.
625
+ #
626
+ # @return [Hash] abs_grade, abs_sharps attributes
627
+ #
628
+ # @api private
200
629
  def value
201
630
  value = {}
202
631
 
@@ -209,19 +638,50 @@ module Musa
209
638
  end
210
639
  end
211
640
 
641
+ # Semantic action for differential octave attribute.
642
+ #
643
+ # Parses relative octave changes: `"^2"` (up 2 octaves), `"v1"` (down 1 octave).
644
+ #
645
+ # @api private
212
646
  module DeltaOctaveAttribute
647
+ # Builds delta octave structure.
648
+ #
649
+ # @return [Hash] delta_octave attribute
650
+ #
651
+ # @api private
213
652
  def value
214
653
  { delta_octave: capture(:sign).value * capture(:number).value }
215
654
  end
216
655
  end
217
656
 
657
+ # Semantic action for absolute octave attribute.
658
+ #
659
+ # Parses absolute octave: `"^2"` (octave 2), used for setting specific octave.
660
+ #
661
+ # @api private
218
662
  module AbsOctaveAttribute
663
+ # Builds absolute octave structure.
664
+ #
665
+ # @return [Hash] abs_octave attribute
666
+ #
667
+ # @api private
219
668
  def value
220
669
  { abs_octave: capture(:number).value }
221
670
  end
222
671
  end
223
672
 
673
+ # Helper module for duration calculations.
674
+ #
675
+ # Calculates duration from notation including dots and slashes.
676
+ # Supports: `_2` (double), `_/2` (half), `_.` (dotted), `_..` (double dotted).
677
+ #
678
+ # @api private
224
679
  module DurationCalculation
680
+ # Calculates duration value.
681
+ #
682
+ # @return [Rational] calculated duration
683
+ #
684
+ # @api private
225
685
  def duration
226
686
  base = capture(:number)&.value
227
687
 
@@ -237,9 +697,19 @@ module Musa
237
697
  end
238
698
  end
239
699
 
700
+ # Semantic action for differential duration attribute (absolute delta).
701
+ #
702
+ # Parses absolute duration changes: `"+1/4"` (add quarter), `"-1/2"` (subtract half).
703
+ #
704
+ # @api private
240
705
  module DeltaDurationAttribute
241
706
  include DurationCalculation
242
707
 
708
+ # Builds delta duration structure.
709
+ #
710
+ # @return [Hash] delta_duration attribute
711
+ #
712
+ # @api private
243
713
  def value
244
714
  sign = capture(:sign).value
245
715
 
@@ -247,21 +717,53 @@ module Musa
247
717
  end
248
718
  end
249
719
 
720
+ # Semantic action for absolute duration attribute.
721
+ #
722
+ # Parses absolute duration: `"1/4"`, `"1"`, `"1/2.."` (double dotted).
723
+ #
724
+ # @api private
250
725
  module AbsDurationAttribute
251
726
  include DurationCalculation
252
727
 
728
+ # Builds absolute duration structure.
729
+ #
730
+ # @return [Hash] abs_duration attribute
731
+ #
732
+ # @api private
253
733
  def value
254
734
  { abs_duration: duration }
255
735
  end
256
736
  end
257
737
 
738
+ # Semantic action for factor duration attribute (multiplicative).
739
+ #
740
+ # Parses duration factors: `"_2"` (double), `"_/2"` (half), `"_*3"` (triple).
741
+ # Most common duration notation in neumas.
742
+ #
743
+ # @api private
258
744
  module FactorDurationAttribute
745
+ # Builds factor duration structure.
746
+ #
747
+ # @return [Hash] factor_duration attribute
748
+ #
749
+ # @api private
259
750
  def value
260
751
  { factor_duration: capture(:number).value ** (capture(:factor).value == '/' ? -1 : 1) }
261
752
  end
262
753
  end
263
754
 
755
+ # Semantic action for absolute velocity attribute.
756
+ #
757
+ # Parses dynamics: `"pp"`, `"p"`, `"mp"`, `"mf"`, `"f"`, `"ff"`, `"fff"`.
758
+ # Converts to numeric velocity levels.
759
+ #
760
+ # @api private
264
761
  module AbsVelocityAttribute
762
+ # Builds absolute velocity structure.
763
+ #
764
+ # @return [Hash] abs_velocity attribute
765
+ #
766
+ # @api private
265
767
  def value
266
768
  if capture(:p)
267
769
  v = -capture(:p).length
@@ -277,7 +779,17 @@ module Musa
277
779
  end
278
780
  end
279
781
 
782
+ # Semantic action for differential velocity attribute.
783
+ #
784
+ # Parses relative dynamics: `"+f"` (louder), `"-p"` (softer).
785
+ #
786
+ # @api private
280
787
  module DeltaVelocityAttribute
788
+ # Builds delta velocity structure.
789
+ #
790
+ # @return [Hash] delta_velocity attribute
791
+ #
792
+ # @api private
281
793
  def value
282
794
  d = capture(:delta).value
283
795
  s = capture(:sign).value
@@ -292,7 +804,18 @@ module Musa
292
804
  end
293
805
  end
294
806
 
807
+ # Semantic action for appogiatura (grace note).
808
+ #
809
+ # Parses grace note notation: `"(+1_/4)+2_"` (grace +1, then main +2).
810
+ # Attaches grace note as modifier to main note.
811
+ #
812
+ # @api private
295
813
  module AppogiaturaNeuma
814
+ # Builds neuma with appogiatura modifier.
815
+ #
816
+ # @return [Hash] neuma with appogiatura in modifiers
817
+ #
818
+ # @api private
296
819
  def value
297
820
  capture(:base).value.tap do |_|
298
821
  _[:gdvd][:modifiers] ||= {}
@@ -301,28 +824,68 @@ module Musa
301
824
  end
302
825
  end
303
826
 
827
+ # Semantic action for symbol values.
828
+ #
829
+ # Parses symbol notation: `":symbol_name"`.
830
+ #
831
+ # @api private
304
832
  module Symbol
833
+ # Builds symbol value structure.
834
+ #
835
+ # @return [Hash] symbol value with kind :value
836
+ #
837
+ # @api private
305
838
  def value
306
839
  { kind: :value,
307
840
  value: capture(:name).value.to_sym }.extend Musa::Neumas::Neuma
308
841
  end
309
842
  end
310
843
 
844
+ # Semantic action for string values.
845
+ #
846
+ # Parses string notation: `"\"text\""`.
847
+ #
848
+ # @api private
311
849
  module String
850
+ # Builds string value structure.
851
+ #
852
+ # @return [Hash] string value with kind :value
853
+ #
854
+ # @api private
312
855
  def value
313
856
  { kind: :value,
314
857
  value: capture(:everything_except_double_quote).value }.extend Musa::Neumas::Neuma
315
858
  end
316
859
  end
317
860
 
861
+ # Semantic action for numeric values.
862
+ #
863
+ # Parses numbers: `42`, `3.14`, `1/2`.
864
+ #
865
+ # @api private
318
866
  module Number
867
+ # Builds number value structure.
868
+ #
869
+ # @return [Hash] number value with kind :value
870
+ #
871
+ # @api private
319
872
  def value
320
873
  { kind: :value,
321
874
  value: capture(:raw_number).value }.extend Musa::Neumas::Neuma
322
875
  end
323
876
  end
324
877
 
878
+ # Semantic action for special values.
879
+ #
880
+ # Parses special keywords: `nil`, `true`, `false`.
881
+ #
882
+ # @api private
325
883
  module Special
884
+ # Builds special value structure.
885
+ #
886
+ # @return [Hash] special value with kind :value
887
+ #
888
+ # @api private
326
889
  def value
327
890
  v = captures(0)
328
891
  { kind: :value,
@@ -333,6 +896,79 @@ module Musa
333
896
 
334
897
  extend self
335
898
 
899
+ # Parses Neumalang notation string or file to structured neuma objects.
900
+ #
901
+ # Main parsing method. Uses Citrus parser to transform text notation
902
+ # into structured neuma series. Optionally decodes GDVD to GDV using
903
+ # provided decoder.
904
+ #
905
+ # ## Parsing Process
906
+ #
907
+ # 1. Parse text with Citrus grammar
908
+ # 2. Apply semantic action modules to build neuma structures
909
+ # 3. Optionally decode GDVD events to GDV with decoder
910
+ # 4. Return serie of neuma objects
911
+ #
912
+ # ## Decoder Integration
913
+ #
914
+ # If `decode_with` parameter provided:
915
+ #
916
+ # - GDVD events decoded to GDV (absolute format)
917
+ # - Requires NeumaDecoder or compatible decoder
918
+ # - Useful for immediate conversion to playable events
919
+ #
920
+ # ## Debug Mode
921
+ #
922
+ # If `debug: true`:
923
+ #
924
+ # - Dumps parse tree to stdout
925
+ # - Shows grammar rule matches
926
+ # - Useful for understanding parsing or debugging grammar
927
+ #
928
+ # @param string_or_file [String, File] neuma notation to parse
929
+ # @param decode_with [Decoder, nil] optional decoder for GDVD→GDV conversion
930
+ # @param debug [Boolean, nil] enable parse tree debugging output
931
+ #
932
+ # @return [Serie, Array] parsed neuma serie or array of neumas
933
+ #
934
+ # @raise [ArgumentError] if string_or_file is not String or File
935
+ # @raise [Citrus::ParseError] if notation has syntax errors
936
+ #
937
+ # @example Parse simple notation
938
+ # neumas = Musa::Neumalang::Neumalang.parse("(0) (+2) (+2) (-1) (0)")
939
+ # # => Serie of GDVD neuma objects
940
+ #
941
+ # @example Parse with decoder (immediate GDV conversion)
942
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
943
+ # decoder = Musa::Neumas::Decoders::NeumaDecoder.new(
944
+ # scale,
945
+ # base_duration: 1/4r
946
+ # )
947
+ #
948
+ # gdvs = Musa::Neumalang::Neumalang.parse(
949
+ # "(0) (+2) (+2) (-1) (0)",
950
+ # decode_with: decoder
951
+ # )
952
+ # # => Serie of GDV events ready for playback
953
+ #
954
+ # @example Parse file
955
+ # file = File.open("melody.neuma")
956
+ # neumas = Musa::Neumalang::Neumalang.parse(file)
957
+ #
958
+ # @example Debug parsing
959
+ # neumas = Musa::Neumalang::Neumalang.parse(
960
+ # "(0) (+2) (+2)",
961
+ # debug: true
962
+ # )
963
+ # # Prints parse tree to stdout
964
+ #
965
+ # @example Complex notation
966
+ # neumas = Musa::Neumalang::Neumalang.parse(
967
+ # "[(0) (+2 .tr) (+4 _) | (+7) (+5 .mor) (+7 _)] (+9 _2)"
968
+ # )
969
+ # # Parallel voices followed by longer note
970
+ #
971
+ # @api public
336
972
  def parse(string_or_file, decode_with: nil, debug: nil)
337
973
  case string_or_file
338
974
  when String
@@ -362,12 +998,51 @@ module Musa
362
998
  end
363
999
  end
364
1000
 
1001
+ # Parses Neumalang notation file to structured neuma objects.
1002
+ #
1003
+ # Convenience method for parsing files. Opens file and calls `parse`.
1004
+ #
1005
+ # @param filename [String] path to neuma notation file
1006
+ # @param decode_with [Decoder, nil] optional decoder for GDVD→GDV conversion
1007
+ # @param debug [Boolean, nil] enable parse tree debugging output
1008
+ #
1009
+ # @return [Serie, Array] parsed neuma serie or array
1010
+ #
1011
+ # @raise [Errno::ENOENT] if file not found
1012
+ # @raise [Citrus::ParseError] if notation has syntax errors
1013
+ #
1014
+ # @example Parse neuma file
1015
+ # neumas = Musa::Neumalang::Neumalang.parse_file("melodies/theme.neuma")
1016
+ #
1017
+ # @example Parse with decoder
1018
+ # scale = Musa::Scales::Scales.et12[440.0].major[60]
1019
+ # decoder = Musa::Neumas::Decoders::NeumaDecoder.new(scale)
1020
+ # gdvs = Musa::Neumalang::Neumalang.parse_file(
1021
+ # "melodies/theme.neuma",
1022
+ # decode_with: decoder
1023
+ # )
1024
+ #
1025
+ # @api public
365
1026
  def parse_file(filename, decode_with: nil, debug: nil)
366
1027
  File.open filename do |file|
367
1028
  parse file, decode_with: decode_with, debug: debug
368
1029
  end
369
1030
  end
370
1031
 
1032
+ # Load Citrus grammar files.
1033
+ #
1034
+ # Grammars are loaded in dependency order:
1035
+ #
1036
+ # 1. terminals.citrus - Basic tokens
1037
+ # 2. datatypes.citrus - Data types
1038
+ # 3. neuma.citrus - Neuma notation
1039
+ # 4. vectors.citrus - Vector notation
1040
+ # 5. process.citrus - Process notation
1041
+ # 6. neumalang.citrus - Top-level grammar
1042
+ #
1043
+ # Grammar classes are loaded into Parser::Grammar namespace.
1044
+ #
1045
+ # @api private
371
1046
  Citrus.load File.join(File.dirname(__FILE__), 'terminals.citrus')
372
1047
  Citrus.load File.join(File.dirname(__FILE__), 'datatypes.citrus')
373
1048
  Citrus.load File.join(File.dirname(__FILE__), 'neuma.citrus')