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
@@ -6,11 +6,137 @@ require_relative 'process-time'
6
6
  require_relative 'process-pdv'
7
7
  require_relative 'process-ps'
8
8
 
9
- module Musa::Datasets
9
+ module Musa::Datasets
10
10
  class Score
11
+ # MusicXML export for scores.
12
+ #
13
+ # ToMXML provides conversion of {Score} objects to MusicXML format,
14
+ # suitable for import into notation software like MuseScore, Finale,
15
+ # or Sibelius.
16
+ #
17
+ # ## Conversion Process
18
+ #
19
+ # 1. Creates MusicXML structure with metadata (title, creators, etc.)
20
+ # 2. Defines parts (instruments) with clefs and time signatures
21
+ # 3. Divides score into measures (bars)
22
+ # 4. Processes events in each measure:
23
+ #
24
+ # - {PDV} events → notes and rests
25
+ # - {PS} events → dynamics markings (crescendo, diminuendo)
26
+ #
27
+ # 5. Fills gaps with rests
28
+ #
29
+ # ## Event Types Supported
30
+ #
31
+ # - **{PDV}** (Pitch/Duration/Velocity): Converted to notes or rests
32
+ # - **{PS}** (Pitch Series): Converted to dynamics markings
33
+ #
34
+ # ## Multi-part Scores
35
+ #
36
+ # Scores can contain multiple instruments, differentiated by the
37
+ # :instrument attribute. Each part is rendered separately.
38
+ #
39
+ # ## Time Representation
40
+ #
41
+ # - Score times are 1-based (first beat is at position 1)
42
+ # - Each measure represents one bar
43
+ # - Duration is specified in beats (1.0 = quarter note if beat_type is 4)
44
+ #
45
+ # @example Basic single-part score
46
+ # score = Musa::Datasets::Score.new
47
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
48
+ # score.at(2r, add: { pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
49
+ #
50
+ # mxml = score.to_mxml(
51
+ # 4, 24, # 4 beats per bar, 24 ticks per beat
52
+ # bpm: 120,
53
+ # title: 'My Song',
54
+ # creators: { composer: 'John Doe' },
55
+ # parts: { piano: { name: 'Piano', clefs: { g: 2 } } }
56
+ # )
57
+ #
58
+ # File.write('score.musicxml', mxml.to_xml.string)
59
+ #
60
+ # @example Multi-part score
61
+ # score = Musa::Datasets::Score.new
62
+ # score.at(1r, add: { instrument: :violin, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
63
+ # score.at(1r, add: { instrument: :cello, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))
64
+ #
65
+ # mxml = score.to_mxml(
66
+ # 4, 24,
67
+ # parts: {
68
+ # violin: { name: 'Violin', clefs: { g: 2 } },
69
+ # cello: { name: 'Cello', clefs: { f: 4 } }
70
+ # }
71
+ # )
72
+ #
73
+ # @example With dynamics
74
+ # score = Musa::Datasets::Score.new
75
+ # score.at(1r, add: { pitch: 60, duration: 2.0 }.extend(Musa::Datasets::PDV))
76
+ # score.at(1r, add: { type: :crescendo, duration: 2.0 }.extend(Musa::Datasets::PS))
77
+ #
78
+ # @see Musa::MusicXML::Builder MusicXML builder
79
+ # @see PDV MIDI-style events
80
+ # @see PS Pitch series for dynamics
11
81
  module ToMXML
12
82
  using Musa::Extension::InspectNice
13
83
 
84
+ # Converts score to MusicXML.
85
+ #
86
+ # Creates complete MusicXML document with metadata, parts, measures,
87
+ # notes, rests, and dynamics markings.
88
+ #
89
+ # @param beats_per_bar [Integer] time signature numerator (e.g., 4 for 4/4)
90
+ # @param ticks_per_beat [Integer] resolution per beat (typically 24)
91
+ #
92
+ # @param bpm [Integer] tempo in beats per minute (default: 90)
93
+ # @param title [String] work title (default: 'Untitled')
94
+ # @param creators [Hash{Symbol => String}] creator roles and names
95
+ # (default: { composer: 'Unknown' })
96
+ # @param encoding_date [DateTime, nil] encoding date for metadata
97
+ # @param parts [Hash{Symbol => Hash}] part definitions
98
+ # Each part: { name: String, abbreviation: String, clefs: Hash }
99
+ # Clefs: { clef_sign: line_number } (e.g., { g: 2, f: 4 } for piano)
100
+ # @param logger [Musa::Logger::Logger, nil] logger for debugging
101
+ # @param do_log [Boolean, nil] enable logging output
102
+ #
103
+ # @return [Musa::MusicXML::Builder::ScorePartwise] MusicXML document
104
+ #
105
+ # @example Simple piano score
106
+ # score = Musa::Datasets::Score.new
107
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
108
+ #
109
+ # mxml = score.to_mxml(
110
+ # 4, 24,
111
+ # bpm: 120,
112
+ # title: 'Invention',
113
+ # creators: { composer: 'J.S. Bach' },
114
+ # parts: { piano: { name: 'Piano', clefs: { g: 2, f: 4 } } }
115
+ # )
116
+ #
117
+ # @example String quartet
118
+ # score = Musa::Datasets::Score.new
119
+ # score.at(1r, add: { instrument: :vln1, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
120
+ # score.at(1r, add: { instrument: :vln2, pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
121
+ # score.at(1r, add: { instrument: :vla, pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
122
+ # score.at(1r, add: { instrument: :vc, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))
123
+ #
124
+ # mxml = score.to_mxml(
125
+ # 4, 24,
126
+ # parts: {
127
+ # vln1: { name: 'Violin I', abbreviation: 'Vln. I', clefs: { g: 2 } },
128
+ # vln2: { name: 'Violin II', abbreviation: 'Vln. II', clefs: { g: 2 } },
129
+ # vla: { name: 'Viola', abbreviation: 'Vla.', clefs: { c: 3 } },
130
+ # vc: { name: 'Cello', abbreviation: 'Vc.', clefs: { f: 4 } }
131
+ # }
132
+ # )
133
+ #
134
+ # @example Export to file
135
+ # score = Musa::Datasets::Score.new
136
+ # score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
137
+ #
138
+ # mxml = score.to_mxml(4, 24, parts: { piano: { name: 'Piano' } })
139
+ # File.write('output.musicxml', mxml.to_xml.string)
14
140
  def to_mxml(beats_per_bar, ticks_per_beat,
15
141
  bpm: nil,
16
142
  title: nil,
@@ -77,6 +203,24 @@ module Musa::Datasets
77
203
 
78
204
  private
79
205
 
206
+ # Fills a MusicXML part with measures and events.
207
+ #
208
+ # Processes each bar (measure) in the score, converting events to
209
+ # MusicXML notes, rests, and dynamics. Handles:
210
+ #
211
+ # - Initial silences (gaps before first event)
212
+ # - Event processing (PDV → notes, PS → dynamics)
213
+ # - Ending silences (filling remainder of measure)
214
+ #
215
+ # @param part [Musa::MusicXML::Builder::Part] MusicXML part to fill
216
+ # @param divisions_per_bar [Integer] total divisions in one bar
217
+ # @param instrument [Symbol, nil] instrument filter (nil for single-part scores)
218
+ # @param logger [Musa::Logger::Logger] logger for debugging
219
+ # @param do_log [Boolean] enable logging
220
+ #
221
+ # @return [void]
222
+ #
223
+ # @api private
80
224
  def fill_part(part, divisions_per_bar, instrument, logger, do_log)
81
225
  measure = nil
82
226
  dynamics_context = nil
@@ -7,6 +7,78 @@ require_relative 'score/render'
7
7
  require_relative '../core-ext/inspect-nice'
8
8
 
9
9
  module Musa::Datasets
10
+ # Time-indexed container for musical events.
11
+ #
12
+ # Score organizes musical events along a timeline, storing them at specific
13
+ # time points and providing efficient queries for time intervals.
14
+ # Implements `Enumerable` for iteration over time slots.
15
+ #
16
+ # ## Purpose
17
+ #
18
+ # Score provides:
19
+ #
20
+ # - **Time-indexed storage**: Events organized by start time (Rational)
21
+ # - **Interval queries**: Find events in time ranges ({#between}, {#changes_between})
22
+ # - **Duration tracking**: Automatically tracks event durations
23
+ # - **Export formats**: MusicXML export via {ToMXML}
24
+ # - **Rendering**: MIDI rendering via {Render}
25
+ # - **Filtering**: Create subsets via {#subset}
26
+ #
27
+ # ## Structure
28
+ #
29
+ # Internally maintains two structures:
30
+ #
31
+ # - **@score**: Hash mapping time → Array of events
32
+ # - **@indexer**: Array of { start, finish, dataset } for interval queries
33
+ #
34
+ # ## Event Requirements
35
+ #
36
+ # Events must:
37
+ #
38
+ # - Extend {Abs} (absolute values, not deltas)
39
+ # - Have a :duration key (from {AbsD})
40
+ #
41
+ # ## Time Representation
42
+ #
43
+ # All times are stored as Rational numbers for exact arithmetic:
44
+ #
45
+ # score.at(0r, add: event) # At time 0
46
+ # score.at(1/4r, add: event) # At quarter note
47
+ #
48
+ # @example Create empty score
49
+ # score = Musa::Datasets::Score.new
50
+ #
51
+ # @example Create from hash
52
+ # score = Score.new({
53
+ # 0r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
54
+ # 1r => [{ pitch: 64, duration: 1.0 }.extend(PDV)]
55
+ # })
56
+ #
57
+ # @example Add events
58
+ # score = Score.new
59
+ # gdv1 = { grade: 0, duration: 1.0 }.extend(GDV)
60
+ # gdv2 = { grade: 2, duration: 1.0 }.extend(GDV)
61
+ # score.at(0r, add: gdv1)
62
+ # score.at(1r, add: gdv2)
63
+ #
64
+ # @example Query time interval
65
+ # events = score.between(0r, 2r)
66
+ # # Returns all events starting in [0, 2) or overlapping interval
67
+ #
68
+ # @example Filter events
69
+ # high_notes = score.subset { |event| event[:pitch] > 60 }
70
+ #
71
+ # @example Get all positions
72
+ # score.positions # => [0r, 1r, 2r, ...]
73
+ #
74
+ # @example Get duration
75
+ # score.duration # => Latest finish time - 1r
76
+ #
77
+ # @see Abs Absolute events (required for score)
78
+ # @see AbsD Duration events (provides :duration)
79
+ # @see ToMXML MusicXML export
80
+ # @see Render MIDI rendering
81
+ # @see Queriable Query capabilities
10
82
  class Score
11
83
  include Enumerable
12
84
 
@@ -19,6 +91,21 @@ module Musa::Datasets
19
91
 
20
92
  using Musa::Extension::InspectNice
21
93
 
94
+ # Creates new score.
95
+ #
96
+ # @param hash [Hash{Rational => Array<Abs>}, nil] optional initial events
97
+ # Hash mapping times to arrays of events
98
+ #
99
+ # @raise [ArgumentError] if hash values aren't Arrays
100
+ #
101
+ # @example Empty score
102
+ # score = Score.new
103
+ #
104
+ # @example With initial events
105
+ # score = Score.new({
106
+ # 0r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
107
+ # 1r => [{ pitch: 64, duration: 1.0 }.extend(PDV)]
108
+ # })
22
109
  def initialize(hash = nil)
23
110
  raise ArgumentError, "'hash' parameter should be a Hash with time and events information" unless hash.nil? || hash.is_a?(Hash)
24
111
 
@@ -36,25 +123,79 @@ module Musa::Datasets
36
123
  end
37
124
  end
38
125
 
126
+ # Clears all events from score.
127
+ #
128
+ # @return [void]
129
+ #
130
+ # @example
131
+ # score.reset
132
+ # score.size # => 0
39
133
  def reset
40
134
  @score.clear
41
135
  @indexer.clear
42
136
  end
43
137
 
138
+ # Gets attribute value.
139
+ #
140
+ # Supports accessing natural keys like :duration, :finish.
141
+ #
142
+ # @param key [Symbol] attribute name
143
+ # @return [Object, nil] attribute value
144
+ #
145
+ # @api private
44
146
  def [](key)
45
147
  if NaturalKeys.include?(key) && self.respond_to?(key)
46
148
  self.send(key)
47
149
  end
48
150
  end
49
151
 
152
+ # Returns latest finish time of all events.
153
+ #
154
+ # @return [Rational, nil] latest finish time, or nil if score is empty
155
+ #
156
+ # @example
157
+ # score.at(0r, add: { duration: 2.0 }.extend(AbsD))
158
+ # score.finish # => 2r
50
159
  def finish
51
160
  @indexer.collect { |i| i[:finish] }.max
52
161
  end
53
162
 
163
+ # Returns total duration of score.
164
+ #
165
+ # Calculated as finish time minus 1.
166
+ #
167
+ # @return [Rational] total duration
168
+ #
169
+ # @example
170
+ # score.at(0r, add: { duration: 2.0 }.extend(AbsD))
171
+ # score.duration # => 1r (finish 2r - 1r)
54
172
  def duration
55
173
  (finish || 1r) - 1r
56
174
  end
57
175
 
176
+ # Adds event at time or gets time slot.
177
+ #
178
+ # Without add parameter, returns array of events at that time.
179
+ # With add parameter, adds event to that time slot.
180
+ #
181
+ # @param time [Numeric] time position (converted to Rational)
182
+ # @param add [Abs, nil] event to add (must extend {Abs} and have :duration)
183
+ #
184
+ # @return [Array<Abs>, nil] time slot if no add, nil if adding
185
+ #
186
+ # @raise [ArgumentError] if add is not an Abs dataset
187
+ #
188
+ # @example Add event
189
+ # gdv = { grade: 0, duration: 1.0 }.extend(GDV)
190
+ # score.at(0r, add: gdv)
191
+ #
192
+ # @example Get time slot
193
+ # events = score.at(0r) # => Array of events at time 0
194
+ #
195
+ # @example Multiple events at same time (chord)
196
+ # score.at(0r, add: { pitch: 60, duration: 1.0 }.extend(PDV))
197
+ # score.at(0r, add: { pitch: 64, duration: 1.0 }.extend(PDV))
198
+ # score.at(0r).size # => 2
58
199
  def at(time, add: nil)
59
200
  time = time.rationalize
60
201
 
@@ -75,22 +216,88 @@ module Musa::Datasets
75
216
  end
76
217
  end
77
218
 
219
+ # Returns number of time positions.
220
+ #
221
+ # @return [Integer] number of distinct time positions
222
+ #
223
+ # @example
224
+ # score.at(0r, add: event1)
225
+ # score.at(0r, add: event2) # Same time
226
+ # score.at(1r, add: event3) # Different time
227
+ # score.size # => 2 (two time positions)
78
228
  def size
79
229
  @score.keys.size
80
230
  end
81
231
 
232
+ # Returns all time positions sorted.
233
+ #
234
+ # @return [Array<Rational>] sorted time positions
235
+ #
236
+ # @example
237
+ # score.at(1r, add: event1)
238
+ # score.at(0r, add: event2)
239
+ # score.positions # => [0r, 1r]
82
240
  def positions
83
241
  @score.keys.sort
84
242
  end
85
243
 
244
+ # Iterates over time slots in order.
245
+ #
246
+ # Yields [time, events] pairs sorted by time.
247
+ # Implements `Enumerable`.
248
+ #
249
+ # @yieldparam time [Rational] time position
250
+ # @yieldparam events [Array<Abs>] events at that time
251
+ #
252
+ # @return [void]
253
+ #
254
+ # @example
255
+ # score.each do |time, events|
256
+ # puts "At #{time}: #{events.size} event(s)"
257
+ # end
86
258
  def each(&block)
87
259
  @score.sort.each(&block)
88
260
  end
89
261
 
262
+ # Converts to hash representation.
263
+ #
264
+ # @return [Hash{Rational => Array<Abs>}] time → events mapping
265
+ #
266
+ # @example
267
+ # hash = score.to_h
268
+ # # => { 0r => [event1, event2], 1r => [event3] }
90
269
  def to_h
91
270
  @score.sort.to_h
92
271
  end
93
272
 
273
+ # Queries events overlapping time interval.
274
+ #
275
+ # Returns events that are active (playing) during the interval [start, finish).
276
+ # Interval uses closed start (included) and open finish (excluded).
277
+ #
278
+ # Events are included if they:
279
+ # - Start before interval finish AND finish after interval start
280
+ # - OR are instant events (start == finish) at interval instant
281
+ #
282
+ # @param closed_interval_start [Rational] interval start (included)
283
+ # @param open_interval_finish [Rational] interval finish (excluded)
284
+ #
285
+ # @return [Array<Hash>] array of event info hashes with:
286
+ # - **:start**: Event start time
287
+ # - **:finish**: Event finish time
288
+ # - **:start_in_interval**: Effective start within interval
289
+ # - **:finish_in_interval**: Effective finish within interval
290
+ # - **:dataset**: The event dataset
291
+ #
292
+ # @example Query bar
293
+ # events = score.between(0r, 4r)
294
+ # # Returns all events overlapping [0, 4)
295
+ #
296
+ # @example Long note spans interval
297
+ # score.at(0r, add: { duration: 10.0 }.extend(AbsD))
298
+ # events = score.between(2r, 4r)
299
+ # # Event included (started before 4, finishes after 2)
300
+ # # start_in_interval: 2r, finish_in_interval: 4r
94
301
  def between(closed_interval_start, open_interval_finish)
95
302
  @indexer
96
303
  .select { |i| i[:start] < open_interval_finish && i[:finish] > closed_interval_start ||
@@ -107,6 +314,37 @@ module Musa::Datasets
107
314
 
108
315
  # TODO hay que implementar un effective_start y effective_finish con el inicio/fin dentro del bar, no absoluto
109
316
 
317
+ # Queries start/finish change events in interval.
318
+ #
319
+ # Returns timeline of note-on/note-off style events for the interval.
320
+ # Useful for real-time rendering or event-based processing.
321
+ #
322
+ # Returns events sorted by time, with :finish events before :start
323
+ # events at the same time (to avoid gaps).
324
+ #
325
+ # @param closed_interval_start [Rational] interval start (included)
326
+ # @param open_interval_finish [Rational] interval finish (excluded)
327
+ #
328
+ # @return [Array<Hash>] array of change event hashes with:
329
+ # - **:change**: :start or :finish
330
+ # - **:time**: When change occurs
331
+ # - **:start**: Event start time
332
+ # - **:finish**: Event finish time
333
+ # - **:start_in_interval**: Effective start within interval
334
+ # - **:finish_in_interval**: Effective finish within interval
335
+ # - **:time_in_interval**: Effective change time within interval
336
+ # - **:dataset**: The event dataset
337
+ #
338
+ # @example Get all changes in bar
339
+ # changes = score.changes_between(0r, 4r)
340
+ # changes.each do |change|
341
+ # case change[:change]
342
+ # when :start
343
+ # puts "Note ON at #{change[:time]}"
344
+ # when :finish
345
+ # puts "Note OFF at #{change[:time]}"
346
+ # end
347
+ # end
110
348
  def changes_between(closed_interval_start, open_interval_finish)
111
349
  (
112
350
  #
@@ -161,6 +399,21 @@ module Musa::Datasets
161
399
  dataset: i[:dataset] } }.extend(QueryableByDataset)
162
400
  end
163
401
 
402
+ # Collects all values for an attribute.
403
+ #
404
+ # Returns set of all unique values across all events.
405
+ #
406
+ # @param attribute [Symbol] attribute key
407
+ #
408
+ # @return [Set] set of unique values
409
+ #
410
+ # @example Get all pitches
411
+ # pitches = score.values_of(:pitch)
412
+ # # => #<Set: {60, 64, 67}>
413
+ #
414
+ # @example Get all grades
415
+ # grades = score.values_of(:grade)
416
+ # # => #<Set: {0, 2, 4}>
164
417
  def values_of(attribute)
165
418
  values = Set[]
166
419
  @score.each_value do |slot|
@@ -169,6 +422,25 @@ module Musa::Datasets
169
422
  values
170
423
  end
171
424
 
425
+ # Creates filtered subset of score.
426
+ #
427
+ # Returns new Score containing only events matching the condition.
428
+ #
429
+ # @yieldparam dataset [Abs] each event dataset
430
+ # @yieldreturn [Boolean] true to include event
431
+ #
432
+ # @return [Score] new filtered score
433
+ #
434
+ # @raise [ArgumentError] if no block given
435
+ #
436
+ # @example Filter by pitch
437
+ # high_notes = score.subset { |event| event[:pitch] > 60 }
438
+ #
439
+ # @example Filter by attribute presence
440
+ # staccato_notes = score.subset { |event| event[:staccato] }
441
+ #
442
+ # @example Filter by grade
443
+ # tonic_notes = score.subset { |event| event[:grade] == 0 }
172
444
  def subset
173
445
  raise ArgumentError, "subset needs a block with the inclusion condition on the dataset" unless block_given?
174
446
 
@@ -183,6 +455,13 @@ module Musa::Datasets
183
455
  filtered_score
184
456
  end
185
457
 
458
+ # Returns formatted string representation.
459
+ #
460
+ # Produces multiline representation suitable for inspection.
461
+ #
462
+ # @return [String] formatted score representation
463
+ #
464
+ # @api private
186
465
  def inspect
187
466
  s = StringIO.new
188
467
 
@@ -2,9 +2,97 @@ require_relative 'dataset'
2
2
  require_relative 'packed-v'
3
3
 
4
4
  module Musa::Datasets
5
+ # Array-based dataset with named key conversion.
6
+ #
7
+ # V (Value) represents datasets stored as arrays (indexed values).
8
+ # Extends {AbsI} for absolute indexed events.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # V provides efficient array-based storage for ordered values and conversion
13
+ # to named key-value pairs ({PackedV}). This is useful for:
14
+ #
15
+ # - Compact storage of sequential values
16
+ # - Converting between array and hash representations
17
+ # - Filtering default values during conversion
18
+ #
19
+ # ## Conversion to PackedV
20
+ #
21
+ # The {#to_packed_V} method converts arrays to hashes using a mapper that
22
+ # defines the correspondence between array indices and hash keys.
23
+ #
24
+ # ### Array Mapper
25
+ #
26
+ # Array mapper defines key names for each position. Position i maps to mapper[i].
27
+ #
28
+ # v = [3, 2, 1].extend(Musa::Datasets::V)
29
+ # pv = v.to_packed_V([:c, :b, :a])
30
+ # # => { c: 3, b: 2, a: 1 }
31
+ #
32
+ # - `nil` mapper entries skip that position
33
+ # - `nil` values skip that position
34
+ #
35
+ # ### Hash Mapper
36
+ #
37
+ # Hash mapper defines both key names (keys) and default values (values).
38
+ # Position i maps to key mapper.keys[i] with default mapper.values[i].
39
+ #
40
+ # v = [3, 2, 1, 400].extend(Musa::Datasets::V)
41
+ # pv = v.to_packed_V({ c: 100, b: 200, a: 300, d: 400 })
42
+ # # => { c: 3, b: 2, a: 1 }
43
+ # # d: 400 omitted because it equals default
44
+ #
45
+ # Values matching their defaults are omitted for compression.
46
+ #
47
+ # @example Basic array to hash conversion
48
+ # v = [60, 1.0, 64].extend(Musa::Datasets::V)
49
+ # pv = v.to_packed_V([:pitch, :duration, :velocity])
50
+ # # => { pitch: 60, duration: 1.0, velocity: 64 }
51
+ #
52
+ # @example With nil mapper (skip position)
53
+ # v = [3, 2, 1].extend(Musa::Datasets::V)
54
+ # pv = v.to_packed_V([:c, nil, :a])
55
+ # # => { c: 3, a: 1 }
56
+ # # Position 1 (value 2) skipped
57
+ #
58
+ # @example With nil value (skip position)
59
+ # v = [3, nil, 1].extend(Musa::Datasets::V)
60
+ # pv = v.to_packed_V([:c, :b, :a])
61
+ # # => { c: 3, a: 1 }
62
+ # # Position 1 (nil value) skipped
63
+ #
64
+ # @example Hash mapper with defaults (compression)
65
+ # v = [3, 2, 1, 400].extend(Musa::Datasets::V)
66
+ # pv = v.to_packed_V({ c: 100, b: 200, a: 300, d: 400 })
67
+ # # => { c: 3, b: 2, a: 1 }
68
+ # # d omitted because value 400 equals default 400
69
+ #
70
+ # @example Partial mapper (fewer keys than values)
71
+ # v = [3, 2, 1].extend(Musa::Datasets::V)
72
+ # pv = v.to_packed_V([:c, :b])
73
+ # # => { c: 3, b: 2 }
74
+ # # Position 2 (value 1) skipped - no mapper
75
+ #
76
+ # @see PackedV Hash-based dataset (inverse)
77
+ # @see AbsI Parent absolute indexed module
5
78
  module V
6
79
  include AbsI
7
80
 
81
+ # Converts array to packed hash (PackedV).
82
+ #
83
+ # @param mapper [Array<Symbol>, Hash{Symbol => Object}] key mapping
84
+ # - Array: maps indices to keys (nil skips)
85
+ # - Hash: maps indices to keys (keys) with defaults (values)
86
+ #
87
+ # @return [PackedV] packed hash dataset
88
+ #
89
+ # @raise [ArgumentError] if mapper is not Array or Hash
90
+ #
91
+ # @example Array mapper
92
+ # v.to_packed_V([:pitch, :duration])
93
+ #
94
+ # @example Hash mapper with defaults
95
+ # v.to_packed_V({ pitch: 60, duration: 1.0 })
8
96
  def to_packed_V(mapper)
9
97
  case mapper
10
98
  when Hash