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
|
@@ -5,6 +5,117 @@ require_relative 'helper'
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
module Musa::Datasets
|
|
8
|
+
# Delta-encoded score events for efficient compression.
|
|
9
|
+
#
|
|
10
|
+
# GDVd (Grade/Duration/Velocity delta) represents musical events using
|
|
11
|
+
# delta encoding - storing only changes from previous events.
|
|
12
|
+
# Extends {DeltaD} for flexible duration encoding and {DeltaI} for indexed deltas.
|
|
13
|
+
#
|
|
14
|
+
# ## Purpose
|
|
15
|
+
#
|
|
16
|
+
# GDVd provides efficient delta encoding for musical sequences:
|
|
17
|
+
#
|
|
18
|
+
# - **Compact storage**: Only changed values are stored
|
|
19
|
+
# - **Efficient serialization**: Neuma format uses delta notation
|
|
20
|
+
# - **Lossless compression**: Full reconstruction via {#to_gdv}
|
|
21
|
+
# - **Musical patterns**: Captures relative motion (intervals, velocity changes)
|
|
22
|
+
#
|
|
23
|
+
# ## Encoding Types
|
|
24
|
+
#
|
|
25
|
+
# Each parameter can be encoded as absolute or delta:
|
|
26
|
+
#
|
|
27
|
+
# ### Pitch Encoding
|
|
28
|
+
#
|
|
29
|
+
# **Absolute**:
|
|
30
|
+
#
|
|
31
|
+
# - **abs_grade**: Set grade to specific value
|
|
32
|
+
# - **abs_sharps**: Set chromatic alteration
|
|
33
|
+
# - **abs_octave**: Set octave to specific value
|
|
34
|
+
#
|
|
35
|
+
# **Delta**:
|
|
36
|
+
#
|
|
37
|
+
# - **delta_grade**: Change grade by semitones
|
|
38
|
+
# - **delta_sharps**: Change chromatic alteration
|
|
39
|
+
# - **delta_interval**: Change by scale interval (with delta_interval_sign)
|
|
40
|
+
# - **delta_octave**: Change octave
|
|
41
|
+
#
|
|
42
|
+
# ### Duration Encoding (from {DeltaD})
|
|
43
|
+
#
|
|
44
|
+
# - **abs_duration**: Set duration
|
|
45
|
+
# - **delta_duration**: Add to duration
|
|
46
|
+
# - **factor_duration**: Multiply duration
|
|
47
|
+
#
|
|
48
|
+
# ### Velocity Encoding
|
|
49
|
+
#
|
|
50
|
+
# - **abs_velocity**: Set velocity
|
|
51
|
+
# - **delta_velocity**: Add to velocity
|
|
52
|
+
#
|
|
53
|
+
# ## Natural Keys
|
|
54
|
+
#
|
|
55
|
+
# - **:abs_grade**, **:abs_sharps**, **:abs_octave**: Absolute pitch
|
|
56
|
+
# - **:delta_grade**, **:delta_sharps**, **:delta_interval**, **:delta_interval_sign**, **:delta_octave**: Delta pitch
|
|
57
|
+
# - **:abs_velocity**, **:delta_velocity**: Velocity encoding
|
|
58
|
+
# - **:abs_duration**, **:delta_duration**, **:factor_duration**: Duration encoding
|
|
59
|
+
# - **:modifiers**: Hash of additional modifiers
|
|
60
|
+
#
|
|
61
|
+
# ## Reconstruction
|
|
62
|
+
#
|
|
63
|
+
# Delta events require a previous event for reconstruction:
|
|
64
|
+
#
|
|
65
|
+
# gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
|
|
66
|
+
# previous = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
67
|
+
# gdv = gdvd.to_gdv(scale, previous: previous)
|
|
68
|
+
# # => { grade: 2, octave: 0, duration: 1.0, velocity: 1 }
|
|
69
|
+
#
|
|
70
|
+
# ## Neuma Delta Notation
|
|
71
|
+
#
|
|
72
|
+
# Delta events use special notation in Neuma format:
|
|
73
|
+
#
|
|
74
|
+
# - **Delta grade**: "+2" or "-2" (semitone change)
|
|
75
|
+
# - **Delta sharps**: "+#" or "-_" (chromatic change)
|
|
76
|
+
# - **Delta octave**: "+o1" or "-o1" (octave change)
|
|
77
|
+
# - **Delta duration**: "+0.5" or "-0.5" (duration change)
|
|
78
|
+
# - **Factor duration**: "*2" or "*0.5" (duration multiply)
|
|
79
|
+
# - **Delta velocity**: "+f" or "-f" (dynamics change)
|
|
80
|
+
#
|
|
81
|
+
# @example First event (absolute encoding)
|
|
82
|
+
# gdvd = { abs_grade: 0, abs_duration: 1.0, abs_velocity: 0 }.extend(GDVd)
|
|
83
|
+
# gdvd.base_duration = 1/4r
|
|
84
|
+
# gdvd.to_neuma # => "(0 4 mp)"
|
|
85
|
+
#
|
|
86
|
+
# @example Delta encoding (unchanged duration)
|
|
87
|
+
# gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
|
|
88
|
+
# gdvd.base_duration = 1/4r
|
|
89
|
+
# gdvd.to_neuma # => "(+2 +f)"
|
|
90
|
+
# # Grade +2 semitones, velocity +1 (one f louder)
|
|
91
|
+
#
|
|
92
|
+
# @example Chromatic change
|
|
93
|
+
# gdvd = { delta_sharps: 1 }.extend(GDVd)
|
|
94
|
+
# gdvd.to_neuma # => "(+#)"
|
|
95
|
+
# # Add one sharp
|
|
96
|
+
#
|
|
97
|
+
# @example Duration multiplication
|
|
98
|
+
# gdvd = { factor_duration: 2 }.extend(GDVd)
|
|
99
|
+
# gdvd.base_duration = 1/4r
|
|
100
|
+
# gdvd.to_neuma # => "(. *2)"
|
|
101
|
+
# # Double duration
|
|
102
|
+
#
|
|
103
|
+
# @example Reconstruction from delta
|
|
104
|
+
# previous = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
105
|
+
# gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
|
|
106
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
107
|
+
# gdv = gdvd.to_gdv(scale, previous: previous)
|
|
108
|
+
# # => { grade: 2, octave: 0, duration: 1.0, velocity: 1 }
|
|
109
|
+
#
|
|
110
|
+
# @example Octave change
|
|
111
|
+
# gdvd = { delta_grade: -2, delta_octave: 1 }.extend(GDVd)
|
|
112
|
+
# gdvd.to_neuma # => "(-2 +o1)"
|
|
113
|
+
# # Down 2 semitones, up one octave
|
|
114
|
+
#
|
|
115
|
+
# @see GDV Absolute score notation
|
|
116
|
+
# @see DeltaD Delta duration encoding
|
|
117
|
+
# @see DeltaI Delta indexed encoding
|
|
118
|
+
# @see Helper String formatting utilities
|
|
8
119
|
module GDVd
|
|
9
120
|
include DeltaD
|
|
10
121
|
include DeltaI
|
|
@@ -13,14 +124,28 @@ module Musa::Datasets
|
|
|
13
124
|
|
|
14
125
|
using Musa::Extension::InspectNice
|
|
15
126
|
|
|
127
|
+
# Natural keys for delta encoding.
|
|
128
|
+
# @return [Array<Symbol>]
|
|
16
129
|
NaturalKeys = (NaturalKeys +
|
|
17
130
|
[:abs_grade, :abs_sharps, :abs_octave,
|
|
18
131
|
:delta_grade, :delta_sharps, :delta_interval_sign, :delta_interval, :delta_octave,
|
|
19
132
|
:abs_velocity, :delta_velocity,
|
|
20
133
|
:modifiers]).freeze
|
|
21
134
|
|
|
135
|
+
# Base duration for time calculations.
|
|
136
|
+
# @return [Rational]
|
|
22
137
|
attr_reader :base_duration
|
|
23
138
|
|
|
139
|
+
# Sets base duration, adjusting existing duration values.
|
|
140
|
+
#
|
|
141
|
+
# When base_duration changes, existing abs_duration and delta_duration
|
|
142
|
+
# are scaled proportionally to maintain actual time values.
|
|
143
|
+
#
|
|
144
|
+
# @param value [Rational] new base duration
|
|
145
|
+
#
|
|
146
|
+
# @example
|
|
147
|
+
# gdvd[:abs_duration] = 1.0
|
|
148
|
+
# gdvd.base_duration = 1/4r # abs_duration scaled by factor
|
|
24
149
|
def base_duration=(value)
|
|
25
150
|
factor = value / (@base_duration || 1)
|
|
26
151
|
@base_duration = value
|
|
@@ -29,6 +154,33 @@ module Musa::Datasets
|
|
|
29
154
|
self[:delta_duration] *= factor if has_key?(:delta_duration)
|
|
30
155
|
end
|
|
31
156
|
|
|
157
|
+
# Reconstructs absolute GDV from delta encoding.
|
|
158
|
+
#
|
|
159
|
+
# Applies delta changes to previous event to create new absolute event.
|
|
160
|
+
# Handles all encoding types (abs_, delta_, factor_) appropriately.
|
|
161
|
+
#
|
|
162
|
+
# @param scale [Musa::Scales::Scale] reference scale for pitch calculations
|
|
163
|
+
# @param previous [GDV] previous absolute event (required for reconstruction)
|
|
164
|
+
#
|
|
165
|
+
# @return [GDV] reconstructed absolute event
|
|
166
|
+
#
|
|
167
|
+
# @example Basic delta reconstruction
|
|
168
|
+
# previous = { grade: 0, octave: 0, duration: 1.0, velocity: 0 }.extend(GDV)
|
|
169
|
+
# gdvd = { delta_grade: 2, delta_velocity: 1 }.extend(GDVd)
|
|
170
|
+
# gdv = gdvd.to_gdv(scale, previous: previous)
|
|
171
|
+
# # => { grade: 2, octave: 0, duration: 1.0, velocity: 1 }
|
|
172
|
+
#
|
|
173
|
+
# @example Absolute override
|
|
174
|
+
# previous = { grade: 0, duration: 1.0 }.extend(GDV)
|
|
175
|
+
# gdvd = { abs_grade: 5, abs_duration: 2.0 }.extend(GDVd)
|
|
176
|
+
# gdv = gdvd.to_gdv(scale, previous: previous)
|
|
177
|
+
# # => { grade: 5, duration: 2.0 }
|
|
178
|
+
#
|
|
179
|
+
# @example Duration factor
|
|
180
|
+
# previous = { grade: 0, duration: 1.0 }.extend(GDV)
|
|
181
|
+
# gdvd = { factor_duration: 2 }.extend(GDVd)
|
|
182
|
+
# gdv = gdvd.to_gdv(scale, previous: previous)
|
|
183
|
+
# # => { grade: 0, duration: 2.0 }
|
|
32
184
|
def to_gdv(scale, previous:)
|
|
33
185
|
r = previous.clone.delete_if {|k,_| !GDV::NaturalKeys.include?(k)}.extend GDV
|
|
34
186
|
|
|
@@ -109,6 +261,18 @@ module Musa::Datasets
|
|
|
109
261
|
r
|
|
110
262
|
end
|
|
111
263
|
|
|
264
|
+
# Normalizes chromatic pitch to scale note.
|
|
265
|
+
#
|
|
266
|
+
# Converts arbitrary grade + sharps to closest scale note representation.
|
|
267
|
+
# If chromatic, returns background note + chromatic alteration.
|
|
268
|
+
#
|
|
269
|
+
# @param scale [Musa::Scales::Scale] reference scale
|
|
270
|
+
# @param grade [Integer] scale degree (wide grade)
|
|
271
|
+
# @param sharps [Integer] chromatic alteration
|
|
272
|
+
#
|
|
273
|
+
# @return [Array(Integer, Integer)] [normalized_grade, normalized_sharps]
|
|
274
|
+
#
|
|
275
|
+
# @api private
|
|
112
276
|
private def normalize_to_scale(scale, grade, sharps)
|
|
113
277
|
note = scale[grade].sharp(sharps)
|
|
114
278
|
background = note.background_note
|
|
@@ -120,6 +284,43 @@ module Musa::Datasets
|
|
|
120
284
|
end
|
|
121
285
|
end
|
|
122
286
|
|
|
287
|
+
# Converts to Neuma delta notation string.
|
|
288
|
+
#
|
|
289
|
+
# Neuma delta format uses special notation for changes:
|
|
290
|
+
#
|
|
291
|
+
# ([grade_delta] [octave_delta] [duration_delta] [velocity_delta] [modifiers...])
|
|
292
|
+
#
|
|
293
|
+
# - **Grade delta**: "+2" or "-2" (semitone change)
|
|
294
|
+
# - **Sharp delta**: "+#" or "-_" (chromatic change)
|
|
295
|
+
# - **Octave delta**: "+o1" or "-o1" (octave change)
|
|
296
|
+
# - **Duration delta**: "+0.5", "-0.5", or "*2" (duration change)
|
|
297
|
+
# - **Velocity delta**: "+f" or "-f" (dynamics change by f's)
|
|
298
|
+
#
|
|
299
|
+
# @return [String] Neuma delta notation
|
|
300
|
+
#
|
|
301
|
+
# @example Delta grade
|
|
302
|
+
# gdvd = { delta_grade: 2 }.extend(GDVd)
|
|
303
|
+
# gdvd.base_duration = 1/4r
|
|
304
|
+
# gdvd.to_neuma # => "(+2)"
|
|
305
|
+
#
|
|
306
|
+
# @example Multiple deltas
|
|
307
|
+
# gdvd = { delta_grade: -2, delta_velocity: 1 }.extend(GDVd)
|
|
308
|
+
# gdvd.base_duration = 1/4r
|
|
309
|
+
# gdvd.to_neuma # => "(-2 +f)"
|
|
310
|
+
#
|
|
311
|
+
# @example Duration factor
|
|
312
|
+
# gdvd = { factor_duration: 2 }.extend(GDVd)
|
|
313
|
+
# gdvd.base_duration = 1/4r
|
|
314
|
+
# gdvd.to_neuma # => "(. *2)"
|
|
315
|
+
#
|
|
316
|
+
# @example Chromatic change
|
|
317
|
+
# gdvd = { delta_sharps: 1 }.extend(GDVd)
|
|
318
|
+
# gdvd.to_neuma # => "(+#)"
|
|
319
|
+
#
|
|
320
|
+
# @example Absolute values
|
|
321
|
+
# gdvd = { abs_grade: 0, abs_duration: 1.0 }.extend(GDVd)
|
|
322
|
+
# gdvd.base_duration = 1/4r
|
|
323
|
+
# gdvd.to_neuma # => "(0 4)"
|
|
123
324
|
def to_neuma
|
|
124
325
|
@base_duration ||= Rational(1,4)
|
|
125
326
|
|
|
@@ -1,19 +1,86 @@
|
|
|
1
1
|
module Musa::Datasets
|
|
2
|
+
# Helper utilities for dataset formatting and string generation.
|
|
3
|
+
#
|
|
4
|
+
# Helper provides utility methods for converting datasets to string
|
|
5
|
+
# representations, particularly for the Neuma notation format.
|
|
6
|
+
#
|
|
7
|
+
# These methods handle:
|
|
8
|
+
#
|
|
9
|
+
# - Sign formatting (+/-)
|
|
10
|
+
# - Velocity to dynamics conversion
|
|
11
|
+
# - Modifier parameter formatting
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
2
14
|
module Helper
|
|
3
15
|
private
|
|
4
16
|
|
|
17
|
+
# Returns '+' for non-negative numbers, empty string for negative.
|
|
18
|
+
#
|
|
19
|
+
# Used for formatting delta values in Neuma notation.
|
|
20
|
+
#
|
|
21
|
+
# @param x [Numeric] number to check
|
|
22
|
+
# @return [String] '+' or ''
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# positive_sign_of(5) # => '+'
|
|
26
|
+
# positive_sign_of(-3) # => ''
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
5
29
|
def positive_sign_of(x)
|
|
6
30
|
x >= 0 ? '+' : ''
|
|
7
31
|
end
|
|
8
32
|
|
|
33
|
+
# Returns '+', '+', or '-' based on number's sign.
|
|
34
|
+
#
|
|
35
|
+
# @param x [Numeric] number to check
|
|
36
|
+
# @return [String] '+' (positive), '+' (zero), or '-' (negative)
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# sign_of(5) # => '+'
|
|
40
|
+
# sign_of(0) # => '+'
|
|
41
|
+
# sign_of(-3) # => '-'
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
9
44
|
def sign_of(x)
|
|
10
45
|
'++-'[x <=> 0]
|
|
11
46
|
end
|
|
12
47
|
|
|
48
|
+
# Converts numeric velocity to dynamics marking.
|
|
49
|
+
#
|
|
50
|
+
# Maps velocity values (-5 to +4) to standard dynamics markings.
|
|
51
|
+
# Range: ppp (-5) to fff (+4), centered at mf (0).
|
|
52
|
+
#
|
|
53
|
+
# @param x [Integer] velocity value
|
|
54
|
+
# @return [String] dynamics marking
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# velocity_of(-5) # => 'ppp'
|
|
58
|
+
# velocity_of(0) # => 'mf'
|
|
59
|
+
# velocity_of(4) # => 'fff'
|
|
60
|
+
#
|
|
61
|
+
# @api private
|
|
13
62
|
def velocity_of(x)
|
|
14
63
|
%w[ppp pp p mp mf f ff fff][x + 3]
|
|
15
64
|
end
|
|
16
65
|
|
|
66
|
+
# Formats modifier with parameters for Neuma notation.
|
|
67
|
+
#
|
|
68
|
+
# Converts modifier keys and their parameters into Neuma string format.
|
|
69
|
+
#
|
|
70
|
+
# @param modificator [Symbol] modifier key name
|
|
71
|
+
# @param parameter_or_parameters [Boolean, Array, Object] modifier parameters
|
|
72
|
+
# @return [String] formatted modifier string
|
|
73
|
+
#
|
|
74
|
+
# @example Boolean modifier (flag)
|
|
75
|
+
# modificator_string(:staccato, true) # => 'staccato'
|
|
76
|
+
#
|
|
77
|
+
# @example Single parameter
|
|
78
|
+
# modificator_string(:pedal, 'down') # => 'pedal("down")'
|
|
79
|
+
#
|
|
80
|
+
# @example Multiple parameters
|
|
81
|
+
# modificator_string(:bend, [2, 'up']) # => 'bend(2, "up")'
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
17
84
|
def modificator_string(modificator, parameter_or_parameters)
|
|
18
85
|
case parameter_or_parameters
|
|
19
86
|
when true
|
|
@@ -27,6 +94,14 @@ module Musa::Datasets
|
|
|
27
94
|
|
|
28
95
|
private
|
|
29
96
|
|
|
97
|
+
# Converts parameter to string representation.
|
|
98
|
+
#
|
|
99
|
+
# Handles different parameter types for Neuma notation.
|
|
100
|
+
#
|
|
101
|
+
# @param parameter [String, Numeric, Symbol] parameter value
|
|
102
|
+
# @return [String] formatted parameter
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
30
105
|
def parameter_to_string(parameter)
|
|
31
106
|
case parameter
|
|
32
107
|
when String
|
data/lib/musa-dsl/datasets/p.rb
CHANGED
|
@@ -5,9 +5,113 @@ require_relative '../series'
|
|
|
5
5
|
require_relative '../sequencer'
|
|
6
6
|
|
|
7
7
|
module Musa::Datasets
|
|
8
|
+
# Point series: sequential points in time with durations.
|
|
9
|
+
#
|
|
10
|
+
# P (Point series) represents sequential points in time as arrays with alternating
|
|
11
|
+
# structure: [point, duration, point, duration, point, ...].
|
|
12
|
+
#
|
|
13
|
+
# ## Structure
|
|
14
|
+
#
|
|
15
|
+
# The array alternates between points and durations:
|
|
16
|
+
#
|
|
17
|
+
# [point₀, duration₀, point₁, duration₁, point₂]
|
|
18
|
+
#
|
|
19
|
+
# - **Points** (odd positions): Any data (numbers, hashes, complex structures, etc.)
|
|
20
|
+
# - **Durations** (even positions): Time between points (numbers)
|
|
21
|
+
# - **Last point**: Final point has no duration (sequence end)
|
|
22
|
+
#
|
|
23
|
+
# This compact format efficiently represents timed sequences without
|
|
24
|
+
# repeating time information.
|
|
25
|
+
#
|
|
26
|
+
# ## Conversions
|
|
27
|
+
#
|
|
28
|
+
# P can be converted to two different representations:
|
|
29
|
+
#
|
|
30
|
+
# ### 1. Timed Series (to_timed_serie)
|
|
31
|
+
#
|
|
32
|
+
# Converts to series of {AbsTimed} events with absolute time and value.
|
|
33
|
+
# Each value gets a timestamp based on cumulative durations.
|
|
34
|
+
#
|
|
35
|
+
# p = [60, 4, 64, 8, 67].extend(P)
|
|
36
|
+
# serie = p.to_timed_serie
|
|
37
|
+
# # Yields:
|
|
38
|
+
# # { time: 0, value: 60 }
|
|
39
|
+
# # { time: 1.0, value: 64 } (4 * base_duration = 1.0)
|
|
40
|
+
# # { time: 3.0, value: 67 } (8 * base_duration = 2.0)
|
|
41
|
+
#
|
|
42
|
+
# ### 2. Parameter Segment Series (to_ps_serie)
|
|
43
|
+
#
|
|
44
|
+
# Converts to series of {PS} (Parameter Segment) objects representing
|
|
45
|
+
# continuous changes between consecutive points.
|
|
46
|
+
#
|
|
47
|
+
# p = [60, 4, 64, 8, 67].extend(P)
|
|
48
|
+
# serie = p.to_ps_serie
|
|
49
|
+
# # Yields PS objects:
|
|
50
|
+
# # { from: 60, to: 64, duration: 1.0, right_open: true }
|
|
51
|
+
# # { from: 64, to: 67, duration: 2.0, right_open: false }
|
|
52
|
+
#
|
|
53
|
+
# ## Point Transformation
|
|
54
|
+
#
|
|
55
|
+
# The {#map} method transforms points while preserving durations:
|
|
56
|
+
#
|
|
57
|
+
# p = [60, 4, 64, 8, 67].extend(P)
|
|
58
|
+
# p2 = p.map { |point| point + 12 }
|
|
59
|
+
# # => [72, 4, 76, 8, 79]
|
|
60
|
+
# # Durations unchanged, points transformed
|
|
61
|
+
#
|
|
62
|
+
# @example Basic point series (MIDI pitches)
|
|
63
|
+
# # MIDI pitches with durations in quarter notes
|
|
64
|
+
# p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
|
|
65
|
+
# # 60 (C4) for 4 quarters → 64 (E4) for 8 quarters → 67 (G4)
|
|
66
|
+
#
|
|
67
|
+
# @example Hash points (complex data structures)
|
|
68
|
+
# p = [
|
|
69
|
+
# { pitch: 60, velocity: 64 }, 4,
|
|
70
|
+
# { pitch: 64, velocity: 80 }, 8,
|
|
71
|
+
# { pitch: 67, velocity: 64 }
|
|
72
|
+
# ].extend(Musa::Datasets::P)
|
|
73
|
+
#
|
|
74
|
+
# @example Convert to timed serie
|
|
75
|
+
# p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
|
|
76
|
+
# serie = p.to_timed_serie(base_duration: 1/4r)
|
|
77
|
+
# # base_duration: quarter note = 1/4 beat
|
|
78
|
+
#
|
|
79
|
+
# @example Start at specific time
|
|
80
|
+
# serie = p.to_timed_serie(time_start: 10)
|
|
81
|
+
# # First event at time 10
|
|
82
|
+
#
|
|
83
|
+
# @example Start time from component
|
|
84
|
+
# p = [{ time: 100, pitch: 60 }, 4, { time: 200, pitch: 64 }].extend(P)
|
|
85
|
+
# serie = p.to_timed_serie(time_start_component: :time)
|
|
86
|
+
# # First event at time 100 (from first point's :time)
|
|
87
|
+
#
|
|
88
|
+
# @example Transform points
|
|
89
|
+
# p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
|
|
90
|
+
# p2 = p.map { |point| point + 12 }
|
|
91
|
+
# # Transform each point (e.g., transpose pitches up one octave)
|
|
92
|
+
#
|
|
93
|
+
# @see PS Parameter segments
|
|
94
|
+
# @see AbsTimed Timed events
|
|
95
|
+
# @see Dataset Parent dataset module
|
|
8
96
|
module P
|
|
9
97
|
include Dataset
|
|
10
98
|
|
|
99
|
+
# Converts to series of parameter segments.
|
|
100
|
+
#
|
|
101
|
+
# Creates {PS} objects representing continuous changes from each point
|
|
102
|
+
# to the next. Useful for glissandi, parameter sweeps, or any continuous
|
|
103
|
+
# interpolation between points.
|
|
104
|
+
#
|
|
105
|
+
# @param base_duration [Rational] duration unit multiplier (default: 1/4r)
|
|
106
|
+
# Durations in P are multiplied by this to get actual time
|
|
107
|
+
#
|
|
108
|
+
# @return [Musa::Series::Serie<PS>] series of parameter segments
|
|
109
|
+
#
|
|
110
|
+
# @example Create parameter segments
|
|
111
|
+
# p = [60, 4, 64, 8, 67].extend(P)
|
|
112
|
+
# serie = p.to_ps_serie
|
|
113
|
+
# segment1 = serie.next_value
|
|
114
|
+
# # => { from: 60, to: 64, duration: 1.0, right_open: true }
|
|
11
115
|
def to_ps_serie(base_duration: nil)
|
|
12
116
|
base_duration ||= 1/4r # TODO review incoherence between neumalang 1/4r base duration for quarter notes and general 1r size of bar
|
|
13
117
|
|
|
@@ -22,6 +126,33 @@ module Musa::Datasets
|
|
|
22
126
|
end
|
|
23
127
|
end
|
|
24
128
|
|
|
129
|
+
# Converts to series of timed events (AbsTimed).
|
|
130
|
+
#
|
|
131
|
+
# Creates series yielding {AbsTimed} events with absolute time and value.
|
|
132
|
+
# Each value is emitted at its calculated time point based on cumulative durations.
|
|
133
|
+
#
|
|
134
|
+
# @param time_start [Numeric] starting time offset (default: 0)
|
|
135
|
+
# @param time_start_component [Symbol] key in first value to use as time offset
|
|
136
|
+
# If provided, adds first[time_start_component] to time_start
|
|
137
|
+
# @param base_duration [Rational] duration unit multiplier (default: 1/4r)
|
|
138
|
+
#
|
|
139
|
+
# @return [PtoTimedSerie] series of timed events
|
|
140
|
+
#
|
|
141
|
+
# @example Basic timed serie
|
|
142
|
+
# p = [60, 4, 64, 8, 67].extend(P)
|
|
143
|
+
# serie = p.to_timed_serie
|
|
144
|
+
# serie.next_value # => { time: 0, value: 60 }
|
|
145
|
+
# serie.next_value # => { time: 1.0, value: 64 }
|
|
146
|
+
# serie.next_value # => { time: 3.0, value: 67 }
|
|
147
|
+
#
|
|
148
|
+
# @example Custom start time
|
|
149
|
+
# serie = p.to_timed_serie(time_start: 10)
|
|
150
|
+
# # First event at time 10
|
|
151
|
+
#
|
|
152
|
+
# @example Start time from component
|
|
153
|
+
# p = [{ time: 100, pitch: 60 }, 4, { pitch: 64 }].extend(P)
|
|
154
|
+
# serie = p.to_timed_serie(time_start_component: :time)
|
|
155
|
+
# # First event at time 100
|
|
25
156
|
def to_timed_serie(time_start: nil, time_start_component: nil, base_duration: nil)
|
|
26
157
|
time_start ||= 0r
|
|
27
158
|
time_start += self.first[time_start_component] if time_start_component
|
|
@@ -31,11 +162,30 @@ module Musa::Datasets
|
|
|
31
162
|
PtoTimedSerie.new(self, base_duration, time_start)
|
|
32
163
|
end
|
|
33
164
|
|
|
165
|
+
# Maps over points, preserving durations.
|
|
166
|
+
#
|
|
167
|
+
# Transforms each point (odd positions) using the block while
|
|
168
|
+
# keeping durations (even positions) unchanged.
|
|
169
|
+
#
|
|
170
|
+
# @yieldparam point [Object] each point in the series
|
|
171
|
+
# @yieldreturn [Object] transformed point
|
|
172
|
+
#
|
|
173
|
+
# @return [P] new P with transformed points
|
|
174
|
+
#
|
|
175
|
+
# @example Transform points (e.g., transpose pitches)
|
|
176
|
+
# p = [60, 4, 64, 8, 67].extend(P)
|
|
177
|
+
# p.map { |point| point + 12 }
|
|
178
|
+
# # => [72, 4, 76, 8, 79]
|
|
179
|
+
#
|
|
180
|
+
# @example Transform hash points
|
|
181
|
+
# p = [{ pitch: 60 }, 4, { pitch: 64 }].extend(P)
|
|
182
|
+
# p.map { |point| point.merge(velocity: 80) }
|
|
183
|
+
# # Adds velocity to each point
|
|
34
184
|
def map(&block)
|
|
35
185
|
i = 0
|
|
36
186
|
clone.map! do |element|
|
|
37
|
-
# Process with block only the
|
|
38
|
-
# structure is <
|
|
187
|
+
# Process with block only the points (points are the alternating elements because P
|
|
188
|
+
# structure is <point> <duration> <point> <duration> <point>)
|
|
39
189
|
#
|
|
40
190
|
if (i += 1) % 2 == 1
|
|
41
191
|
block.call(element)
|
|
@@ -45,9 +195,26 @@ module Musa::Datasets
|
|
|
45
195
|
end
|
|
46
196
|
end
|
|
47
197
|
|
|
198
|
+
# Series adapter for P to AbsTimed conversion.
|
|
199
|
+
#
|
|
200
|
+
# PtoTimedSerie is a {Musa::Series::Serie} that converts a {P} (point series)
|
|
201
|
+
# into a series of {AbsTimed} events. It reads the alternating point/duration
|
|
202
|
+
# structure and emits timed events.
|
|
203
|
+
#
|
|
204
|
+
# This class is created by {P#to_timed_serie} and should not be instantiated
|
|
205
|
+
# directly.
|
|
206
|
+
#
|
|
207
|
+
# @api private
|
|
48
208
|
class PtoTimedSerie
|
|
49
209
|
include Musa::Series::Serie.base
|
|
50
210
|
|
|
211
|
+
# Creates new timed serie adapter.
|
|
212
|
+
#
|
|
213
|
+
# @param origin [P] source point series
|
|
214
|
+
# @param base_duration [Rational] duration unit multiplier
|
|
215
|
+
# @param time_start [Numeric] starting time offset
|
|
216
|
+
#
|
|
217
|
+
# @api private
|
|
51
218
|
def initialize(origin, base_duration, time_start)
|
|
52
219
|
@origin = origin
|
|
53
220
|
@base_duration = base_duration
|
|
@@ -58,8 +225,16 @@ module Musa::Datasets
|
|
|
58
225
|
mark_as_prototype!
|
|
59
226
|
end
|
|
60
227
|
|
|
228
|
+
# Source point series.
|
|
229
|
+
# @return [P]
|
|
61
230
|
attr_accessor :origin
|
|
231
|
+
|
|
232
|
+
# Duration unit multiplier.
|
|
233
|
+
# @return [Rational]
|
|
62
234
|
attr_accessor :base_duration
|
|
235
|
+
|
|
236
|
+
# Starting time offset.
|
|
237
|
+
# @return [Numeric]
|
|
63
238
|
attr_accessor :time_start
|
|
64
239
|
|
|
65
240
|
private def _init
|
|
@@ -2,9 +2,100 @@ require_relative 'e'
|
|
|
2
2
|
require_relative 'v'
|
|
3
3
|
|
|
4
4
|
module Musa::Datasets
|
|
5
|
+
# Hash-based dataset with array conversion.
|
|
6
|
+
#
|
|
7
|
+
# PackedV (Packed Value) represents datasets stored as hashes (named key-value pairs).
|
|
8
|
+
# Extends {AbsI} for absolute indexed events.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# PackedV provides named key-value storage for musical data and conversion
|
|
13
|
+
# to indexed arrays ({V}). This is useful for:
|
|
14
|
+
#
|
|
15
|
+
# - Semantic naming of values (pitch, duration, velocity)
|
|
16
|
+
# - Sparse data (only store non-default values)
|
|
17
|
+
# - Converting between hash and array representations
|
|
18
|
+
# - Serialization to readable formats
|
|
19
|
+
#
|
|
20
|
+
# ## Conversion to V
|
|
21
|
+
#
|
|
22
|
+
# The {#to_V} method converts hashes to arrays using a mapper that
|
|
23
|
+
# defines the correspondence between hash keys and array positions.
|
|
24
|
+
#
|
|
25
|
+
# ### Array Mapper
|
|
26
|
+
#
|
|
27
|
+
# Array mapper defines the order of keys in resulting array.
|
|
28
|
+
# Position i contains value for key mapper[i].
|
|
29
|
+
#
|
|
30
|
+
# pv = { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PackedV)
|
|
31
|
+
# v = pv.to_V([:pitch, :duration, :velocity])
|
|
32
|
+
# # => [60, 1.0, nil]
|
|
33
|
+
# # velocity missing, becomes nil
|
|
34
|
+
#
|
|
35
|
+
# - Missing keys become `nil` in array
|
|
36
|
+
#
|
|
37
|
+
# ### Hash Mapper
|
|
38
|
+
#
|
|
39
|
+
# Hash mapper defines both key order (keys) and default values (values).
|
|
40
|
+
# Position i contains value for key mapper.keys[i], using mapper.values[i]
|
|
41
|
+
# as default if key is missing or value is nil.
|
|
42
|
+
#
|
|
43
|
+
# pv = { pitch: 60 }.extend(Musa::Datasets::PackedV)
|
|
44
|
+
# v = pv.to_V({ pitch: 60, duration: 1.0, velocity: 64 })
|
|
45
|
+
# # => [60, 1.0, 64]
|
|
46
|
+
# # duration and velocity use defaults
|
|
47
|
+
#
|
|
48
|
+
# Defaults fill in missing or nil values.
|
|
49
|
+
#
|
|
50
|
+
# @example Basic hash to array conversion
|
|
51
|
+
# pv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PackedV)
|
|
52
|
+
# v = pv.to_V([:pitch, :duration, :velocity])
|
|
53
|
+
# # => [60, 1.0, 64]
|
|
54
|
+
#
|
|
55
|
+
# @example Missing keys become nil (array mapper)
|
|
56
|
+
# pv = { a: 1, c: 3 }.extend(Musa::Datasets::PackedV)
|
|
57
|
+
# v = pv.to_V([:c, :b, :a])
|
|
58
|
+
# # => [3, nil, 1]
|
|
59
|
+
# # b missing, becomes nil
|
|
60
|
+
#
|
|
61
|
+
# @example Hash mapper with defaults
|
|
62
|
+
# pv = { a: 1, b: nil, c: 3 }.extend(Musa::Datasets::PackedV)
|
|
63
|
+
# v = pv.to_V({ c: 100, b: 200, a: 300, d: 400 })
|
|
64
|
+
# # => [3, 200, 1, 400]
|
|
65
|
+
# # b nil → uses default 200
|
|
66
|
+
# # d missing → uses default 400
|
|
67
|
+
#
|
|
68
|
+
# @example Partial mapper (fewer keys in mapper)
|
|
69
|
+
# pv = { a: 1, b: 2, c: 3 }.extend(Musa::Datasets::PackedV)
|
|
70
|
+
# v = pv.to_V([:c, :b])
|
|
71
|
+
# # => [3, 2]
|
|
72
|
+
# # Only c and b extracted
|
|
73
|
+
#
|
|
74
|
+
# @example Key order matters
|
|
75
|
+
# pv = { a: 1, b: 2, c: 3 }.extend(Musa::Datasets::PackedV)
|
|
76
|
+
# v = pv.to_V([:c, :b, :a])
|
|
77
|
+
# # => [3, 2, 1]
|
|
78
|
+
#
|
|
79
|
+
# @see V Array-based dataset (inverse)
|
|
80
|
+
# @see AbsI Parent absolute indexed module
|
|
5
81
|
module PackedV
|
|
6
82
|
include AbsI
|
|
7
83
|
|
|
84
|
+
# Converts packed hash to array (V).
|
|
85
|
+
#
|
|
86
|
+
# @param mapper [Array<Symbol>, Hash{Symbol => Object}] key mapping
|
|
87
|
+
# - Array: maps keys to indices (order matters)
|
|
88
|
+
# - Hash: maps keys (keys) to indices with defaults (values)
|
|
89
|
+
#
|
|
90
|
+
# @return [V] array dataset
|
|
91
|
+
#
|
|
92
|
+
# @raise [ArgumentError] if mapper is not Array or Hash
|
|
93
|
+
#
|
|
94
|
+
# @example Array mapper
|
|
95
|
+
# pv.to_V([:pitch, :duration, :velocity])
|
|
96
|
+
#
|
|
97
|
+
# @example Hash mapper with defaults
|
|
98
|
+
# pv.to_V({ pitch: 60, duration: 1.0, velocity: 64 })
|
|
8
99
|
def to_V(mapper)
|
|
9
100
|
case mapper
|
|
10
101
|
when Hash
|