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
data/lib/musa-dsl/datasets/e.rb
CHANGED
|
@@ -1,70 +1,254 @@
|
|
|
1
1
|
require_relative 'dataset'
|
|
2
2
|
|
|
3
3
|
module Musa::Datasets
|
|
4
|
+
# Base module for musical events.
|
|
5
|
+
#
|
|
6
|
+
# E (Event) is the base module for all dataset types representing musical events.
|
|
7
|
+
# It provides validation interface and defines the concept of "natural keys" -
|
|
8
|
+
# keys that are inherent to the dataset type.
|
|
9
|
+
#
|
|
10
|
+
# ## Natural Keys
|
|
11
|
+
#
|
|
12
|
+
# Each dataset type defines which keys are "natural" to it (i.e., semantically
|
|
13
|
+
# meaningful for that type). Keys not in NaturalKeys are considered modifiers
|
|
14
|
+
# or extensions.
|
|
15
|
+
#
|
|
16
|
+
# ## Validation
|
|
17
|
+
#
|
|
18
|
+
# Events can be validated to ensure they contain required keys and valid values.
|
|
19
|
+
# Subclasses should override {#valid?} to implement type-specific validation.
|
|
20
|
+
#
|
|
21
|
+
# @example Basic validation
|
|
22
|
+
# event = { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::E)
|
|
23
|
+
# event.valid? # => true
|
|
24
|
+
# event.validate! # Returns if valid, raises if not
|
|
25
|
+
#
|
|
26
|
+
# @see Abs Absolute value events
|
|
27
|
+
# @see Delta Delta (incremental) events
|
|
4
28
|
module E
|
|
5
29
|
include Dataset
|
|
6
30
|
|
|
31
|
+
# Natural keys for base events (empty).
|
|
32
|
+
# @return [Array<Symbol>]
|
|
7
33
|
NaturalKeys = [].freeze
|
|
8
34
|
|
|
9
|
-
#
|
|
10
|
-
# TODO should valid? and validate! be on Dataset instead of E? P dataset inherits from Dataset but probably it could be validated
|
|
35
|
+
# Checks if event is valid.
|
|
11
36
|
#
|
|
37
|
+
# Base implementation always returns true. Subclasses should override
|
|
38
|
+
# to implement specific validation logic.
|
|
39
|
+
#
|
|
40
|
+
# @return [Boolean] true if valid
|
|
41
|
+
#
|
|
42
|
+
# @example
|
|
43
|
+
# event.valid? # => true
|
|
12
44
|
def valid?
|
|
13
45
|
true
|
|
14
46
|
end
|
|
15
47
|
|
|
48
|
+
# Validates event, raising if invalid.
|
|
49
|
+
#
|
|
50
|
+
# @raise [RuntimeError] if event is not valid
|
|
51
|
+
# @return [void]
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# event.validate! # Raises if invalid
|
|
16
55
|
def validate!
|
|
17
56
|
raise RuntimeError, "Invalid dataset #{self}" unless valid?
|
|
18
57
|
end
|
|
19
58
|
end
|
|
20
59
|
|
|
60
|
+
# Events with absolute values.
|
|
61
|
+
#
|
|
62
|
+
# Abs (Absolute) represents events where all values are absolute (not relative).
|
|
63
|
+
# Examples: actual MIDI pitch 60, duration 1.0 seconds, velocity 64.
|
|
64
|
+
#
|
|
65
|
+
# Contrast with {Delta} where values are incremental.
|
|
66
|
+
#
|
|
67
|
+
# @see Delta Incremental events
|
|
68
|
+
# @see AbsI Absolute indexed (arrays)
|
|
69
|
+
# @see AbsTimed Absolute with time
|
|
70
|
+
# @see AbsD Absolute with duration
|
|
21
71
|
module Abs
|
|
22
72
|
include E
|
|
23
73
|
end
|
|
24
74
|
|
|
75
|
+
# Events with delta (incremental) values.
|
|
76
|
+
#
|
|
77
|
+
# Delta represents events where values are incremental changes from a previous
|
|
78
|
+
# state. Examples: pitch +2 semitones, duration +0.5 beats, velocity -10.
|
|
79
|
+
#
|
|
80
|
+
# Delta encoding is efficient for sequences where consecutive events have
|
|
81
|
+
# similar values.
|
|
82
|
+
#
|
|
83
|
+
# @example Delta vs Absolute
|
|
84
|
+
# # Absolute encoding (3 events)
|
|
85
|
+
# { pitch: 60, duration: 1.0 }
|
|
86
|
+
# { pitch: 62, duration: 1.0 }
|
|
87
|
+
# { pitch: 64, duration: 1.0 }
|
|
88
|
+
#
|
|
89
|
+
# # Delta encoding (same 3 events)
|
|
90
|
+
# { abs_pitch: 60, abs_duration: 1.0 } # First event absolute
|
|
91
|
+
# { delta_pitch: +2 } # Duration unchanged
|
|
92
|
+
# { delta_pitch: +2 } # Duration unchanged
|
|
93
|
+
#
|
|
94
|
+
# @see Abs Absolute events
|
|
95
|
+
# @see DeltaD Delta with duration
|
|
25
96
|
module Delta
|
|
26
97
|
include E
|
|
27
98
|
end
|
|
28
99
|
|
|
100
|
+
# Absolute indexed events (array-based).
|
|
101
|
+
#
|
|
102
|
+
# AbsI represents absolute events stored in indexed structures (arrays).
|
|
103
|
+
# Used by {V} and {PackedV} modules.
|
|
104
|
+
#
|
|
105
|
+
# @see Abs Parent absolute module
|
|
106
|
+
# @see V Value arrays
|
|
107
|
+
# @see PackedV Packed value hashes
|
|
29
108
|
module AbsI
|
|
30
109
|
include Abs
|
|
31
110
|
end
|
|
32
111
|
|
|
112
|
+
# Absolute events with time component.
|
|
113
|
+
#
|
|
114
|
+
# AbsTimed represents absolute events that occur at a specific time point.
|
|
115
|
+
# The `:time` key indicates when the event occurs.
|
|
116
|
+
#
|
|
117
|
+
# ## Natural Keys
|
|
118
|
+
#
|
|
119
|
+
# - **:time**: Absolute time position
|
|
120
|
+
#
|
|
121
|
+
# @example Timed event
|
|
122
|
+
# { time: 0.0, value: { pitch: 60 } }.extend(AbsTimed)
|
|
123
|
+
# { time: 1.0, value: { pitch: 64 } }.extend(AbsTimed)
|
|
124
|
+
#
|
|
125
|
+
# @see Abs Parent absolute module
|
|
126
|
+
# @see P Pitch series (produces AbsTimed)
|
|
33
127
|
module AbsTimed
|
|
34
128
|
include Abs
|
|
35
129
|
|
|
130
|
+
# Natural keys including time.
|
|
131
|
+
# @return [Array<Symbol>]
|
|
36
132
|
NaturalKeys = (NaturalKeys + [:time]).freeze
|
|
37
133
|
end
|
|
38
134
|
|
|
135
|
+
# Delta indexed events (array-based deltas).
|
|
136
|
+
#
|
|
137
|
+
# DeltaI represents delta events stored in indexed structures.
|
|
138
|
+
#
|
|
139
|
+
# @see Delta Parent delta module
|
|
39
140
|
module DeltaI
|
|
40
141
|
include Delta
|
|
41
142
|
end
|
|
42
143
|
|
|
144
|
+
# Absolute events with duration.
|
|
145
|
+
#
|
|
146
|
+
# AbsD represents absolute events that have duration - they occupy a time span
|
|
147
|
+
# rather than occurring at a single instant.
|
|
148
|
+
#
|
|
149
|
+
# ## Natural Keys
|
|
150
|
+
#
|
|
151
|
+
# - **:duration**: Total duration of the event process
|
|
152
|
+
# - **:note_duration**: Actual note duration (may differ for staccato, etc.)
|
|
153
|
+
# - **:forward_duration**: Time until next event (may be 0 for simultaneous events)
|
|
154
|
+
#
|
|
155
|
+
# ## Duration Types
|
|
156
|
+
#
|
|
157
|
+
# **duration**: How long the event process lasts (note playing, dynamics change, etc.)
|
|
158
|
+
#
|
|
159
|
+
# **note_duration**: Actual note length. For staccato, this is shorter than duration.
|
|
160
|
+
# Defaults to duration if not specified.
|
|
161
|
+
#
|
|
162
|
+
# **forward_duration**: Time to wait before next event. Can be:
|
|
163
|
+
#
|
|
164
|
+
# - Same as duration (default): next event starts when this one ends
|
|
165
|
+
# - Less than duration: events overlap
|
|
166
|
+
# - Zero: next event starts simultaneously
|
|
167
|
+
# - More than duration: gap/rest before next event
|
|
168
|
+
#
|
|
169
|
+
# @example Basic duration
|
|
170
|
+
# { pitch: 60, duration: 1.0 }.extend(AbsD)
|
|
171
|
+
# event.duration # => 1.0
|
|
172
|
+
# event.note_duration # => 1.0 (defaults to duration)
|
|
173
|
+
# event.forward_duration # => 1.0 (defaults to duration)
|
|
174
|
+
#
|
|
175
|
+
# @example Staccato note
|
|
176
|
+
# { pitch: 60, duration: 1.0, note_duration: 0.5 }.extend(AbsD)
|
|
177
|
+
# # Note sounds for 0.5, but next event waits 1.0
|
|
178
|
+
#
|
|
179
|
+
# @example Simultaneous events
|
|
180
|
+
# { pitch: 60, duration: 1.0, forward_duration: 0 }.extend(AbsD)
|
|
181
|
+
# # Next event starts immediately (chord)
|
|
182
|
+
#
|
|
183
|
+
# @see Abs Parent absolute module
|
|
184
|
+
# @see PS Pitch series with duration
|
|
185
|
+
# @see PDV Pitch/Duration/Velocity
|
|
186
|
+
# @see GDV Grade/Duration/Velocity
|
|
43
187
|
module AbsD
|
|
44
188
|
include Abs
|
|
45
189
|
|
|
190
|
+
# Natural keys including duration variants.
|
|
191
|
+
# @return [Array<Symbol>]
|
|
46
192
|
NaturalKeys = (NaturalKeys +
|
|
47
193
|
[:duration, # duration of the process (note reproduction, dynamics evolution, etc)
|
|
48
194
|
:note_duration, # duration of the note (a staccato note is effectively shorter than elapsed duration until next note)
|
|
49
195
|
:forward_duration # duration to wait until next event (if 0 means the next event should be executed at the same time than this one)
|
|
50
196
|
]).freeze
|
|
51
197
|
|
|
198
|
+
# Returns forward duration (time until next event).
|
|
199
|
+
#
|
|
200
|
+
# Defaults to `:duration` if `:forward_duration` not specified.
|
|
201
|
+
#
|
|
202
|
+
# @return [Numeric] forward duration
|
|
203
|
+
#
|
|
204
|
+
# @example
|
|
205
|
+
# event.forward_duration # => 1.0
|
|
52
206
|
def forward_duration
|
|
53
207
|
self[:forward_duration] || self[:duration]
|
|
54
208
|
end
|
|
55
209
|
|
|
210
|
+
# Returns actual note duration.
|
|
211
|
+
#
|
|
212
|
+
# Defaults to `:duration` if `:note_duration` not specified.
|
|
213
|
+
#
|
|
214
|
+
# @return [Numeric] note duration
|
|
215
|
+
#
|
|
216
|
+
# @example
|
|
217
|
+
# event.note_duration # => 0.5 (staccato)
|
|
56
218
|
def note_duration
|
|
57
219
|
self[:note_duration] || self[:duration]
|
|
58
220
|
end
|
|
59
221
|
|
|
222
|
+
# Returns event duration.
|
|
223
|
+
#
|
|
224
|
+
# @return [Numeric] duration
|
|
225
|
+
#
|
|
226
|
+
# @example
|
|
227
|
+
# event.duration # => 1.0
|
|
60
228
|
def duration
|
|
61
229
|
self[:duration]
|
|
62
230
|
end
|
|
63
231
|
|
|
232
|
+
# Checks if thing can be converted to AbsD.
|
|
233
|
+
#
|
|
234
|
+
# @param thing [Object] object to check
|
|
235
|
+
# @return [Boolean] true if compatible
|
|
236
|
+
#
|
|
237
|
+
# @example
|
|
238
|
+
# AbsD.is_compatible?({ duration: 1.0 }) # => true
|
|
239
|
+
# AbsD.is_compatible?({ pitch: 60 }) # => false
|
|
64
240
|
def self.is_compatible?(thing)
|
|
65
241
|
thing.is_a?(AbsD) || thing.is_a?(Hash) && thing.has_key?(:duration)
|
|
66
242
|
end
|
|
67
243
|
|
|
244
|
+
# Converts thing to AbsD if possible.
|
|
245
|
+
#
|
|
246
|
+
# @param thing [Object] object to convert
|
|
247
|
+
# @return [AbsD] AbsD dataset
|
|
248
|
+
# @raise [ArgumentError] if thing cannot be converted
|
|
249
|
+
#
|
|
250
|
+
# @example
|
|
251
|
+
# AbsD.to_AbsD({ duration: 1.0 }) # => AbsD dataset
|
|
68
252
|
def self.to_AbsD(thing)
|
|
69
253
|
if thing.is_a?(AbsD)
|
|
70
254
|
thing
|
|
@@ -5,6 +5,136 @@ require_relative 'pdv'
|
|
|
5
5
|
require_relative 'helper'
|
|
6
6
|
|
|
7
7
|
module Musa::Datasets
|
|
8
|
+
# Score-style musical events with scale degrees.
|
|
9
|
+
#
|
|
10
|
+
# GDV (Grade/Duration/Velocity) represents musical events using score notation
|
|
11
|
+
# with scale degrees, octaves, and dynamics. Extends {AbsD} for duration support.
|
|
12
|
+
#
|
|
13
|
+
# ## Purpose
|
|
14
|
+
#
|
|
15
|
+
# GDV is the score representation layer of the dataset framework:
|
|
16
|
+
#
|
|
17
|
+
# - Uses scale degrees (grade) instead of absolute pitches
|
|
18
|
+
# - Uses dynamics markings (velocity -5 to +4) instead of MIDI velocity
|
|
19
|
+
# - Human-readable and musically meaningful
|
|
20
|
+
# - Independent of specific tuning or scale
|
|
21
|
+
#
|
|
22
|
+
# Contrast with {PDV} which uses MIDI absolute pitches and velocities.
|
|
23
|
+
#
|
|
24
|
+
# ## Natural Keys
|
|
25
|
+
#
|
|
26
|
+
# - **:grade**: Scale degree (integer, 0-based)
|
|
27
|
+
# - **:sharps**: Chromatic alteration (integer, positive = sharp, negative = flat)
|
|
28
|
+
# - **:octave**: Octave offset (integer, 0 = base octave)
|
|
29
|
+
# - **:velocity**: Dynamics (-3 to +4, where 0 = mp, 1 = mf)
|
|
30
|
+
# - **:silence**: Indicates rest (boolean or symbol)
|
|
31
|
+
# - **:duration**: Event duration (from {AbsD})
|
|
32
|
+
# - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
|
|
33
|
+
#
|
|
34
|
+
# ## Pitch Representation
|
|
35
|
+
#
|
|
36
|
+
# Pitches are specified as:
|
|
37
|
+
#
|
|
38
|
+
# - **grade**: Position in scale (0 = first note, 1 = second note, etc.)
|
|
39
|
+
# - **octave**: Octave offset (0 = base, 1 = up one octave, -1 = down one octave)
|
|
40
|
+
# - **sharps**: Chromatic alteration (1 = sharp, -1 = flat, 2 = double sharp, etc.)
|
|
41
|
+
#
|
|
42
|
+
# Example in C major scale:
|
|
43
|
+
#
|
|
44
|
+
# - C4 = { grade: 0, octave: 0 }
|
|
45
|
+
# - D4 = { grade: 1, octave: 0 }
|
|
46
|
+
# - C5 = { grade: 0, octave: 1 }
|
|
47
|
+
# - C#4 = { grade: 0, octave: 0, sharps: 1 }
|
|
48
|
+
#
|
|
49
|
+
# ## Velocity (Dynamics)
|
|
50
|
+
#
|
|
51
|
+
# Velocity represents musical dynamics in range -3 to +4:
|
|
52
|
+
#
|
|
53
|
+
# -3: ppp (pianississimo)
|
|
54
|
+
# -2: pp (pianissimo)
|
|
55
|
+
# -1: p (piano)
|
|
56
|
+
# 0: mp (mezzo-piano)
|
|
57
|
+
# +1: mf (mezzo-forte)
|
|
58
|
+
# +2: f (forte)
|
|
59
|
+
# +3: ff (fortissimo)
|
|
60
|
+
# +4: fff (fortississimo)
|
|
61
|
+
#
|
|
62
|
+
# ## Conversions
|
|
63
|
+
#
|
|
64
|
+
# ### To PDV (MIDI)
|
|
65
|
+
#
|
|
66
|
+
# Converts score notation to MIDI using a scale:
|
|
67
|
+
#
|
|
68
|
+
# gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
69
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
70
|
+
# pdv = gdv.to_pdv(scale)
|
|
71
|
+
# # => { pitch: 60, duration: 1.0, velocity: 64 }
|
|
72
|
+
#
|
|
73
|
+
# ### To GDVd (Delta Encoding)
|
|
74
|
+
#
|
|
75
|
+
# Converts to delta encoding for efficient storage:
|
|
76
|
+
#
|
|
77
|
+
# gdv1 = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
78
|
+
# gdv2 = { grade: 2, octave: 0, duration: 1.0, velocity: 1 }.extend(GDV)
|
|
79
|
+
# gdvd = gdv2.to_gdvd(scale, previous: gdv1)
|
|
80
|
+
# # => { delta_grade: 2, delta_velocity: 1 }
|
|
81
|
+
#
|
|
82
|
+
# ### To Neuma Notation
|
|
83
|
+
#
|
|
84
|
+
# Converts to Neuma string format for serialization:
|
|
85
|
+
#
|
|
86
|
+
# gdv = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
87
|
+
# gdv.base_duration = 1/4r
|
|
88
|
+
# gdv.to_neuma # => "(0 4 mp)"
|
|
89
|
+
#
|
|
90
|
+
# ## MIDI Velocity Mapping
|
|
91
|
+
#
|
|
92
|
+
# Dynamics are mapped to MIDI velocities using interpolation:
|
|
93
|
+
#
|
|
94
|
+
# -3 (ppp) → 16
|
|
95
|
+
# -2 (pp) → 33
|
|
96
|
+
# -1 (p) → 49
|
|
97
|
+
# 0 (mp) → 64
|
|
98
|
+
# +1 (mf) → 80
|
|
99
|
+
# +2 (f) → 96
|
|
100
|
+
# +3 (ff) → 112
|
|
101
|
+
# +4 (fff) → 127
|
|
102
|
+
#
|
|
103
|
+
# @example Basic score event
|
|
104
|
+
# gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(Musa::Datasets::GDV)
|
|
105
|
+
# gdv.base_duration = 1/4r
|
|
106
|
+
# # First scale degree, base octave, 1 beat, mp dynamics
|
|
107
|
+
#
|
|
108
|
+
# @example Chromatic alteration
|
|
109
|
+
# gdv = { grade: 0, octave: 0, sharps: 1, duration: 1.0 }.extend(GDV)
|
|
110
|
+
# # First scale degree sharp (C# in C major)
|
|
111
|
+
#
|
|
112
|
+
# @example Silence (rest)
|
|
113
|
+
# gdv = { grade: :silence, duration: 1.0 }.extend(GDV)
|
|
114
|
+
# # Rest for 1 beat
|
|
115
|
+
#
|
|
116
|
+
# @example Convert to MIDI
|
|
117
|
+
# gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
118
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
119
|
+
# pdv = gdv.to_pdv(scale)
|
|
120
|
+
# # => { pitch: 60, duration: 1.0, velocity: 64 }
|
|
121
|
+
#
|
|
122
|
+
# @example Convert to delta encoding
|
|
123
|
+
# gdv1 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
124
|
+
# gdv2 = { grade: 2, duration: 1.0, velocity: 1 }.extend(GDV)
|
|
125
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
126
|
+
# gdvd = gdv2.to_gdvd(scale, previous: gdv1)
|
|
127
|
+
# # => { delta_grade: 2, delta_velocity: 1 }
|
|
128
|
+
#
|
|
129
|
+
# @example Convert to Neuma notation
|
|
130
|
+
# gdv = { grade: 0, octave: 1, duration: 1.0, velocity: 2 }.extend(GDV)
|
|
131
|
+
# gdv.base_duration = 1/4r
|
|
132
|
+
# gdv.to_neuma # => "(0 o1 4 ff)"
|
|
133
|
+
#
|
|
134
|
+
# @see PDV MIDI-style representation
|
|
135
|
+
# @see GDVd Delta encoding
|
|
136
|
+
# @see AbsD Absolute duration events
|
|
137
|
+
# @see Helper String formatting utilities
|
|
8
138
|
module GDV
|
|
9
139
|
using Musa::Extension::InspectNice
|
|
10
140
|
|
|
@@ -12,14 +142,60 @@ module Musa::Datasets
|
|
|
12
142
|
|
|
13
143
|
include Helper
|
|
14
144
|
|
|
145
|
+
# Natural keys for score events.
|
|
146
|
+
# @return [Array<Symbol>]
|
|
15
147
|
NaturalKeys = (NaturalKeys + [:grade, :sharps, :octave, :velocity, :silence]).freeze
|
|
16
148
|
|
|
149
|
+
# Base duration for time calculations.
|
|
150
|
+
# @return [Rational]
|
|
17
151
|
attr_accessor :base_duration
|
|
18
152
|
|
|
153
|
+
# MIDI velocity mapping for dynamics.
|
|
154
|
+
#
|
|
155
|
+
# Maps dynamics values (-5 to +4) to MIDI velocities (0-127).
|
|
156
|
+
# Used for interpolation in {#to_pdv}.
|
|
157
|
+
#
|
|
158
|
+
# @return [Array<Integer>] MIDI velocity breakpoints
|
|
159
|
+
# @api private
|
|
19
160
|
# TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
|
|
20
161
|
# ppp = 16 ... fff = 127 (-5 ... 4) the standard used by Musescore 3 and others starts at ppp = 16
|
|
21
162
|
VELOCITY_MAP = [1, 8, 16, 33, 49, 64, 80, 96, 112, 127].freeze
|
|
22
163
|
|
|
164
|
+
# Converts to PDV (MIDI representation).
|
|
165
|
+
#
|
|
166
|
+
# Translates score notation to MIDI using a scale:
|
|
167
|
+
# - Scale degree → MIDI pitch (via scale lookup)
|
|
168
|
+
# - Dynamics → MIDI velocity (via interpolation)
|
|
169
|
+
# - Duration values copied
|
|
170
|
+
# - Additional keys preserved
|
|
171
|
+
#
|
|
172
|
+
# @param scale [Musa::Scales::Scale] reference scale for pitch conversion
|
|
173
|
+
#
|
|
174
|
+
# @return [PDV] MIDI representation dataset
|
|
175
|
+
#
|
|
176
|
+
# @example Basic conversion
|
|
177
|
+
# gdv = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
178
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
179
|
+
# pdv = gdv.to_pdv(scale)
|
|
180
|
+
# # => { pitch: 60, duration: 1.0, velocity: 64 }
|
|
181
|
+
#
|
|
182
|
+
# @example Chromatic note
|
|
183
|
+
# gdv = { grade: 0, octave: 0, sharps: 1, duration: 1.0 }.extend(GDV)
|
|
184
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
185
|
+
# pdv = gdv.to_pdv(scale)
|
|
186
|
+
# # => { pitch: 61, duration: 1.0 }
|
|
187
|
+
#
|
|
188
|
+
# @example Silence
|
|
189
|
+
# gdv = { grade: :silence, duration: 1.0 }.extend(GDV)
|
|
190
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
191
|
+
# pdv = gdv.to_pdv(scale)
|
|
192
|
+
# # => { pitch: :silence, duration: 1.0 }
|
|
193
|
+
#
|
|
194
|
+
# @example Dynamics interpolation
|
|
195
|
+
# gdv = { grade: 0, velocity: 0.5 }.extend(GDV)
|
|
196
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
197
|
+
# pdv = gdv.to_pdv(scale)
|
|
198
|
+
# # velocity 0.5 interpolates between mf (64) and f (80)
|
|
23
199
|
def to_pdv(scale)
|
|
24
200
|
pdv = {}.extend PDV
|
|
25
201
|
pdv.base_duration = @base_duration
|
|
@@ -50,7 +226,7 @@ module Musa::Datasets
|
|
|
50
226
|
else
|
|
51
227
|
self[:velocity] < -5 ? -5 : 4
|
|
52
228
|
end
|
|
53
|
-
|
|
229
|
+
|
|
54
230
|
index_min = index.floor
|
|
55
231
|
index_max = index.ceil
|
|
56
232
|
|
|
@@ -65,6 +241,51 @@ module Musa::Datasets
|
|
|
65
241
|
pdv
|
|
66
242
|
end
|
|
67
243
|
|
|
244
|
+
# Converts to Neuma notation string.
|
|
245
|
+
#
|
|
246
|
+
# Neuma is a compact text format for score notation. Format:
|
|
247
|
+
#
|
|
248
|
+
# (grade[sharps] [octave] [duration] [velocity] [modifiers...])
|
|
249
|
+
#
|
|
250
|
+
# - **grade**: Scale degree number (0, 1, 2...) or 'silence' for rests
|
|
251
|
+
# - **sharps**: '#' for sharp, '_' for flat (e.g., "0#" = first degree sharp)
|
|
252
|
+
# - **octave**: 'o' + number (e.g., "o1" = up one octave, "o-1" = down one)
|
|
253
|
+
# - **duration**: Number of base_duration units
|
|
254
|
+
# - **velocity**: Dynamics string (ppp, pp, p, mp, mf, f, ff, fff)
|
|
255
|
+
# - **modifiers**: Additional key-value pairs (e.g., "staccato")
|
|
256
|
+
#
|
|
257
|
+
# @return [String] Neuma notation
|
|
258
|
+
#
|
|
259
|
+
# @example Basic note
|
|
260
|
+
# gdv = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
261
|
+
# gdv.base_duration = 1/4r
|
|
262
|
+
# gdv.to_neuma # => "(0 4 mf)"
|
|
263
|
+
# # grade 0, duration 4 quarters, mf dynamics
|
|
264
|
+
#
|
|
265
|
+
# @example With octave
|
|
266
|
+
# gdv = { grade: 2, octave: 1, duration: 0.5, velocity: 2 }.extend(GDV)
|
|
267
|
+
# gdv.base_duration = 1/4r
|
|
268
|
+
# gdv.to_neuma # => "(2 o1 2 ff)"
|
|
269
|
+
#
|
|
270
|
+
# @example Sharp note
|
|
271
|
+
# gdv = { grade: 0, sharps: 1, duration: 1.0 }.extend(GDV)
|
|
272
|
+
# gdv.base_duration = 1/4r
|
|
273
|
+
# gdv.to_neuma # => "(0# 4)"
|
|
274
|
+
#
|
|
275
|
+
# @example Flat note
|
|
276
|
+
# gdv = { grade: 1, sharps: -1, duration: 1.0 }.extend(GDV)
|
|
277
|
+
# gdv.base_duration = 1/4r
|
|
278
|
+
# gdv.to_neuma # => "(1_ 4)"
|
|
279
|
+
#
|
|
280
|
+
# @example Silence
|
|
281
|
+
# gdv = { grade: :silence, duration: 1.0 }.extend(GDV)
|
|
282
|
+
# gdv.base_duration = 1/4r
|
|
283
|
+
# gdv.to_neuma # => "(silence 4)"
|
|
284
|
+
#
|
|
285
|
+
# @example With modifiers
|
|
286
|
+
# gdv = { grade: 0, duration: 1.0, staccato: true }.extend(GDV)
|
|
287
|
+
# gdv.base_duration = 1/4r
|
|
288
|
+
# gdv.to_neuma # => "(0 4 staccato)"
|
|
68
289
|
def to_neuma
|
|
69
290
|
@base_duration ||= Rational(1, 4)
|
|
70
291
|
|
|
@@ -80,7 +301,7 @@ module Musa::Datasets
|
|
|
80
301
|
if self[:sharps] > 0
|
|
81
302
|
attributes[c] += '#' * self[:sharps]
|
|
82
303
|
elsif self[:sharps] < 0
|
|
83
|
-
attributes[c] += '_' * self[:sharps]
|
|
304
|
+
attributes[c] += '_' * self[:sharps].abs
|
|
84
305
|
end
|
|
85
306
|
end
|
|
86
307
|
end
|
|
@@ -98,12 +319,68 @@ module Musa::Datasets
|
|
|
98
319
|
'(' + attributes.join(' ') + ')'
|
|
99
320
|
end
|
|
100
321
|
|
|
322
|
+
# Converts velocity number to dynamics string.
|
|
323
|
+
#
|
|
324
|
+
# Maps numeric velocity (-3 to +4) to standard dynamics markings.
|
|
325
|
+
#
|
|
326
|
+
# @param x [Integer] velocity value
|
|
327
|
+
# @return [String] dynamics marking
|
|
328
|
+
#
|
|
329
|
+
# @example
|
|
330
|
+
# velocity_of(-3) # => "ppp"
|
|
331
|
+
# velocity_of(0) # => "mp"
|
|
332
|
+
# velocity_of(1) # => "mf"
|
|
333
|
+
# velocity_of(4) # => "fff"
|
|
334
|
+
#
|
|
335
|
+
# @api private
|
|
101
336
|
def velocity_of(x)
|
|
102
337
|
%w[ppp pp p mp mf f ff fff][x + 3]
|
|
103
338
|
end
|
|
104
339
|
|
|
105
340
|
private :velocity_of
|
|
106
341
|
|
|
342
|
+
# Converts to GDVd (delta encoding).
|
|
343
|
+
#
|
|
344
|
+
# Creates delta-encoded representation relative to a previous event.
|
|
345
|
+
# Only changed values are included, making the representation compact.
|
|
346
|
+
#
|
|
347
|
+
# Without previous event (first in sequence):
|
|
348
|
+
# - Uses abs_ keys for all values
|
|
349
|
+
#
|
|
350
|
+
# With previous event:
|
|
351
|
+
# - Uses delta_ keys for changed values
|
|
352
|
+
# - Omits unchanged values
|
|
353
|
+
# - Uses abs_ keys when changing from nil to value
|
|
354
|
+
#
|
|
355
|
+
# @param scale [Musa::Scales::Scale] reference scale for grade calculation
|
|
356
|
+
# @param previous [GDV, nil] previous event for delta calculation
|
|
357
|
+
#
|
|
358
|
+
# @return [GDVd] delta-encoded dataset
|
|
359
|
+
#
|
|
360
|
+
# @example First event (no previous)
|
|
361
|
+
# gdv = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
362
|
+
# gdvd = gdv.to_gdvd(scale)
|
|
363
|
+
# # => { abs_grade: 0, abs_duration: 1.0, abs_velocity: 0 }
|
|
364
|
+
#
|
|
365
|
+
# @example Changed values
|
|
366
|
+
# gdv1 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
367
|
+
# gdv2 = { grade: 2, duration: 1.0, velocity: 1 }.extend(GDV)
|
|
368
|
+
# gdvd = gdv2.to_gdvd(scale, previous: gdv1)
|
|
369
|
+
# # => { delta_grade: 2, delta_velocity: 1 }
|
|
370
|
+
# # duration unchanged, so omitted
|
|
371
|
+
#
|
|
372
|
+
# @example Unchanged values
|
|
373
|
+
# gdv1 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
374
|
+
# gdv2 = { grade: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
375
|
+
# gdvd = gdv2.to_gdvd(scale, previous: gdv1)
|
|
376
|
+
# # => {}
|
|
377
|
+
# # Everything unchanged
|
|
378
|
+
#
|
|
379
|
+
# @example Chromatic alteration
|
|
380
|
+
# gdv1 = { grade: 0, octave: 0 }.extend(GDV)
|
|
381
|
+
# gdv2 = { grade: 0, octave: 0, sharps: 1 }.extend(GDV)
|
|
382
|
+
# gdvd = gdv2.to_gdvd(scale, previous: gdv1)
|
|
383
|
+
# # => { delta_sharps: 1 }
|
|
107
384
|
def to_gdvd(scale, previous: nil)
|
|
108
385
|
gdvd = {}.extend GDVd
|
|
109
386
|
gdvd.base_duration = @base_duration
|