musa-dsl 0.30.2 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.version +6 -0
- data/.yardopts +7 -0
- data/Gemfile +0 -1
- data/README.md +227 -6
- data/docs/README.md +83 -0
- data/docs/api-reference.md +86 -0
- data/docs/getting-started/quick-start.md +93 -0
- data/docs/getting-started/tutorial.md +58 -0
- data/docs/subsystems/core-extensions.md +316 -0
- data/docs/subsystems/datasets.md +465 -0
- data/docs/subsystems/generative.md +290 -0
- data/docs/subsystems/matrix.md +63 -0
- data/docs/subsystems/midi.md +123 -0
- data/docs/subsystems/music.md +544 -0
- data/docs/subsystems/musicxml-builder.md +264 -0
- data/docs/subsystems/neumas.md +71 -0
- data/docs/subsystems/repl.md +135 -0
- data/docs/subsystems/sequencer.md +98 -0
- data/docs/subsystems/series.md +302 -0
- data/docs/subsystems/transcription.md +152 -0
- data/docs/subsystems/transport.md +177 -0
- data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
- data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
- data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
- data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
- data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
- data/lib/musa-dsl/core-ext/extension.rb +53 -0
- data/lib/musa-dsl/core-ext/hashify.rb +162 -1
- data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
- data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
- data/lib/musa-dsl/core-ext/with.rb +114 -0
- data/lib/musa-dsl/datasets/dataset.rb +109 -0
- data/lib/musa-dsl/datasets/delta-d.rb +78 -0
- data/lib/musa-dsl/datasets/e.rb +186 -2
- data/lib/musa-dsl/datasets/gdv.rb +279 -2
- data/lib/musa-dsl/datasets/gdvd.rb +201 -0
- data/lib/musa-dsl/datasets/helper.rb +75 -0
- data/lib/musa-dsl/datasets/p.rb +177 -2
- data/lib/musa-dsl/datasets/packed-v.rb +91 -0
- data/lib/musa-dsl/datasets/pdv.rb +136 -1
- data/lib/musa-dsl/datasets/ps.rb +134 -4
- data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
- data/lib/musa-dsl/datasets/score/render.rb +105 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
- data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
- data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
- data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
- data/lib/musa-dsl/datasets/score.rb +279 -0
- data/lib/musa-dsl/datasets/v.rb +88 -0
- data/lib/musa-dsl/generative/darwin.rb +215 -1
- data/lib/musa-dsl/generative/generative-grammar.rb +387 -0
- data/lib/musa-dsl/generative/markov.rb +135 -3
- data/lib/musa-dsl/generative/rules.rb +312 -4
- data/lib/musa-dsl/generative/variatio.rb +286 -2
- data/lib/musa-dsl/logger/logger.rb +267 -2
- data/lib/musa-dsl/matrix/matrix.rb +256 -10
- data/lib/musa-dsl/midi/midi-recorder.rb +113 -2
- data/lib/musa-dsl/midi/midi-voices.rb +275 -4
- data/lib/musa-dsl/music/chord-definition.rb +233 -1
- data/lib/musa-dsl/music/chord-definitions.rb +33 -6
- data/lib/musa-dsl/music/chords.rb +353 -2
- data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +70 -206
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_dominant_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/bebop/bebop_minor_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_major_scale_kind.rb +100 -0
- data/lib/musa-dsl/music/scale_kinds/blues/blues_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/chromatic_scale_kind.rb +79 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/double_harmonic_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/hungarian_minor_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_major_scale_kind.rb +102 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/neapolitan_minor_scale_kind.rb +101 -0
- data/lib/musa-dsl/music/scale_kinds/ethnic/phrygian_dominant_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/harmonic_major/harmonic_major_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/major_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/altered_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/dorian_b2_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/locrian_sharp2_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_augmented_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/lydian_dominant_scale_kind.rb +106 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/melodic_minor_scale_kind.rb +104 -0
- data/lib/musa-dsl/music/scale_kinds/melodic_minor/mixolydian_b6_scale_kind.rb +103 -0
- data/lib/musa-dsl/music/scale_kinds/minor_harmonic_scale_kind.rb +125 -0
- data/lib/musa-dsl/music/scale_kinds/minor_natural_scale_kind.rb +123 -0
- data/lib/musa-dsl/music/scale_kinds/modes/dorian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/locrian_scale_kind.rb +114 -0
- data/lib/musa-dsl/music/scale_kinds/modes/lydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/mixolydian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/modes/phrygian_scale_kind.rb +111 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_major_scale_kind.rb +93 -0
- data/lib/musa-dsl/music/scale_kinds/pentatonic/pentatonic_minor_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_hw_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/diminished_wh_scale_kind.rb +110 -0
- data/lib/musa-dsl/music/scale_kinds/symmetric/whole_tone_scale_kind.rb +99 -0
- data/lib/musa-dsl/music/scale_systems/equally_tempered_12_tone_scale_system.rb +80 -0
- data/lib/musa-dsl/music/scale_systems/twelve_semitones_scale_system.rb +60 -0
- data/lib/musa-dsl/music/scales.rb +1384 -40
- data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
- data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
- data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
- data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
- data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
- data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
- data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
- data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
- data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
- data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
- data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
- data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
- data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
- data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
- data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
- data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
- data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
- data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
- data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
- data/lib/musa-dsl/neumas/neumas.rb +67 -0
- data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
- data/lib/musa-dsl/repl/repl.rb +550 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
- data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
- data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
- data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
- data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
- data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
- data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
- data/lib/musa-dsl/series/array-to-serie.rb +37 -1
- data/lib/musa-dsl/series/base-series.rb +843 -5
- data/lib/musa-dsl/series/buffer-serie.rb +54 -0
- data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +64 -0
- data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
- data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
- data/lib/musa-dsl/series/proxy-serie.rb +67 -0
- data/lib/musa-dsl/series/quantizer-serie.rb +57 -7
- data/lib/musa-dsl/series/queue-serie.rb +78 -0
- data/lib/musa-dsl/series/series-composer.rb +701 -0
- data/lib/musa-dsl/series/timed-serie.rb +473 -28
- data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
- data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
- data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
- data/lib/musa-dsl/transcription/transcription.rb +265 -0
- data/lib/musa-dsl/transport/clock.rb +125 -0
- data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
- data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
- data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
- data/lib/musa-dsl/transport/timer-clock.rb +183 -1
- data/lib/musa-dsl/transport/timer.rb +83 -0
- data/lib/musa-dsl/transport/transport.rb +318 -0
- data/lib/musa-dsl/version.rb +2 -1
- data/lib/musa-dsl.rb +132 -25
- data/musa-dsl.gemspec +25 -18
- metadata +158 -16
|
@@ -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
|
|
@@ -1,8 +1,84 @@
|
|
|
1
1
|
require 'prime'
|
|
2
2
|
|
|
3
|
+
# PDV event processing for MusicXML export.
|
|
4
|
+
#
|
|
5
|
+
# Converts {PDV} (Pitch/Duration/Velocity) events to MusicXML notes and rests.
|
|
6
|
+
# Handles pitch mapping, duration decomposition, ties, articulations, and
|
|
7
|
+
# ornaments.
|
|
8
|
+
#
|
|
9
|
+
# ## Processing Steps
|
|
10
|
+
#
|
|
11
|
+
# 1. Extract pitch, octave, and accidentals from MIDI pitch
|
|
12
|
+
# 2. Calculate effective duration within measure (may span bars)
|
|
13
|
+
# 3. Decompose duration into MusicXML-compatible note values
|
|
14
|
+
# 4. Add backup/forward if needed for voice positioning
|
|
15
|
+
# 5. Create MusicXML note/rest elements with all attributes
|
|
16
|
+
#
|
|
17
|
+
# ## Articulations & Ornaments Supported
|
|
18
|
+
#
|
|
19
|
+
# - **:st** → staccato / staccatissimo (1 or > 1)
|
|
20
|
+
# - **:tr** → trill
|
|
21
|
+
# - **:mor** → mordent (:down/:low) or inverted mordent (:up/true)
|
|
22
|
+
# - **:turn** → turn (:up/true) or inverted turn (:down/:low)
|
|
23
|
+
# - **:grace** → grace note (with slur)
|
|
24
|
+
# - **:graced** → note receiving grace note (with slur)
|
|
25
|
+
# - **:voice** → voice number for polyphony
|
|
26
|
+
#
|
|
27
|
+
# ## Ties Across Measures
|
|
28
|
+
#
|
|
29
|
+
# Notes spanning bar lines are automatically tied. Duration is decomposed
|
|
30
|
+
# and tie start/stop/continue markers added appropriately.
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
3
33
|
module Musa::Datasets::Score::ToMXML
|
|
4
34
|
using Musa::Extension::InspectNice
|
|
5
35
|
|
|
36
|
+
# Processes PDV event to MusicXML note or rest.
|
|
37
|
+
#
|
|
38
|
+
# Converts a single PDV event to one or more MusicXML note/rest elements.
|
|
39
|
+
# Handles duration decomposition, ties, backup/forward for polyphony,
|
|
40
|
+
# and all articulations/ornaments.
|
|
41
|
+
#
|
|
42
|
+
# @param measure [Musa::MusicXML::Builder::Measure] target measure
|
|
43
|
+
# @param bar [Integer] bar number (1-based)
|
|
44
|
+
# @param divisions_per_bar [Integer] total divisions in bar
|
|
45
|
+
# @param element [Hash] event hash from score query
|
|
46
|
+
# Contains :start, :finish, :dataset keys
|
|
47
|
+
# @param pointer [Rational] current position in bar (0-1)
|
|
48
|
+
# @param logger [Musa::Logger::Logger] logger for debugging
|
|
49
|
+
# @param do_log [Boolean] enable logging
|
|
50
|
+
#
|
|
51
|
+
# @return [Rational] updated pointer position
|
|
52
|
+
#
|
|
53
|
+
# @raise [NotImplementedError] if tuplet ratios found (not yet supported)
|
|
54
|
+
#
|
|
55
|
+
# @example Simple quarter note
|
|
56
|
+
# element = {
|
|
57
|
+
# start: 1r,
|
|
58
|
+
# finish: 2r,
|
|
59
|
+
# dataset: { pitch: 60, duration: 1r }.extend(Musa::Datasets::PDV)
|
|
60
|
+
# }
|
|
61
|
+
# pointer = process_pdv(measure, 1, 96, element, 0r, logger, false)
|
|
62
|
+
# # Adds C4 quarter note, returns 1r
|
|
63
|
+
#
|
|
64
|
+
# @example Rest
|
|
65
|
+
# element = {
|
|
66
|
+
# start: 1r,
|
|
67
|
+
# finish: 2r,
|
|
68
|
+
# dataset: { pitch: :silence, duration: 1r }.extend(Musa::Datasets::PDV)
|
|
69
|
+
# }
|
|
70
|
+
# pointer = process_pdv(measure, 1, 96, element, 0r, logger, false)
|
|
71
|
+
# # Adds quarter rest, returns 1r
|
|
72
|
+
#
|
|
73
|
+
# @example Note with articulation
|
|
74
|
+
# dataset = { pitch: 64, duration: 1/2r, st: true }.extend(Musa::Datasets::PDV)
|
|
75
|
+
# # Adds staccato eighth note
|
|
76
|
+
#
|
|
77
|
+
# @example Tied note across bar
|
|
78
|
+
# element = { start: 1r, finish: 3r, dataset: { pitch: 60, duration: 2r } }
|
|
79
|
+
# # Automatically tied: tie-start in bar 1, tie-stop in bar 2
|
|
80
|
+
#
|
|
81
|
+
# @api private
|
|
6
82
|
private def process_pdv(measure, bar, divisions_per_bar, element, pointer, logger, do_log)
|
|
7
83
|
|
|
8
84
|
pitch, octave, sharps = pitch_and_octave_and_sharps(element[:dataset])
|
|
@@ -127,6 +203,34 @@ module Musa::Datasets::Score::ToMXML
|
|
|
127
203
|
pointer
|
|
128
204
|
end
|
|
129
205
|
|
|
206
|
+
# Converts MIDI pitch to note name, octave, and accidental.
|
|
207
|
+
#
|
|
208
|
+
# Maps MIDI pitch number (0-127) to MusicXML pitch representation.
|
|
209
|
+
# Middle C (MIDI 60) = C4 in scientific pitch notation.
|
|
210
|
+
#
|
|
211
|
+
# @param pdv [Hash] PDV dataset with :pitch key
|
|
212
|
+
#
|
|
213
|
+
# @return [Array(String, Integer, Integer), Array(Symbol, nil, nil)]
|
|
214
|
+
# - For pitches: [note_name, octave, sharps]
|
|
215
|
+
# - For silence: [:silence, nil, nil]
|
|
216
|
+
#
|
|
217
|
+
# @example Middle C
|
|
218
|
+
# pitch_and_octave_and_sharps({ pitch: 60 })
|
|
219
|
+
# # => ["C", 4, 0]
|
|
220
|
+
#
|
|
221
|
+
# @example C#4
|
|
222
|
+
# pitch_and_octave_and_sharps({ pitch: 61 })
|
|
223
|
+
# # => ["C", 4, 1]
|
|
224
|
+
#
|
|
225
|
+
# @example A4 (440Hz)
|
|
226
|
+
# pitch_and_octave_and_sharps({ pitch: 69 })
|
|
227
|
+
# # => ["A", 4, 0]
|
|
228
|
+
#
|
|
229
|
+
# @example Rest
|
|
230
|
+
# pitch_and_octave_and_sharps({ pitch: :silence })
|
|
231
|
+
# # => [:silence, nil, nil]
|
|
232
|
+
#
|
|
233
|
+
# @api private
|
|
130
234
|
private def pitch_and_octave_and_sharps(pdv)
|
|
131
235
|
if pdv[:pitch] == :silence
|
|
132
236
|
[:silence, nil, nil]
|
|
@@ -145,16 +249,49 @@ module Musa::Datasets::Score::ToMXML
|
|
|
145
249
|
end
|
|
146
250
|
end
|
|
147
251
|
|
|
252
|
+
# Converts MIDI velocity to dynamics index.
|
|
253
|
+
#
|
|
254
|
+
# Maps MIDI velocity (0-127) to dynamics marking index (0-10).
|
|
255
|
+
# Used for determining dynamics from velocity values.
|
|
256
|
+
#
|
|
257
|
+
# @param midi_velocity [Integer, nil] MIDI velocity value
|
|
258
|
+
#
|
|
259
|
+
# @return [Integer, nil] dynamics index (0-10), or nil if no velocity
|
|
260
|
+
#
|
|
261
|
+
# @example Pianissimo
|
|
262
|
+
# dynamics_index_of(16) # => 3 (ppp)
|
|
263
|
+
#
|
|
264
|
+
# @example Mezzo-forte
|
|
265
|
+
# dynamics_index_of(64) # => 6 (mf)
|
|
266
|
+
#
|
|
267
|
+
# @example Fortissimo
|
|
268
|
+
# dynamics_index_of(100) # => 9 (ff)
|
|
269
|
+
#
|
|
270
|
+
# @api private
|
|
148
271
|
private def dynamics_index_of(midi_velocity)
|
|
149
272
|
return nil unless midi_velocity
|
|
150
273
|
|
|
151
274
|
# ppp = midi 16 ... fff = midi 127
|
|
152
275
|
# mp = dynamics index 6; dynamics = 0..10
|
|
153
276
|
# TODO create a customizable MIDI velocity to score dynamics bidirectional conversor
|
|
154
|
-
[0..0, 1..1, 2..8, 9..16, 17..33, 34..
|
|
277
|
+
[0..0, 1..1, 2..8, 9..16, 17..33, 34..48, 49..64, 65..80, 81..96, 97..112, 113..127]
|
|
155
278
|
.index { |r| r.cover? midi_velocity.round.to_i }
|
|
156
279
|
end
|
|
157
280
|
|
|
281
|
+
# Converts dynamics index to MusicXML dynamics string.
|
|
282
|
+
#
|
|
283
|
+
# Maps dynamics index (0-10) to standard dynamics marking string.
|
|
284
|
+
#
|
|
285
|
+
# @param dynamics_index [Integer, nil] dynamics index
|
|
286
|
+
#
|
|
287
|
+
# @return [String, nil] dynamics marking string, or nil if no index
|
|
288
|
+
#
|
|
289
|
+
# @example
|
|
290
|
+
# dynamics_to_string(3) # => "ppp"
|
|
291
|
+
# dynamics_to_string(6) # => "mp"
|
|
292
|
+
# dynamics_to_string(9) # => "ff"
|
|
293
|
+
#
|
|
294
|
+
# @api private
|
|
158
295
|
private def dynamics_to_string(dynamics_index)
|
|
159
296
|
return nil unless dynamics_index
|
|
160
297
|
['pppppp', 'ppppp', 'pppp', 'ppp', 'pp', 'p', 'mp', 'mf', 'f', 'ff', 'fff'][dynamics_index.round.to_i]
|
|
@@ -1,9 +1,120 @@
|
|
|
1
|
+
# PS event processing for MusicXML export.
|
|
2
|
+
#
|
|
3
|
+
# Converts {PS} (Pitch Series) events to MusicXML dynamics markings.
|
|
4
|
+
# Handles crescendo, diminuendo wedges (hairpins), and static dynamics markings.
|
|
5
|
+
#
|
|
6
|
+
# ## Processing Steps
|
|
7
|
+
#
|
|
8
|
+
# 1. Extract dynamics type (:crescendo, :diminuendo, or :dynamics)
|
|
9
|
+
# 2. For wedges: determine if it's the start or end of the marking
|
|
10
|
+
# 3. Add dynamics marking at wedge start/end if level changed
|
|
11
|
+
# 4. Add wedge element with appropriate type and niente attribute
|
|
12
|
+
# 5. Track last dynamics to avoid redundant markings
|
|
13
|
+
#
|
|
14
|
+
# ## Dynamics Types Supported
|
|
15
|
+
#
|
|
16
|
+
# - **:crescendo** → crescendo wedge (hairpin opening)
|
|
17
|
+
# - Uses :from attribute for starting dynamics level
|
|
18
|
+
# - Uses :to attribute for ending dynamics level
|
|
19
|
+
# - Supports niente (from silence) when :from == 0
|
|
20
|
+
#
|
|
21
|
+
# - **:diminuendo** → diminuendo wedge (hairpin closing)
|
|
22
|
+
# - Uses :from attribute for starting dynamics level
|
|
23
|
+
# - Uses :to attribute for ending dynamics level
|
|
24
|
+
# - Supports niente (to silence) when :to == 0
|
|
25
|
+
#
|
|
26
|
+
# - **:dynamics** → static dynamics marking (pp, mf, ff, etc.)
|
|
27
|
+
# - Uses :from attribute for dynamics level
|
|
28
|
+
# - No wedge created, only dynamics text
|
|
29
|
+
#
|
|
30
|
+
# ## Dynamics Levels
|
|
31
|
+
#
|
|
32
|
+
# Dynamics levels are numeric indices (0-10) converted to standard markings:
|
|
33
|
+
# - 0: silence (niente)
|
|
34
|
+
# - 1-3: ppp range
|
|
35
|
+
# - 4-5: pp-p range
|
|
36
|
+
# - 6: mp
|
|
37
|
+
# - 7: mf
|
|
38
|
+
# - 8-9: f-ff range
|
|
39
|
+
# - 10: fff
|
|
40
|
+
#
|
|
41
|
+
# ## Context Tracking
|
|
42
|
+
#
|
|
43
|
+
# Uses DynamicsContext to track the last dynamics marking, preventing
|
|
44
|
+
# duplicate markings when consecutive events have the same level.
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
1
47
|
module Musa::Datasets::Score::ToMXML
|
|
2
48
|
using Musa::Extension::InspectNice
|
|
3
49
|
|
|
50
|
+
# Context for tracking dynamics state across events.
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
4
53
|
DynamicsContext = Struct.new(:last_dynamics)
|
|
5
54
|
private_constant :DynamicsContext
|
|
6
55
|
|
|
56
|
+
# Processes PS event to MusicXML dynamics marking.
|
|
57
|
+
#
|
|
58
|
+
# Converts a single PS event to one or more MusicXML dynamics/wedge elements.
|
|
59
|
+
# Handles crescendo/diminuendo wedges and static dynamics markings. Tracks
|
|
60
|
+
# context to avoid redundant markings.
|
|
61
|
+
#
|
|
62
|
+
# @param measure [Musa::MusicXML::Builder::Measure] target measure
|
|
63
|
+
# @param element [Hash] event hash from score query
|
|
64
|
+
# Contains :dataset (PS event), :change (:start/:finish for wedges)
|
|
65
|
+
# @param context [DynamicsContext, nil] dynamics tracking context
|
|
66
|
+
# @param logger [Musa::Logger::Logger] logger for debugging
|
|
67
|
+
# @param do_log [Boolean] enable logging
|
|
68
|
+
#
|
|
69
|
+
# @return [DynamicsContext] updated context with last dynamics
|
|
70
|
+
#
|
|
71
|
+
# @example Crescendo from pp to ff
|
|
72
|
+
# element_start = {
|
|
73
|
+
# dataset: { type: :crescendo, from: 4, to: 9, duration: 2r }.extend(Musa::Datasets::PS),
|
|
74
|
+
# change: :start
|
|
75
|
+
# }
|
|
76
|
+
# context = process_ps(measure, element_start, nil, logger, false)
|
|
77
|
+
# # Adds "pp" dynamics and crescendo wedge start
|
|
78
|
+
#
|
|
79
|
+
# element_finish = {
|
|
80
|
+
# dataset: { type: :crescendo, from: 4, to: 9, duration: 2r }.extend(Musa::Datasets::PS),
|
|
81
|
+
# change: :finish
|
|
82
|
+
# }
|
|
83
|
+
# context = process_ps(measure, element_finish, context, logger, false)
|
|
84
|
+
# # Adds wedge stop and "ff" dynamics
|
|
85
|
+
#
|
|
86
|
+
# @example Diminuendo to silence (niente)
|
|
87
|
+
# element_start = {
|
|
88
|
+
# dataset: { type: :diminuendo, from: 7, to: 0, duration: 1r }.extend(Musa::Datasets::PS),
|
|
89
|
+
# change: :start
|
|
90
|
+
# }
|
|
91
|
+
# process_ps(measure, element_start, nil, logger, false)
|
|
92
|
+
# # Adds "mf" dynamics and diminuendo wedge start
|
|
93
|
+
#
|
|
94
|
+
# element_finish = {
|
|
95
|
+
# dataset: { type: :diminuendo, from: 7, to: 0, duration: 1r }.extend(Musa::Datasets::PS),
|
|
96
|
+
# change: :finish
|
|
97
|
+
# }
|
|
98
|
+
# process_ps(measure, element_finish, context, logger, false)
|
|
99
|
+
# # Adds wedge stop with niente=true (diminuendo to silence)
|
|
100
|
+
#
|
|
101
|
+
# @example Crescendo from silence (niente)
|
|
102
|
+
# element_start = {
|
|
103
|
+
# dataset: { type: :crescendo, from: 0, to: 6, duration: 1r }.extend(Musa::Datasets::PS),
|
|
104
|
+
# change: :start
|
|
105
|
+
# }
|
|
106
|
+
# process_ps(measure, element_start, nil, logger, false)
|
|
107
|
+
# # Adds crescendo wedge with niente=true (from silence)
|
|
108
|
+
#
|
|
109
|
+
# @example Static dynamics marking
|
|
110
|
+
# element = {
|
|
111
|
+
# dataset: { type: :dynamics, from: 8, duration: 0r }.extend(Musa::Datasets::PS),
|
|
112
|
+
# change: :start
|
|
113
|
+
# }
|
|
114
|
+
# process_ps(measure, element, nil, logger, false)
|
|
115
|
+
# # Adds "f" dynamics marking only
|
|
116
|
+
#
|
|
117
|
+
# @api private
|
|
7
118
|
private def process_ps(measure, element, context, logger, do_log)
|
|
8
119
|
context ||= DynamicsContext.new
|
|
9
120
|
|