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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Transport - Timing & Clocks
|
|
2
|
+
|
|
3
|
+
Comprehensive timing infrastructure connecting clock sources to the sequencer. The transport system manages musical playback lifecycle, timing synchronization, and position control.
|
|
4
|
+
|
|
5
|
+
**Architecture:**
|
|
6
|
+
```
|
|
7
|
+
Clock --ticks--> Transport --tick()--> Sequencer --events--> Music
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The system provides precise timing control with support for internal timers, MIDI clock synchronization, and manual control for testing and integration.
|
|
11
|
+
|
|
12
|
+
## Clock - Timing Sources
|
|
13
|
+
|
|
14
|
+
**Clock** is the abstract base class for timing sources. All clocks generate regular ticks that drive the sequencer forward. Multiple clock implementations are available for different use cases.
|
|
15
|
+
|
|
16
|
+
### Clock Activation Models
|
|
17
|
+
|
|
18
|
+
Clocks use two different activation models:
|
|
19
|
+
|
|
20
|
+
**Automatic Activation** (DummyClock):
|
|
21
|
+
- Begins generating ticks immediately when `transport.start` is called
|
|
22
|
+
- No external activation required
|
|
23
|
+
- Appropriate for testing, batch processing, simulations
|
|
24
|
+
|
|
25
|
+
**External Activation** (TimerClock, InputMidiClock, ExternalTickClock):
|
|
26
|
+
- Requires external signal/control to begin generating ticks
|
|
27
|
+
- `transport.start` blocks waiting for activation
|
|
28
|
+
- Appropriate for live coding, DAW sync, external control
|
|
29
|
+
|
|
30
|
+
### Available Clock Types
|
|
31
|
+
|
|
32
|
+
**DummyClock** - Simplified clock for testing (automatic activation):
|
|
33
|
+
- Fast playback without real-time constraints
|
|
34
|
+
- Immediately begins generating ticks
|
|
35
|
+
- Useful for test suites or batch generation
|
|
36
|
+
- No external dependencies
|
|
37
|
+
|
|
38
|
+
**TimerClock** - Internal high-precision timer-based clock (external activation):
|
|
39
|
+
- Standalone compositions with internal timing
|
|
40
|
+
- Requires calling `clock.start()` from another thread
|
|
41
|
+
- Configurable BPM (tempo) and ticks per beat
|
|
42
|
+
- Can dynamically change tempo during playback
|
|
43
|
+
- Appropriate for live coding clients
|
|
44
|
+
|
|
45
|
+
**InputMidiClock** - Synchronized to external MIDI Clock messages (external activation):
|
|
46
|
+
- DAW-synchronized playback
|
|
47
|
+
- Waits for MIDI "Start" (0xFA) message to begin ticks
|
|
48
|
+
- Automatically follows external MIDI Clock Start/Stop/Continue
|
|
49
|
+
- Locked to external timing source
|
|
50
|
+
|
|
51
|
+
**ExternalTickClock** - Manually triggered ticks (external activation):
|
|
52
|
+
- Testing and debugging with precise control
|
|
53
|
+
- Integration with external systems (game engines, etc.)
|
|
54
|
+
- Call `clock.tick()` manually to generate each tick
|
|
55
|
+
- Frame-by-frame control
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require 'musa-dsl'
|
|
59
|
+
|
|
60
|
+
# TimerClock - Internal timer-based timing
|
|
61
|
+
timer_clock = Musa::Clock::TimerClock.new(
|
|
62
|
+
bpm: 120, # Beats per minute
|
|
63
|
+
ticks_per_beat: 24 # Resolution
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# InputMidiClock - Synchronized to external MIDI Clock
|
|
67
|
+
require 'midi-communications'
|
|
68
|
+
midi_input = MIDICommunications::Input.gets # Select MIDI input
|
|
69
|
+
|
|
70
|
+
midi_clock = Musa::Clock::InputMidiClock.new(midi_input)
|
|
71
|
+
|
|
72
|
+
# ExternalTickClock - Manual tick control
|
|
73
|
+
external_clock = Musa::Clock::ExternalTickClock.new
|
|
74
|
+
|
|
75
|
+
# DummyClock - For testing (100 ticks)
|
|
76
|
+
dummy_clock = Musa::Clock::DummyClock.new(100)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Transport - Playback Lifecycle Manager
|
|
80
|
+
|
|
81
|
+
**Transport** connects a clock to a sequencer and manages the playback lifecycle. It provides methods for starting/stopping playback, seeking to different positions, and registering callbacks for lifecycle events.
|
|
82
|
+
|
|
83
|
+
**Lifecycle phases:**
|
|
84
|
+
1. **before_begin** - Run once before first start (initialization)
|
|
85
|
+
2. **on_start** - Run each time transport starts
|
|
86
|
+
3. **Running** - Clock generates ticks → sequencer processes events
|
|
87
|
+
4. **on_change_position** - Run when position jumps/seeks
|
|
88
|
+
5. **after_stop** - Run when transport stops
|
|
89
|
+
|
|
90
|
+
**Key methods:**
|
|
91
|
+
- `start` - Start playback (blocks while running)
|
|
92
|
+
- `stop` - Stop playback
|
|
93
|
+
- `change_position_to(bars: n)` - Seek to position (in bars)
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
require 'musa-dsl'
|
|
97
|
+
|
|
98
|
+
# Create clock
|
|
99
|
+
clock = Musa::Clock::TimerClock.new(bpm: 120, ticks_per_beat: 24)
|
|
100
|
+
|
|
101
|
+
# Create transport
|
|
102
|
+
transport = Musa::Transport::Transport.new(
|
|
103
|
+
clock,
|
|
104
|
+
4, # beats_per_bar (time signature numerator)
|
|
105
|
+
24 # ticks_per_beat (resolution)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Access sequencer through transport
|
|
109
|
+
sequencer = transport.sequencer
|
|
110
|
+
|
|
111
|
+
# Schedule events
|
|
112
|
+
sequencer.at 1 do
|
|
113
|
+
puts "Starting at bar 1!"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sequencer.at 4 do
|
|
117
|
+
puts "Reached bar 4"
|
|
118
|
+
transport.stop
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Register lifecycle callbacks
|
|
122
|
+
transport.before_begin do
|
|
123
|
+
puts "Initializing (runs once)..."
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
transport.on_start do
|
|
127
|
+
puts "Transport started!"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
transport.after_stop do
|
|
131
|
+
puts "Transport stopped, cleaning up..."
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# IMPORTANT: TimerClock requires external activation
|
|
135
|
+
# Start transport in background thread (it will block waiting)
|
|
136
|
+
thread = Thread.new { transport.start }
|
|
137
|
+
sleep 0.1 # Let transport initialize
|
|
138
|
+
|
|
139
|
+
# Activate clock from external control (e.g., live coding client)
|
|
140
|
+
clock.start # NOW ticks begin generating
|
|
141
|
+
|
|
142
|
+
# Wait for completion
|
|
143
|
+
thread.join
|
|
144
|
+
|
|
145
|
+
# Seeking example (in separate context)
|
|
146
|
+
# transport.change_position_to(bars: 2) # Jump to bar 2
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Complete example with MIDI Clock synchronization:**
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
require 'musa-dsl'
|
|
153
|
+
require 'midi-communications'
|
|
154
|
+
|
|
155
|
+
# Setup MIDI-synchronized clock
|
|
156
|
+
midi_input = MIDICommunications::Input.gets
|
|
157
|
+
clock = Musa::Clock::InputMidiClock.new(midi_input)
|
|
158
|
+
|
|
159
|
+
# Create transport
|
|
160
|
+
transport = Musa::Transport::Transport.new(clock, 4, 24)
|
|
161
|
+
|
|
162
|
+
# Schedule events
|
|
163
|
+
transport.sequencer.at 1 do
|
|
164
|
+
puts "Synchronized start at bar 1!"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Start and wait for MIDI Clock Start message
|
|
168
|
+
transport.start
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## API Reference
|
|
172
|
+
|
|
173
|
+
**Complete API documentation:**
|
|
174
|
+
- [Musa::Transport](https://rubydoc.info/gems/musa-dsl/Musa/Transport) - Playback lifecycle management
|
|
175
|
+
- [Musa::Clock](https://rubydoc.info/gems/musa-dsl/Musa/Clock) - Timing sources and clock implementations
|
|
176
|
+
|
|
177
|
+
**Source code:** `lib/transport/`
|
|
@@ -1,6 +1,74 @@
|
|
|
1
|
+
require_relative 'extension'
|
|
2
|
+
|
|
1
3
|
module Musa
|
|
2
4
|
module Extension
|
|
5
|
+
# Refinement that expands Range objects within arrays into their constituent elements.
|
|
6
|
+
#
|
|
7
|
+
# This is particularly useful in musical contexts where arrays may contain both
|
|
8
|
+
# individual values and ranges (like MIDI note ranges or channel ranges), and you
|
|
9
|
+
# need to work with the fully expanded list.
|
|
10
|
+
#
|
|
11
|
+
# ## Use Cases
|
|
12
|
+
#
|
|
13
|
+
# - Expanding MIDI channel specifications: [0, 2..4, 7] → [0, 2, 3, 4, 7]
|
|
14
|
+
# - Expanding note ranges in chord definitions
|
|
15
|
+
# - Processing mixed literal and range values in musical parameters
|
|
16
|
+
# - Any scenario where ranges need to be flattened for iteration
|
|
17
|
+
#
|
|
18
|
+
# @example Basic usage
|
|
19
|
+
# using Musa::Extension::ExplodeRanges
|
|
20
|
+
#
|
|
21
|
+
# [1, 3..5, 8].explode_ranges
|
|
22
|
+
# # => [1, 3, 4, 5, 8]
|
|
23
|
+
#
|
|
24
|
+
# @example MIDI channels
|
|
25
|
+
# using Musa::Extension::ExplodeRanges
|
|
26
|
+
#
|
|
27
|
+
# channels = [0, 2..4, 7, 9..10]
|
|
28
|
+
# channels.explode_ranges
|
|
29
|
+
# # => [0, 2, 3, 4, 7, 9, 10]
|
|
30
|
+
#
|
|
31
|
+
# @example Mixed with other array methods
|
|
32
|
+
# using Musa::Extension::ExplodeRanges
|
|
33
|
+
#
|
|
34
|
+
# [1..3, 5, 7..9].explode_ranges.map { |n| n * 2 }
|
|
35
|
+
# # => [2, 4, 6, 10, 14, 16, 18]
|
|
36
|
+
#
|
|
37
|
+
# @see Musa::MIDIVoices::MIDIVoices#initialize Uses this for channel expansion
|
|
38
|
+
# @note This refinement must be activated with `using Musa::Extension::ExplodeRanges`
|
|
39
|
+
#
|
|
40
|
+
# ## Methods Added
|
|
41
|
+
#
|
|
42
|
+
# ### Array
|
|
43
|
+
# - {Array#explode_ranges} - Expands all Range objects in the array into their individual elements
|
|
3
44
|
module ExplodeRanges
|
|
45
|
+
# @!method explode_ranges
|
|
46
|
+
# Expands all Range objects in the array into their individual elements.
|
|
47
|
+
#
|
|
48
|
+
# Iterates through the array and converts any Range objects to their
|
|
49
|
+
# constituent elements via `to_a`, leaving non-Range elements unchanged.
|
|
50
|
+
# The result is a new flat array.
|
|
51
|
+
#
|
|
52
|
+
# @note This method is added to Array via refinement. Requires `using Musa::Extension::ExplodeRanges`.
|
|
53
|
+
#
|
|
54
|
+
# @return [Array] new array with all ranges expanded.
|
|
55
|
+
#
|
|
56
|
+
# @example Empty ranges
|
|
57
|
+
# using Musa::Extension::ExplodeRanges
|
|
58
|
+
# [1, (5..4), 8].explode_ranges # (5..4) is empty
|
|
59
|
+
# # => [1, 8]
|
|
60
|
+
#
|
|
61
|
+
# @example Exclusive ranges
|
|
62
|
+
# using Musa::Extension::ExplodeRanges
|
|
63
|
+
# [1, (3...6), 9].explode_ranges
|
|
64
|
+
# # => [1, 3, 4, 5, 9]
|
|
65
|
+
#
|
|
66
|
+
# @example Nested arrays are NOT expanded recursively
|
|
67
|
+
# using Musa::Extension::ExplodeRanges
|
|
68
|
+
# [1, [2..4], 5].explode_ranges
|
|
69
|
+
# # => [1, [2..4], 5] # Inner range NOT expanded
|
|
70
|
+
class ::Array; end
|
|
71
|
+
|
|
4
72
|
refine Array do
|
|
5
73
|
def explode_ranges
|
|
6
74
|
array = []
|
|
@@ -1,8 +1,87 @@
|
|
|
1
|
+
require_relative 'extension'
|
|
1
2
|
require_relative 'deep-copy'
|
|
2
3
|
|
|
3
4
|
module Musa
|
|
4
5
|
module Extension
|
|
6
|
+
# Refinement that converts any object to an array, with optional repetition and defaults.
|
|
7
|
+
#
|
|
8
|
+
# This refinement is essential for normalizing parameters in the DSL, allowing users
|
|
9
|
+
# to provide either single values or arrays and have them processed uniformly.
|
|
10
|
+
#
|
|
11
|
+
# ## Core Behavior
|
|
12
|
+
#
|
|
13
|
+
# - **Object**: Wraps in array; nil becomes []
|
|
14
|
+
# - **Array**: Returns clone or cycles to requested size
|
|
15
|
+
# - **size parameter**: Repeats/cycles to achieve target length
|
|
16
|
+
# - **default parameter**: Replaces nil values
|
|
17
|
+
#
|
|
18
|
+
# ## Use Cases
|
|
19
|
+
#
|
|
20
|
+
# - Normalizing velocity parameters (single value or per-note array)
|
|
21
|
+
# - Ensuring consistent array handling in DSL methods
|
|
22
|
+
# - Cycling patterns to fill required lengths
|
|
23
|
+
# - Providing default values for missing data
|
|
24
|
+
#
|
|
25
|
+
# @example Basic object wrapping
|
|
26
|
+
# using Musa::Extension::Arrayfy
|
|
27
|
+
#
|
|
28
|
+
# 5.arrayfy # => [5]
|
|
29
|
+
# nil.arrayfy # => []
|
|
30
|
+
# [1, 2, 3].arrayfy # => [1, 2, 3]
|
|
31
|
+
#
|
|
32
|
+
# @example Repetition with size
|
|
33
|
+
# using Musa::Extension::Arrayfy
|
|
34
|
+
#
|
|
35
|
+
# 5.arrayfy(size: 3) # => [5, 5, 5]
|
|
36
|
+
# [1, 2].arrayfy(size: 5) # => [1, 2, 1, 2, 1]
|
|
37
|
+
# [1, 2, 3].arrayfy(size: 2) # => [1, 2]
|
|
38
|
+
#
|
|
39
|
+
# @example Default values for nil
|
|
40
|
+
# using Musa::Extension::Arrayfy
|
|
41
|
+
#
|
|
42
|
+
# nil.arrayfy(size: 3, default: 0) # => [0, 0, 0]
|
|
43
|
+
# [1, nil, 3].arrayfy(size: 5, default: -1) # => [1, -1, 3, 1, -1]
|
|
44
|
+
#
|
|
45
|
+
# @example Musical application - velocity normalization
|
|
46
|
+
# using Musa::Extension::Arrayfy
|
|
47
|
+
#
|
|
48
|
+
# # User provides single velocity for chord
|
|
49
|
+
# velocities = 90.arrayfy(size: 3) # => [90, 90, 90]
|
|
50
|
+
#
|
|
51
|
+
# # User provides array of velocities that cycles
|
|
52
|
+
# velocities = [80, 100].arrayfy(size: 5) # => [80, 100, 80, 100, 80]
|
|
53
|
+
#
|
|
54
|
+
# @see Musa::MIDIVoices::MIDIVoice#note Uses arrayfy for velocity normalization
|
|
55
|
+
# @note This refinement must be activated with `using Musa::Extension::Arrayfy`
|
|
56
|
+
# @note Arrays are cloned and singleton class modules are preserved
|
|
57
|
+
#
|
|
58
|
+
# ## Methods Added
|
|
59
|
+
#
|
|
60
|
+
# ### Object
|
|
61
|
+
# - {Object#arrayfy} - Converts any object into an array, optionally repeated to a target size
|
|
62
|
+
#
|
|
63
|
+
# ### Array
|
|
64
|
+
# - {Array#arrayfy} - Clones or cycles the array to achieve the target size, with nil replacement
|
|
5
65
|
module Arrayfy
|
|
66
|
+
# @!method arrayfy(size: nil, default: nil)
|
|
67
|
+
# Converts any object into an array, optionally repeated to a target size.
|
|
68
|
+
#
|
|
69
|
+
# @note This method is added to Object via refinement. Requires `using Musa::Extension::Arrayfy`.
|
|
70
|
+
#
|
|
71
|
+
# @param size [Integer, nil] target array length. If nil, returns single-element array.
|
|
72
|
+
# @param default [Object, nil] value to use instead of nil.
|
|
73
|
+
#
|
|
74
|
+
# @return [Array] single element repeated size times, or wrapped in array if size is nil.
|
|
75
|
+
#
|
|
76
|
+
# @example With size
|
|
77
|
+
# using Musa::Extension::Arrayfy
|
|
78
|
+
# "hello".arrayfy(size: 3) # => ["hello", "hello", "hello"]
|
|
79
|
+
#
|
|
80
|
+
# @example Nil handling
|
|
81
|
+
# using Musa::Extension::Arrayfy
|
|
82
|
+
# nil.arrayfy(size: 2, default: :empty) # => [:empty, :empty]
|
|
83
|
+
class ::Object; end
|
|
84
|
+
|
|
6
85
|
refine Object do
|
|
7
86
|
def arrayfy(size: nil, default: nil)
|
|
8
87
|
if size
|
|
@@ -17,6 +96,37 @@ module Musa
|
|
|
17
96
|
|
|
18
97
|
# TODO add a refinement for Hash? Should receive a list parameter with the ordered keys
|
|
19
98
|
|
|
99
|
+
# @!method arrayfy(size: nil, default: nil)
|
|
100
|
+
# Clones or cycles the array to achieve the target size, with nil replacement.
|
|
101
|
+
#
|
|
102
|
+
# The cycling behavior multiplies the array enough times to reach or exceed
|
|
103
|
+
# the target size, then takes exactly the requested number of elements.
|
|
104
|
+
# Singleton class modules (like P, V dataset extensions) are preserved.
|
|
105
|
+
#
|
|
106
|
+
# @note This method is added to Array via refinement. Requires `using Musa::Extension::Arrayfy`.
|
|
107
|
+
#
|
|
108
|
+
# @param size [Integer, nil] target length. If nil, returns clone of array.
|
|
109
|
+
# @param default [Object, nil] value to replace nil elements with.
|
|
110
|
+
#
|
|
111
|
+
# @return [Array] processed array of exactly the requested size.
|
|
112
|
+
#
|
|
113
|
+
# @example Cycling shorter array
|
|
114
|
+
# using Musa::Extension::Arrayfy
|
|
115
|
+
# [1, 2].arrayfy(size: 5) # => [1, 2, 1, 2, 1]
|
|
116
|
+
#
|
|
117
|
+
# @example Truncating longer array
|
|
118
|
+
# using Musa::Extension::Arrayfy
|
|
119
|
+
# [1, 2, 3, 4, 5].arrayfy(size: 3) # => [1, 2, 3]
|
|
120
|
+
#
|
|
121
|
+
# @example Preserving dataset modules
|
|
122
|
+
# using Musa::Extension::Arrayfy
|
|
123
|
+
# p_sequence = [60, 1, 62].extend(Musa::Datasets::P)
|
|
124
|
+
# p_sequence.arrayfy(size: 6) # Result also extended with P
|
|
125
|
+
#
|
|
126
|
+
# @note The cycling formula: array * (size / array.size + (size % array.size).zero? ? 0 : 1)
|
|
127
|
+
# ensures enough repetitions to reach target size.
|
|
128
|
+
class ::Array; end
|
|
129
|
+
|
|
20
130
|
refine Array do
|
|
21
131
|
def arrayfy(size: nil, default: nil)
|
|
22
132
|
if size
|
|
@@ -1,10 +1,52 @@
|
|
|
1
|
+
require_relative 'extension'
|
|
2
|
+
|
|
1
3
|
module Musa
|
|
2
4
|
module Extension
|
|
5
|
+
# Module providing metaprogramming methods for creating DSL builder patterns.
|
|
6
|
+
#
|
|
7
|
+
# AttributeBuilder defines class methods that generate instance methods for
|
|
8
|
+
# creating and managing collections of objects in a DSL-friendly way. It's
|
|
9
|
+
# heavily used throughout Musa DSL to create fluent, expressive APIs.
|
|
10
|
+
#
|
|
11
|
+
# ## Method Categories
|
|
12
|
+
#
|
|
13
|
+
# - **Adders to Hash**: Create methods that build hash-based collections
|
|
14
|
+
# - **Adders to Array**: Create methods that build array-based collections
|
|
15
|
+
# - **Builders**: Create single-object getter/setter DSL methods
|
|
16
|
+
#
|
|
17
|
+
# ## Naming Conventions
|
|
18
|
+
#
|
|
19
|
+
# - `add_item` / `item`: singular form adds one object
|
|
20
|
+
# - `items`: plural form adds multiple or retrieves collection
|
|
21
|
+
# - Automatic pluralization (item → items) unless specified
|
|
22
|
+
#
|
|
23
|
+
# @example Using in a class
|
|
24
|
+
# class Score
|
|
25
|
+
# extend Musa::Extension::AttributeBuilder
|
|
26
|
+
#
|
|
27
|
+
# def initialize
|
|
28
|
+
# @tracks = {}
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# attr_tuple_adder_to_hash :track, Track
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# score = Score.new
|
|
35
|
+
# score.add_track :piano, params
|
|
36
|
+
# score.tracks # => { piano: Track(...) }
|
|
37
|
+
#
|
|
38
|
+
# @see Musa::Datasets Score classes use these extensively
|
|
3
39
|
module AttributeBuilder
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
40
|
+
# Creates methods for adding id/value tuples to a hash collection.
|
|
41
|
+
#
|
|
42
|
+
# Generates:
|
|
43
|
+
# - `add_#{name}(id, parameter)` → creates instance and adds to hash
|
|
44
|
+
# - `#{plural}(**parameters)` → batch add or retrieve hash
|
|
7
45
|
#
|
|
46
|
+
# @param name [Symbol] singular name for the item.
|
|
47
|
+
# @param klass [Class] class to instantiate (receives id, parameter).
|
|
48
|
+
# @param plural [Symbol, nil] plural name (defaults to name + 's').
|
|
49
|
+
# @param variable [Symbol, nil] instance variable name (defaults to '@' + plural).
|
|
8
50
|
def attr_tuple_adder_to_hash(name, klass, plural: nil, variable: nil)
|
|
9
51
|
|
|
10
52
|
plural ||= name.to_s + 's'
|
|
@@ -26,10 +68,15 @@ module Musa
|
|
|
26
68
|
end
|
|
27
69
|
end
|
|
28
70
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
71
|
+
# Creates methods for adding id/value tuples to an array collection.
|
|
72
|
+
#
|
|
73
|
+
# Similar to attr_tuple_adder_to_hash but stores items in an array instead of hash.
|
|
74
|
+
# Useful when order matters or duplicates are allowed.
|
|
75
|
+
#
|
|
76
|
+
# @param name [Symbol] singular name for the item.
|
|
77
|
+
# @param klass [Class] class to instantiate.
|
|
78
|
+
# @param plural [Symbol, nil] plural name.
|
|
79
|
+
# @param variable [Symbol, nil] instance variable name.
|
|
33
80
|
def attr_tuple_adder_to_array(name, klass, plural: nil, variable: nil)
|
|
34
81
|
|
|
35
82
|
plural ||= name.to_s + 's'
|
|
@@ -51,10 +98,14 @@ module Musa
|
|
|
51
98
|
end
|
|
52
99
|
end
|
|
53
100
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
101
|
+
# Creates methods for adding complex objects (with multiple parameters) to an array.
|
|
102
|
+
#
|
|
103
|
+
# Supports both positional and keyword arguments when creating instances.
|
|
104
|
+
#
|
|
105
|
+
# @param name [Symbol] singular name.
|
|
106
|
+
# @param klass [Class] class to instantiate.
|
|
107
|
+
# @param plural [Symbol, nil] plural name.
|
|
108
|
+
# @param variable [Symbol, nil] instance variable name.
|
|
58
109
|
def attr_complex_adder_to_array(name, klass, plural: nil, variable: nil)
|
|
59
110
|
|
|
60
111
|
plural ||= name.to_s + 's'
|
|
@@ -86,10 +137,14 @@ module Musa
|
|
|
86
137
|
end
|
|
87
138
|
|
|
88
139
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
|
|
140
|
+
# Creates methods for adding complex objects with custom construction logic.
|
|
141
|
+
#
|
|
142
|
+
# The block receives parameters and should construct and add the object.
|
|
143
|
+
#
|
|
144
|
+
# @param name [Symbol] singular name.
|
|
145
|
+
# @param plural [Symbol, nil] plural name.
|
|
146
|
+
# @param variable [Symbol, nil] instance variable name.
|
|
147
|
+
# @yield Constructor block executed in instance context.
|
|
93
148
|
def attr_complex_adder_to_custom(name, plural: nil, variable: nil, &constructor_and_adder)
|
|
94
149
|
|
|
95
150
|
plural ||= name.to_s + 's'
|
|
@@ -121,9 +176,11 @@ module Musa
|
|
|
121
176
|
end
|
|
122
177
|
end
|
|
123
178
|
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
179
|
+
# Creates a simple getter/setter DSL method for a single value.
|
|
180
|
+
#
|
|
181
|
+
# @param name [Symbol] attribute name.
|
|
182
|
+
# @param klass [Class, nil] class to instantiate (nil = use value as-is).
|
|
183
|
+
# @param variable [Symbol, nil] instance variable name.
|
|
127
184
|
def attr_simple_builder(name, klass = nil, variable: nil)
|
|
128
185
|
variable ||= ('@' + name.to_s).to_sym
|
|
129
186
|
|
|
@@ -140,34 +197,38 @@ module Musa
|
|
|
140
197
|
attr_writer name
|
|
141
198
|
end
|
|
142
199
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
|
|
200
|
+
# Creates a getter/setter DSL method for a single id/value tuple.
|
|
201
|
+
#
|
|
202
|
+
# @param name [Symbol] attribute name.
|
|
203
|
+
# @param klass [Class] class to instantiate.
|
|
204
|
+
# @param variable [Symbol, nil] instance variable name.
|
|
146
205
|
def attr_tuple_builder(name, klass, variable: nil)
|
|
147
206
|
variable ||= ('@' + name.to_s).to_sym
|
|
148
207
|
|
|
149
208
|
define_method name do |**parameters, &block|
|
|
150
|
-
raise ArgumentError, "Method #{name} can only create instances with one id: value arguments pattern" unless parameters.size == 1
|
|
151
|
-
|
|
152
209
|
if parameters.empty?
|
|
153
210
|
instance_variable_get variable
|
|
154
|
-
|
|
211
|
+
elsif parameters.size == 1
|
|
155
212
|
parameter = parameters.first
|
|
156
213
|
klass.new(*parameter, &block).tap do |object|
|
|
157
214
|
instance_variable_set variable, object
|
|
158
215
|
end
|
|
216
|
+
else
|
|
217
|
+
raise ArgumentError, "Method #{name} can only create instances with one id: value arguments pattern"
|
|
159
218
|
end
|
|
160
219
|
end
|
|
161
220
|
|
|
162
221
|
attr_writer name
|
|
163
222
|
end
|
|
164
223
|
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
|
|
224
|
+
# Creates a getter/setter DSL method for complex objects with multiple parameters.
|
|
225
|
+
#
|
|
226
|
+
# Supports optional first_parameter that's automatically prepended when constructing.
|
|
227
|
+
#
|
|
228
|
+
# @param name [Symbol] attribute name.
|
|
229
|
+
# @param klass [Class] class to instantiate.
|
|
230
|
+
# @param variable [Symbol, nil] instance variable name.
|
|
231
|
+
# @param first_parameter [Object, nil] parameter automatically prepended to constructor.
|
|
171
232
|
def attr_complex_builder(name, klass, variable: nil, first_parameter: nil)
|
|
172
233
|
variable ||= ('@' + name.to_s).to_sym
|
|
173
234
|
|