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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +233 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +180 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
- data/lib/musa-dsl/generative/markov.rb +133 -3
- data/lib/musa-dsl/generative/rules.rb +258 -4
- data/lib/musa-dsl/generative/variatio.rb +217 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
- data/lib/musa-dsl/midi/midi-voices.rb +265 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +308 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
- data/lib/musa-dsl/music/scales.rb +957 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +48 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
- data/lib/musa-dsl/series/queue-serie.rb +65 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +1 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +12 -10
- 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
|
-
|
|
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
|