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
|
@@ -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
|
|
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
|