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
|
@@ -1,8 +1,84 @@
|
|
|
1
1
|
require 'prime'
|
|
2
2
|
|
|
3
|
+
# PDV event processing for MusicXML export.
|
|
4
|
+
#
|
|
5
|
+
# Converts {PDV} (Pitch/Duration/Velocity) events to MusicXML notes and rests.
|
|
6
|
+
# Handles pitch mapping, duration decomposition, ties, articulations, and
|
|
7
|
+
# ornaments.
|
|
8
|
+
#
|
|
9
|
+
# ## Processing Steps
|
|
10
|
+
#
|
|
11
|
+
# 1. Extract pitch, octave, and accidentals from MIDI pitch
|
|
12
|
+
# 2. Calculate effective duration within measure (may span bars)
|
|
13
|
+
# 3. Decompose duration into MusicXML-compatible note values
|
|
14
|
+
# 4. Add backup/forward if needed for voice positioning
|
|
15
|
+
# 5. Create MusicXML note/rest elements with all attributes
|
|
16
|
+
#
|
|
17
|
+
# ## Articulations & Ornaments Supported
|
|
18
|
+
#
|
|
19
|
+
# - **:st** → staccato / staccatissimo (1 or > 1)
|
|
20
|
+
# - **:tr** → trill
|
|
21
|
+
# - **:mor** → mordent (:down/:low) or inverted mordent (:up/true)
|
|
22
|
+
# - **:turn** → turn (:up/true) or inverted turn (:down/:low)
|
|
23
|
+
# - **:grace** → grace note (with slur)
|
|
24
|
+
# - **:graced** → note receiving grace note (with slur)
|
|
25
|
+
# - **:voice** → voice number for polyphony
|
|
26
|
+
#
|
|
27
|
+
# ## Ties Across Measures
|
|
28
|
+
#
|
|
29
|
+
# Notes spanning bar lines are automatically tied. Duration is decomposed
|
|
30
|
+
# and tie start/stop/continue markers added appropriately.
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
3
33
|
module Musa::Datasets::Score::ToMXML
|
|
4
34
|
using Musa::Extension::InspectNice
|
|
5
35
|
|
|
36
|
+
# Processes PDV event to MusicXML note or rest.
|
|
37
|
+
#
|
|
38
|
+
# Converts a single PDV event to one or more MusicXML note/rest elements.
|
|
39
|
+
# Handles duration decomposition, ties, backup/forward for polyphony,
|
|
40
|
+
# and all articulations/ornaments.
|
|
41
|
+
#
|
|
42
|
+
# @param measure [Musa::MusicXML::Builder::Measure] target measure
|
|
43
|
+
# @param bar [Integer] bar number (1-based)
|
|
44
|
+
# @param divisions_per_bar [Integer] total divisions in bar
|
|
45
|
+
# @param element [Hash] event hash from score query
|
|
46
|
+
# Contains :start, :finish, :dataset keys
|
|
47
|
+
# @param pointer [Rational] current position in bar (0-1)
|
|
48
|
+
# @param logger [Musa::Logger::Logger] logger for debugging
|
|
49
|
+
# @param do_log [Boolean] enable logging
|
|
50
|
+
#
|
|
51
|
+
# @return [Rational] updated pointer position
|
|
52
|
+
#
|
|
53
|
+
# @raise [NotImplementedError] if tuplet ratios found (not yet supported)
|
|
54
|
+
#
|
|
55
|
+
# @example Simple quarter note
|
|
56
|
+
# element = {
|
|
57
|
+
# start: 1r,
|
|
58
|
+
# finish: 2r,
|
|
59
|
+
# dataset: { pitch: 60, duration: 1r }.extend(Musa::Datasets::PDV)
|
|
60
|
+
# }
|
|
61
|
+
# pointer = process_pdv(measure, 1, 96, element, 0r, logger, false)
|
|
62
|
+
# # Adds C4 quarter note, returns 1r
|
|
63
|
+
#
|
|
64
|
+
# @example Rest
|
|
65
|
+
# element = {
|
|
66
|
+
# start: 1r,
|
|
67
|
+
# finish: 2r,
|
|
68
|
+
# dataset: { pitch: :silence, duration: 1r }.extend(Musa::Datasets::PDV)
|
|
69
|
+
# }
|
|
70
|
+
# pointer = process_pdv(measure, 1, 96, element, 0r, logger, false)
|
|
71
|
+
# # Adds quarter rest, returns 1r
|
|
72
|
+
#
|
|
73
|
+
# @example Note with articulation
|
|
74
|
+
# dataset = { pitch: 64, duration: 1/2r, st: true }.extend(Musa::Datasets::PDV)
|
|
75
|
+
# # Adds staccato eighth note
|
|
76
|
+
#
|
|
77
|
+
# @example Tied note across bar
|
|
78
|
+
# element = { start: 1r, finish: 3r, dataset: { pitch: 60, duration: 2r } }
|
|
79
|
+
# # Automatically tied: tie-start in bar 1, tie-stop in bar 2
|
|
80
|
+
#
|
|
81
|
+
# @api private
|
|
6
82
|
private def process_pdv(measure, bar, divisions_per_bar, element, pointer, logger, do_log)
|
|
7
83
|
|
|
8
84
|
pitch, octave, sharps = pitch_and_octave_and_sharps(element[:dataset])
|
|
@@ -127,6 +203,34 @@ module Musa::Datasets::Score::ToMXML
|
|
|
127
203
|
pointer
|
|
128
204
|
end
|
|
129
205
|
|
|
206
|
+
# Converts MIDI pitch to note name, octave, and accidental.
|
|
207
|
+
#
|
|
208
|
+
# Maps MIDI pitch number (0-127) to MusicXML pitch representation.
|
|
209
|
+
# Middle C (MIDI 60) = C4 in scientific pitch notation.
|
|
210
|
+
#
|
|
211
|
+
# @param pdv [Hash] PDV dataset with :pitch key
|
|
212
|
+
#
|
|
213
|
+
# @return [Array(String, Integer, Integer), Array(Symbol, nil, nil)]
|
|
214
|
+
# - For pitches: [note_name, octave, sharps]
|
|
215
|
+
# - For silence: [:silence, nil, nil]
|
|
216
|
+
#
|
|
217
|
+
# @example Middle C
|
|
218
|
+
# pitch_and_octave_and_sharps({ pitch: 60 })
|
|
219
|
+
# # => ["C", 4, 0]
|
|
220
|
+
#
|
|
221
|
+
# @example C#4
|
|
222
|
+
# pitch_and_octave_and_sharps({ pitch: 61 })
|
|
223
|
+
# # => ["C", 4, 1]
|
|
224
|
+
#
|
|
225
|
+
# @example A4 (440Hz)
|
|
226
|
+
# pitch_and_octave_and_sharps({ pitch: 69 })
|
|
227
|
+
# # => ["A", 4, 0]
|
|
228
|
+
#
|
|
229
|
+
# @example Rest
|
|
230
|
+
# pitch_and_octave_and_sharps({ pitch: :silence })
|
|
231
|
+
# # => [:silence, nil, nil]
|
|
232
|
+
#
|
|
233
|
+
# @api private
|
|
130
234
|
private def pitch_and_octave_and_sharps(pdv)
|
|
131
235
|
if pdv[:pitch] == :silence
|
|
132
236
|
[:silence, nil, nil]
|
|
@@ -145,16 +249,49 @@ module Musa::Datasets::Score::ToMXML
|
|
|
145
249
|
end
|
|
146
250
|
end
|
|
147
251
|
|
|
252
|
+
# Converts MIDI velocity to dynamics index.
|
|
253
|
+
#
|
|
254
|
+
# Maps MIDI velocity (0-127) to dynamics marking index (0-10).
|
|
255
|
+
# Used for determining dynamics from velocity values.
|
|
256
|
+
#
|
|
257
|
+
# @param midi_velocity [Integer, nil] MIDI velocity value
|
|
258
|
+
#
|
|
259
|
+
# @return [Integer, nil] dynamics index (0-10), or nil if no velocity
|
|
260
|
+
#
|
|
261
|
+
# @example Pianissimo
|
|
262
|
+
# dynamics_index_of(16) # => 3 (ppp)
|
|
263
|
+
#
|
|
264
|
+
# @example Mezzo-forte
|
|
265
|
+
# dynamics_index_of(64) # => 6 (mf)
|
|
266
|
+
#
|
|
267
|
+
# @example Fortissimo
|
|
268
|
+
# dynamics_index_of(100) # => 9 (ff)
|
|
269
|
+
#
|
|
270
|
+
# @api private
|
|
148
271
|
private def dynamics_index_of(midi_velocity)
|
|
149
272
|
return nil unless midi_velocity
|
|
150
273
|
|
|
151
274
|
# ppp = midi 16 ... fff = midi 127
|
|
152
275
|
# mp = dynamics index 6; dynamics = 0..10
|
|
153
276
|
# TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
|
|
154
|
-
[0..0, 1..1, 2..8, 9..16, 17..33, 34..
|
|
277
|
+
[0..0, 1..1, 2..8, 9..16, 17..33, 34..48, 49..64, 65..80, 81..96, 97..112, 113..127]
|
|
155
278
|
.index { |r| r.cover? midi_velocity.round.to_i }
|
|
156
279
|
end
|
|
157
280
|
|
|
281
|
+
# Converts dynamics index to MusicXML dynamics string.
|
|
282
|
+
#
|
|
283
|
+
# Maps dynamics index (0-10) to standard dynamics marking string.
|
|
284
|
+
#
|
|
285
|
+
# @param dynamics_index [Integer, nil] dynamics index
|
|
286
|
+
#
|
|
287
|
+
# @return [String, nil] dynamics marking string, or nil if no index
|
|
288
|
+
#
|
|
289
|
+
# @example
|
|
290
|
+
# dynamics_to_string(3) # => "ppp"
|
|
291
|
+
# dynamics_to_string(6) # => "mp"
|
|
292
|
+
# dynamics_to_string(9) # => "ff"
|
|
293
|
+
#
|
|
294
|
+
# @api private
|
|
158
295
|
private def dynamics_to_string(dynamics_index)
|
|
159
296
|
return nil unless dynamics_index
|
|
160
297
|
['pppppp', 'ppppp', 'pppp', 'ppp', 'pp', 'p', 'mp', 'mf', 'f', 'ff', 'fff'][dynamics_index.round.to_i]
|
|
@@ -1,9 +1,120 @@
|
|
|
1
|
+
# PS event processing for MusicXML export.
|
|
2
|
+
#
|
|
3
|
+
# Converts {PS} (Pitch Series) events to MusicXML dynamics markings.
|
|
4
|
+
# Handles crescendo, diminuendo wedges (hairpins), and static dynamics markings.
|
|
5
|
+
#
|
|
6
|
+
# ## Processing Steps
|
|
7
|
+
#
|
|
8
|
+
# 1. Extract dynamics type (:crescendo, :diminuendo, or :dynamics)
|
|
9
|
+
# 2. For wedges: determine if it's the start or end of the marking
|
|
10
|
+
# 3. Add dynamics marking at wedge start/end if level changed
|
|
11
|
+
# 4. Add wedge element with appropriate type and niente attribute
|
|
12
|
+
# 5. Track last dynamics to avoid redundant markings
|
|
13
|
+
#
|
|
14
|
+
# ## Dynamics Types Supported
|
|
15
|
+
#
|
|
16
|
+
# - **:crescendo** → crescendo wedge (hairpin opening)
|
|
17
|
+
# - Uses :from attribute for starting dynamics level
|
|
18
|
+
# - Uses :to attribute for ending dynamics level
|
|
19
|
+
# - Supports niente (from silence) when :from == 0
|
|
20
|
+
#
|
|
21
|
+
# - **:diminuendo** → diminuendo wedge (hairpin closing)
|
|
22
|
+
# - Uses :from attribute for starting dynamics level
|
|
23
|
+
# - Uses :to attribute for ending dynamics level
|
|
24
|
+
# - Supports niente (to silence) when :to == 0
|
|
25
|
+
#
|
|
26
|
+
# - **:dynamics** → static dynamics marking (pp, mf, ff, etc.)
|
|
27
|
+
# - Uses :from attribute for dynamics level
|
|
28
|
+
# - No wedge created, only dynamics text
|
|
29
|
+
#
|
|
30
|
+
# ## Dynamics Levels
|
|
31
|
+
#
|
|
32
|
+
# Dynamics levels are numeric indices (0-10) converted to standard markings:
|
|
33
|
+
# - 0: silence (niente)
|
|
34
|
+
# - 1-3: ppp range
|
|
35
|
+
# - 4-5: pp-p range
|
|
36
|
+
# - 6: mp
|
|
37
|
+
# - 7: mf
|
|
38
|
+
# - 8-9: f-ff range
|
|
39
|
+
# - 10: fff
|
|
40
|
+
#
|
|
41
|
+
# ## Context Tracking
|
|
42
|
+
#
|
|
43
|
+
# Uses DynamicsContext to track the last dynamics marking, preventing
|
|
44
|
+
# duplicate markings when consecutive events have the same level.
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
1
47
|
module Musa::Datasets::Score::ToMXML
|
|
2
48
|
using Musa::Extension::InspectNice
|
|
3
49
|
|
|
50
|
+
# Context for tracking dynamics state across events.
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
4
53
|
DynamicsContext = Struct.new(:last_dynamics)
|
|
5
54
|
private_constant :DynamicsContext
|
|
6
55
|
|
|
56
|
+
# Processes PS event to MusicXML dynamics marking.
|
|
57
|
+
#
|
|
58
|
+
# Converts a single PS event to one or more MusicXML dynamics/wedge elements.
|
|
59
|
+
# Handles crescendo/diminuendo wedges and static dynamics markings. Tracks
|
|
60
|
+
# context to avoid redundant markings.
|
|
61
|
+
#
|
|
62
|
+
# @param measure [Musa::MusicXML::Builder::Measure] target measure
|
|
63
|
+
# @param element [Hash] event hash from score query
|
|
64
|
+
# Contains :dataset (PS event), :change (:start/:finish for wedges)
|
|
65
|
+
# @param context [DynamicsContext, nil] dynamics tracking context
|
|
66
|
+
# @param logger [Musa::Logger::Logger] logger for debugging
|
|
67
|
+
# @param do_log [Boolean] enable logging
|
|
68
|
+
#
|
|
69
|
+
# @return [DynamicsContext] updated context with last dynamics
|
|
70
|
+
#
|
|
71
|
+
# @example Crescendo from pp to ff
|
|
72
|
+
# element_start = {
|
|
73
|
+
# dataset: { type: :crescendo, from: 4, to: 9, duration: 2r }.extend(Musa::Datasets::PS),
|
|
74
|
+
# change: :start
|
|
75
|
+
# }
|
|
76
|
+
# context = process_ps(measure, element_start, nil, logger, false)
|
|
77
|
+
# # Adds "pp" dynamics and crescendo wedge start
|
|
78
|
+
#
|
|
79
|
+
# element_finish = {
|
|
80
|
+
# dataset: { type: :crescendo, from: 4, to: 9, duration: 2r }.extend(Musa::Datasets::PS),
|
|
81
|
+
# change: :finish
|
|
82
|
+
# }
|
|
83
|
+
# context = process_ps(measure, element_finish, context, logger, false)
|
|
84
|
+
# # Adds wedge stop and "ff" dynamics
|
|
85
|
+
#
|
|
86
|
+
# @example Diminuendo to silence (niente)
|
|
87
|
+
# element_start = {
|
|
88
|
+
# dataset: { type: :diminuendo, from: 7, to: 0, duration: 1r }.extend(Musa::Datasets::PS),
|
|
89
|
+
# change: :start
|
|
90
|
+
# }
|
|
91
|
+
# process_ps(measure, element_start, nil, logger, false)
|
|
92
|
+
# # Adds "mf" dynamics and diminuendo wedge start
|
|
93
|
+
#
|
|
94
|
+
# element_finish = {
|
|
95
|
+
# dataset: { type: :diminuendo, from: 7, to: 0, duration: 1r }.extend(Musa::Datasets::PS),
|
|
96
|
+
# change: :finish
|
|
97
|
+
# }
|
|
98
|
+
# process_ps(measure, element_finish, context, logger, false)
|
|
99
|
+
# # Adds wedge stop with niente=true (diminuendo to silence)
|
|
100
|
+
#
|
|
101
|
+
# @example Crescendo from silence (niente)
|
|
102
|
+
# element_start = {
|
|
103
|
+
# dataset: { type: :crescendo, from: 0, to: 6, duration: 1r }.extend(Musa::Datasets::PS),
|
|
104
|
+
# change: :start
|
|
105
|
+
# }
|
|
106
|
+
# process_ps(measure, element_start, nil, logger, false)
|
|
107
|
+
# # Adds crescendo wedge with niente=true (from silence)
|
|
108
|
+
#
|
|
109
|
+
# @example Static dynamics marking
|
|
110
|
+
# element = {
|
|
111
|
+
# dataset: { type: :dynamics, from: 8, duration: 0r }.extend(Musa::Datasets::PS),
|
|
112
|
+
# change: :start
|
|
113
|
+
# }
|
|
114
|
+
# process_ps(measure, element, nil, logger, false)
|
|
115
|
+
# # Adds "f" dynamics marking only
|
|
116
|
+
#
|
|
117
|
+
# @api private
|
|
7
118
|
private def process_ps(measure, element, context, logger, do_log)
|
|
8
119
|
context ||= DynamicsContext.new
|
|
9
120
|
|
|
@@ -1,9 +1,47 @@
|
|
|
1
1
|
require 'prime'
|
|
2
2
|
|
|
3
|
+
# Time and duration processing for MusicXML export.
|
|
4
|
+
#
|
|
5
|
+
# This module provides helper methods for converting musical durations
|
|
6
|
+
# to MusicXML note types, dots, and tuplet ratios. Handles the complex
|
|
7
|
+
# mathematics of decomposing arbitrary rational durations into standard
|
|
8
|
+
# notation elements.
|
|
9
|
+
#
|
|
10
|
+
# ## Duration Representation
|
|
11
|
+
#
|
|
12
|
+
# Durations are Rational numbers where 1 = one beat (typically quarter note).
|
|
13
|
+
# - 1r = quarter note
|
|
14
|
+
# - 1/2r = eighth note
|
|
15
|
+
# - 3/2r = dotted quarter
|
|
16
|
+
# - 1/3r = eighth note triplet
|
|
17
|
+
#
|
|
18
|
+
# ## Decomposition Process
|
|
19
|
+
#
|
|
20
|
+
# 1. **Decompose**: Break duration into sum of simple durations (powers of 2)
|
|
21
|
+
# 2. **Integrate**: Combine consecutive halves into dotted notes
|
|
22
|
+
# 3. **Type & Dots**: Determine note type and dot count
|
|
23
|
+
# 4. **Tuplet Ratio**: Calculate tuplet modification if needed
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
3
26
|
module Musa::Datasets::Score::ToMXML
|
|
4
27
|
private
|
|
5
28
|
|
|
29
|
+
# Decomposes duration into dotted note representation.
|
|
30
|
+
#
|
|
31
|
+
# Internal class representing the breakdown of an element's duration
|
|
32
|
+
# within a measure. Handles ties across bar lines and duration decomposition.
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
6
35
|
class ElementDurationDecomposition
|
|
36
|
+
# Creates duration decomposition for element.
|
|
37
|
+
#
|
|
38
|
+
# @param element [Hash] event with :start, :finish, :dataset keys
|
|
39
|
+
# @param bar [Integer] bar number (1-based)
|
|
40
|
+
# @param bar_size [Rational] bar duration (default: 1r)
|
|
41
|
+
#
|
|
42
|
+
# @note This method is experimental and currently unused. See TODO comment.
|
|
43
|
+
#
|
|
44
|
+
# @api private
|
|
7
45
|
def initialize(element, bar, bar_size = 1r) # TODO remove (unused because of bad strategy to time groups)
|
|
8
46
|
@continue_from_previous_bar = element[:start] < bar
|
|
9
47
|
@continue_to_next_bar = element[:finish] >= bar + bar_size
|
|
@@ -17,7 +55,25 @@ module Musa::Datasets::Score::ToMXML
|
|
|
17
55
|
@duration_decomposition = integrate_as_dotteable_durations(decompose_as_sum_of_simple_durations(@duration))
|
|
18
56
|
end
|
|
19
57
|
|
|
20
|
-
|
|
58
|
+
# Whether note continues from previous bar (tied).
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
attr_reader :continue_from_previous_bar
|
|
61
|
+
|
|
62
|
+
# Whether note continues to next bar (tied).
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
attr_reader :continue_to_next_bar
|
|
65
|
+
|
|
66
|
+
# Start time within bar.
|
|
67
|
+
# @return [Rational]
|
|
68
|
+
attr_reader :start
|
|
69
|
+
|
|
70
|
+
# Total duration.
|
|
71
|
+
# @return [Rational]
|
|
72
|
+
attr_reader :duration
|
|
73
|
+
|
|
74
|
+
# Duration broken into dotteable components.
|
|
75
|
+
# @return [Array<Rational>]
|
|
76
|
+
attr_reader :duration_decomposition
|
|
21
77
|
|
|
22
78
|
def to_s
|
|
23
79
|
"ElementDurationDecomposition(#{@duration}) = [#{@duration_decomposition}]"
|
|
@@ -28,6 +84,21 @@ module Musa::Datasets::Score::ToMXML
|
|
|
28
84
|
|
|
29
85
|
private_constant :ElementDurationDecomposition
|
|
30
86
|
|
|
87
|
+
# Optimizes time and tuplet representation (experimental).
|
|
88
|
+
#
|
|
89
|
+
# Attempts to find optimal tuplet grouping for elements in a bar.
|
|
90
|
+
# Currently unused due to incomplete implementation.
|
|
91
|
+
#
|
|
92
|
+
# @param elements [Array] PDV events
|
|
93
|
+
# @param bar [Integer] bar number
|
|
94
|
+
# @param bar_size [Rational] bar duration
|
|
95
|
+
#
|
|
96
|
+
# @return [nil] incomplete implementation
|
|
97
|
+
#
|
|
98
|
+
# @note This method is experimental and currently unused. See TODO comment.
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
# @todo Complete or remove this experimental method
|
|
31
102
|
def time_and_tuplet_optimize(elements, bar, bar_size = 1r) # TODO remove (unused because of bad strategy to time groups)
|
|
32
103
|
decompositions = elements.collect { |pdv| ElementDurationDecomposition.new(pdv, bar, bar_size) }
|
|
33
104
|
|
|
@@ -46,6 +117,32 @@ module Musa::Datasets::Score::ToMXML
|
|
|
46
117
|
nil
|
|
47
118
|
end
|
|
48
119
|
|
|
120
|
+
# Decomposes duration into sum of simple durations.
|
|
121
|
+
#
|
|
122
|
+
# Breaks a rational duration into sum of fractions with power-of-2 denominators.
|
|
123
|
+
# This is the first step in converting arbitrary durations to standard notation.
|
|
124
|
+
#
|
|
125
|
+
# Uses greedy algorithm: repeatedly subtracts largest possible simple duration.
|
|
126
|
+
#
|
|
127
|
+
# @param duration [Rational] duration to decompose
|
|
128
|
+
#
|
|
129
|
+
# @return [Array<Rational>] simple durations that sum to input
|
|
130
|
+
#
|
|
131
|
+
# @raise [ArgumentError] if duration cannot be decomposed
|
|
132
|
+
#
|
|
133
|
+
# @example Quarter note
|
|
134
|
+
# decompose_as_sum_of_simple_durations(1r)
|
|
135
|
+
# # => [1r]
|
|
136
|
+
#
|
|
137
|
+
# @example Dotted quarter
|
|
138
|
+
# decompose_as_sum_of_simple_durations(3/2r)
|
|
139
|
+
# # => [1r, 1/2r]
|
|
140
|
+
#
|
|
141
|
+
# @example Complex duration
|
|
142
|
+
# decompose_as_sum_of_simple_durations(5/8r)
|
|
143
|
+
# # => [1/2r, 1/8r]
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
49
146
|
def decompose_as_sum_of_simple_durations(duration)
|
|
50
147
|
return [] if duration.zero?
|
|
51
148
|
|
|
@@ -69,6 +166,17 @@ module Musa::Datasets::Score::ToMXML
|
|
|
69
166
|
summands
|
|
70
167
|
end
|
|
71
168
|
|
|
169
|
+
# Generates all combinations of array elements.
|
|
170
|
+
#
|
|
171
|
+
# @param numbers [Array] elements to combine
|
|
172
|
+
#
|
|
173
|
+
# @return [Array<Array>] all unique combinations (excluding empty)
|
|
174
|
+
#
|
|
175
|
+
# @example
|
|
176
|
+
# all_combinations([2, 3])
|
|
177
|
+
# # => [[2], [3], [2, 3]]
|
|
178
|
+
#
|
|
179
|
+
# @api private
|
|
72
180
|
def all_combinations(numbers)
|
|
73
181
|
all_combinations = []
|
|
74
182
|
i = 1
|
|
@@ -80,6 +188,28 @@ module Musa::Datasets::Score::ToMXML
|
|
|
80
188
|
all_combinations.uniq
|
|
81
189
|
end
|
|
82
190
|
|
|
191
|
+
# Integrates consecutive halves into dotted durations.
|
|
192
|
+
#
|
|
193
|
+
# Combines simple durations where each is half the previous into
|
|
194
|
+
# single dotted duration. Example: [1r, 1/2r] → [3/2r] (dotted quarter).
|
|
195
|
+
#
|
|
196
|
+
# @param simple_durations [Array<Rational>] simple durations from decomposition
|
|
197
|
+
#
|
|
198
|
+
# @return [Array<Rational>] integrated dotted durations
|
|
199
|
+
#
|
|
200
|
+
# @example Dotted quarter
|
|
201
|
+
# integrate_as_dotteable_durations([1r, 1/2r])
|
|
202
|
+
# # => [3/2r]
|
|
203
|
+
#
|
|
204
|
+
# @example Double-dotted half
|
|
205
|
+
# integrate_as_dotteable_durations([1/2r, 1/4r, 1/8r])
|
|
206
|
+
# # => [7/8r]
|
|
207
|
+
#
|
|
208
|
+
# @example Non-dotteable
|
|
209
|
+
# integrate_as_dotteable_durations([1r, 1/4r])
|
|
210
|
+
# # => [1r, 1/4r] (no integration possible)
|
|
211
|
+
#
|
|
212
|
+
# @api private
|
|
83
213
|
def integrate_as_dotteable_durations(simple_durations)
|
|
84
214
|
integrated_durations = []
|
|
85
215
|
last = nil
|
|
@@ -94,6 +224,32 @@ module Musa::Datasets::Score::ToMXML
|
|
|
94
224
|
integrated_durations
|
|
95
225
|
end
|
|
96
226
|
|
|
227
|
+
# Calculates note type, dots, and tuplet ratio.
|
|
228
|
+
#
|
|
229
|
+
# Converts a dotteable duration into MusicXML note representation:
|
|
230
|
+
# - **type**: Base note type (quarter, eighth, etc.)
|
|
231
|
+
# - **dots**: Number of dots (0-3 typically)
|
|
232
|
+
# - **tuplet_ratio**: Tuplet modification (3:2 for triplets, etc.)
|
|
233
|
+
#
|
|
234
|
+
# @param noteable_duration [Rational] duration to convert
|
|
235
|
+
#
|
|
236
|
+
# @return [Array(String, Integer, Rational)] [type, dots, tuplet_ratio]
|
|
237
|
+
#
|
|
238
|
+
# @raise [ArgumentError] if duration cannot be represented with dots
|
|
239
|
+
#
|
|
240
|
+
# @example Quarter note
|
|
241
|
+
# type_and_dots_and_tuplet_ratio(1r)
|
|
242
|
+
# # => ["quarter", 0, 1r]
|
|
243
|
+
#
|
|
244
|
+
# @example Dotted quarter
|
|
245
|
+
# type_and_dots_and_tuplet_ratio(3/2r)
|
|
246
|
+
# # => ["quarter", 1, 1r]
|
|
247
|
+
#
|
|
248
|
+
# @example Eighth triplet
|
|
249
|
+
# type_and_dots_and_tuplet_ratio(1/3r)
|
|
250
|
+
# # => ["eighth", 0, 3/2r]
|
|
251
|
+
#
|
|
252
|
+
# @api private
|
|
97
253
|
def type_and_dots_and_tuplet_ratio(noteable_duration)
|
|
98
254
|
r = decompose_as_sum_of_simple_durations(noteable_duration)
|
|
99
255
|
n = r.shift
|
|
@@ -117,6 +273,18 @@ module Musa::Datasets::Score::ToMXML
|
|
|
117
273
|
return type, dots, tuplet_ratio
|
|
118
274
|
end
|
|
119
275
|
|
|
276
|
+
# Finds nearest power of 2 greater than or equal to number.
|
|
277
|
+
#
|
|
278
|
+
# @param number [Numeric] number to round up
|
|
279
|
+
#
|
|
280
|
+
# @return [Integer] nearest upper power of 2
|
|
281
|
+
#
|
|
282
|
+
# @example
|
|
283
|
+
# nearest_upper_power_of_2(5) # => 8
|
|
284
|
+
# nearest_upper_power_of_2(8) # => 8
|
|
285
|
+
# nearest_upper_power_of_2(9) # => 16
|
|
286
|
+
#
|
|
287
|
+
# @api private
|
|
120
288
|
def nearest_upper_power_of_2(number)
|
|
121
289
|
return 0 if number.zero?
|
|
122
290
|
|
|
@@ -127,6 +295,18 @@ module Musa::Datasets::Score::ToMXML
|
|
|
127
295
|
2 ** (exp_floor + plus)
|
|
128
296
|
end
|
|
129
297
|
|
|
298
|
+
# Finds nearest power of 2 less than or equal to number.
|
|
299
|
+
#
|
|
300
|
+
# @param number [Numeric] number to round down
|
|
301
|
+
#
|
|
302
|
+
# @return [Integer] nearest lower power of 2
|
|
303
|
+
#
|
|
304
|
+
# @example
|
|
305
|
+
# nearest_lower_power_of_2(5) # => 4
|
|
306
|
+
# nearest_lower_power_of_2(8) # => 8
|
|
307
|
+
# nearest_lower_power_of_2(15) # => 8
|
|
308
|
+
#
|
|
309
|
+
# @api private
|
|
130
310
|
def nearest_lower_power_of_2(number)
|
|
131
311
|
return 0 if number.zero?
|
|
132
312
|
|
|
@@ -135,6 +315,25 @@ module Musa::Datasets::Score::ToMXML
|
|
|
135
315
|
2 ** exp_floor
|
|
136
316
|
end
|
|
137
317
|
|
|
318
|
+
# Converts duration to MusicXML note type name.
|
|
319
|
+
#
|
|
320
|
+
# Maps inverse powers of 2 to standard note type names.
|
|
321
|
+
# Duration must be power of 2 between 1/1024 and maxima (8 whole notes).
|
|
322
|
+
#
|
|
323
|
+
# @param base_type_duration [Numeric] duration as power of 2
|
|
324
|
+
#
|
|
325
|
+
# @return [String] MusicXML note type name
|
|
326
|
+
#
|
|
327
|
+
# @raise [ArgumentError] if duration is not power of 2 or out of range
|
|
328
|
+
#
|
|
329
|
+
# @example Standard durations
|
|
330
|
+
# type_of(1r) # => "quarter"
|
|
331
|
+
# type_of(1/2r) # => "eighth"
|
|
332
|
+
# type_of(1/4r) # => "16th"
|
|
333
|
+
# type_of(2r) # => "half"
|
|
334
|
+
# type_of(4r) # => "whole"
|
|
335
|
+
#
|
|
336
|
+
# @api private
|
|
138
337
|
def type_of(base_type_duration)
|
|
139
338
|
duration_log2i = Math.log2(base_type_duration)
|
|
140
339
|
|