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
|
@@ -4,15 +4,150 @@ require_relative 'gdv'
|
|
|
4
4
|
require_relative 'helper'
|
|
5
5
|
|
|
6
6
|
module Musa::Datasets
|
|
7
|
+
# MIDI-style musical events with absolute pitches.
|
|
8
|
+
#
|
|
9
|
+
# PDV (Pitch/Duration/Velocity) represents musical events using MIDI-like
|
|
10
|
+
# absolute pitch numbers. Extends {AbsD} for duration support.
|
|
11
|
+
#
|
|
12
|
+
# ## Purpose
|
|
13
|
+
#
|
|
14
|
+
# PDV is the MIDI representation layer of the dataset framework:
|
|
15
|
+
#
|
|
16
|
+
# - Uses absolute MIDI pitch numbers (0-127)
|
|
17
|
+
# - Uses MIDI velocity values (0-127)
|
|
18
|
+
# - Direct mapping to MIDI messages
|
|
19
|
+
# - Machine-oriented (not human-readable)
|
|
20
|
+
#
|
|
21
|
+
# Contrast with {GDV} which uses score notation (scale degrees, dynamics).
|
|
22
|
+
#
|
|
23
|
+
# ## Natural Keys
|
|
24
|
+
#
|
|
25
|
+
# - **:pitch**: MIDI pitch number (0-127) or :silence for rests
|
|
26
|
+
# - **:velocity**: MIDI velocity (0-127)
|
|
27
|
+
# - **:duration**: Event duration (from {AbsD})
|
|
28
|
+
# - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
|
|
29
|
+
#
|
|
30
|
+
# ## Conversions
|
|
31
|
+
#
|
|
32
|
+
# ### To GDV (Score Notation)
|
|
33
|
+
#
|
|
34
|
+
# Converts MIDI pitches to scale degrees using a scale reference:
|
|
35
|
+
#
|
|
36
|
+
# pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
|
|
37
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
38
|
+
# gdv = pdv.to_gdv(scale)
|
|
39
|
+
# # => { grade: 0, octave: 0, duration: 1.0, velocity: 0 }
|
|
40
|
+
#
|
|
41
|
+
# - **Pitch → Grade**: Finds closest scale degree
|
|
42
|
+
# - **Chromatic notes**: Represented as grade + sharps
|
|
43
|
+
# - **Velocity**: Maps MIDI 0-127 to dynamics -5 to +4 (ppp to fff)
|
|
44
|
+
#
|
|
45
|
+
# ### Velocity Mapping
|
|
46
|
+
#
|
|
47
|
+
# MIDI velocities are mapped to musical dynamics:
|
|
48
|
+
#
|
|
49
|
+
# MIDI 1-1 → velocity -5 (ppp)
|
|
50
|
+
# MIDI 2-8 → velocity -4 (pp)
|
|
51
|
+
# MIDI 9-16 → velocity -3 (p)
|
|
52
|
+
# MIDI 17-33 → velocity -2 (mp)
|
|
53
|
+
# MIDI 34-48 → velocity -1 (mf-)
|
|
54
|
+
# MIDI 49-64 → velocity 0 (mf)
|
|
55
|
+
# MIDI 65-80 → velocity +1 (f)
|
|
56
|
+
# MIDI 81-96 → velocity +2 (ff)
|
|
57
|
+
# MIDI 97-112 → velocity +3 (fff-)
|
|
58
|
+
# MIDI 113-127 → velocity +4 (fff)
|
|
59
|
+
#
|
|
60
|
+
# ## Base Duration
|
|
61
|
+
#
|
|
62
|
+
# The `base_duration` attribute defines the unit for duration values,
|
|
63
|
+
# typically 1/4r (quarter note).
|
|
64
|
+
#
|
|
65
|
+
# @example Basic MIDI event
|
|
66
|
+
# pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PDV)
|
|
67
|
+
# pdv.base_duration = 1/4r
|
|
68
|
+
# # C4 (middle C) for 1 beat at mf dynamics
|
|
69
|
+
#
|
|
70
|
+
# @example Silence (rest)
|
|
71
|
+
# pdv = { pitch: :silence, duration: 1.0 }.extend(PDV)
|
|
72
|
+
# # Rest for 1 beat
|
|
73
|
+
#
|
|
74
|
+
# @example With articulation
|
|
75
|
+
# pdv = {
|
|
76
|
+
# pitch: 64,
|
|
77
|
+
# duration: 1.0,
|
|
78
|
+
# note_duration: 0.5, # Staccato
|
|
79
|
+
# velocity: 80
|
|
80
|
+
# }.extend(PDV)
|
|
81
|
+
#
|
|
82
|
+
# @example Convert to score notation
|
|
83
|
+
# pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
|
|
84
|
+
# pdv.base_duration = 1/4r
|
|
85
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
86
|
+
# gdv = pdv.to_gdv(scale)
|
|
87
|
+
# # => { grade: 0, octave: 0, duration: 1.0, velocity: 0 }
|
|
88
|
+
#
|
|
89
|
+
# @example Chromatic pitch
|
|
90
|
+
# pdv = { pitch: 61, duration: 1.0, velocity: 64 }.extend(PDV)
|
|
91
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
92
|
+
# gdv = pdv.to_gdv(scale)
|
|
93
|
+
# # => { grade: 0, octave: 0, sharps: 1, duration: 1.0, velocity: 0 }
|
|
94
|
+
# # C# represented as C (grade 0) + 1 sharp
|
|
95
|
+
#
|
|
96
|
+
# @example Preserve additional keys
|
|
97
|
+
# pdv = {
|
|
98
|
+
# pitch: 60,
|
|
99
|
+
# duration: 1.0,
|
|
100
|
+
# velocity: 64,
|
|
101
|
+
# custom_key: :value
|
|
102
|
+
# }.extend(PDV)
|
|
103
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
104
|
+
# gdv = pdv.to_gdv(scale)
|
|
105
|
+
# # custom_key copied to GDV (not a natural key)
|
|
106
|
+
#
|
|
107
|
+
# @see GDV Score-style representation
|
|
108
|
+
# @see AbsD Absolute duration events
|
|
109
|
+
# @see Helper String formatting utilities
|
|
7
110
|
module PDV
|
|
8
111
|
include AbsD
|
|
9
112
|
|
|
10
113
|
include Helper
|
|
11
114
|
|
|
115
|
+
# Natural keys for MIDI events.
|
|
116
|
+
# @return [Array<Symbol>]
|
|
12
117
|
NaturalKeys = (NaturalKeys + [:pitch, :velocity]).freeze
|
|
13
118
|
|
|
119
|
+
# Base duration for time calculations.
|
|
120
|
+
# @return [Rational]
|
|
14
121
|
attr_accessor :base_duration
|
|
15
122
|
|
|
123
|
+
# Converts to GDV (score notation).
|
|
124
|
+
#
|
|
125
|
+
# Translates MIDI representation to score notation using a scale:
|
|
126
|
+
# - MIDI pitch → scale degree (grade + octave + sharps)
|
|
127
|
+
# - MIDI velocity → dynamics (-5 to +4)
|
|
128
|
+
# - Duration values copied
|
|
129
|
+
# - Additional keys preserved
|
|
130
|
+
#
|
|
131
|
+
# @param scale [Musa::Scales::Scale] reference scale for pitch conversion
|
|
132
|
+
#
|
|
133
|
+
# @return [GDV] score notation dataset
|
|
134
|
+
#
|
|
135
|
+
# @example Basic conversion
|
|
136
|
+
# pdv = { pitch: 60, duration: 1.0, velocity: 64 }.extend(PDV)
|
|
137
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
138
|
+
# gdv = pdv.to_gdv(scale)
|
|
139
|
+
#
|
|
140
|
+
# @example Chromatic note
|
|
141
|
+
# pdv = { pitch: 61, duration: 1.0 }.extend(PDV)
|
|
142
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
143
|
+
# gdv = pdv.to_gdv(scale)
|
|
144
|
+
# # => { grade: 0, octave: 0, sharps: 1, duration: 1.0 }
|
|
145
|
+
#
|
|
146
|
+
# @example Silence
|
|
147
|
+
# pdv = { pitch: :silence, duration: 1.0 }.extend(PDV)
|
|
148
|
+
# scale = Musa::Scales::Scales.et12[440.0].major[60]
|
|
149
|
+
# gdv = pdv.to_gdv(scale)
|
|
150
|
+
# # => { grade: :silence, duration: 1.0 }
|
|
16
151
|
def to_gdv(scale)
|
|
17
152
|
gdv = {}.extend GDV
|
|
18
153
|
gdv.base_duration = @base_duration
|
|
@@ -39,7 +174,7 @@ module Musa::Datasets
|
|
|
39
174
|
if self[:velocity]
|
|
40
175
|
# ppp = 16 ... fff = 127
|
|
41
176
|
# TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
|
|
42
|
-
gdv[:velocity] = [1..1, 2..8, 9..16, 17..33, 34..
|
|
177
|
+
gdv[:velocity] = [1..1, 2..8, 9..16, 17..33, 34..48, 49..64, 65..80, 81..96, 97..112, 113..127].index { |r| r.cover? self[:velocity] } - 5
|
|
43
178
|
end
|
|
44
179
|
|
|
45
180
|
(keys - NaturalKeys).each { |k| gdv[k] = self[k] }
|
data/lib/musa-dsl/datasets/ps.rb
CHANGED
|
@@ -4,31 +4,161 @@ require_relative 'score'
|
|
|
4
4
|
require_relative '../sequencer'
|
|
5
5
|
|
|
6
6
|
module Musa::Datasets
|
|
7
|
+
# Parameter segments for continuous changes between multidimensional points.
|
|
8
|
+
#
|
|
9
|
+
# PS (Parameter Segment) represents a continuous change from one point
|
|
10
|
+
# to another over a duration. Extends {AbsD} for duration support.
|
|
11
|
+
#
|
|
12
|
+
# ## Purpose
|
|
13
|
+
#
|
|
14
|
+
# PS is used to represent:
|
|
15
|
+
#
|
|
16
|
+
# - **Glissandi**: Continuous pitch slides (portamento)
|
|
17
|
+
# - **Parameter sweeps**: Gradual changes in any sonic parameter
|
|
18
|
+
# - **Interpolations**: Smooth transitions between multidimensional states
|
|
19
|
+
#
|
|
20
|
+
# Unlike discrete events that jump from one value to another, PS represents
|
|
21
|
+
# the continuous path between values.
|
|
22
|
+
#
|
|
23
|
+
# ## Natural Keys
|
|
24
|
+
#
|
|
25
|
+
# - **:from**: Starting value (number, array, or hash)
|
|
26
|
+
# - **:to**: Ending value (must match :from type and structure)
|
|
27
|
+
# - **:right_open**: Whether endpoint is included (true = open interval)
|
|
28
|
+
# - **:duration**: Duration of the change (from {AbsD})
|
|
29
|
+
# - **:note_duration**, **:forward_duration**: Additional duration keys (from {AbsD})
|
|
30
|
+
#
|
|
31
|
+
# ## Value Types
|
|
32
|
+
#
|
|
33
|
+
# ### Single Values
|
|
34
|
+
#
|
|
35
|
+
# { from: 60, to: 72, duration: 1.0 }
|
|
36
|
+
# # Single value glissando
|
|
37
|
+
#
|
|
38
|
+
# ### Arrays (parallel interpolation)
|
|
39
|
+
#
|
|
40
|
+
# { from: [60, 64], to: [72, 76], duration: 1.0 }
|
|
41
|
+
# # Both values interpolate in parallel
|
|
42
|
+
# # Arrays must be same size
|
|
43
|
+
#
|
|
44
|
+
# ### Hashes (named parameters)
|
|
45
|
+
#
|
|
46
|
+
# { from: { pitch: 60, velocity: 64 },
|
|
47
|
+
# to: { pitch: 72, velocity: 80 },
|
|
48
|
+
# duration: 1.0 }
|
|
49
|
+
# # Multiple parameters interpolate together
|
|
50
|
+
# # Hashes must have same keys
|
|
51
|
+
#
|
|
52
|
+
# ## Right Open Intervals
|
|
53
|
+
#
|
|
54
|
+
# The :right_open flag determines if the ending value is reached:
|
|
55
|
+
#
|
|
56
|
+
# - **false** (closed): Interpolation reaches :to value
|
|
57
|
+
# - **true** (open): Interpolation stops just before :to value
|
|
58
|
+
#
|
|
59
|
+
# This is important for consecutive segments where you don't want
|
|
60
|
+
# discontinuities at the boundaries.
|
|
61
|
+
#
|
|
62
|
+
# @example Basic parameter segment (pitch glissando)
|
|
63
|
+
# ps = { from: 60, to: 72, duration: 2.0 }.extend(Musa::Datasets::PS)
|
|
64
|
+
# # Continuous slide from C4 to C5 over 2 beats
|
|
65
|
+
#
|
|
66
|
+
# @example Parallel interpolation (multidimensional)
|
|
67
|
+
# ps = {
|
|
68
|
+
# from: [60, 64], # C4 and E4
|
|
69
|
+
# to: [72, 76], # C5 and E5
|
|
70
|
+
# duration: 1.0
|
|
71
|
+
# }.extend(PS)
|
|
72
|
+
# # Both parameters move in parallel
|
|
73
|
+
#
|
|
74
|
+
# @example Multiple parameters (sonic gesture)
|
|
75
|
+
# ps = {
|
|
76
|
+
# from: { pitch: 60, velocity: 64, pan: -1.0 },
|
|
77
|
+
# to: { pitch: 72, velocity: 80, pan: 1.0 },
|
|
78
|
+
# duration: 2.0
|
|
79
|
+
# }.extend(PS)
|
|
80
|
+
# # Pitch, velocity, and pan all change smoothly
|
|
81
|
+
#
|
|
82
|
+
# @example Right open interval
|
|
83
|
+
# ps1 = { from: 60, to: 64, duration: 1.0, right_open: true }.extend(PS)
|
|
84
|
+
# ps2 = { from: 64, to: 67, duration: 1.0, right_open: false }.extend(PS)
|
|
85
|
+
# # ps1 stops just before 64, ps2 starts at 64 - no discontinuity
|
|
86
|
+
#
|
|
87
|
+
# @example Created from P point series
|
|
88
|
+
# p = [60, 4, 64, 8, 67].extend(P)
|
|
89
|
+
# serie = p.to_ps_serie
|
|
90
|
+
# ps1 = serie.next_value
|
|
91
|
+
# # => { from: 60, to: 64, duration: 1.0, right_open: true }
|
|
92
|
+
#
|
|
93
|
+
# @see AbsD Parent absolute duration module
|
|
94
|
+
# @see P Point series (source of PS)
|
|
95
|
+
# @see Helper String formatting utilities
|
|
7
96
|
module PS
|
|
8
97
|
include AbsD
|
|
9
98
|
|
|
10
99
|
include Helper
|
|
11
100
|
|
|
101
|
+
# Natural keys including segment endpoints.
|
|
102
|
+
# @return [Array<Symbol>]
|
|
12
103
|
NaturalKeys = (NaturalKeys + [:from, :to, :right_open]).freeze
|
|
13
104
|
|
|
105
|
+
# Base duration for time calculations.
|
|
106
|
+
# @return [Rational]
|
|
14
107
|
attr_accessor :base_duration
|
|
15
108
|
|
|
109
|
+
# Converts to Neuma notation string.
|
|
110
|
+
#
|
|
111
|
+
# @return [String] Neuma notation
|
|
112
|
+
# @todo Not yet implemented
|
|
16
113
|
def to_neuma
|
|
17
|
-
|
|
114
|
+
raise NotImplementedError, 'PS to_neuma conversion is not yet implemented'
|
|
18
115
|
end
|
|
19
116
|
|
|
117
|
+
# Converts to PDV (Pitch/Duration/Velocity).
|
|
118
|
+
#
|
|
119
|
+
# @return [PDV] PDV dataset
|
|
120
|
+
# @todo Not yet implemented
|
|
20
121
|
def to_pdv
|
|
21
|
-
|
|
122
|
+
raise NotImplementedError, 'PS to_pdv conversion is not yet implemented'
|
|
22
123
|
end
|
|
23
124
|
|
|
125
|
+
# Converts to GDV (Grade/Duration/Velocity).
|
|
126
|
+
#
|
|
127
|
+
# @return [GDV] GDV dataset
|
|
128
|
+
# @todo Not yet implemented
|
|
24
129
|
def to_gdv
|
|
25
|
-
|
|
130
|
+
raise NotImplementedError, 'PS to_gdv conversion is not yet implemented'
|
|
26
131
|
end
|
|
27
132
|
|
|
133
|
+
# Converts to absolute indexed format.
|
|
134
|
+
#
|
|
135
|
+
# @return [AbsI] indexed dataset
|
|
136
|
+
# @todo Not yet implemented
|
|
28
137
|
def to_absI
|
|
29
|
-
|
|
138
|
+
raise NotImplementedError, 'PS to_absI conversion is not yet implemented'
|
|
30
139
|
end
|
|
31
140
|
|
|
141
|
+
# Validates PS structure.
|
|
142
|
+
#
|
|
143
|
+
# Checks that:
|
|
144
|
+
# - :from and :to have compatible types
|
|
145
|
+
# - Arrays have same size
|
|
146
|
+
# - Hashes have same keys
|
|
147
|
+
# - Duration is positive numeric
|
|
148
|
+
#
|
|
149
|
+
# @return [Boolean] true if valid
|
|
150
|
+
#
|
|
151
|
+
# @example Valid array segment
|
|
152
|
+
# ps = { from: [60, 64], to: [72, 76], duration: 1.0 }.extend(PS)
|
|
153
|
+
# ps.valid? # => true
|
|
154
|
+
#
|
|
155
|
+
# @example Invalid - mismatched array sizes
|
|
156
|
+
# ps = { from: [60, 64], to: [72], duration: 1.0 }.extend(PS)
|
|
157
|
+
# ps.valid? # => false
|
|
158
|
+
#
|
|
159
|
+
# @example Invalid - mismatched hash keys
|
|
160
|
+
# ps = { from: { a: 1 }, to: { b: 2 }, duration: 1.0 }.extend(PS)
|
|
161
|
+
# ps.valid? # => false
|
|
32
162
|
def valid?
|
|
33
163
|
case self[:from]
|
|
34
164
|
when Array
|
|
@@ -1,11 +1,71 @@
|
|
|
1
1
|
module Musa::Datasets
|
|
2
2
|
class Score
|
|
3
|
+
# Query extensions for Score result sets.
|
|
4
|
+
#
|
|
5
|
+
# Queriable provides mixins that extend query results from Score methods
|
|
6
|
+
# with convenient filtering, grouping, and sorting capabilities.
|
|
7
|
+
#
|
|
8
|
+
# Two result types are supported:
|
|
9
|
+
# - **Time slot queries**: Direct event arrays from {Score#at}
|
|
10
|
+
# - **Interval queries**: Result hashes from {Score#between} and {Score#changes_between}
|
|
11
|
+
#
|
|
12
|
+
# These modules are applied automatically to query results and provide
|
|
13
|
+
# chainable query methods for further filtering.
|
|
14
|
+
#
|
|
15
|
+
# @see Score Score class using these modules
|
|
3
16
|
module Queriable
|
|
17
|
+
# Query methods for time slot arrays.
|
|
18
|
+
#
|
|
19
|
+
# QueryableByTimeSlot extends Arrays returned by {Score#at} with query methods.
|
|
20
|
+
# Each event in the array is a dataset (hash) with musical attributes.
|
|
21
|
+
#
|
|
22
|
+
# Methods access attributes directly on events.
|
|
23
|
+
#
|
|
24
|
+
# @example Group events by pitch
|
|
25
|
+
# events = score.at(0r) # Returns array extended with QueryableByTimeSlot
|
|
26
|
+
# by_pitch = events.group_by_attribute(:pitch)
|
|
27
|
+
# # => { 60 => [event1, event2], 64 => [event3] }
|
|
28
|
+
#
|
|
29
|
+
# @example Select events with attribute
|
|
30
|
+
# staccato = events.select_by_attribute(:staccato)
|
|
31
|
+
# # Returns events where :staccato is not nil
|
|
32
|
+
#
|
|
33
|
+
# @example Select by value
|
|
34
|
+
# forte = events.select_by_attribute(:velocity, 1)
|
|
35
|
+
# # Returns events where velocity == 1
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
4
38
|
module QueryableByTimeSlot
|
|
39
|
+
# Groups events by attribute value.
|
|
40
|
+
#
|
|
41
|
+
# @param attribute [Symbol] attribute to group by
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash{Object => Array}] grouped events, values extended with QueryableByTimeSlot
|
|
44
|
+
#
|
|
45
|
+
# @example Group by grade
|
|
46
|
+
# by_grade = events.group_by_attribute(:grade)
|
|
47
|
+
# # => { 0 => [events with grade 0], 2 => [events with grade 2] }
|
|
5
48
|
def group_by_attribute(attribute)
|
|
6
49
|
group_by { |e| e[attribute] }.transform_values! { |e| e.extend(QueryableByTimeSlot) }
|
|
7
50
|
end
|
|
8
51
|
|
|
52
|
+
# Selects events by attribute presence or value.
|
|
53
|
+
#
|
|
54
|
+
# Without value: selects events where attribute is not nil.
|
|
55
|
+
# With value: selects events where attribute equals value.
|
|
56
|
+
#
|
|
57
|
+
# @param attribute [Symbol] attribute to filter by
|
|
58
|
+
# @param value [Object, nil] optional value to match
|
|
59
|
+
#
|
|
60
|
+
# @return [Array] filtered events, extended with QueryableByTimeSlot
|
|
61
|
+
#
|
|
62
|
+
# @example Select with attribute present
|
|
63
|
+
# events.select_by_attribute(:staccato)
|
|
64
|
+
# # Events where :staccato is not nil
|
|
65
|
+
#
|
|
66
|
+
# @example Select by specific value
|
|
67
|
+
# events.select_by_attribute(:pitch, 60)
|
|
68
|
+
# # Events where pitch == 60
|
|
9
69
|
def select_by_attribute(attribute, value = nil)
|
|
10
70
|
if value.nil?
|
|
11
71
|
select { |e| !e[attribute].nil? }
|
|
@@ -14,6 +74,17 @@ module Musa::Datasets
|
|
|
14
74
|
end.extend(QueryableByTimeSlot)
|
|
15
75
|
end
|
|
16
76
|
|
|
77
|
+
# Sorts events by attribute value.
|
|
78
|
+
#
|
|
79
|
+
# First filters to events with the attribute, then sorts by its value.
|
|
80
|
+
#
|
|
81
|
+
# @param attribute [Symbol] attribute to sort by
|
|
82
|
+
#
|
|
83
|
+
# @return [Array] sorted events, extended with QueryableByTimeSlot
|
|
84
|
+
#
|
|
85
|
+
# @example Sort by pitch
|
|
86
|
+
# sorted = events.sort_by_attribute(:pitch)
|
|
87
|
+
# # Events sorted by ascending pitch
|
|
17
88
|
def sort_by_attribute(attribute)
|
|
18
89
|
select_by_attribute(attribute).sort_by { |e| e[attribute] }.extend(QueryableByTimeSlot)
|
|
19
90
|
end
|
|
@@ -21,11 +92,57 @@ module Musa::Datasets
|
|
|
21
92
|
|
|
22
93
|
private_constant :QueryableByTimeSlot
|
|
23
94
|
|
|
95
|
+
# Query methods for interval query results.
|
|
96
|
+
#
|
|
97
|
+
# QueryableByDataset extends Arrays returned by {Score#between} and
|
|
98
|
+
# {Score#changes_between} with query methods. Each element is a hash
|
|
99
|
+
# containing timing info and a :dataset key with the event.
|
|
100
|
+
#
|
|
101
|
+
# Methods access attributes through the :dataset key.
|
|
102
|
+
#
|
|
103
|
+
# @example Interval query result structure
|
|
104
|
+
# results = score.between(0r, 4r)
|
|
105
|
+
# # Each result: { start: ..., finish: ..., dataset: event, ... }
|
|
106
|
+
#
|
|
107
|
+
# @example Group by pitch
|
|
108
|
+
# by_pitch = results.group_by_attribute(:pitch)
|
|
109
|
+
# # Groups by event[:dataset][:pitch]
|
|
110
|
+
#
|
|
111
|
+
# @example Select with custom condition
|
|
112
|
+
# high = results.subset { |event| event[:pitch] > 60 }
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
24
115
|
module QueryableByDataset
|
|
116
|
+
# Groups results by dataset attribute value.
|
|
117
|
+
#
|
|
118
|
+
# @param attribute [Symbol] dataset attribute to group by
|
|
119
|
+
#
|
|
120
|
+
# @return [Hash{Object => Array}] grouped results, values extended with QueryableByDataset
|
|
121
|
+
#
|
|
122
|
+
# @example Group by velocity
|
|
123
|
+
# by_velocity = results.group_by_attribute(:velocity)
|
|
124
|
+
# # => { 0 => [results with velocity 0], 1 => [results with velocity 1] }
|
|
25
125
|
def group_by_attribute(attribute)
|
|
26
126
|
group_by { |e| e[:dataset][attribute] }.transform_values! { |e| e.extend(QueryableByDataset) }
|
|
27
127
|
end
|
|
28
128
|
|
|
129
|
+
# Selects results by dataset attribute presence or value.
|
|
130
|
+
#
|
|
131
|
+
# Without value: selects where dataset attribute is not nil.
|
|
132
|
+
# With value: selects where dataset attribute equals value.
|
|
133
|
+
#
|
|
134
|
+
# @param attribute [Symbol] dataset attribute to filter by
|
|
135
|
+
# @param value [Object, nil] optional value to match
|
|
136
|
+
#
|
|
137
|
+
# @return [Array] filtered results, extended with QueryableByDataset
|
|
138
|
+
#
|
|
139
|
+
# @example Select with attribute
|
|
140
|
+
# results.select_by_attribute(:staccato)
|
|
141
|
+
# # Where dataset[:staccato] is not nil
|
|
142
|
+
#
|
|
143
|
+
# @example Select by value
|
|
144
|
+
# results.select_by_attribute(:grade, 0)
|
|
145
|
+
# # Where dataset[:grade] == 0
|
|
29
146
|
def select_by_attribute(attribute, value = nil)
|
|
30
147
|
if value.nil?
|
|
31
148
|
select { |e| !e[:dataset][attribute].nil? }
|
|
@@ -34,11 +151,36 @@ module Musa::Datasets
|
|
|
34
151
|
end.extend(QueryableByDataset)
|
|
35
152
|
end
|
|
36
153
|
|
|
154
|
+
# Filters results by custom condition on dataset.
|
|
155
|
+
#
|
|
156
|
+
# @yieldparam dataset [Hash] event dataset
|
|
157
|
+
# @yieldreturn [Boolean] true to include result
|
|
158
|
+
#
|
|
159
|
+
# @return [Array] filtered results, extended with QueryableByDataset
|
|
160
|
+
#
|
|
161
|
+
# @raise [ArgumentError] if no block given
|
|
162
|
+
#
|
|
163
|
+
# @example Filter by pitch range
|
|
164
|
+
# results.subset { |event| event[:pitch] > 60 && event[:pitch] < 72 }
|
|
165
|
+
#
|
|
166
|
+
# @example Filter by multiple conditions
|
|
167
|
+
# results.subset { |event| event[:grade] == 0 && event[:velocity] > 0 }
|
|
37
168
|
def subset
|
|
38
169
|
raise ArgumentError, "subset needs a block with the inclusion condition on the dataset" unless block_given?
|
|
39
170
|
select { |e| yield e[:dataset] }.extend(QueryableByDataset)
|
|
40
171
|
end
|
|
41
172
|
|
|
173
|
+
# Sorts results by dataset attribute value.
|
|
174
|
+
#
|
|
175
|
+
# First filters to results with the attribute, then sorts by its value.
|
|
176
|
+
#
|
|
177
|
+
# @param attribute [Symbol] dataset attribute to sort by
|
|
178
|
+
#
|
|
179
|
+
# @return [Array] sorted results, extended with QueryableByDataset
|
|
180
|
+
#
|
|
181
|
+
# @example Sort by start time within interval
|
|
182
|
+
# sorted = results.sort_by_attribute(:pitch)
|
|
183
|
+
# # Results sorted by ascending pitch
|
|
42
184
|
def sort_by_attribute(attribute)
|
|
43
185
|
select_by_attribute(attribute).sort_by { |e| e[:dataset][attribute] }.extend(QueryableByDataset)
|
|
44
186
|
end
|
|
@@ -46,4 +188,4 @@ module Musa::Datasets
|
|
|
46
188
|
|
|
47
189
|
private_constant :QueryableByDataset
|
|
48
190
|
end
|
|
49
|
-
end; end
|
|
191
|
+
end; end
|
|
@@ -4,7 +4,111 @@ require_relative '../../series'
|
|
|
4
4
|
module Musa
|
|
5
5
|
module Datasets
|
|
6
6
|
class Score
|
|
7
|
+
# Real-time rendering of scores on sequencers.
|
|
8
|
+
#
|
|
9
|
+
# Render provides the {#render} method for playing back scores on a
|
|
10
|
+
# {Musa::Sequencer::Sequencer}. Events are scheduled at their score times
|
|
11
|
+
# relative to the sequencer's current position.
|
|
12
|
+
#
|
|
13
|
+
# ## Time Calculation
|
|
14
|
+
#
|
|
15
|
+
# Score times are 1-based (first beat is 1), but sequencer waits are
|
|
16
|
+
# 0-based. The conversion is:
|
|
17
|
+
#
|
|
18
|
+
# effective_wait = score_time - 1
|
|
19
|
+
#
|
|
20
|
+
# So score time 1 becomes wait 0 (immediate), time 2 becomes wait 1, etc.
|
|
21
|
+
#
|
|
22
|
+
# ## Nested Scores
|
|
23
|
+
#
|
|
24
|
+
# Scores can contain other scores. When a nested score is encountered,
|
|
25
|
+
# it's rendered recursively at the appropriate time.
|
|
26
|
+
#
|
|
27
|
+
# @example Basic rendering
|
|
28
|
+
# score = Musa::Datasets::Score.new
|
|
29
|
+
# score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
30
|
+
# score.at(2r, add: { pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
31
|
+
#
|
|
32
|
+
# seq = Musa::Sequencer::Sequencer.new(4, 24)
|
|
33
|
+
# score.render(on: seq) do |event|
|
|
34
|
+
# puts "Play #{event[:pitch]} at #{seq.position}"
|
|
35
|
+
# end
|
|
36
|
+
# seq.run
|
|
37
|
+
#
|
|
38
|
+
# @example Nested scores
|
|
39
|
+
# inner = Musa::Datasets::Score.new
|
|
40
|
+
# inner.at(1r, add: { pitch: 67 }.extend(Musa::Datasets::AbsD))
|
|
41
|
+
# inner.at(2r, add: { pitch: 69 }.extend(Musa::Datasets::AbsD))
|
|
42
|
+
#
|
|
43
|
+
# outer = Musa::Datasets::Score.new
|
|
44
|
+
# outer.at(1r, add: { pitch: 60 }.extend(Musa::Datasets::AbsD))
|
|
45
|
+
# outer.at(2r, add: inner) # Nested score
|
|
46
|
+
# # inner plays at sequencer time 2r
|
|
47
|
+
#
|
|
48
|
+
# @see Musa::Sequencer::Sequencer Sequencer for playback
|
|
49
|
+
# @see Score#at Adding events to scores
|
|
7
50
|
module Render
|
|
51
|
+
# Renders score on sequencer.
|
|
52
|
+
#
|
|
53
|
+
# Schedules all events in the score on the sequencer, calling the block
|
|
54
|
+
# for each event at its scheduled time. Score times are converted to
|
|
55
|
+
# sequencer wait times (score_time - 1).
|
|
56
|
+
#
|
|
57
|
+
# Supports nested scores recursively.
|
|
58
|
+
#
|
|
59
|
+
# @param on [Musa::Sequencer::Sequencer] sequencer to render on
|
|
60
|
+
#
|
|
61
|
+
# @yieldparam event [Abs] each event to process
|
|
62
|
+
# Block is called at the scheduled time with the event dataset
|
|
63
|
+
#
|
|
64
|
+
# @return [nil]
|
|
65
|
+
#
|
|
66
|
+
# @raise [ArgumentError] if element is not Abs or Score
|
|
67
|
+
#
|
|
68
|
+
# @example MIDI output
|
|
69
|
+
# require 'midi-communications'
|
|
70
|
+
#
|
|
71
|
+
# score = Musa::Datasets::Score.new
|
|
72
|
+
# score.at(1r, add: { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PDV))
|
|
73
|
+
#
|
|
74
|
+
# midi_out = MIDICommunications::Output.gets
|
|
75
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
76
|
+
#
|
|
77
|
+
# score.render(on: sequencer) do |event|
|
|
78
|
+
# if event[:pitch]
|
|
79
|
+
# midi_out.puts(0x90, event[:pitch], event[:velocity] || 64)
|
|
80
|
+
# sequencer.at event[:duration] do
|
|
81
|
+
# midi_out.puts(0x80, event[:pitch], event[:velocity] || 64)
|
|
82
|
+
# end
|
|
83
|
+
# end
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# sequencer.run
|
|
87
|
+
#
|
|
88
|
+
# @example Console output
|
|
89
|
+
# score = Musa::Datasets::Score.new
|
|
90
|
+
# score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
|
|
91
|
+
#
|
|
92
|
+
# seq = Musa::Sequencer::Sequencer.new(4, 24)
|
|
93
|
+
# score.render(on: seq) do |event|
|
|
94
|
+
# puts "Time #{seq.position}: #{event.inspect}"
|
|
95
|
+
# end
|
|
96
|
+
# seq.run
|
|
97
|
+
#
|
|
98
|
+
# @example Nested score rendering
|
|
99
|
+
# inner = Musa::Datasets::Score.new
|
|
100
|
+
# inner.at(1r, add: { pitch: 67 }.extend(Musa::Datasets::PDV))
|
|
101
|
+
#
|
|
102
|
+
# outer = Musa::Datasets::Score.new
|
|
103
|
+
# outer.at(1r, add: { pitch: 60 }.extend(Musa::Datasets::PDV))
|
|
104
|
+
# outer.at(2r, add: inner)
|
|
105
|
+
#
|
|
106
|
+
# seq = Musa::Sequencer::Sequencer.new(4, 24)
|
|
107
|
+
# outer.render(on: seq) do |event|
|
|
108
|
+
# puts "Event: #{event[:pitch]}"
|
|
109
|
+
# end
|
|
110
|
+
# seq.run
|
|
111
|
+
# # Inner scores automatically rendered at their scheduled times
|
|
8
112
|
def render(on:, &block)
|
|
9
113
|
@score.keys.each do |score_at|
|
|
10
114
|
effective_wait = score_at - 1r
|
|
@@ -32,4 +136,4 @@ module Musa
|
|
|
32
136
|
end
|
|
33
137
|
end
|
|
34
138
|
end
|
|
35
|
-
end
|
|
139
|
+
end
|