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
|
@@ -4,7 +4,101 @@ module Musa
|
|
|
4
4
|
module MusicXML
|
|
5
5
|
module Builder
|
|
6
6
|
module Internal
|
|
7
|
+
# Pitched note with specific step, octave, and optional alteration.
|
|
8
|
+
#
|
|
9
|
+
# PitchedNote represents notes with defined pitches (as opposed to rests or
|
|
10
|
+
# unpitched percussion). It extends {Note} with pitch information: step (C-G),
|
|
11
|
+
# octave (scientific pitch notation), and optional chromatic alteration.
|
|
12
|
+
#
|
|
13
|
+
# ## Pitch Components
|
|
14
|
+
#
|
|
15
|
+
# ### Step
|
|
16
|
+
# The diatonic pitch class: 'C', 'D', 'E', 'F', 'G', 'A', 'B'
|
|
17
|
+
#
|
|
18
|
+
# ### Octave
|
|
19
|
+
# Scientific pitch notation (middle C = C4):
|
|
20
|
+
# - Octave 0: C0 to B0 (subcontra octave)
|
|
21
|
+
# - Octave 4: C4 to B4 (one-line octave, middle C)
|
|
22
|
+
# - Octave 8: C8 to B8 (five-line octave)
|
|
23
|
+
#
|
|
24
|
+
# ### Alter
|
|
25
|
+
# Chromatic alteration in semitones:
|
|
26
|
+
# - **-2**: Double flat
|
|
27
|
+
# - **-1**: Flat
|
|
28
|
+
# - **0**: Natural (can be omitted)
|
|
29
|
+
# - **+1**: Sharp
|
|
30
|
+
# - **+2**: Double sharp
|
|
31
|
+
#
|
|
32
|
+
# ## Accidentals
|
|
33
|
+
#
|
|
34
|
+
# The `alter` parameter changes the sounding pitch, while the `accidental`
|
|
35
|
+
# parameter controls visual display:
|
|
36
|
+
#
|
|
37
|
+
# - **alter**: Affects playback (actual pitch)
|
|
38
|
+
# - **accidental**: Visual symbol (sharp, flat, natural, etc.)
|
|
39
|
+
#
|
|
40
|
+
# Usually both are specified together, but you can have:
|
|
41
|
+
# - alter without accidental (implied by key signature)
|
|
42
|
+
# - accidental without alter (cautionary accidental)
|
|
43
|
+
#
|
|
44
|
+
# ## Usage
|
|
45
|
+
#
|
|
46
|
+
# Created via Measure#add_pitch or Measure#pitch:
|
|
47
|
+
#
|
|
48
|
+
# measure.pitch 'C', octave: 5, duration: 4, type: 'quarter'
|
|
49
|
+
# measure.add_pitch step: 'F', alter: 1, octave: 4, duration: 2, type: 'eighth'
|
|
50
|
+
#
|
|
51
|
+
# @example Middle C quarter note
|
|
52
|
+
# PitchedNote.new('C', octave: 4, duration: 4, type: 'quarter')
|
|
53
|
+
#
|
|
54
|
+
# @example F# with sharp symbol
|
|
55
|
+
# PitchedNote.new('F', alter: 1, octave: 5, duration: 2, type: 'eighth',
|
|
56
|
+
# accidental: 'sharp')
|
|
57
|
+
#
|
|
58
|
+
# @example Bb dotted half note with staccato
|
|
59
|
+
# PitchedNote.new('B', alter: -1, octave: 4, duration: 6, type: 'half',
|
|
60
|
+
# dots: 1, accidental: 'flat', staccato: true)
|
|
61
|
+
#
|
|
62
|
+
# @example High A with trill
|
|
63
|
+
# PitchedNote.new('A', octave: 6, duration: 8, type: 'whole',
|
|
64
|
+
# trill_mark: true)
|
|
65
|
+
#
|
|
66
|
+
# @example Chord notes (C major triad)
|
|
67
|
+
# measure.pitch 'C', octave: 4, duration: 4, type: 'quarter'
|
|
68
|
+
# measure.pitch 'E', octave: 4, duration: 4, type: 'quarter', chord: true
|
|
69
|
+
# measure.pitch 'G', octave: 4, duration: 4, type: 'quarter', chord: true
|
|
70
|
+
#
|
|
71
|
+
# @example Grace note with slur
|
|
72
|
+
# PitchedNote.new('D', octave: 5, grace: true, type: 'eighth',
|
|
73
|
+
# slur: 'start')
|
|
74
|
+
#
|
|
75
|
+
# @see Note Base class with all notation attributes
|
|
76
|
+
# @see Rest Rest notes
|
|
77
|
+
# @see UnpitchedNote Unpitched percussion
|
|
78
|
+
# @see Measure Container for adding notes
|
|
7
79
|
class PitchedNote < Note
|
|
80
|
+
# Creates a pitched note.
|
|
81
|
+
#
|
|
82
|
+
# @param _step [String, nil] step as positional parameter (alternative to keyword)
|
|
83
|
+
# @param step [String, nil] diatonic step: 'C', 'D', 'E', 'F', 'G', 'A', 'B'
|
|
84
|
+
# @param alter [Integer, nil] semitone alteration: -2 (double flat), -1 (flat),
|
|
85
|
+
# 0 (natural), +1 (sharp), +2 (double sharp)
|
|
86
|
+
# @param octave [Integer] octave number (scientific pitch notation, middle C = 4)
|
|
87
|
+
#
|
|
88
|
+
# @param (see Note#initialize) All Note parameters are supported
|
|
89
|
+
#
|
|
90
|
+
# @example C natural in octave 4, quarter note
|
|
91
|
+
# PitchedNote.new('C', octave: 4, duration: 4, type: 'quarter')
|
|
92
|
+
#
|
|
93
|
+
# @example F sharp in octave 5, eighth note
|
|
94
|
+
# PitchedNote.new('F', alter: 1, octave: 5, duration: 2, type: 'eighth',
|
|
95
|
+
# accidental: 'sharp')
|
|
96
|
+
#
|
|
97
|
+
# @example B flat with keyword syntax
|
|
98
|
+
# PitchedNote.new(step: 'B', alter: -1, octave: 4, duration: 4,
|
|
99
|
+
# type: 'quarter', accidental: 'flat')
|
|
100
|
+
#
|
|
101
|
+
# For detailed parameter documentation, see {Note#initialize}
|
|
8
102
|
def initialize(_step = nil, step: nil, alter: nil, octave:,
|
|
9
103
|
pizzicato: nil, # true
|
|
10
104
|
grace: nil, # true
|
|
@@ -104,12 +198,42 @@ module Musa
|
|
|
104
198
|
super
|
|
105
199
|
end
|
|
106
200
|
|
|
201
|
+
# Step builder/setter.
|
|
202
|
+
# @overload step(value)
|
|
203
|
+
# Sets step via DSL
|
|
204
|
+
# @param value [String] diatonic step ('C'-'G')
|
|
205
|
+
# @overload step=(value)
|
|
206
|
+
# Sets step via assignment
|
|
207
|
+
# @param value [String] diatonic step ('C'-'G')
|
|
107
208
|
attr_simple_builder :step
|
|
209
|
+
|
|
210
|
+
# Alter builder/setter.
|
|
211
|
+
# @overload alter(value)
|
|
212
|
+
# Sets alteration via DSL
|
|
213
|
+
# @param value [Integer] semitone alteration (-2 to +2)
|
|
214
|
+
# @overload alter=(value)
|
|
215
|
+
# Sets alteration via assignment
|
|
216
|
+
# @param value [Integer] semitone alteration (-2 to +2)
|
|
108
217
|
attr_simple_builder :alter
|
|
218
|
+
|
|
219
|
+
# Octave builder/setter.
|
|
220
|
+
# @overload octave(value)
|
|
221
|
+
# Sets octave via DSL
|
|
222
|
+
# @param value [Integer] octave number
|
|
223
|
+
# @overload octave=(value)
|
|
224
|
+
# Sets octave via assignment
|
|
225
|
+
# @param value [Integer] octave number
|
|
109
226
|
attr_simple_builder :octave
|
|
110
227
|
|
|
111
228
|
private
|
|
112
229
|
|
|
230
|
+
# Outputs the pitch XML element.
|
|
231
|
+
#
|
|
232
|
+
# @param io [IO] output stream
|
|
233
|
+
# @param indent [Integer] indentation level
|
|
234
|
+
# @return [void]
|
|
235
|
+
#
|
|
236
|
+
# @api private
|
|
113
237
|
def specific_to_xml(io, indent:)
|
|
114
238
|
tabs = "\t" * indent
|
|
115
239
|
|
|
@@ -4,7 +4,83 @@ module Musa
|
|
|
4
4
|
module MusicXML
|
|
5
5
|
module Builder
|
|
6
6
|
module Internal
|
|
7
|
+
# Rest (silence) with specified duration or full measure.
|
|
8
|
+
#
|
|
9
|
+
# Rest represents musical silence. It extends {Note} with rest-specific
|
|
10
|
+
# features, particularly the measure rest attribute for whole-measure silences.
|
|
11
|
+
#
|
|
12
|
+
# ## Types of Rests
|
|
13
|
+
#
|
|
14
|
+
# ### Regular Rests
|
|
15
|
+
# Rests with explicit duration and type (whole, half, quarter, eighth, etc.):
|
|
16
|
+
#
|
|
17
|
+
# Rest.new(duration: 4, type: 'half')
|
|
18
|
+
# Rest.new(duration: 1, type: 'sixteenth', dots: 1)
|
|
19
|
+
#
|
|
20
|
+
# ### Measure Rests
|
|
21
|
+
# Full-measure rests that automatically fill the entire measure regardless
|
|
22
|
+
# of time signature:
|
|
23
|
+
#
|
|
24
|
+
# Rest.new(duration: 8, type: 'whole', measure: true)
|
|
25
|
+
#
|
|
26
|
+
# The `measure: true` attribute tells notation software to center the rest
|
|
27
|
+
# and adjust its appearance based on the time signature.
|
|
28
|
+
#
|
|
29
|
+
# ## Dotted Rests
|
|
30
|
+
#
|
|
31
|
+
# Like notes, rests can have augmentation dots:
|
|
32
|
+
#
|
|
33
|
+
# Rest.new(duration: 3, type: 'quarter', dots: 1) # Dotted quarter
|
|
34
|
+
# Rest.new(duration: 7, type: 'half', dots: 2) # Double-dotted half
|
|
35
|
+
#
|
|
36
|
+
# ## Multi-Voice Rests
|
|
37
|
+
#
|
|
38
|
+
# In polyphonic music, rests can be assigned to specific voices:
|
|
39
|
+
#
|
|
40
|
+
# Rest.new(duration: 2, type: 'quarter', voice: 2)
|
|
41
|
+
#
|
|
42
|
+
# ## Usage
|
|
43
|
+
#
|
|
44
|
+
# Created via Measure#add_rest or Measure#rest:
|
|
45
|
+
#
|
|
46
|
+
# measure.rest duration: 4, type: 'half'
|
|
47
|
+
# measure.add_rest duration: 8, type: 'whole', measure: true
|
|
48
|
+
#
|
|
49
|
+
# @example Quarter rest
|
|
50
|
+
# Rest.new(duration: 2, type: 'quarter')
|
|
51
|
+
#
|
|
52
|
+
# @example Measure rest (whole measure)
|
|
53
|
+
# Rest.new(duration: 8, type: 'whole', measure: true)
|
|
54
|
+
#
|
|
55
|
+
# @example Dotted eighth rest
|
|
56
|
+
# Rest.new(duration: 3, type: 'eighth', dots: 1)
|
|
57
|
+
#
|
|
58
|
+
# @example Rest in specific voice
|
|
59
|
+
# Rest.new(duration: 4, type: 'half', voice: 2)
|
|
60
|
+
#
|
|
61
|
+
# @example Rest with fermata
|
|
62
|
+
# Rest.new(duration: 8, type: 'whole', fermata: true)
|
|
63
|
+
#
|
|
64
|
+
# @see Note Base class with all notation attributes
|
|
65
|
+
# @see PitchedNote Pitched notes
|
|
66
|
+
# @see UnpitchedNote Unpitched percussion
|
|
67
|
+
# @see Measure Container for adding rests
|
|
7
68
|
class Rest < Note
|
|
69
|
+
# Creates a rest.
|
|
70
|
+
#
|
|
71
|
+
# @param measure [Boolean, nil] measure rest (fills entire measure)
|
|
72
|
+
# @param (see Note#initialize) All Note parameters are supported
|
|
73
|
+
#
|
|
74
|
+
# @example Quarter rest
|
|
75
|
+
# Rest.new(duration: 2, type: 'quarter')
|
|
76
|
+
#
|
|
77
|
+
# @example Whole measure rest
|
|
78
|
+
# Rest.new(duration: 8, type: 'whole', measure: true)
|
|
79
|
+
#
|
|
80
|
+
# @example Dotted eighth rest in voice 2
|
|
81
|
+
# Rest.new(duration: 3, type: 'eighth', dots: 1, voice: 2)
|
|
82
|
+
#
|
|
83
|
+
# For detailed parameter documentation, see {Note#initialize}
|
|
8
84
|
def initialize(pizzicato: nil, # true
|
|
9
85
|
measure: nil, # true
|
|
10
86
|
grace: nil, # true
|
|
@@ -102,10 +178,27 @@ module Musa
|
|
|
102
178
|
super
|
|
103
179
|
end
|
|
104
180
|
|
|
181
|
+
# Measure rest builder/setter.
|
|
182
|
+
#
|
|
183
|
+
# Indicates whether this is a full-measure rest.
|
|
184
|
+
#
|
|
185
|
+
# @overload measure(value)
|
|
186
|
+
# Sets measure rest via DSL
|
|
187
|
+
# @param value [Boolean] true for measure rest
|
|
188
|
+
# @overload measure=(value)
|
|
189
|
+
# Sets measure rest via assignment
|
|
190
|
+
# @param value [Boolean] true for measure rest
|
|
105
191
|
attr_simple_builder :measure
|
|
106
192
|
|
|
107
193
|
private
|
|
108
194
|
|
|
195
|
+
# Outputs the rest XML element.
|
|
196
|
+
#
|
|
197
|
+
# @param io [IO] output stream
|
|
198
|
+
# @param indent [Integer] indentation level
|
|
199
|
+
# @return [void]
|
|
200
|
+
#
|
|
201
|
+
# @api private
|
|
109
202
|
def specific_to_xml(io, indent:)
|
|
110
203
|
tabs = "\t" * indent
|
|
111
204
|
io.puts "#{tabs}<rest #{"measure=\"yes\"" if @measure}/>"
|
|
@@ -12,12 +12,137 @@ require_relative 'helper'
|
|
|
12
12
|
module Musa
|
|
13
13
|
module MusicXML
|
|
14
14
|
module Builder
|
|
15
|
+
# Main entry point for creating MusicXML scores.
|
|
16
|
+
#
|
|
17
|
+
# ScorePartwise represents the root `<score-partwise>` element of a MusicXML 3.0
|
|
18
|
+
# document. It contains metadata (work info, creators, rights), part definitions,
|
|
19
|
+
# and the actual musical content organized by parts and measures.
|
|
20
|
+
#
|
|
21
|
+
# ## Structure
|
|
22
|
+
#
|
|
23
|
+
# A ScorePartwise document contains:
|
|
24
|
+
#
|
|
25
|
+
# - **Metadata**: work title/number, movement title/number, creators, rights
|
|
26
|
+
# - **Part List**: part and part-group declarations with names/abbreviations
|
|
27
|
+
# - **Parts**: actual musical content (measures with notes, dynamics, etc.)
|
|
28
|
+
#
|
|
29
|
+
# ## Usage Patterns
|
|
30
|
+
#
|
|
31
|
+
# ### Constructor Style
|
|
32
|
+
#
|
|
33
|
+
# Set properties via constructor parameters and use `add_*` methods:
|
|
34
|
+
#
|
|
35
|
+
# score = ScorePartwise.new(
|
|
36
|
+
# work_title: "Symphony No. 1",
|
|
37
|
+
# creators: { composer: "Composer Name" }
|
|
38
|
+
# )
|
|
39
|
+
# part = score.add_part(:p1, name: "Violin")
|
|
40
|
+
# measure = part.add_measure(divisions: 2)
|
|
41
|
+
#
|
|
42
|
+
# ### DSL Style
|
|
43
|
+
#
|
|
44
|
+
# Use blocks with method names as setters/builders:
|
|
45
|
+
#
|
|
46
|
+
# score = ScorePartwise.new do
|
|
47
|
+
# work_title "Symphony No. 1"
|
|
48
|
+
# creators composer: "Composer Name"
|
|
49
|
+
# part :p1, name: "Violin" do
|
|
50
|
+
# measure do
|
|
51
|
+
# # measure content
|
|
52
|
+
# end
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# ## Part Groups
|
|
57
|
+
#
|
|
58
|
+
# Parts can be organized into groups (for orchestral sections, etc.):
|
|
59
|
+
#
|
|
60
|
+
# score.add_group 1, type: 'start', name: "Strings"
|
|
61
|
+
# score.add_part :p1, name: "Violin I"
|
|
62
|
+
# score.add_part :p2, name: "Violin II"
|
|
63
|
+
# score.add_group 1, type: 'stop'
|
|
64
|
+
#
|
|
65
|
+
# ## XML Output
|
|
66
|
+
#
|
|
67
|
+
# Generate MusicXML 3.0 compliant XML:
|
|
68
|
+
#
|
|
69
|
+
# File.open('score.xml', 'w') do |f|
|
|
70
|
+
# score.to_xml(f)
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# # Or get as string:
|
|
74
|
+
# xml_string = score.to_xml.string
|
|
75
|
+
#
|
|
76
|
+
# @example Complete score with two parts
|
|
77
|
+
# score = ScorePartwise.new do
|
|
78
|
+
# work_title "Duet"
|
|
79
|
+
# creators composer: "J. Composer"
|
|
80
|
+
# encoding_date DateTime.new(2024, 1, 1)
|
|
81
|
+
#
|
|
82
|
+
# part :p1, name: "Flute" do
|
|
83
|
+
# measure do
|
|
84
|
+
# attributes do
|
|
85
|
+
# divisions 4
|
|
86
|
+
# key fifths: 1 # G major
|
|
87
|
+
# time beats: 3, beat_type: 4
|
|
88
|
+
# clef sign: 'G', line: 2
|
|
89
|
+
# end
|
|
90
|
+
# pitch 'G', octave: 4, duration: 4, type: 'quarter'
|
|
91
|
+
# pitch 'A', octave: 4, duration: 4, type: 'quarter'
|
|
92
|
+
# pitch 'B', octave: 4, duration: 4, type: 'quarter'
|
|
93
|
+
# end
|
|
94
|
+
# end
|
|
95
|
+
#
|
|
96
|
+
# part :p2, name: "Piano" do
|
|
97
|
+
# measure do
|
|
98
|
+
# attributes do
|
|
99
|
+
# divisions 4
|
|
100
|
+
# key fifths: 1
|
|
101
|
+
# time beats: 3, beat_type: 4
|
|
102
|
+
# clef sign: 'G', line: 2
|
|
103
|
+
# end
|
|
104
|
+
# pitch 'D', octave: 4, duration: 12, type: 'half', dots: 1
|
|
105
|
+
# end
|
|
106
|
+
# end
|
|
107
|
+
# end
|
|
108
|
+
#
|
|
109
|
+
# @see Internal::Part Part implementation
|
|
110
|
+
# @see Internal::PartGroup Part grouping
|
|
111
|
+
# @see Internal::Measure Measure implementation
|
|
15
112
|
class ScorePartwise
|
|
16
113
|
extend Musa::Extension::AttributeBuilder
|
|
17
114
|
include Musa::Extension::With
|
|
18
115
|
|
|
19
116
|
include Internal::Helper::ToXML
|
|
20
117
|
|
|
118
|
+
# Creates a new MusicXML score.
|
|
119
|
+
#
|
|
120
|
+
# @param work_number [Integer, nil] opus or catalog number
|
|
121
|
+
# @param work_title [String, nil] title of the work
|
|
122
|
+
# @param movement_number [Integer, String, nil] movement number
|
|
123
|
+
# @param movement_title [String, nil] movement title
|
|
124
|
+
# @param encoding_date [DateTime, nil] encoding date (default: now)
|
|
125
|
+
# @param creators [Hash{Symbol => String}, nil] creators by type (e.g., composer: "Name")
|
|
126
|
+
# @param rights [Hash{Symbol => String}, nil] rights by type (e.g., lyrics: "Name")
|
|
127
|
+
# @yield Optional DSL block for building score structure
|
|
128
|
+
#
|
|
129
|
+
# @example With metadata in constructor
|
|
130
|
+
# ScorePartwise.new(
|
|
131
|
+
# work_title: "Sonata in C",
|
|
132
|
+
# work_number: 1,
|
|
133
|
+
# movement_title: "Allegro",
|
|
134
|
+
# creators: { composer: "Mozart", arranger: "Smith" },
|
|
135
|
+
# rights: { lyrics: "Public Domain" }
|
|
136
|
+
# )
|
|
137
|
+
#
|
|
138
|
+
# @example With DSL block
|
|
139
|
+
# ScorePartwise.new do
|
|
140
|
+
# work_title "Sonata in C"
|
|
141
|
+
# creators composer: "Mozart"
|
|
142
|
+
# part :p1, name: "Piano" do
|
|
143
|
+
# # ...
|
|
144
|
+
# end
|
|
145
|
+
# end
|
|
21
146
|
def initialize(work_number: nil, work_title: nil,
|
|
22
147
|
movement_number: nil, movement_title: nil,
|
|
23
148
|
encoding_date: nil,
|
|
@@ -44,17 +169,117 @@ module Musa
|
|
|
44
169
|
with &block if block_given?
|
|
45
170
|
end
|
|
46
171
|
|
|
172
|
+
# Work title builder/setter.
|
|
173
|
+
#
|
|
174
|
+
# @overload work_title(value)
|
|
175
|
+
# Sets work title via DSL
|
|
176
|
+
# @param value [String] work title
|
|
177
|
+
# @overload work_title=(value)
|
|
178
|
+
# Sets work title via assignment
|
|
179
|
+
# @param value [String] work title
|
|
47
180
|
attr_simple_builder :work_title
|
|
181
|
+
|
|
182
|
+
# Work number builder/setter.
|
|
183
|
+
#
|
|
184
|
+
# @overload work_number(value)
|
|
185
|
+
# Sets work number via DSL
|
|
186
|
+
# @param value [Integer] work number
|
|
187
|
+
# @overload work_number=(value)
|
|
188
|
+
# Sets work number via assignment
|
|
189
|
+
# @param value [Integer] work number
|
|
48
190
|
attr_simple_builder :work_number
|
|
49
191
|
|
|
192
|
+
# Movement title builder/setter.
|
|
193
|
+
#
|
|
194
|
+
# @overload movement_title(value)
|
|
195
|
+
# Sets movement title via DSL
|
|
196
|
+
# @param value [String] movement title
|
|
197
|
+
# @overload movement_title=(value)
|
|
198
|
+
# Sets movement title via assignment
|
|
199
|
+
# @param value [String] movement title
|
|
50
200
|
attr_simple_builder :movement_title
|
|
201
|
+
|
|
202
|
+
# Movement number builder/setter.
|
|
203
|
+
#
|
|
204
|
+
# @overload movement_number(value)
|
|
205
|
+
# Sets movement number via DSL
|
|
206
|
+
# @param value [Integer, String] movement number
|
|
207
|
+
# @overload movement_number=(value)
|
|
208
|
+
# Sets movement number via assignment
|
|
209
|
+
# @param value [Integer, String] movement number
|
|
51
210
|
attr_simple_builder :movement_number
|
|
52
211
|
|
|
212
|
+
# Encoding date builder/setter.
|
|
213
|
+
#
|
|
214
|
+
# @overload encoding_date(value)
|
|
215
|
+
# Sets encoding date via DSL
|
|
216
|
+
# @param value [DateTime] encoding date
|
|
217
|
+
# @overload encoding_date=(value)
|
|
218
|
+
# Sets encoding date via assignment
|
|
219
|
+
# @param value [DateTime] encoding date
|
|
53
220
|
attr_simple_builder :encoding_date
|
|
54
221
|
|
|
222
|
+
# Adds rights information (single or multiple).
|
|
223
|
+
#
|
|
224
|
+
# Rights specify copyright, licensing, or attribution information.
|
|
225
|
+
#
|
|
226
|
+
# @overload rights(hash)
|
|
227
|
+
# Adds multiple rights via hash (DSL style)
|
|
228
|
+
# @param hash [Hash{Symbol => String}] rights by type
|
|
229
|
+
# @overload add_rights(type, name)
|
|
230
|
+
# Adds single rights entry (method style)
|
|
231
|
+
# @param type [Symbol, String] rights type (e.g., :lyrics, :arrangement)
|
|
232
|
+
# @param name [String] rights holder name
|
|
233
|
+
#
|
|
234
|
+
# @example DSL style
|
|
235
|
+
# score.rights lyrics: "John Doe", music: "Jane Smith"
|
|
236
|
+
#
|
|
237
|
+
# @example Method style
|
|
238
|
+
# score.add_rights :lyrics, "John Doe"
|
|
239
|
+
# score.add_rights :music, "Jane Smith"
|
|
55
240
|
attr_tuple_adder_to_array :rights, Internal::Rights, plural: :rights
|
|
241
|
+
|
|
242
|
+
# Adds creator information (single or multiple).
|
|
243
|
+
#
|
|
244
|
+
# Creators specify who created various aspects of the work.
|
|
245
|
+
#
|
|
246
|
+
# @overload creators(hash)
|
|
247
|
+
# Adds multiple creators via hash (DSL style)
|
|
248
|
+
# @param hash [Hash{Symbol => String}] creators by type
|
|
249
|
+
# @overload add_creator(type, name)
|
|
250
|
+
# Adds single creator entry (method style)
|
|
251
|
+
# @param type [Symbol, String] creator type (e.g., :composer, :lyricist, :arranger)
|
|
252
|
+
# @param name [String] creator name
|
|
253
|
+
#
|
|
254
|
+
# @example DSL style
|
|
255
|
+
# score.creators composer: "Mozart", lyricist: "Da Ponte"
|
|
256
|
+
#
|
|
257
|
+
# @example Method style
|
|
258
|
+
# score.add_creator :composer, "Mozart"
|
|
259
|
+
# score.add_creator :lyricist, "Da Ponte"
|
|
56
260
|
attr_tuple_adder_to_array :creator, Internal::Creator
|
|
57
261
|
|
|
262
|
+
# Adds a part to the score.
|
|
263
|
+
#
|
|
264
|
+
# Parts represent individual instruments or voices in the score. Each part
|
|
265
|
+
# contains measures with musical content.
|
|
266
|
+
#
|
|
267
|
+
# @option name [String] full part name (displayed in score)
|
|
268
|
+
# @option abbreviation [String, nil] abbreviated name (for subsequent systems)
|
|
269
|
+
# @yield Optional DSL block for defining measures
|
|
270
|
+
# @return [Internal::Part] the created part
|
|
271
|
+
#
|
|
272
|
+
# @example DSL style
|
|
273
|
+
# score.part :p1, name: "Violin I", abbreviation: "Vln. I" do
|
|
274
|
+
# measure do
|
|
275
|
+
# pitch 'A', octave: 4, duration: 4, type: 'quarter'
|
|
276
|
+
# end
|
|
277
|
+
# end
|
|
278
|
+
#
|
|
279
|
+
# @example Method style
|
|
280
|
+
# part = score.add_part(:p1, name: "Violin I", abbreviation: "Vln. I")
|
|
281
|
+
# measure = part.add_measure
|
|
282
|
+
# measure.add_pitch step: 'A', octave: 4, duration: 4, type: 'quarter'
|
|
58
283
|
attr_complex_adder_to_custom :part, variable: :@parts do |id, name:, abbreviation: nil, &block|
|
|
59
284
|
Internal::Part.new(id, name: name, abbreviation: abbreviation, &block).tap do |part|
|
|
60
285
|
@parts[id] = part
|
|
@@ -62,17 +287,64 @@ module Musa
|
|
|
62
287
|
end
|
|
63
288
|
end
|
|
64
289
|
|
|
290
|
+
# Adds a part group to organize parts.
|
|
291
|
+
#
|
|
292
|
+
# Part groups bracket multiple parts together (e.g., string section, choir).
|
|
293
|
+
# Groups are defined by matching start/stop pairs with the same number.
|
|
294
|
+
#
|
|
295
|
+
# @option type [String] 'start' or 'stop'
|
|
296
|
+
# @option name [String, nil] group name (displayed on bracket)
|
|
297
|
+
# @option abbreviation [String, nil] abbreviated group name
|
|
298
|
+
# @option symbol [String, nil] bracket symbol (e.g., 'bracket', 'brace')
|
|
299
|
+
# @option group_barline [Boolean, String, nil] whether barlines connect across group
|
|
300
|
+
# @option group_time [Boolean, String, nil] whether time signatures are shared
|
|
301
|
+
# @return [Internal::PartGroup] the created group
|
|
302
|
+
#
|
|
303
|
+
# @example Bracketing string section
|
|
304
|
+
# score.add_group 1, type: 'start', name: "Strings", symbol: 'bracket'
|
|
305
|
+
# score.add_part :p1, name: "Violin I"
|
|
306
|
+
# score.add_part :p2, name: "Violin II"
|
|
307
|
+
# score.add_part :p3, name: "Viola"
|
|
308
|
+
# score.add_group 1, type: 'stop'
|
|
309
|
+
#
|
|
310
|
+
# @example Nested groups
|
|
311
|
+
# score.add_group 1, type: 'start', name: "Orchestra"
|
|
312
|
+
# score.add_group 2, type: 'start', name: "Woodwinds"
|
|
313
|
+
# score.add_part :p1, name: "Flute"
|
|
314
|
+
# score.add_part :p2, name: "Oboe"
|
|
315
|
+
# score.add_group 2, type: 'stop'
|
|
316
|
+
# score.add_group 1, type: 'stop'
|
|
65
317
|
attr_complex_adder_to_custom :group do |*parameters, **key_parameters|
|
|
66
318
|
Internal::PartGroup.new(*parameters, **key_parameters).tap do |group|
|
|
67
319
|
@groups_and_parts << group
|
|
68
320
|
end
|
|
69
321
|
end
|
|
70
322
|
|
|
323
|
+
# Generates the complete MusicXML document structure.
|
|
324
|
+
#
|
|
325
|
+
# Creates a MusicXML 3.0 Partwise document with:
|
|
326
|
+
# - XML declaration and DOCTYPE
|
|
327
|
+
# - Work and movement metadata
|
|
328
|
+
# - Identification section (creators, rights, encoding info)
|
|
329
|
+
# - Part list (part and group declarations)
|
|
330
|
+
# - Part content (measures with notes)
|
|
331
|
+
#
|
|
332
|
+
# The encoding section automatically includes:
|
|
333
|
+
# - Encoding date (from @encoding_date)
|
|
334
|
+
# - Software attribution: "MusaDSL: MusicXML output formatter"
|
|
335
|
+
#
|
|
336
|
+
# @param io [IO] output stream to write XML to
|
|
337
|
+
# @param indent [Integer] current indentation level
|
|
338
|
+
# @param tabs [String] precomputed tab string for current indent
|
|
339
|
+
# @return [void]
|
|
340
|
+
#
|
|
341
|
+
# @api private
|
|
71
342
|
def _to_xml(io, indent:, tabs:)
|
|
72
343
|
io.puts "#{tabs}<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"
|
|
73
344
|
io.puts "#{tabs}<!DOCTYPE score-partwise PUBLIC \"-//Recordare//DTD MusicXML 3.0 Partwise//EN\" \"http://www.musicxml.org/dtds/partwise.dtd\">"
|
|
74
345
|
io.puts "#{tabs}<score-partwise version=\"3.0\">"
|
|
75
346
|
|
|
347
|
+
# Work section (optional)
|
|
76
348
|
if @work_number || @work_title
|
|
77
349
|
io.puts"#{tabs}\t<work>"
|
|
78
350
|
io.puts"#{tabs}\t\t<work-number>#{@work_number.to_i}</work-number>" if @work_number
|
|
@@ -80,9 +352,11 @@ module Musa
|
|
|
80
352
|
io.puts"#{tabs}\t</work>"
|
|
81
353
|
end
|
|
82
354
|
|
|
355
|
+
# Movement metadata (optional)
|
|
83
356
|
io.puts"#{tabs}\t<movement-number>#{@movement_number.to_i}</movement-number>" if @movement_number
|
|
84
357
|
io.puts"#{tabs}\t<movement-title>#{@movement_title}</movement-title>" if @movement_title
|
|
85
358
|
|
|
359
|
+
# Identification section (required)
|
|
86
360
|
io.puts "#{tabs}\t<identification>"
|
|
87
361
|
|
|
88
362
|
@creators.each do |creator|
|
|
@@ -100,12 +374,14 @@ module Musa
|
|
|
100
374
|
|
|
101
375
|
io.puts "#{tabs}\t</identification>"
|
|
102
376
|
|
|
377
|
+
# Part list section (required)
|
|
103
378
|
io.puts "#{tabs}\t<part-list>"
|
|
104
379
|
@groups_and_parts.each do |group_or_part|
|
|
105
380
|
group_or_part.header_to_xml(io, indent: indent + 2)
|
|
106
381
|
end
|
|
107
382
|
io.puts "#{tabs}\t</part-list>"
|
|
108
383
|
|
|
384
|
+
# Parts content (measures with notes)
|
|
109
385
|
@parts.each_value do |part|
|
|
110
386
|
part.to_xml(io, indent: indent + 1)
|
|
111
387
|
end
|
|
@@ -4,17 +4,46 @@ module Musa
|
|
|
4
4
|
module MusicXML
|
|
5
5
|
module Builder
|
|
6
6
|
module Internal
|
|
7
|
+
# Base class for typed text elements (creators, rights).
|
|
8
|
+
#
|
|
9
|
+
# TypedText represents MusicXML elements that have a type attribute and
|
|
10
|
+
# text content, such as `<creator type="composer">Name</creator>`.
|
|
11
|
+
#
|
|
12
|
+
# This is a private base class used by {Creator} and {Rights}.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
7
15
|
class TypedText
|
|
8
16
|
include Helper::ToXML
|
|
9
17
|
|
|
18
|
+
# Creates a typed text element.
|
|
19
|
+
#
|
|
20
|
+
# @param type [String, Symbol, nil] element type attribute
|
|
21
|
+
# @param text [String] text content
|
|
10
22
|
def initialize(type = nil, text)
|
|
11
23
|
@type = type
|
|
12
24
|
@text = text
|
|
13
25
|
end
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
# Type attribute value.
|
|
28
|
+
# @return [String, Symbol, nil]
|
|
29
|
+
attr_accessor :type
|
|
30
|
+
|
|
31
|
+
# Text content.
|
|
32
|
+
# @return [String]
|
|
33
|
+
attr_accessor :text
|
|
34
|
+
|
|
35
|
+
# XML tag name (set by subclasses).
|
|
36
|
+
# @return [String]
|
|
16
37
|
attr_reader :tag
|
|
17
38
|
|
|
39
|
+
# Generates XML for this typed text element.
|
|
40
|
+
#
|
|
41
|
+
# @param io [IO] output stream
|
|
42
|
+
# @param indent [Integer] indentation level
|
|
43
|
+
# @param tabs [String] tab string
|
|
44
|
+
# @return [void]
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
18
47
|
def _to_xml(io, indent:, tabs:)
|
|
19
48
|
io.puts "#{tabs}<#{tag}#{" type=\"#{@type}\"" if @type}>#{@text}</#{tag}>"
|
|
20
49
|
end
|
|
@@ -22,14 +51,46 @@ module Musa
|
|
|
22
51
|
|
|
23
52
|
private_constant :TypedText
|
|
24
53
|
|
|
54
|
+
# Creator metadata for MusicXML identification section.
|
|
55
|
+
#
|
|
56
|
+
# Represents a `<creator>` element specifying who created various aspects
|
|
57
|
+
# of the work (composer, lyricist, arranger, etc.).
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# creator = Creator.new(:composer, "Ludwig van Beethoven")
|
|
61
|
+
# creator.to_xml # => <creator type="composer">Ludwig van Beethoven</creator>
|
|
25
62
|
class Creator < TypedText
|
|
63
|
+
# Creates a creator entry.
|
|
64
|
+
#
|
|
65
|
+
# @param type [String, Symbol] creator type (e.g., :composer, :lyricist, :arranger)
|
|
66
|
+
# @param name [String] creator's name
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# Creator.new(:composer, "Mozart")
|
|
70
|
+
# Creator.new(:lyricist, "Da Ponte")
|
|
26
71
|
def initialize(type, name)
|
|
27
72
|
@tag = 'creator'
|
|
28
73
|
super type, name
|
|
29
74
|
end
|
|
30
75
|
end
|
|
31
76
|
|
|
77
|
+
# Rights metadata for MusicXML identification section.
|
|
78
|
+
#
|
|
79
|
+
# Represents a `<rights>` element specifying copyright, licensing, or
|
|
80
|
+
# attribution information.
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# rights = Rights.new(:lyrics, "Copyright 2024 Publisher Name")
|
|
84
|
+
# rights.to_xml # => <rights type="lyrics">Copyright 2024 Publisher Name</rights>
|
|
32
85
|
class Rights < TypedText
|
|
86
|
+
# Creates a rights entry.
|
|
87
|
+
#
|
|
88
|
+
# @param type [String, Symbol] rights type (e.g., :lyrics, :music, :arrangement)
|
|
89
|
+
# @param name [String] rights statement or holder name
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# Rights.new(:music, "Copyright 2024 ACME Publishing")
|
|
93
|
+
# Rights.new(:lyrics, "Public Domain")
|
|
33
94
|
def initialize(type, name)
|
|
34
95
|
@tag = 'rights'
|
|
35
96
|
super type, name
|