musa-dsl 0.30.2 → 0.41.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 +5 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/Gemfile +0 -1
- 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 +544 -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 +215 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
- data/lib/musa-dsl/generative/markov.rb +135 -3
- data/lib/musa-dsl/generative/rules.rb +312 -4
- data/lib/musa-dsl/generative/variatio.rb +286 -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 +113 -2
- data/lib/musa-dsl/midi/midi-voices.rb +275 -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 +353 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +1384 -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 +54 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -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 +57 -7
- data/lib/musa-dsl/series/queue-serie.rb +78 -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 +2 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +25 -18
- metadata +158 -16
|
@@ -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
|
|
|
@@ -6,11 +6,137 @@ require_relative 'process-time'
|
|
|
6
6
|
require_relative 'process-pdv'
|
|
7
7
|
require_relative 'process-ps'
|
|
8
8
|
|
|
9
|
-
module Musa::Datasets
|
|
9
|
+
module Musa::Datasets
|
|
10
10
|
class Score
|
|
11
|
+
# MusicXML export for scores.
|
|
12
|
+
#
|
|
13
|
+
# ToMXML provides conversion of {Score} objects to MusicXML format,
|
|
14
|
+
# suitable for import into notation software like MuseScore, Finale,
|
|
15
|
+
# or Sibelius.
|
|
16
|
+
#
|
|
17
|
+
# ## Conversion Process
|
|
18
|
+
#
|
|
19
|
+
# 1. Creates MusicXML structure with metadata (title, creators, etc.)
|
|
20
|
+
# 2. Defines parts (instruments) with clefs and time signatures
|
|
21
|
+
# 3. Divides score into measures (bars)
|
|
22
|
+
# 4. Processes events in each measure:
|
|
23
|
+
#
|
|
24
|
+
# - {PDV} events → notes and rests
|
|
25
|
+
# - {PS} events → dynamics markings (crescendo, diminuendo)
|
|
26
|
+
#
|
|
27
|
+
# 5. Fills gaps with rests
|
|
28
|
+
#
|
|
29
|
+
# ## Event Types Supported
|
|
30
|
+
#
|
|
31
|
+
# - **{PDV}** (Pitch/Duration/Velocity): Converted to notes or rests
|
|
32
|
+
# - **{PS}** (Pitch Series): Converted to dynamics markings
|
|
33
|
+
#
|
|
34
|
+
# ## Multi-part Scores
|
|
35
|
+
#
|
|
36
|
+
# Scores can contain multiple instruments, differentiated by the
|
|
37
|
+
# :instrument attribute. Each part is rendered separately.
|
|
38
|
+
#
|
|
39
|
+
# ## Time Representation
|
|
40
|
+
#
|
|
41
|
+
# - Score times are 1-based (first beat is at position 1)
|
|
42
|
+
# - Each measure represents one bar
|
|
43
|
+
# - Duration is specified in beats (1.0 = quarter note if beat_type is 4)
|
|
44
|
+
#
|
|
45
|
+
# @example Basic single-part score
|
|
46
|
+
# score = Musa::Datasets::Score.new
|
|
47
|
+
# score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
48
|
+
# score.at(2r, add: { pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
49
|
+
#
|
|
50
|
+
# mxml = score.to_mxml(
|
|
51
|
+
# 4, 24, # 4 beats per bar, 24 ticks per beat
|
|
52
|
+
# bpm: 120,
|
|
53
|
+
# title: 'My Song',
|
|
54
|
+
# creators: { composer: 'John Doe' },
|
|
55
|
+
# parts: { piano: { name: 'Piano', clefs: { g: 2 } } }
|
|
56
|
+
# )
|
|
57
|
+
#
|
|
58
|
+
# File.write('score.musicxml', mxml.to_xml.string)
|
|
59
|
+
#
|
|
60
|
+
# @example Multi-part score
|
|
61
|
+
# score = Musa::Datasets::Score.new
|
|
62
|
+
# score.at(1r, add: { instrument: :violin, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
63
|
+
# score.at(1r, add: { instrument: :cello, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
64
|
+
#
|
|
65
|
+
# mxml = score.to_mxml(
|
|
66
|
+
# 4, 24,
|
|
67
|
+
# parts: {
|
|
68
|
+
# violin: { name: 'Violin', clefs: { g: 2 } },
|
|
69
|
+
# cello: { name: 'Cello', clefs: { f: 4 } }
|
|
70
|
+
# }
|
|
71
|
+
# )
|
|
72
|
+
#
|
|
73
|
+
# @example With dynamics
|
|
74
|
+
# score = Musa::Datasets::Score.new
|
|
75
|
+
# score.at(1r, add: { pitch: 60, duration: 2.0 }.extend(Musa::Datasets::PDV))
|
|
76
|
+
# score.at(1r, add: { type: :crescendo, duration: 2.0 }.extend(Musa::Datasets::PS))
|
|
77
|
+
#
|
|
78
|
+
# @see Musa::MusicXML::Builder MusicXML builder
|
|
79
|
+
# @see PDV MIDI-style events
|
|
80
|
+
# @see PS Pitch series for dynamics
|
|
11
81
|
module ToMXML
|
|
12
82
|
using Musa::Extension::InspectNice
|
|
13
83
|
|
|
84
|
+
# Converts score to MusicXML.
|
|
85
|
+
#
|
|
86
|
+
# Creates complete MusicXML document with metadata, parts, measures,
|
|
87
|
+
# notes, rests, and dynamics markings.
|
|
88
|
+
#
|
|
89
|
+
# @param beats_per_bar [Integer] time signature numerator (e.g., 4 for 4/4)
|
|
90
|
+
# @param ticks_per_beat [Integer] resolution per beat (typically 24)
|
|
91
|
+
#
|
|
92
|
+
# @param bpm [Integer] tempo in beats per minute (default: 90)
|
|
93
|
+
# @param title [String] work title (default: 'Untitled')
|
|
94
|
+
# @param creators [Hash{Symbol => String}] creator roles and names
|
|
95
|
+
# (default: { composer: 'Unknown' })
|
|
96
|
+
# @param encoding_date [DateTime, nil] encoding date for metadata
|
|
97
|
+
# @param parts [Hash{Symbol => Hash}] part definitions
|
|
98
|
+
# Each part: { name: String, abbreviation: String, clefs: Hash }
|
|
99
|
+
# Clefs: { clef_sign: line_number } (e.g., { g: 2, f: 4 } for piano)
|
|
100
|
+
# @param logger [Musa::Logger::Logger, nil] logger for debugging
|
|
101
|
+
# @param do_log [Boolean, nil] enable logging output
|
|
102
|
+
#
|
|
103
|
+
# @return [Musa::MusicXML::Builder::ScorePartwise] MusicXML document
|
|
104
|
+
#
|
|
105
|
+
# @example Simple piano score
|
|
106
|
+
# score = Musa::Datasets::Score.new
|
|
107
|
+
# score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
108
|
+
#
|
|
109
|
+
# mxml = score.to_mxml(
|
|
110
|
+
# 4, 24,
|
|
111
|
+
# bpm: 120,
|
|
112
|
+
# title: 'Invention',
|
|
113
|
+
# creators: { composer: 'J.S. Bach' },
|
|
114
|
+
# parts: { piano: { name: 'Piano', clefs: { g: 2, f: 4 } } }
|
|
115
|
+
# )
|
|
116
|
+
#
|
|
117
|
+
# @example String quartet
|
|
118
|
+
# score = Musa::Datasets::Score.new
|
|
119
|
+
# score.at(1r, add: { instrument: :vln1, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
120
|
+
# score.at(1r, add: { instrument: :vln2, pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
121
|
+
# score.at(1r, add: { instrument: :vla, pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
122
|
+
# score.at(1r, add: { instrument: :vc, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
123
|
+
#
|
|
124
|
+
# mxml = score.to_mxml(
|
|
125
|
+
# 4, 24,
|
|
126
|
+
# parts: {
|
|
127
|
+
# vln1: { name: 'Violin I', abbreviation: 'Vln. I', clefs: { g: 2 } },
|
|
128
|
+
# vln2: { name: 'Violin II', abbreviation: 'Vln. II', clefs: { g: 2 } },
|
|
129
|
+
# vla: { name: 'Viola', abbreviation: 'Vla.', clefs: { c: 3 } },
|
|
130
|
+
# vc: { name: 'Cello', abbreviation: 'Vc.', clefs: { f: 4 } }
|
|
131
|
+
# }
|
|
132
|
+
# )
|
|
133
|
+
#
|
|
134
|
+
# @example Export to file
|
|
135
|
+
# score = Musa::Datasets::Score.new
|
|
136
|
+
# score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
137
|
+
#
|
|
138
|
+
# mxml = score.to_mxml(4, 24, parts: { piano: { name: 'Piano' } })
|
|
139
|
+
# File.write('output.musicxml', mxml.to_xml.string)
|
|
14
140
|
def to_mxml(beats_per_bar, ticks_per_beat,
|
|
15
141
|
bpm: nil,
|
|
16
142
|
title: nil,
|
|
@@ -77,6 +203,24 @@ module Musa::Datasets
|
|
|
77
203
|
|
|
78
204
|
private
|
|
79
205
|
|
|
206
|
+
# Fills a MusicXML part with measures and events.
|
|
207
|
+
#
|
|
208
|
+
# Processes each bar (measure) in the score, converting events to
|
|
209
|
+
# MusicXML notes, rests, and dynamics. Handles:
|
|
210
|
+
#
|
|
211
|
+
# - Initial silences (gaps before first event)
|
|
212
|
+
# - Event processing (PDV → notes, PS → dynamics)
|
|
213
|
+
# - Ending silences (filling remainder of measure)
|
|
214
|
+
#
|
|
215
|
+
# @param part [Musa::MusicXML::Builder::Part] MusicXML part to fill
|
|
216
|
+
# @param divisions_per_bar [Integer] total divisions in one bar
|
|
217
|
+
# @param instrument [Symbol, nil] instrument filter (nil for single-part scores)
|
|
218
|
+
# @param logger [Musa::Logger::Logger] logger for debugging
|
|
219
|
+
# @param do_log [Boolean] enable logging
|
|
220
|
+
#
|
|
221
|
+
# @return [void]
|
|
222
|
+
#
|
|
223
|
+
# @api private
|
|
80
224
|
def fill_part(part, divisions_per_bar, instrument, logger, do_log)
|
|
81
225
|
measure = nil
|
|
82
226
|
dynamics_context = nil
|