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
|
@@ -2,36 +2,301 @@ require 'logger'
|
|
|
2
2
|
require_relative '../core-ext/inspect-nice'
|
|
3
3
|
|
|
4
4
|
module Musa
|
|
5
|
+
# Logging utilities for Musa DSL.
|
|
6
|
+
#
|
|
7
|
+
# Provides a specialized logger that integrates with the sequencer to display
|
|
8
|
+
# musical position information alongside standard log messages.
|
|
9
|
+
#
|
|
10
|
+
# ## Purpose
|
|
11
|
+
#
|
|
12
|
+
# When working with sequenced musical compositions, it's crucial to know at
|
|
13
|
+
# what point in musical time events occur. This logger automatically prepends
|
|
14
|
+
# the sequencer's current position (in bars) to each log entry, making
|
|
15
|
+
# debugging and monitoring much more intuitive.
|
|
16
|
+
#
|
|
17
|
+
# ## Integration with Sequencer
|
|
18
|
+
#
|
|
19
|
+
# The logger reads the sequencer's position at the moment each log message
|
|
20
|
+
# is generated. Since positions are typically Rational numbers representing
|
|
21
|
+
# bars (e.g., 4/4 = 1 bar), the InspectNice refinement ensures they display
|
|
22
|
+
# in a readable decimal format.
|
|
23
|
+
#
|
|
24
|
+
# ## Common Use Cases
|
|
25
|
+
#
|
|
26
|
+
# - Debugging sequencer timing issues
|
|
27
|
+
# - Monitoring MIDI output events with their musical timestamps
|
|
28
|
+
# - Tracking series evaluation progress
|
|
29
|
+
# - Logging voice state changes during playback
|
|
30
|
+
# - Performance analysis and timing verification
|
|
31
|
+
#
|
|
32
|
+
# @example Complete workflow
|
|
33
|
+
# require 'musa-dsl'
|
|
34
|
+
#
|
|
35
|
+
# # Setup
|
|
36
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
37
|
+
# logger = Musa::Logger::Logger.new(sequencer: sequencer)
|
|
38
|
+
# logger.level = Logger::INFO
|
|
39
|
+
#
|
|
40
|
+
# # In your composition
|
|
41
|
+
# sequencer.at 0 do
|
|
42
|
+
# logger.info "Composition started"
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# sequencer.at 4 do
|
|
46
|
+
# logger.info "First phrase complete"
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# sequencer.run
|
|
50
|
+
#
|
|
51
|
+
# # Output:
|
|
52
|
+
# # 0.000: [INFO] Composition started
|
|
53
|
+
# # 4.000: [INFO] First phrase complete
|
|
54
|
+
#
|
|
55
|
+
# @see Musa::Logger::Logger
|
|
56
|
+
# @see Musa::Sequencer::Sequencer
|
|
57
|
+
# @see Musa::Extension::InspectNice
|
|
5
58
|
module Logger
|
|
59
|
+
# Custom logger that displays sequencer position with log messages.
|
|
60
|
+
#
|
|
61
|
+
# This logger extends Ruby's standard Logger class to prepend the current
|
|
62
|
+
# sequencer position to each log message, making it easy to track events
|
|
63
|
+
# in musical time during composition and playback.
|
|
64
|
+
#
|
|
65
|
+
# ## Features
|
|
66
|
+
#
|
|
67
|
+
# - Automatic sequencer position formatting in log output
|
|
68
|
+
# - Configurable position precision (integer and decimal digits)
|
|
69
|
+
# - Conditional formatting (position only shown when sequencer is provided)
|
|
70
|
+
# - Uses InspectNice refinements for better Rational display
|
|
71
|
+
# - Defaults to STDERR output with WARN level
|
|
72
|
+
#
|
|
73
|
+
# ## Log Format
|
|
74
|
+
#
|
|
75
|
+
# The formatted log output follows this pattern:
|
|
76
|
+
#
|
|
77
|
+
# [position]: [LEVEL] [progname] message
|
|
78
|
+
#
|
|
79
|
+
# Where:
|
|
80
|
+
#
|
|
81
|
+
# - `position` is the sequencer position (only if sequencer provided)
|
|
82
|
+
# - `LEVEL` is the severity level (omitted for DEBUG)
|
|
83
|
+
# - `progname` is the program/module name (optional)
|
|
84
|
+
# - `message` is the actual log message
|
|
85
|
+
#
|
|
86
|
+
# @example Basic usage without sequencer
|
|
87
|
+
# require 'musa-dsl'
|
|
88
|
+
#
|
|
89
|
+
# logger = Musa::Logger::Logger.new
|
|
90
|
+
# logger.warn "Something happened"
|
|
91
|
+
# # Output: [WARN] Something happened
|
|
92
|
+
#
|
|
93
|
+
# @example With sequencer integration
|
|
94
|
+
# require 'musa-dsl'
|
|
95
|
+
#
|
|
96
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
97
|
+
# logger = Musa::Logger::Logger.new(sequencer: sequencer)
|
|
98
|
+
#
|
|
99
|
+
# sequencer.at 4.5r do
|
|
100
|
+
# logger.info "Note played"
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
# sequencer.run
|
|
104
|
+
#
|
|
105
|
+
# # Output: 4.500: [INFO] Note played
|
|
106
|
+
#
|
|
107
|
+
# @example With custom position format
|
|
108
|
+
# require 'musa-dsl'
|
|
109
|
+
#
|
|
110
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
111
|
+
#
|
|
112
|
+
# # 5 integer digits, 2 decimal places
|
|
113
|
+
# logger = Musa::Logger::Logger.new(
|
|
114
|
+
# sequencer: sequencer,
|
|
115
|
+
# position_format: 5.2
|
|
116
|
+
# )
|
|
117
|
+
# logger.level = Logger::DEBUG
|
|
118
|
+
#
|
|
119
|
+
# # At position 123.456:
|
|
120
|
+
# sequencer.at 123.456r do
|
|
121
|
+
# logger.debug "Debugging info"
|
|
122
|
+
# end
|
|
123
|
+
#
|
|
124
|
+
# sequencer.run
|
|
125
|
+
#
|
|
126
|
+
# # Output: 123.46: Debugging info
|
|
127
|
+
#
|
|
128
|
+
# @example With program name
|
|
129
|
+
# require 'musa-dsl'
|
|
130
|
+
#
|
|
131
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
132
|
+
# logger = Musa::Logger::Logger.new(sequencer: sequencer)
|
|
133
|
+
# logger.level = Logger::INFO
|
|
134
|
+
#
|
|
135
|
+
# sequencer.at 4.5r do
|
|
136
|
+
# logger.info('MIDIVoice') { "Playing note 60" }
|
|
137
|
+
# end
|
|
138
|
+
#
|
|
139
|
+
# sequencer.run
|
|
140
|
+
#
|
|
141
|
+
# # Output: 4.500: [INFO] [MIDIVoice] Playing note 60
|
|
142
|
+
#
|
|
143
|
+
# @example Real-world scenario with multiple components
|
|
144
|
+
# require 'musa-dsl'
|
|
145
|
+
#
|
|
146
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
147
|
+
# logger = Musa::Logger::Logger.new(sequencer: sequencer)
|
|
148
|
+
# logger.level = Logger::DEBUG
|
|
149
|
+
#
|
|
150
|
+
# # Different components log at different times
|
|
151
|
+
# sequencer.at 0 do
|
|
152
|
+
# logger.info('Transport') { "Starting playback" }
|
|
153
|
+
# end
|
|
154
|
+
#
|
|
155
|
+
# sequencer.at 1.5r do
|
|
156
|
+
# logger.debug('Series') { "Evaluating next value" }
|
|
157
|
+
# end
|
|
158
|
+
#
|
|
159
|
+
# sequencer.at 2.25r do
|
|
160
|
+
# logger.warn('MIDIVoice') { "Note overflow detected" }
|
|
161
|
+
# end
|
|
162
|
+
#
|
|
163
|
+
# sequencer.run
|
|
164
|
+
#
|
|
165
|
+
# # Output:
|
|
166
|
+
# # 0.000: [INFO] [Transport] Starting playback
|
|
167
|
+
# # 1.500: [Series] Evaluating next value
|
|
168
|
+
# # 2.250: [WARN] [MIDIVoice] Note overflow detected
|
|
169
|
+
#
|
|
170
|
+
# @example Changing log level dynamically
|
|
171
|
+
# require 'musa-dsl'
|
|
172
|
+
#
|
|
173
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
174
|
+
# logger = Musa::Logger::Logger.new(sequencer: sequencer)
|
|
175
|
+
# logger.level = Logger::DEBUG # Show all messages
|
|
176
|
+
#
|
|
177
|
+
# # Later, reduce verbosity
|
|
178
|
+
# logger.level = Logger::WARN # Only warnings and errors
|
|
179
|
+
#
|
|
180
|
+
# @see Musa::Sequencer::Sequencer
|
|
181
|
+
# @see Musa::Extension::InspectNice
|
|
182
|
+
# @note The logger inherits all standard Ruby Logger methods (debug, info, warn, error, fatal).
|
|
183
|
+
# @note Position values are formatted as floating point for readability, even though
|
|
184
|
+
# the sequencer internally uses Rational numbers.
|
|
6
185
|
class Logger < ::Logger
|
|
7
186
|
using Musa::Extension::InspectNice
|
|
8
187
|
|
|
188
|
+
# Creates a new logger with optional sequencer integration.
|
|
189
|
+
#
|
|
190
|
+
# @param sequencer [Musa::Sequencer::Sequencer, nil] sequencer whose position
|
|
191
|
+
# will be displayed in log messages. When nil, position is not shown.
|
|
192
|
+
# @param position_format [Numeric, nil] format specification for position display.
|
|
193
|
+
# The integer part specifies the number of digits before the decimal point,
|
|
194
|
+
# and the decimal part (×10) specifies digits after the decimal point.
|
|
195
|
+
# Defaults to 3.3 (3 integer digits, 3 decimal places).
|
|
196
|
+
#
|
|
197
|
+
# @example Position format examples
|
|
198
|
+
# require 'musa-dsl'
|
|
199
|
+
#
|
|
200
|
+
# sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
201
|
+
#
|
|
202
|
+
# # Different formats for position display:
|
|
203
|
+
# # 3.3 => " 4.500" (3 digits, 3 decimals) - default
|
|
204
|
+
# # 5.2 => " 123.46" (5 digits, 2 decimals)
|
|
205
|
+
# # 2.0 => " 4" (2 digits, no decimals)
|
|
206
|
+
# # 4.4 => " 4.5000" (4 digits, 4 decimals)
|
|
207
|
+
#
|
|
208
|
+
# logger_compact = Musa::Logger::Logger.new(sequencer: sequencer, position_format: 2.0)
|
|
209
|
+
# logger_precise = Musa::Logger::Logger.new(sequencer: sequencer, position_format: 4.4)
|
|
210
|
+
#
|
|
211
|
+
# @note The logger outputs to STDERR by default with level set to WARN.
|
|
212
|
+
# @note Uses InspectNice refinements for better formatting of Rationals and Hashes.
|
|
213
|
+
# @note The sequencer's position is read at log time, not at event scheduling time.
|
|
214
|
+
# This means the position reflects when the log message is actually generated.
|
|
9
215
|
def initialize(sequencer: nil, position_format: nil)
|
|
10
216
|
super STDERR, level: WARN
|
|
11
217
|
|
|
218
|
+
# Store sequencer reference for position queries in formatter
|
|
12
219
|
@sequencer = sequencer
|
|
13
|
-
@position_format = position_format || 3.3
|
|
14
220
|
|
|
221
|
+
# Store position format specification
|
|
222
|
+
@position_format = position_format || 3.3
|
|
15
223
|
|
|
224
|
+
# Custom formatter that integrates sequencer position with log messages.
|
|
225
|
+
#
|
|
226
|
+
# This proc is called by Ruby's Logger for each log entry. It captures
|
|
227
|
+
# @sequencer and @position_format from the enclosing scope to format
|
|
228
|
+
# messages with musical timing information.
|
|
229
|
+
#
|
|
230
|
+
# The formatter constructs messages in the format:
|
|
231
|
+
# [position]: [LEVEL] [progname] message
|
|
232
|
+
#
|
|
233
|
+
# Position calculation:
|
|
234
|
+
#
|
|
235
|
+
# - Splits position_format into integer and decimal parts
|
|
236
|
+
# - Example: 3.3 => 3 integer digits + 3 decimal digits
|
|
237
|
+
# - Formats sequencer position with calculated precision
|
|
238
|
+
# - Right-aligns position in the allocated width
|
|
239
|
+
#
|
|
240
|
+
# Severity handling:
|
|
241
|
+
#
|
|
242
|
+
# - DEBUG level: severity not shown in output
|
|
243
|
+
# - Other levels: shown as [WARN], [INFO], [ERROR], [FATAL]
|
|
244
|
+
#
|
|
245
|
+
# Spacing:
|
|
246
|
+
#
|
|
247
|
+
# - Adds separator space only if position, level, or progname are present
|
|
248
|
+
# - Empty messages output just a newline
|
|
249
|
+
#
|
|
250
|
+
# @param severity [String] log level (DEBUG, INFO, WARN, ERROR, FATAL)
|
|
251
|
+
# @param time [Time] timestamp of the log event (not used in current implementation)
|
|
252
|
+
# @param progname [String, nil] program/component name
|
|
253
|
+
# @param msg [String, nil] the actual log message
|
|
254
|
+
# @return [String] formatted log line with newline
|
|
16
255
|
self.formatter = proc do |severity, time, progname, msg|
|
|
256
|
+
# Omit severity label for DEBUG level
|
|
17
257
|
level = "[#{severity}] " unless severity == 'DEBUG'
|
|
18
258
|
|
|
19
259
|
if msg
|
|
260
|
+
# Calculate and format sequencer position if available
|
|
20
261
|
position = if @sequencer
|
|
262
|
+
# Extract integer and decimal digit counts from position_format
|
|
263
|
+
# e.g., 3.3 => integer_digits=3, decimal_digits=3
|
|
21
264
|
integer_digits = @position_format.to_i
|
|
22
265
|
decimal_digits = ((@position_format - integer_digits) * 10).round
|
|
23
266
|
|
|
24
|
-
|
|
267
|
+
# Format position: total width includes digits + decimal point + ': '
|
|
268
|
+
# Right-aligned to keep positions visually aligned in logs
|
|
269
|
+
"%#{integer_digits + decimal_digits + 1}s: " % ("%.#{decimal_digits}f" % @sequencer.position.to_f)
|
|
25
270
|
end
|
|
26
271
|
|
|
272
|
+
# Wrap progname in brackets if provided
|
|
27
273
|
progname = "[#{progname}]" if progname
|
|
28
274
|
|
|
275
|
+
# Construct final message with conditional spacing
|
|
29
276
|
"#{position}#{level}#{progname}#{' ' if position || level || progname}#{msg}\n"
|
|
30
277
|
else
|
|
278
|
+
# Empty message case
|
|
31
279
|
"\n"
|
|
32
280
|
end
|
|
33
281
|
end
|
|
34
282
|
end
|
|
283
|
+
|
|
284
|
+
# Override level getter to handle encoding compatibility issues.
|
|
285
|
+
#
|
|
286
|
+
# Ruby's Logger (>= 1.7.0) has an encoding bug in level_override that causes
|
|
287
|
+
# Encoding::CompatibilityError when mixing UTF-8 strings from Musa with
|
|
288
|
+
# Logger's BINARY (ASCII-8BIT) internal strings.
|
|
289
|
+
#
|
|
290
|
+
# This override catches the encoding error and returns the level directly
|
|
291
|
+
# from the instance variable, bypassing the buggy level_override method.
|
|
292
|
+
#
|
|
293
|
+
# @return [Integer] current log level (DEBUG=0, INFO=1, WARN=2, ERROR=3, FATAL=4)
|
|
294
|
+
def level
|
|
295
|
+
super
|
|
296
|
+
rescue Encoding::CompatibilityError
|
|
297
|
+
# Bypass level_override and return raw level
|
|
298
|
+
@level
|
|
299
|
+
end
|
|
35
300
|
end
|
|
36
301
|
end
|
|
37
302
|
end
|
|
@@ -1,15 +1,157 @@
|
|
|
1
1
|
require 'matrix'
|
|
2
2
|
|
|
3
|
+
require_relative '../datasets/v'
|
|
3
4
|
require_relative '../datasets/p'
|
|
4
5
|
require_relative '../sequencer'
|
|
5
6
|
|
|
6
7
|
module Musa
|
|
7
|
-
module Matrix
|
|
8
|
-
## TODO should exist this module?
|
|
9
|
-
end
|
|
10
|
-
|
|
11
8
|
module Extension
|
|
9
|
+
# Refinements for Array and Matrix classes to support musical structure conversions.
|
|
10
|
+
#
|
|
11
|
+
# These refinements add methods to convert between matrix representations and
|
|
12
|
+
# Musa's P (point sequence) format, which is used extensively in the DSL for
|
|
13
|
+
# representing musical gestures and trajectories.
|
|
14
|
+
#
|
|
15
|
+
# ## Background
|
|
16
|
+
#
|
|
17
|
+
# In Musa DSL, musical gestures are often represented as sequences of points
|
|
18
|
+
# in multidimensional space, where dimensions can represent time, pitch, velocity,
|
|
19
|
+
# or other musical parameters. The P format provides a compact representation
|
|
20
|
+
# suitable for sequencer playback and transformation.
|
|
21
|
+
#
|
|
22
|
+
# ## Matrix to P Conversion
|
|
23
|
+
#
|
|
24
|
+
# A matrix of points (rows = time steps, columns = parameters) can be converted
|
|
25
|
+
# to P format where:
|
|
26
|
+
#
|
|
27
|
+
# - One dimension represents time (usually the first column)
|
|
28
|
+
# - Other dimensions represent musical parameters
|
|
29
|
+
# - Each P is an array extended with P module
|
|
30
|
+
# - The P contains alternating values (arrays extended with V) and durations (numbers):
|
|
31
|
+
# [value1, duration1, value2, duration2, ..., valueN].extend(P)
|
|
32
|
+
#
|
|
33
|
+
# ## Use Cases
|
|
34
|
+
#
|
|
35
|
+
# - Converting recorded MIDI data to playable sequences
|
|
36
|
+
# - Transforming algorithmic compositions from matrix form to time-based sequences
|
|
37
|
+
# - Merging fragmented musical gestures that share connection points
|
|
38
|
+
# - Decomposing complex trajectories into simpler monotonic segments
|
|
39
|
+
#
|
|
40
|
+
# @example Basic matrix conversion
|
|
41
|
+
# using Musa::Extension::Matrix
|
|
42
|
+
#
|
|
43
|
+
# # Matrix: [time, pitch]
|
|
44
|
+
# matrix = Matrix[[0, 60], [1, 62], [2, 64]]
|
|
45
|
+
# p_sequences = matrix.to_p(time_dimension: 0)
|
|
46
|
+
# # Returns an array with one P:
|
|
47
|
+
# # [[60], 1, [62], 1, [64]].extend(P)
|
|
48
|
+
# # Where [60], [62], [64] are arrays extended with V module
|
|
49
|
+
#
|
|
50
|
+
# @example Multi-dimensional musical parameters
|
|
51
|
+
# using Musa::Extension::Matrix
|
|
52
|
+
#
|
|
53
|
+
# # Matrix: [time, pitch, velocity]
|
|
54
|
+
# matrix = Matrix[[0, 60, 100], [0.5, 62, 110], [1, 64, 120]]
|
|
55
|
+
# p_sequences = matrix.to_p(time_dimension: 0, keep_time: false)
|
|
56
|
+
# # Returns an array with one P:
|
|
57
|
+
# # [[60, 100], 0.5, [62, 110], 0.5, [64, 120]].extend(P)
|
|
58
|
+
# # Time dimension removed, used only for duration calculation
|
|
59
|
+
#
|
|
60
|
+
# @example Condensing connected matrices
|
|
61
|
+
# using Musa::Extension::Matrix
|
|
62
|
+
#
|
|
63
|
+
# # Two phrases that connect at [1, 62]
|
|
64
|
+
# phrase1 = Matrix[[0, 60], [1, 62]]
|
|
65
|
+
# phrase2 = Matrix[[1, 62], [2, 64], [3, 65]]
|
|
66
|
+
#
|
|
67
|
+
# [phrase1, phrase2].to_p(time_dimension: 0)
|
|
68
|
+
# # Returns an array with one P (merged):
|
|
69
|
+
# # [[60], 1, [62], 1, [64], 1, [65]].extend(P)
|
|
70
|
+
# # Both phrases merged into one continuous sequence
|
|
71
|
+
#
|
|
72
|
+
# @see Musa::Datasets::P
|
|
73
|
+
# @see Musa::Datasets::V
|
|
74
|
+
# @note These refinements must be activated with `using Musa::Extension::Matrix`
|
|
75
|
+
# in the scope where you want to use them.
|
|
76
|
+
#
|
|
77
|
+
# ## Methods Added
|
|
78
|
+
#
|
|
79
|
+
# ### Array
|
|
80
|
+
# - {Array#indexes_of_values} - Creates a hash mapping values to their indices
|
|
81
|
+
# - {Array#to_p} - Converts an array of matrices to P sequences
|
|
82
|
+
# - {Array#condensed_matrices} - Condenses matrices that share common boundary rows
|
|
83
|
+
#
|
|
84
|
+
# ### Matrix
|
|
85
|
+
# - {::Matrix#to_p} - Converts a matrix to P format (see examples in module documentation)
|
|
86
|
+
# - {::Matrix#_rows} - Provides direct access to internal rows array (private API)
|
|
12
87
|
module Matrix
|
|
88
|
+
# @!method indexes_of_values
|
|
89
|
+
# Creates a hash mapping values to their indices in the array.
|
|
90
|
+
#
|
|
91
|
+
# This method scans the array and builds an inverted index where each
|
|
92
|
+
# unique value maps to an array of positions where it appears.
|
|
93
|
+
#
|
|
94
|
+
# @note This method is added to Array via refinement. Requires `using Musa::Extension::Matrix`.
|
|
95
|
+
#
|
|
96
|
+
# @return [Hash{Object => Array<Integer>}] hash where keys are array values
|
|
97
|
+
# and values are arrays of indices.
|
|
98
|
+
#
|
|
99
|
+
# @example
|
|
100
|
+
# using Musa::Extension::Matrix
|
|
101
|
+
# [10, 20, 10, 30, 20].indexes_of_values
|
|
102
|
+
# # => { 10 => [0, 2], 20 => [1, 4], 30 => [3] }
|
|
103
|
+
class ::Array; end
|
|
104
|
+
|
|
105
|
+
# @!method to_p(time_dimension:, keep_time: nil)
|
|
106
|
+
# Converts an array of matrices to an array of P sequences.
|
|
107
|
+
#
|
|
108
|
+
# This method processes each matrix in the array, first condensing matrices
|
|
109
|
+
# that share common endpoints, then converting each resulting matrix to
|
|
110
|
+
# P (point sequence) format.
|
|
111
|
+
#
|
|
112
|
+
# @note This method is added to Array via refinement. Requires `using Musa::Extension::Matrix`.
|
|
113
|
+
#
|
|
114
|
+
# @param time_dimension [Integer] index of the dimension to treat as time.
|
|
115
|
+
# @param keep_time [Boolean, nil] whether to preserve the time dimension in the output.
|
|
116
|
+
# When false or nil, the time dimension is removed and used only for duration calculations.
|
|
117
|
+
#
|
|
118
|
+
# @return [Array<Musa::Datasets::P>] array of P sequences, one per condensed matrix.
|
|
119
|
+
#
|
|
120
|
+
# @example Converting array of matrices
|
|
121
|
+
# using Musa::Extension::Matrix
|
|
122
|
+
# matrices = [Matrix[[0, 60], [1, 62]], Matrix[[2, 64], [3, 65]]]
|
|
123
|
+
# result = matrices.to_p(time_dimension: 0)
|
|
124
|
+
# # Returns array of P sequences, one per matrix (or merged if they connect)
|
|
125
|
+
#
|
|
126
|
+
# @see #condensed_matrices
|
|
127
|
+
# @see ::Matrix#to_p
|
|
128
|
+
class ::Array; end
|
|
129
|
+
|
|
130
|
+
# @!method condensed_matrices
|
|
131
|
+
# Condenses matrices that share common boundary rows.
|
|
132
|
+
#
|
|
133
|
+
# This method merges matrices that have matching first or last rows,
|
|
134
|
+
# effectively connecting musical gestures that share endpoints. This is
|
|
135
|
+
# particularly useful for creating continuous trajectories from fragmented
|
|
136
|
+
# matrix segments.
|
|
137
|
+
#
|
|
138
|
+
# The algorithm compares each matrix with all previously processed matrices,
|
|
139
|
+
# looking for matches at either the beginning or end of the row sequence.
|
|
140
|
+
# When a match is found, the matrices are merged.
|
|
141
|
+
#
|
|
142
|
+
# @note This method is added to Array via refinement. Requires `using Musa::Extension::Matrix`.
|
|
143
|
+
#
|
|
144
|
+
# @return [Array<::Matrix>] condensed array of matrices with shared boundaries merged.
|
|
145
|
+
#
|
|
146
|
+
# @example
|
|
147
|
+
# using Musa::Extension::Matrix
|
|
148
|
+
# # Matrix A ends where Matrix B begins -> they merge
|
|
149
|
+
# a = Matrix[[0, 60], [1, 62]]
|
|
150
|
+
# b = Matrix[[1, 62], [2, 64]]
|
|
151
|
+
# [a, b].condensed_matrices
|
|
152
|
+
# # => [Matrix[[0, 60], [1, 62], [2, 64]]]
|
|
153
|
+
class ::Array; end
|
|
154
|
+
|
|
13
155
|
refine Array do
|
|
14
156
|
def indexes_of_values
|
|
15
157
|
indexes = {}
|
|
@@ -66,6 +208,113 @@ module Musa
|
|
|
66
208
|
end
|
|
67
209
|
end
|
|
68
210
|
|
|
211
|
+
# @!method to_p(time_dimension:, keep_time: nil)
|
|
212
|
+
# Converts a matrix to one or more P (point sequence) representations.
|
|
213
|
+
#
|
|
214
|
+
# This method decomposes the matrix into directional segments based on the
|
|
215
|
+
# time dimension, then converts each segment into a P sequence format suitable
|
|
216
|
+
# for representing musical gestures in Musa DSL.
|
|
217
|
+
#
|
|
218
|
+
# A P sequence is an array extended with the P module, containing alternating
|
|
219
|
+
# value arrays (extended with V module) and numeric time deltas:
|
|
220
|
+
# [value1, delta1, value2, delta2, ..., valueN].extend(P)
|
|
221
|
+
# where each value is an array extended with V module.
|
|
222
|
+
#
|
|
223
|
+
# The decomposition process identifies monotonic segments in the time dimension,
|
|
224
|
+
# handling cases where the temporal ordering might have reversals or
|
|
225
|
+
# non-linearities.
|
|
226
|
+
#
|
|
227
|
+
# @note This method is added to Matrix via refinement. Requires `using Musa::Extension::Matrix`.
|
|
228
|
+
#
|
|
229
|
+
# @param time_dimension [Integer] index of the dimension representing time (typically 0).
|
|
230
|
+
# @param keep_time [Boolean, nil] if true, the time dimension is preserved in each point;
|
|
231
|
+
# if false/nil, it's removed and used only for computing deltas.
|
|
232
|
+
#
|
|
233
|
+
# @return [Array<Musa::Datasets::P>] array of P sequences, one per directional segment.
|
|
234
|
+
#
|
|
235
|
+
# @example Basic conversion
|
|
236
|
+
# using Musa::Extension::Matrix
|
|
237
|
+
# matrix = Matrix[[0, 60], [1, 62], [2, 64]]
|
|
238
|
+
# result = matrix.to_p(time_dimension: 0)
|
|
239
|
+
# # => Array with one P object:
|
|
240
|
+
# # [[60], 1, [62], 1, [64]].extend(P)
|
|
241
|
+
# # Each value like [60] is extended with V module
|
|
242
|
+
#
|
|
243
|
+
# @example Keeping time dimension
|
|
244
|
+
# using Musa::Extension::Matrix
|
|
245
|
+
# matrix = Matrix[[0, 60, 100], [1, 62, 110], [2, 64, 120]]
|
|
246
|
+
# result = matrix.to_p(time_dimension: 0, keep_time: true)
|
|
247
|
+
# # => Array with one P object:
|
|
248
|
+
# # [[0, 60, 100], 1, [1, 62, 110], 1, [2, 64, 120]].extend(P)
|
|
249
|
+
# # Time dimension kept in each value array
|
|
250
|
+
#
|
|
251
|
+
# @see Musa::Datasets::P
|
|
252
|
+
# @see Musa::Datasets::V
|
|
253
|
+
#
|
|
254
|
+
# @api public
|
|
255
|
+
class ::Matrix; end
|
|
256
|
+
|
|
257
|
+
# @!method _rows
|
|
258
|
+
# Provides direct access to the internal rows array of the matrix.
|
|
259
|
+
#
|
|
260
|
+
# This is a utility method used primarily by {Array#condensed_matrices}
|
|
261
|
+
# to manipulate matrix rows when merging matrices with shared boundaries.
|
|
262
|
+
#
|
|
263
|
+
# @note This method is added to Matrix via refinement. Requires `using Musa::Extension::Matrix`.
|
|
264
|
+
#
|
|
265
|
+
# @return [Array<Array>] the internal @rows instance variable.
|
|
266
|
+
#
|
|
267
|
+
# @api private
|
|
268
|
+
# @note This method accesses Ruby's Matrix internals and should be used with caution.
|
|
269
|
+
class ::Matrix; end
|
|
270
|
+
|
|
271
|
+
# @!method decompose(array, time_dimension)
|
|
272
|
+
# Decomposes an array of points into directional segments based on a time dimension.
|
|
273
|
+
#
|
|
274
|
+
# This private method analyzes the array to find monotonic (non-decreasing)
|
|
275
|
+
# sequences in the specified time dimension. It scans bidirectionally from
|
|
276
|
+
# each point to discover maximal segments where time progresses consistently.
|
|
277
|
+
#
|
|
278
|
+
# The algorithm:
|
|
279
|
+
#
|
|
280
|
+
# 1. Groups points by their time values
|
|
281
|
+
# 2. Iterates through time values in sorted order
|
|
282
|
+
# 3. For each unprocessed index, scans backward and forward
|
|
283
|
+
# 4. Collects points while time is non-decreasing
|
|
284
|
+
# 5. Returns segments with 2+ points
|
|
285
|
+
#
|
|
286
|
+
# @param array [Array<Array>] array of point arrays (each point is an array of coordinates).
|
|
287
|
+
# @param time_dimension [Integer] index of the dimension representing time.
|
|
288
|
+
#
|
|
289
|
+
# @return [Array<Array<Array>>] array of directional segments, each segment
|
|
290
|
+
# being an array of points.
|
|
291
|
+
#
|
|
292
|
+
# @example
|
|
293
|
+
# # Points with time in dimension 0
|
|
294
|
+
# points = [[0, 10], [1, 20], [0.5, 15], [2, 30]]
|
|
295
|
+
# decompose(points, 0)
|
|
296
|
+
# # => [[[1, 20], [0.5, 15], [0, 10]], [[0, 10], [0.5, 15], [1, 20], [2, 30]]]
|
|
297
|
+
# # Two segments: one going backward in time, one forward
|
|
298
|
+
#
|
|
299
|
+
# @todo POTENTIAL LOGIC INCONSISTENCY: Review the direction logic in backward and forward scans.
|
|
300
|
+
# - Line 300 comment: "Scan backward... while time is non-decreasing"
|
|
301
|
+
# - Line 306 code: `while i >= 0 && array[i][time_dimension] >= xx`
|
|
302
|
+
# - Line 316 comment: "Scan forward... while time is non-decreasing"
|
|
303
|
+
# - Line 322 code: `while i < array.size && array[i][time_dimension] >= xx`
|
|
304
|
+
# Both scans use `>= xx`, which seems contradictory. When scanning backward
|
|
305
|
+
# (decreasing indices), for "non-decreasing time" we might expect `<= xx`
|
|
306
|
+
# (times getting smaller or equal as we go back in indices). Currently both
|
|
307
|
+
# directions use the same comparison operator.
|
|
308
|
+
# IMPLEMENT TESTS to verify expected behavior with various input patterns and
|
|
309
|
+
# confirm whether this is intentional or a bug. Test cases should include:
|
|
310
|
+
# - Forward-only monotonic sequences
|
|
311
|
+
# - Backward-only monotonic sequences
|
|
312
|
+
# - Mixed direction sequences
|
|
313
|
+
# - The example documented above
|
|
314
|
+
#
|
|
315
|
+
# @api private
|
|
316
|
+
class ::Matrix; end
|
|
317
|
+
|
|
69
318
|
refine ::Matrix do
|
|
70
319
|
def to_p(time_dimension:, keep_time: nil)
|
|
71
320
|
decompose(self.to_a, time_dimension).collect do |points|
|
|
@@ -105,10 +354,9 @@ module Musa
|
|
|
105
354
|
|
|
106
355
|
x_dim_values_indexes.keys.sort.each do |value|
|
|
107
356
|
x_dim_values_indexes[value].each do |index|
|
|
108
|
-
#
|
|
109
|
-
# hacia un lado
|
|
110
|
-
#
|
|
357
|
+
# Scan in both directions from this point to find monotonic segments
|
|
111
358
|
unless used_indexes.include?(index)
|
|
359
|
+
# Scan backward (decreasing indices) while time is non-decreasing
|
|
112
360
|
i = index
|
|
113
361
|
xx = array[i][time_dimension]
|
|
114
362
|
|
|
@@ -124,9 +372,7 @@ module Musa
|
|
|
124
372
|
|
|
125
373
|
directional_segments << a if a.size > 1
|
|
126
374
|
|
|
127
|
-
#
|
|
128
|
-
# y hacia el otro
|
|
129
|
-
#
|
|
375
|
+
# Scan forward (increasing indices) while time is non-decreasing
|
|
130
376
|
i = index
|
|
131
377
|
xx = array[i][time_dimension]
|
|
132
378
|
|