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,17 +1,162 @@
|
|
|
1
1
|
module Musa
|
|
2
|
+
# MusicXML generation system.
|
|
3
|
+
#
|
|
4
|
+
# This module provides a comprehensive DSL for generating MusicXML 3.0 files
|
|
5
|
+
# programmatically. It uses a builder pattern with a flexible API that supports
|
|
6
|
+
# both constructor-based and DSL-style notation creation.
|
|
7
|
+
#
|
|
8
|
+
# ## Architecture
|
|
9
|
+
#
|
|
10
|
+
# The MusicXML builder system is organized hierarchically:
|
|
11
|
+
#
|
|
12
|
+
# ScorePartwise (root)
|
|
13
|
+
# ├── Metadata (work, movement, creators, rights)
|
|
14
|
+
# ├── PartGroup (grouping)
|
|
15
|
+
# └── Part
|
|
16
|
+
# └── Measure
|
|
17
|
+
# ├── Attributes (key, time, clef, divisions)
|
|
18
|
+
# ├── Direction (dynamics, tempo, expressions)
|
|
19
|
+
# ├── PitchedNote / Rest / UnpitchedNote
|
|
20
|
+
# └── Backup / Forward (timeline navigation)
|
|
21
|
+
#
|
|
22
|
+
# ## DSL Features
|
|
23
|
+
#
|
|
24
|
+
# The builder provides two equivalent ways to create scores:
|
|
25
|
+
#
|
|
26
|
+
# 1. **Constructor + add methods**: Imperative style
|
|
27
|
+
# 2. **DSL blocks**: Declarative style with `with` blocks
|
|
28
|
+
#
|
|
29
|
+
# Both styles leverage `AttributeBuilder` and `With` mixins from core-ext.
|
|
30
|
+
#
|
|
31
|
+
# ## Use Cases
|
|
32
|
+
#
|
|
33
|
+
# - Algorithmic composition with MusicXML export
|
|
34
|
+
# - Score generation from Musa DSL performances
|
|
35
|
+
# - Converting MIDI recordings to notation
|
|
36
|
+
# - Creating notation examples programmatically
|
|
37
|
+
#
|
|
38
|
+
# @example Simple score with DSL style
|
|
39
|
+
# score = Musa::MusicXML::Builder::ScorePartwise.new do
|
|
40
|
+
# work_title "My Composition"
|
|
41
|
+
# creators composer: "Composer Name"
|
|
42
|
+
#
|
|
43
|
+
# part :p1, name: "Piano" do
|
|
44
|
+
# measure do
|
|
45
|
+
# attributes do
|
|
46
|
+
# divisions 2
|
|
47
|
+
# key fifths: 0 # C major
|
|
48
|
+
# time beats: 4, beat_type: 4
|
|
49
|
+
# clef sign: 'G', line: 2
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# pitch 'C', octave: 4, duration: 2, type: 'quarter'
|
|
53
|
+
# pitch 'D', octave: 4, duration: 2, type: 'quarter'
|
|
54
|
+
# pitch 'E', octave: 4, duration: 2, type: 'quarter'
|
|
55
|
+
# pitch 'F', octave: 4, duration: 2, type: 'quarter'
|
|
56
|
+
# end
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# File.write('output.xml', score.to_xml.string)
|
|
61
|
+
#
|
|
62
|
+
# @example Constructor + add methods style
|
|
63
|
+
# score = Musa::MusicXML::Builder::ScorePartwise.new
|
|
64
|
+
# score.work_title = "My Composition"
|
|
65
|
+
# score.add_creator "composer", "Composer Name"
|
|
66
|
+
#
|
|
67
|
+
# part = score.add_part :p1, name: "Piano"
|
|
68
|
+
# measure = part.add_measure divisions: 2
|
|
69
|
+
# measure.attributes.last.add_key fifths: 0
|
|
70
|
+
# measure.attributes.last.add_time beats: 4, beat_type: 4
|
|
71
|
+
# measure.attributes.last.add_clef sign: 'G', line: 2
|
|
72
|
+
#
|
|
73
|
+
# measure.add_pitch step: 'C', octave: 4, duration: 2, type: 'quarter'
|
|
74
|
+
#
|
|
75
|
+
# File.write('output.xml', score.to_xml.string)
|
|
76
|
+
#
|
|
77
|
+
# @see Builder Main builder namespace
|
|
78
|
+
# @see Builder::ScorePartwise Entry point for score creation
|
|
2
79
|
module MusicXML
|
|
80
|
+
# Builder classes for MusicXML generation.
|
|
81
|
+
#
|
|
82
|
+
# Contains all the builder classes that construct MusicXML elements.
|
|
83
|
+
# The main entry point is {ScorePartwise}.
|
|
84
|
+
#
|
|
85
|
+
# @see ScorePartwise Main score builder
|
|
3
86
|
module Builder
|
|
87
|
+
# Internal implementation classes for MusicXML builder.
|
|
88
|
+
#
|
|
89
|
+
# This module contains the actual implementation classes used by the builder.
|
|
90
|
+
# Users should access these through the main `Musa::MusicXML::Builder` namespace.
|
|
91
|
+
#
|
|
92
|
+
# @api private
|
|
4
93
|
module Internal
|
|
94
|
+
# Helper modules and methods for MusicXML generation.
|
|
95
|
+
#
|
|
96
|
+
# Provides shared functionality for XML serialization, element construction,
|
|
97
|
+
# and value formatting across all builder classes.
|
|
5
98
|
module Helper
|
|
99
|
+
# Mixin for classes not yet implemented.
|
|
100
|
+
#
|
|
101
|
+
# Used as a placeholder for MusicXML elements that are planned but not
|
|
102
|
+
# yet implemented. Raises `NotImplementedError` when attempting to use.
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
6
105
|
module NotImplemented
|
|
106
|
+
# Placeholder initializer accepting any parameters.
|
|
107
|
+
#
|
|
108
|
+
# @param _args [Hash] ignored keyword arguments
|
|
7
109
|
def initialize(**_args); end
|
|
8
110
|
|
|
111
|
+
# Raises error indicating the class is not implemented.
|
|
112
|
+
#
|
|
113
|
+
# @param io [IO, nil] ignored
|
|
114
|
+
# @param indent [Integer, nil] ignored
|
|
115
|
+
# @raise [NotImplementedError] always raised with helpful message
|
|
9
116
|
def to_xml(io = nil, indent: nil)
|
|
10
117
|
raise NotImplementedError, "#{self.class} not yet implemented. Ask Javier do his work!"
|
|
11
118
|
end
|
|
12
119
|
end
|
|
13
120
|
|
|
121
|
+
# Mixin for XML serialization capability.
|
|
122
|
+
#
|
|
123
|
+
# Provides the public `to_xml` interface that handles IO and indentation
|
|
124
|
+
# setup, delegating to the private `_to_xml` method for actual XML generation.
|
|
125
|
+
#
|
|
126
|
+
# ## Usage
|
|
127
|
+
#
|
|
128
|
+
# Classes including this module must implement `_to_xml(io, indent:, tabs:)`.
|
|
129
|
+
#
|
|
130
|
+
# @example Including in a class
|
|
131
|
+
# class MyElement
|
|
132
|
+
# include Musa::MusicXML::Builder::Internal::Helper::ToXML
|
|
133
|
+
#
|
|
134
|
+
# private
|
|
135
|
+
#
|
|
136
|
+
# def _to_xml(io, indent:, tabs:)
|
|
137
|
+
# io.puts "#{tabs}<my-element />"
|
|
138
|
+
# end
|
|
139
|
+
# end
|
|
140
|
+
#
|
|
141
|
+
# element = MyElement.new
|
|
142
|
+
# element.to_xml # => StringIO with XML content
|
|
14
143
|
module ToXML
|
|
144
|
+
# Converts the object to MusicXML format.
|
|
145
|
+
#
|
|
146
|
+
# This method sets up the IO stream and indentation, then delegates to
|
|
147
|
+
# the private `_to_xml` method for actual XML generation.
|
|
148
|
+
#
|
|
149
|
+
# @param io [IO, StringIO, nil] output stream (creates StringIO if nil)
|
|
150
|
+
# @param indent [Integer, nil] indentation level (default: 0)
|
|
151
|
+
# @return [IO, StringIO] the io parameter, containing the XML output
|
|
152
|
+
#
|
|
153
|
+
# @example Writing to file
|
|
154
|
+
# File.open('output.xml', 'w') do |f|
|
|
155
|
+
# element.to_xml(f)
|
|
156
|
+
# end
|
|
157
|
+
#
|
|
158
|
+
# @example Getting XML as string
|
|
159
|
+
# xml_string = element.to_xml.string
|
|
15
160
|
def to_xml(io = nil, indent: nil)
|
|
16
161
|
io ||= StringIO.new
|
|
17
162
|
indent ||= 0
|
|
@@ -25,10 +170,34 @@ module Musa
|
|
|
25
170
|
|
|
26
171
|
private
|
|
27
172
|
|
|
173
|
+
# Abstract method for XML generation.
|
|
174
|
+
#
|
|
175
|
+
# Subclasses must implement this method to generate their XML content.
|
|
176
|
+
#
|
|
177
|
+
# @param io [IO] output stream to write XML to
|
|
178
|
+
# @param indent [Integer] current indentation level
|
|
179
|
+
# @param tabs [String] precomputed tab string for current indent
|
|
180
|
+
# @return [void]
|
|
181
|
+
#
|
|
182
|
+
# @api private
|
|
28
183
|
def _to_xml(io, indent:, tabs:); end
|
|
29
184
|
end
|
|
30
185
|
|
|
186
|
+
# Mixin for XML header serialization (used in part-list).
|
|
187
|
+
#
|
|
188
|
+
# Similar to {ToXML}, but for elements that appear in the `<part-list>`
|
|
189
|
+
# section of MusicXML (parts and part groups).
|
|
190
|
+
#
|
|
191
|
+
# Classes including this module must implement `_header_to_xml(io, indent:, tabs:)`.
|
|
31
192
|
module HeaderToXML
|
|
193
|
+
# Converts the object's header representation to MusicXML.
|
|
194
|
+
#
|
|
195
|
+
# Used for elements that appear in the `<part-list>` section, such as
|
|
196
|
+
# `<score-part>` and `<part-group>` declarations.
|
|
197
|
+
#
|
|
198
|
+
# @param io [IO, StringIO, nil] output stream (creates StringIO if nil)
|
|
199
|
+
# @param indent [Integer, nil] indentation level (default: 0)
|
|
200
|
+
# @return [IO, StringIO] the io parameter, containing the XML output
|
|
32
201
|
def header_to_xml(io = nil, indent: nil)
|
|
33
202
|
io ||= StringIO.new
|
|
34
203
|
indent ||= 0
|
|
@@ -42,11 +211,40 @@ module Musa
|
|
|
42
211
|
|
|
43
212
|
private
|
|
44
213
|
|
|
214
|
+
# Abstract method for header XML generation.
|
|
215
|
+
#
|
|
216
|
+
# Subclasses must implement this method to generate their header XML.
|
|
217
|
+
#
|
|
218
|
+
# @param io [IO] output stream to write XML to
|
|
219
|
+
# @param indent [Integer] current indentation level
|
|
220
|
+
# @param tabs [String] precomputed tab string for current indent
|
|
221
|
+
# @return [void]
|
|
222
|
+
#
|
|
223
|
+
# @api private
|
|
45
224
|
def _header_to_xml(io, indent:, tabs:); end
|
|
46
225
|
end
|
|
47
226
|
|
|
48
227
|
private
|
|
49
228
|
|
|
229
|
+
# Creates class instance from Hash or returns existing instance.
|
|
230
|
+
#
|
|
231
|
+
# This helper method provides flexible parameter handling, allowing
|
|
232
|
+
# methods to accept either a fully-constructed instance or a hash of
|
|
233
|
+
# constructor parameters.
|
|
234
|
+
#
|
|
235
|
+
# @param klass [Class] expected class type
|
|
236
|
+
# @param hash_or_class_instance [klass, Hash, nil] value to process
|
|
237
|
+
# @return [klass, nil] instance of klass, or nil
|
|
238
|
+
#
|
|
239
|
+
# @raise [ArgumentError] if value is not klass, Hash, or nil
|
|
240
|
+
#
|
|
241
|
+
# @example Flexible parameter acceptance
|
|
242
|
+
# # Method can accept either:
|
|
243
|
+
# time_modification: { actual_notes: 3, normal_notes: 2 }
|
|
244
|
+
# # or:
|
|
245
|
+
# time_modification: TimeModification.new(actual_notes: 3, normal_notes: 2)
|
|
246
|
+
#
|
|
247
|
+
# @api private
|
|
50
248
|
def make_instance_if_needed(klass, hash_or_class_instance)
|
|
51
249
|
case hash_or_class_instance
|
|
52
250
|
when klass
|
|
@@ -60,6 +258,33 @@ module Musa
|
|
|
60
258
|
end
|
|
61
259
|
end
|
|
62
260
|
|
|
261
|
+
# Converts value to XML attribute string with boolean support.
|
|
262
|
+
#
|
|
263
|
+
# Handles three types of values:
|
|
264
|
+
# - **String/Numeric**: Outputs as attribute value
|
|
265
|
+
# - **true**: Outputs specified true_value (if provided)
|
|
266
|
+
# - **false**: Outputs specified false_value (if provided)
|
|
267
|
+
# - **Other**: Returns empty string (omits attribute)
|
|
268
|
+
#
|
|
269
|
+
# @param value [Object] value to convert
|
|
270
|
+
# @param attribute [String] attribute name
|
|
271
|
+
# @param true_value [String, nil] value to use for `true`
|
|
272
|
+
# @param false_value [String, nil] value to use for `false`
|
|
273
|
+
# @return [String] formatted attribute string or empty string
|
|
274
|
+
#
|
|
275
|
+
# @example String value
|
|
276
|
+
# decode_bool_or_string_attribute('above', 'placement')
|
|
277
|
+
# # => ' placement="above"'
|
|
278
|
+
#
|
|
279
|
+
# @example Boolean with mappings
|
|
280
|
+
# decode_bool_or_string_attribute(true, 'bracket', 'yes', 'no')
|
|
281
|
+
# # => ' bracket="yes"'
|
|
282
|
+
#
|
|
283
|
+
# @example Nil value
|
|
284
|
+
# decode_bool_or_string_attribute(nil, 'placement')
|
|
285
|
+
# # => ''
|
|
286
|
+
#
|
|
287
|
+
# @api private
|
|
63
288
|
def decode_bool_or_string_attribute(value, attribute, true_value = nil, false_value = nil)
|
|
64
289
|
if value.is_a?(String) || value.is_a?(Numeric)
|
|
65
290
|
" #{attribute}=\"#{value}\""
|
|
@@ -72,6 +297,21 @@ module Musa
|
|
|
72
297
|
end
|
|
73
298
|
end
|
|
74
299
|
|
|
300
|
+
# Converts value to XML element content with boolean support.
|
|
301
|
+
#
|
|
302
|
+
# Similar to {#decode_bool_or_string_attribute} but for element content
|
|
303
|
+
# rather than attributes.
|
|
304
|
+
#
|
|
305
|
+
# @param value [Object] value to convert
|
|
306
|
+
# @param true_value [String, nil] value to use for `true`
|
|
307
|
+
# @param false_value [String, nil] value to use for `false`
|
|
308
|
+
# @return [String] formatted content string or empty string
|
|
309
|
+
#
|
|
310
|
+
# @example
|
|
311
|
+
# decode_bool_or_string_value(true, 'yes', 'no') # => 'yes'
|
|
312
|
+
# decode_bool_or_string_value('dashed') # => 'dashed'
|
|
313
|
+
#
|
|
314
|
+
# @api private
|
|
75
315
|
def decode_bool_or_string_value(value, true_value = nil, false_value = nil)
|
|
76
316
|
if value.is_a?(String) || value.is_a?(Numeric)
|
|
77
317
|
value
|
|
@@ -13,12 +13,117 @@ module Musa
|
|
|
13
13
|
module MusicXML
|
|
14
14
|
module Builder
|
|
15
15
|
module Internal
|
|
16
|
+
# Measure container for musical content.
|
|
17
|
+
#
|
|
18
|
+
# Measure represents a single measure (bar) of music, containing musical elements
|
|
19
|
+
# in chronological order: attributes, notes, rests, backup/forward commands, and
|
|
20
|
+
# directions (dynamics, tempo markings, etc.).
|
|
21
|
+
#
|
|
22
|
+
# ## Element Order
|
|
23
|
+
#
|
|
24
|
+
# Elements within a measure follow MusicXML's sequential model:
|
|
25
|
+
# 1. **Attributes** (key, time, clef, divisions) - typically in first measure
|
|
26
|
+
# 2. **Directions** (tempo, dynamics) - before or between notes
|
|
27
|
+
# 3. **Notes/Rests** - musical content
|
|
28
|
+
# 4. **Backup/Forward** - timeline navigation for multiple voices/staves
|
|
29
|
+
#
|
|
30
|
+
# ## Multiple Voices and Staves
|
|
31
|
+
#
|
|
32
|
+
# For piano (grand staff) or polyphonic notation, use backup to rewind the timeline:
|
|
33
|
+
#
|
|
34
|
+
# measure do
|
|
35
|
+
# # Right hand (treble clef)
|
|
36
|
+
# pitch 'C', octave: 5, duration: 4, type: 'quarter', staff: 1
|
|
37
|
+
# pitch 'D', octave: 5, duration: 4, type: 'quarter', staff: 1
|
|
38
|
+
#
|
|
39
|
+
# backup 8 # Rewind to start of measure
|
|
40
|
+
#
|
|
41
|
+
# # Left hand (bass clef)
|
|
42
|
+
# pitch 'C', octave: 3, duration: 8, type: 'half', staff: 2
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# ## Divisions
|
|
46
|
+
#
|
|
47
|
+
# The `divisions` attribute sets timing resolution (divisions per quarter note).
|
|
48
|
+
# Higher values allow finer rhythmic subdivisions:
|
|
49
|
+
# - **divisions: 1** → quarter, half, whole only
|
|
50
|
+
# - **divisions: 2** → adds eighths
|
|
51
|
+
# - **divisions: 4** → adds sixteenths
|
|
52
|
+
# - **divisions: 8** → adds thirty-seconds
|
|
53
|
+
# - **divisions: 16** → allows complex tuplets
|
|
54
|
+
#
|
|
55
|
+
# Duration is calculated as: `duration = (note_type_value * divisions) / beat_type`
|
|
56
|
+
#
|
|
57
|
+
# @example Simple measure with quarter notes
|
|
58
|
+
# measure = Measure.new(1, divisions: 2) do
|
|
59
|
+
# attributes do
|
|
60
|
+
# key fifths: 0 # C major
|
|
61
|
+
# time beats: 4, beat_type: 4
|
|
62
|
+
# clef sign: 'G', line: 2
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# pitch 'C', octave: 4, duration: 2, type: 'quarter'
|
|
66
|
+
# pitch 'D', octave: 4, duration: 2, type: 'quarter'
|
|
67
|
+
# pitch 'E', octave: 4, duration: 2, type: 'quarter'
|
|
68
|
+
# pitch 'F', octave: 4, duration: 2, type: 'quarter'
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# @example Measure with dynamics and tempo
|
|
72
|
+
# measure do
|
|
73
|
+
# metronome beat_unit: 'quarter', per_minute: 120
|
|
74
|
+
#
|
|
75
|
+
# direction do
|
|
76
|
+
# dynamics 'p'
|
|
77
|
+
# wedge 'crescendo'
|
|
78
|
+
# end
|
|
79
|
+
#
|
|
80
|
+
# pitch 'C', octave: 4, duration: 4, type: 'quarter'
|
|
81
|
+
# pitch 'D', octave: 4, duration: 4, type: 'quarter'
|
|
82
|
+
#
|
|
83
|
+
# direction do
|
|
84
|
+
# wedge 'stop'
|
|
85
|
+
# dynamics 'f'
|
|
86
|
+
# end
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# @see Attributes Musical attributes (key, time, clef)
|
|
90
|
+
# @see PitchedNote Pitched note
|
|
91
|
+
# @see Rest Rest
|
|
92
|
+
# @see Direction Tempo, dynamics, expressions
|
|
16
93
|
class Measure
|
|
17
94
|
extend Musa::Extension::AttributeBuilder
|
|
18
95
|
include Musa::Extension::With
|
|
19
96
|
|
|
20
97
|
include Helper::ToXML
|
|
21
98
|
|
|
99
|
+
# Creates a new measure.
|
|
100
|
+
#
|
|
101
|
+
# @param number [Integer] measure number (automatically assigned by Part)
|
|
102
|
+
# @param divisions [Integer, nil] divisions per quarter note (timing resolution)
|
|
103
|
+
# @param key_cancel [Integer, nil] key cancellation
|
|
104
|
+
# @param key_fifths [Integer, nil] key signature (-7 to +7, circle of fifths)
|
|
105
|
+
# @param key_mode [String, nil] mode ('major' or 'minor')
|
|
106
|
+
# @param time_senza_misura [Boolean, nil] unmeasured time
|
|
107
|
+
# @param time_beats [Integer, nil] time signature numerator
|
|
108
|
+
# @param time_beat_type [Integer, nil] time signature denominator
|
|
109
|
+
# @param clef_sign [String, nil] clef sign ('G', 'F', 'C')
|
|
110
|
+
# @param clef_line [Integer, nil] clef line number
|
|
111
|
+
# @param clef_octave_change [Integer, nil] octave transposition
|
|
112
|
+
# @yield Optional DSL block for adding measure content
|
|
113
|
+
#
|
|
114
|
+
# @example First measure with all attributes
|
|
115
|
+
# Measure.new(1,
|
|
116
|
+
# divisions: 4,
|
|
117
|
+
# key_fifths: 2, # D major
|
|
118
|
+
# time_beats: 3, time_beat_type: 4,
|
|
119
|
+
# clef_sign: 'G', clef_line: 2
|
|
120
|
+
# )
|
|
121
|
+
#
|
|
122
|
+
# @example Measure with DSL block
|
|
123
|
+
# Measure.new(2) do
|
|
124
|
+
# pitch 'E', octave: 4, duration: 4, type: 'quarter'
|
|
125
|
+
# rest duration: 4, type: 'quarter'
|
|
126
|
+
# end
|
|
22
127
|
def initialize(number, divisions: nil,
|
|
23
128
|
key_cancel: nil, key_fifths: nil, key_mode: nil,
|
|
24
129
|
time_senza_misura: nil, time_beats: nil, time_beat_type: nil,
|
|
@@ -44,9 +149,39 @@ module Musa
|
|
|
44
149
|
with &block if block_given?
|
|
45
150
|
end
|
|
46
151
|
|
|
152
|
+
# Measure number.
|
|
153
|
+
# @return [Integer]
|
|
47
154
|
attr_accessor :number
|
|
155
|
+
|
|
156
|
+
# Ordered list of elements in this measure.
|
|
157
|
+
# @return [Array<Object>] notes, rests, attributes, directions, etc.
|
|
48
158
|
attr_reader :elements
|
|
49
159
|
|
|
160
|
+
# Adds musical attributes to the measure.
|
|
161
|
+
#
|
|
162
|
+
# Attributes define key signature, time signature, clef, and timing divisions.
|
|
163
|
+
# Typically appear at the start of the first measure or when they change.
|
|
164
|
+
#
|
|
165
|
+
# @option divisions [Integer, nil] divisions per quarter note
|
|
166
|
+
# @option key_cancel [Integer, nil] key to cancel
|
|
167
|
+
# @option key_fifths [Integer, nil] key signature (-7 to +7)
|
|
168
|
+
# @option key_mode [String, nil] 'major' or 'minor'
|
|
169
|
+
# @option time_senza_misura [Boolean, nil] unmeasured time
|
|
170
|
+
# @option time_beats [Integer, nil] time signature numerator
|
|
171
|
+
# @option time_beat_type [Integer, nil] time signature denominator
|
|
172
|
+
# @option clef_sign [String, nil] 'G', 'F', or 'C'
|
|
173
|
+
# @option clef_line [Integer, nil] clef line
|
|
174
|
+
# @option clef_octave_change [Integer, nil] octave transposition
|
|
175
|
+
# @yield Optional DSL block for adding keys, times, clefs
|
|
176
|
+
# @return [Attributes] the created attributes object
|
|
177
|
+
#
|
|
178
|
+
# @example Via DSL block
|
|
179
|
+
# measure.attributes do
|
|
180
|
+
# divisions 4
|
|
181
|
+
# key fifths: 1 # G major
|
|
182
|
+
# time beats: 3, beat_type: 4
|
|
183
|
+
# clef sign: 'G', line: 2
|
|
184
|
+
# end
|
|
50
185
|
attr_complex_adder_to_custom :attributes, plural: :attributes, variable: :@attributes do
|
|
51
186
|
| divisions: nil,
|
|
52
187
|
key_cancel: nil, key_fifths: nil, key_mode: nil,
|
|
@@ -65,62 +200,211 @@ module Musa
|
|
|
65
200
|
end
|
|
66
201
|
end
|
|
67
202
|
|
|
203
|
+
# Adds a pitched note.
|
|
204
|
+
#
|
|
205
|
+
# @return [PitchedNote] the created note
|
|
206
|
+
#
|
|
207
|
+
# @example
|
|
208
|
+
# measure.pitch 'C', octave: 4, duration: 4, type: 'quarter'
|
|
209
|
+
# measure.pitch step: 'E', octave: 4, duration: 2, type: 'eighth', dots: 1
|
|
210
|
+
#
|
|
211
|
+
# @see PitchedNote For full parameter list
|
|
68
212
|
attr_complex_adder_to_custom :pitch do | *parameters, **key_parameters |
|
|
69
213
|
PitchedNote.new(*parameters, **key_parameters).tap { |note| @elements << note }
|
|
70
214
|
end
|
|
71
215
|
|
|
216
|
+
# Adds a rest.
|
|
217
|
+
#
|
|
218
|
+
# @return [Rest] the created rest
|
|
219
|
+
#
|
|
220
|
+
# @example
|
|
221
|
+
# measure.rest duration: 4, type: 'quarter'
|
|
222
|
+
# measure.rest duration: 8, type: 'half', measure: true # whole measure rest
|
|
223
|
+
#
|
|
224
|
+
# @see Rest For full parameter list
|
|
72
225
|
attr_complex_adder_to_custom :rest do | *parameters, **key_parameters |
|
|
73
226
|
Rest.new(*parameters, **key_parameters).tap { |rest| @elements << rest }
|
|
74
227
|
end
|
|
75
228
|
|
|
229
|
+
# Adds an unpitched note (for percussion).
|
|
230
|
+
#
|
|
231
|
+
# @return [UnpitchedNote] the created unpitched note
|
|
232
|
+
#
|
|
233
|
+
# @see UnpitchedNote For details
|
|
76
234
|
attr_complex_adder_to_custom :unpitched do | *parameters, **key_parameters |
|
|
77
235
|
UnpitchedNote.new(*parameters, **key_parameters).tap { |unpitched| @elements << unpitched }
|
|
78
236
|
end
|
|
79
237
|
|
|
238
|
+
# Rewinds the musical timeline.
|
|
239
|
+
#
|
|
240
|
+
# Backup moves the current time position backward by the specified duration,
|
|
241
|
+
# allowing multiple voices or staves to be layered in the same time span.
|
|
242
|
+
#
|
|
243
|
+
# @return [Backup] the created backup element
|
|
244
|
+
#
|
|
245
|
+
# @example Piano grand staff
|
|
246
|
+
# measure do
|
|
247
|
+
# pitch 'C', octave: 5, duration: 8, type: 'half', staff: 1
|
|
248
|
+
# backup 8 # Rewind to start
|
|
249
|
+
# pitch 'C', octave: 3, duration: 8, type: 'half', staff: 2
|
|
250
|
+
# end
|
|
251
|
+
#
|
|
252
|
+
# @see Forward For moving forward
|
|
80
253
|
attr_complex_adder_to_custom :backup do |duration|
|
|
81
254
|
Backup.new(duration).tap { |backup| @elements << backup }
|
|
82
255
|
end
|
|
83
256
|
|
|
257
|
+
# Advances the musical timeline.
|
|
258
|
+
#
|
|
259
|
+
# Forward moves the current time position forward without sounding,
|
|
260
|
+
# creating rests or gaps in the timeline.
|
|
261
|
+
#
|
|
262
|
+
# @return [Forward] the created forward element
|
|
263
|
+
#
|
|
264
|
+
# @example Skip to beat 3
|
|
265
|
+
# measure do
|
|
266
|
+
# pitch 'C', octave: 4, duration: 2, type: 'quarter'
|
|
267
|
+
# forward 4 # Skip 2 beats
|
|
268
|
+
# pitch 'D', octave: 4, duration: 2, type: 'quarter'
|
|
269
|
+
# end
|
|
84
270
|
attr_complex_adder_to_custom :forward do |duration, voice: nil, staff: nil|
|
|
85
271
|
Forward.new(duration, voice: voice, staff: staff).tap { |forward| @elements << forward }
|
|
86
272
|
end
|
|
87
273
|
|
|
274
|
+
# Adds a direction element (dynamics, tempo, expressions).
|
|
275
|
+
#
|
|
276
|
+
# Directions contain non-note musical instructions like dynamics (p, f),
|
|
277
|
+
# tempo markings, wedges (crescendo/diminuendo), pedal marks, etc.
|
|
278
|
+
#
|
|
279
|
+
# @yield Optional DSL block for direction content
|
|
280
|
+
# @return [Direction] the created direction
|
|
281
|
+
#
|
|
282
|
+
# @example Dynamics with crescendo
|
|
283
|
+
# measure.direction do
|
|
284
|
+
# dynamics 'p'
|
|
285
|
+
# wedge 'crescendo'
|
|
286
|
+
# end
|
|
287
|
+
#
|
|
288
|
+
# @see Direction For direction types
|
|
88
289
|
attr_complex_adder_to_custom :direction do |*parameters, **key_parameters, &block|
|
|
89
290
|
Direction.new(*parameters, **key_parameters, &block).tap { |direction| @elements << direction }
|
|
90
291
|
end
|
|
91
292
|
|
|
293
|
+
# Direction shortcuts - these create a Direction automatically.
|
|
294
|
+
#
|
|
295
|
+
# The following methods are convenience shortcuts that create a Direction
|
|
296
|
+
# element containing the specified direction type. They accept placement and
|
|
297
|
+
# offset parameters that are passed to the Direction wrapper.
|
|
298
|
+
|
|
299
|
+
# Adds a metronome (tempo) marking.
|
|
300
|
+
#
|
|
301
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
302
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
303
|
+
# @option beat_unit [String] note value ('quarter', 'half', etc.)
|
|
304
|
+
# @option per_minute [Numeric] tempo in BPM
|
|
305
|
+
# @yield Optional block
|
|
306
|
+
# @return [Direction] direction containing metronome
|
|
307
|
+
#
|
|
308
|
+
# @example
|
|
309
|
+
# measure.metronome beat_unit: 'quarter', per_minute: 120
|
|
92
310
|
attr_complex_adder_to_custom(:metronome) {
|
|
93
311
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
94
312
|
direction(placement: placement, offset: offset) { metronome *p, **kp, &b } }
|
|
95
313
|
|
|
314
|
+
# Adds a wedge (crescendo/diminuendo).
|
|
315
|
+
#
|
|
316
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
317
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
318
|
+
# @option niente [Boolean, nil] niente attribute
|
|
319
|
+
# @return [Direction] direction containing wedge
|
|
320
|
+
#
|
|
321
|
+
# @example
|
|
322
|
+
# measure.wedge 'crescendo', niente: true
|
|
96
323
|
attr_complex_adder_to_custom(:wedge) {
|
|
97
324
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
98
325
|
direction(placement: placement, offset: offset) { wedge *p, **kp, &b } }
|
|
99
326
|
|
|
327
|
+
# Adds dynamics (p, pp, f, ff, etc.).
|
|
328
|
+
#
|
|
329
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
330
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
331
|
+
# @return [Direction] direction containing dynamics
|
|
332
|
+
#
|
|
333
|
+
# @example
|
|
334
|
+
# measure.dynamics 'pp'
|
|
335
|
+
# measure.dynamics ['mf', 'sf'] # Multiple dynamics
|
|
100
336
|
attr_complex_adder_to_custom(:dynamics) {
|
|
101
337
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
102
338
|
direction(placement: placement, offset: offset) { dynamics *p, **kp, &b } }
|
|
103
339
|
|
|
340
|
+
# Adds pedal marking.
|
|
341
|
+
#
|
|
342
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
343
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
344
|
+
# @option line [Boolean, nil] show pedal line
|
|
345
|
+
# @return [Direction] direction containing pedal
|
|
346
|
+
#
|
|
347
|
+
# @example
|
|
348
|
+
# measure.pedal 'start', line: true
|
|
104
349
|
attr_complex_adder_to_custom(:pedal) {
|
|
105
350
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
106
351
|
direction(placement: placement, offset: offset) { pedal *p, **kp, &b } }
|
|
107
352
|
|
|
353
|
+
# Adds bracket notation.
|
|
354
|
+
#
|
|
355
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
356
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
357
|
+
# @option line_end [String, nil] line end type
|
|
358
|
+
# @return [Direction] direction containing bracket
|
|
108
359
|
attr_complex_adder_to_custom(:bracket) {
|
|
109
360
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
110
361
|
direction(placement: placement, offset: offset) { bracket *p, **kp, &b } }
|
|
111
362
|
|
|
363
|
+
# Adds dashed line.
|
|
364
|
+
#
|
|
365
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
366
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
367
|
+
# @return [Direction] direction containing dashes
|
|
112
368
|
attr_complex_adder_to_custom(:dashes) {
|
|
113
369
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
114
370
|
direction(placement: placement, offset: offset) { dashes *p, **kp, &b } }
|
|
115
371
|
|
|
372
|
+
# Adds text annotation.
|
|
373
|
+
#
|
|
374
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
375
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
376
|
+
# @return [Direction] direction containing words
|
|
377
|
+
#
|
|
378
|
+
# @example
|
|
379
|
+
# measure.words "rit.", placement: 'above'
|
|
116
380
|
attr_complex_adder_to_custom(:words) {
|
|
117
381
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
118
382
|
direction(placement: placement, offset: offset) { words *p, **kp, &b } }
|
|
119
383
|
|
|
384
|
+
# Adds octave shift (8va/8vb).
|
|
385
|
+
#
|
|
386
|
+
# @option placement [String, nil] 'above' or 'below'
|
|
387
|
+
# @option offset [Numeric, nil] offset in divisions
|
|
388
|
+
# @option size [Integer, nil] octave shift size (8 or 15)
|
|
389
|
+
# @return [Direction] direction containing octave_shift
|
|
390
|
+
#
|
|
391
|
+
# @example
|
|
392
|
+
# measure.octave_shift 'up', size: 8
|
|
120
393
|
attr_complex_adder_to_custom(:octave_shift) {
|
|
121
394
|
|*p, placement: nil, offset: nil, **kp, &b|
|
|
122
395
|
direction(placement: placement, offset: offset) { octave_shift *p, **kp, &b } }
|
|
123
396
|
|
|
397
|
+
# Generates the measure XML element with all contained elements.
|
|
398
|
+
#
|
|
399
|
+
# Outputs elements in the order they were added, which must follow
|
|
400
|
+
# MusicXML's element ordering rules (attributes, then notes/directions/backup/forward).
|
|
401
|
+
#
|
|
402
|
+
# @param io [IO] output stream
|
|
403
|
+
# @param indent [Integer] indentation level
|
|
404
|
+
# @param tabs [String] tab string
|
|
405
|
+
# @return [void]
|
|
406
|
+
#
|
|
407
|
+
# @api private
|
|
124
408
|
def _to_xml(io, indent:, tabs:)
|
|
125
409
|
io.puts "#{tabs}<measure number=\"#{@number.to_i}\">"
|
|
126
410
|
|