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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. 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
- attr_accessor :type, :text
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