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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. 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
- "%#{integer_digits + decimal_digits + 1}s: " % ("%.#{decimal_digits}f" % sequencer.position.to_f)
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