musa-dsl 0.30.2 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/Gemfile +0 -1
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +544 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +215 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
- data/lib/musa-dsl/generative/markov.rb +135 -3
- data/lib/musa-dsl/generative/rules.rb +312 -4
- data/lib/musa-dsl/generative/variatio.rb +286 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
- data/lib/musa-dsl/midi/midi-voices.rb +275 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +353 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +1384 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +54 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
- data/lib/musa-dsl/series/queue-serie.rb +78 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +2 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +25 -18
- metadata +158 -16
|
@@ -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
|
|
data/lib/musa-dsl/datasets/v.rb
CHANGED
|
@@ -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
|