musa-dsl 0.42.6 → 0.42.7
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/README.md +46 -1
- data/docs/subsystems/sequencer.md +84 -0
- data/lib/musa-dsl/sequencer/base-sequencer.rb +57 -15
- data/lib/musa-dsl/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08cad9ebc734d1e470cbf691738f1f08b9042796a4cf01adadb0bb2d126ef121'
|
|
4
|
+
data.tar.gz: f5b9a5ec33e63d511507fa27f42f64b7b126e9104c0d14c747c95445f953dc13
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ca2b3741c4d4d83bf3318efe27a30b4730a9a2664ec3a5a8ad8d64f54a71697ee3b119d6ecbbfc61b2f2f4a800d516c5f857d55a012611afa4ab6f120f0ca7a
|
|
7
|
+
data.tar.gz: e0956d3b3593c171e22ebf448182655f3e754bef2af495e96fbff4a096c8c0511d937cc5732f6d57d1ec7b0ea9d3bbd01c79bf1732c7ed06dd3d8965361f300f
|
data/README.md
CHANGED
|
@@ -29,7 +29,52 @@ Musa-DSL is a programming language DSL (Domain-Specific Language) based on Ruby
|
|
|
29
29
|
- **Neumalang Notation** - Intuitive text-based and customizable musical (or sound) notation
|
|
30
30
|
- **Transcription System** - Convert musical gestures to MIDI and MusicXML with ornament transcription expansion
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Getting Started
|
|
33
|
+
|
|
34
|
+
### Recommended Editor: RubyMine
|
|
35
|
+
|
|
36
|
+
[RubyMine](https://www.jetbrains.com/ruby/) provides the best experience for MusaDSL development: intelligent autocomplete of methods and parameters, hover documentation, and type inference help you discover the API as you write.
|
|
37
|
+
|
|
38
|
+
**Free licenses available:**
|
|
39
|
+
- [Non-commercial use](https://www.jetbrains.com/non-commercial/) — for learning, hobbies, open-source, content creation
|
|
40
|
+
- [Students](https://www.jetbrains.com/academy/student-pack/) and [Teachers/Researchers](https://www.jetbrains.com/academy/teacher-pack/) — with institutional email
|
|
41
|
+
|
|
42
|
+
VSCode with the Ruby LSP extension also works well, though Ruby autocomplete and hover documentation are less complete.
|
|
43
|
+
|
|
44
|
+
### AI Composition Assistant: Claude Code Plugin
|
|
45
|
+
|
|
46
|
+
The fastest way to learn and compose with MusaDSL is through **[Nota](https://github.com/javier-sy/nota-plugin-for-claude)** — a plugin for [Claude Code](https://claude.ai/code) that provides:
|
|
47
|
+
|
|
48
|
+
- **`/nota:explain`** — Ask any question about MusaDSL and get sourced answers with working code examples
|
|
49
|
+
- **`/nota:think`** — Creative ideation across multiple musical dimensions
|
|
50
|
+
- **`/nota:code`** — Describe your musical intention in natural language and get verified MusaDSL code
|
|
51
|
+
- **`/nota:analyze`** — Structured analysis of your compositions
|
|
52
|
+
- **`/nota:best-practices`** — Consolidate recurring patterns into searchable best practices
|
|
53
|
+
|
|
54
|
+
The plugin includes a semantic knowledge base covering all MusaDSL documentation, API reference, 22+ demo projects, and 12 built-in composition best practices. Your compositions, analyses, and practices become searchable knowledge that enriches future sessions.
|
|
55
|
+
|
|
56
|
+
**Requirements:** [Ruby 3.4+](https://www.ruby-lang.org/) and a [Voyage AI](https://dash.voyageai.com/) API key (free tier is sufficient for personal use).
|
|
57
|
+
|
|
58
|
+
**Install in Claude Code:**
|
|
59
|
+
|
|
60
|
+
First, add the Nota marketplace:
|
|
61
|
+
```
|
|
62
|
+
/plugin marketplace add javier-sy/nota-plugin-for-claude
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then install the plugin:
|
|
66
|
+
```
|
|
67
|
+
/plugin install nota@yeste.studio
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then add your Voyage AI API key to your shell profile:
|
|
71
|
+
```
|
|
72
|
+
export VOYAGE_API_KEY="your-key-here"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Run `/nota:setup` to verify the installation.
|
|
76
|
+
|
|
77
|
+
### Framework Installation
|
|
33
78
|
|
|
34
79
|
Add to your Gemfile:
|
|
35
80
|
|
|
@@ -39,6 +39,7 @@ transport.sequencer.with do
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Play series (play): reproduces series with automatic timing
|
|
42
|
+
# Default mode is :wait — each element's :duration determines the wait before the next
|
|
42
43
|
at 5 do
|
|
43
44
|
play melody do |note:, duration:, control:|
|
|
44
45
|
puts "Playing note: #{note}, duration: #{duration}"
|
|
@@ -46,6 +47,7 @@ transport.sequencer.with do
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
# Recurring event (every) with stop control
|
|
50
|
+
# Note: every passes control: as a keyword — declare only the params you need
|
|
49
51
|
beat_loop = nil
|
|
50
52
|
at 10 do
|
|
51
53
|
# Store control object to stop it later
|
|
@@ -102,6 +104,88 @@ every 1/4r do ... end
|
|
|
102
104
|
at 0.5 do ... end
|
|
103
105
|
```
|
|
104
106
|
|
|
107
|
+
## Block Parameter Flexibility (SmartProcBinder)
|
|
108
|
+
|
|
109
|
+
All scheduling methods (`every`, `play`, `move`, `play_timed`) pass parameters to user blocks via **SmartProcBinder**. This means blocks can declare **only the parameters they need** — undeclared parameters are silently ignored.
|
|
110
|
+
|
|
111
|
+
**Important**: keyword parameters (like `control:`) must be declared as **keyword arguments** in the block signature (`|control:|`), not as positional arguments (`|control|`).
|
|
112
|
+
|
|
113
|
+
### Parameters available per method
|
|
114
|
+
|
|
115
|
+
| Method | Positional params | Keyword params |
|
|
116
|
+
|--------|-------------------|----------------|
|
|
117
|
+
| `every` | _(none)_ | `control:` |
|
|
118
|
+
| `play` | element (+ hash keys as keywords) | `control:` |
|
|
119
|
+
| `move` | value, next_value | `control:`, `duration:`, `quantized_duration:`, `started_ago:`, `position_jitter:`, `duration_jitter:`, `right_open:` |
|
|
120
|
+
| `play_timed` | values (+ extra attributes as keywords) | `time:`, `started_ago:`, `control:` |
|
|
121
|
+
|
|
122
|
+
### Examples
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# every — no params needed
|
|
126
|
+
every 1r, duration: 4r do
|
|
127
|
+
puts "tick at #{position}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# every — with control keyword
|
|
131
|
+
every 1r do |control:|
|
|
132
|
+
puts "iteration #{control._execution_counter}"
|
|
133
|
+
control.stop if some_condition
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# play — hash keys become keywords
|
|
137
|
+
melody = S({ note: 60, duration: 1 }, { note: 64, duration: 1/2r })
|
|
138
|
+
play melody do |note:, duration:|
|
|
139
|
+
voice.note(note, duration: duration)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# play — with control keyword
|
|
143
|
+
play melody do |note:, duration:, control:|
|
|
144
|
+
voice.note(note, duration: duration)
|
|
145
|
+
control.stop if note == 64
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# move — only positional value
|
|
149
|
+
move from: 0, to: 127, duration: 4r, every: 1/4r do |value|
|
|
150
|
+
midi_cc(7, value.round)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# move — with keyword metadata
|
|
154
|
+
move from: 60, to: 72, duration: 4r, every: 1/4r do |value, next_value, control:, duration:|
|
|
155
|
+
puts "value=#{value.round} next=#{next_value&.round} dur=#{duration}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# play_timed — full signature
|
|
159
|
+
play_timed(timed_serie) do |values, time:, started_ago:, control:|
|
|
160
|
+
puts "values=#{values} at time=#{time}"
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Play Modes
|
|
165
|
+
|
|
166
|
+
`play` supports three modes that determine how series elements are scheduled. The default mode is `:wait`.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# :wait (default) — each element must have :duration; the sequencer waits
|
|
170
|
+
# that duration before consuming the next element
|
|
171
|
+
progression = S({ grade: 0, duration: 1 }, { grade: 3, duration: 1 })
|
|
172
|
+
play progression do |grade:, duration:|
|
|
173
|
+
puts "Grade #{grade}, duration #{duration}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# :at — each element must have :at; the sequencer schedules it at that absolute position
|
|
177
|
+
events = S({ note: 60, at: 1 }, { note: 64, at: 3 })
|
|
178
|
+
play events, mode: :at do |note:, at:|
|
|
179
|
+
puts "Note #{note} at position #{at}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# :neumalang — full Neumalang DSL processing with decoder
|
|
183
|
+
play neuma_serie, mode: :neumalang, decoder: decoder do |gdv|
|
|
184
|
+
pdv = gdv.to_pdv(scale)
|
|
185
|
+
voice.note(pdv[:pitch], velocity: pdv[:velocity], duration: pdv[:duration])
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
105
189
|
## Control Objects and `.stop`
|
|
106
190
|
|
|
107
191
|
All scheduling methods (`at`, `wait`, `now`, `play`, `play_timed`, `every`, `move`) return a control object that supports `.stop` to cancel execution. Calling `.stop` on the control prevents the associated block from running at its scheduled position, or stops further iterations for series/recurring operations.
|
|
@@ -38,6 +38,22 @@ module Musa
|
|
|
38
38
|
# - **Event Handlers**: Hierarchical event pub/sub system
|
|
39
39
|
# - **Controls**: Objects returned by scheduling methods for lifecycle management
|
|
40
40
|
#
|
|
41
|
+
# ## Block Parameter Flexibility (SmartProcBinder)
|
|
42
|
+
#
|
|
43
|
+
# All scheduling methods (`every`, `play`, `move`, `play_timed`) pass parameters
|
|
44
|
+
# to user blocks via SmartProcBinder. This means blocks can declare **only the
|
|
45
|
+
# parameters they need** — undeclared parameters are silently ignored.
|
|
46
|
+
#
|
|
47
|
+
# Keyword parameters (like `control:`) must be declared as keyword arguments
|
|
48
|
+
# in the block signature (`|control:|`), not as positional arguments (`|control|`).
|
|
49
|
+
#
|
|
50
|
+
# | Method | Positional params | Keyword params |
|
|
51
|
+
# |--------|-------------------|----------------|
|
|
52
|
+
# | `every` | _(none)_ | `control:` |
|
|
53
|
+
# | `play` | element (+ hash keys as keywords) | `control:` |
|
|
54
|
+
# | `move` | value, next_value | `control:`, `duration:`, `quantized_duration:`, `started_ago:`, `position_jitter:`, `duration_jitter:`, `right_open:` |
|
|
55
|
+
# | `play_timed` | values (+ extra attributes as keywords) | `time:`, `started_ago:`, `control:` |
|
|
56
|
+
#
|
|
41
57
|
# ## Tick-based vs Tickless
|
|
42
58
|
#
|
|
43
59
|
# **Tick-based** (beats_per_bar and ticks_per_beat specified):
|
|
@@ -642,20 +658,23 @@ module Musa
|
|
|
642
658
|
# Timing determined by mode.
|
|
643
659
|
#
|
|
644
660
|
# @param serie [Series] series to play
|
|
645
|
-
# @param mode [Symbol] running mode (:at, :wait, :neumalang)
|
|
661
|
+
# @param mode [Symbol] running mode (:at, :wait, :neumalang). Defaults to :wait
|
|
646
662
|
# @param parameter [Symbol, nil] duration parameter name from serie values
|
|
647
663
|
# @param on_stop [Proc, nil] callback when play stops (any reason, including manual stop)
|
|
648
664
|
# @param after_bars [Numeric, nil] delay for after callback
|
|
649
665
|
# @param after [Proc, nil] callback after play completes naturally (NOT on manual stop)
|
|
650
666
|
# @param context [Object, nil] context for neumalang processing
|
|
651
667
|
# @param mode_args [Hash] additional mode-specific parameters
|
|
652
|
-
# @yield
|
|
668
|
+
# @yield block executed for each serie value (via SmartProcBinder — declare only the parameters you need)
|
|
669
|
+
# @yieldparam element [Object] the current serie element (positional). When the element is a Hash,
|
|
670
|
+
# its keys are also available as keyword arguments (e.g., `|note:, duration:|`)
|
|
671
|
+
# @yieldparam control [PlayControl] the play control object (keyword, optional)
|
|
653
672
|
# @return [PlayControl] control object
|
|
654
673
|
#
|
|
655
674
|
# ## Available Running Modes
|
|
656
675
|
#
|
|
676
|
+
# - **:wait** (default): Elements with duration specify wait time before next element
|
|
657
677
|
# - **:at**: Elements specify absolute positions via :at key
|
|
658
|
-
# - **:wait**: Elements with duration specify wait time
|
|
659
678
|
# - **:neumalang**: Full Neumalang DSL with variables, commands, series, etc.
|
|
660
679
|
#
|
|
661
680
|
#
|
|
@@ -755,7 +774,11 @@ module Musa
|
|
|
755
774
|
# @param on_stop [Proc, nil] callback when playback stops
|
|
756
775
|
# @param after_bars [Numeric, nil] schedule after completion
|
|
757
776
|
# @param after [Proc, nil] block after completion
|
|
758
|
-
# @yield
|
|
777
|
+
# @yield block for each timed value (via SmartProcBinder — declare only the parameters you need)
|
|
778
|
+
# @yieldparam values [Hash, Array] current component values (positional). Hash in hash mode, Array in array mode
|
|
779
|
+
# @yieldparam time [Rational] absolute position of this event (keyword, optional)
|
|
780
|
+
# @yieldparam started_ago [Hash, Array] time since each component's last update (keyword, optional)
|
|
781
|
+
# @yieldparam control [PlayTimedControl] the play_timed control object (keyword, optional)
|
|
759
782
|
# @return [PlayTimedControl] control object
|
|
760
783
|
#
|
|
761
784
|
# @example Hash mode timed series
|
|
@@ -842,6 +865,13 @@ module Musa
|
|
|
842
865
|
# - **condition**: condition block returns false
|
|
843
866
|
# - **nil interval**: immediate stop after first execution
|
|
844
867
|
#
|
|
868
|
+
# ## Block Parameters (via SmartProcBinder)
|
|
869
|
+
#
|
|
870
|
+
# The block receives the following keyword parameter via SmartProcBinder.
|
|
871
|
+
# You can declare only the parameters you need — undeclared ones are silently ignored.
|
|
872
|
+
#
|
|
873
|
+
# - **control:** [EveryControl] — the control object for the current loop
|
|
874
|
+
#
|
|
845
875
|
# @param interval [Numeric, nil] interval between executions (nil = once)
|
|
846
876
|
# @param duration [Numeric, nil] total duration
|
|
847
877
|
# @param till [Numeric, nil] end position
|
|
@@ -849,18 +879,16 @@ module Musa
|
|
|
849
879
|
# @param on_stop [Proc, nil] callback when loop stops
|
|
850
880
|
# @param after_bars [Numeric, nil] schedule after completion
|
|
851
881
|
# @param after [Proc, nil] block after completion
|
|
852
|
-
# @
|
|
882
|
+
# @yieldparam control [EveryControl] the loop's control object (keyword, optional)
|
|
853
883
|
# @return [EveryControl] control object
|
|
854
884
|
#
|
|
855
|
-
# @example
|
|
856
|
-
# seq.every(1, till: 8) {
|
|
857
|
-
#
|
|
858
|
-
# @example Every 4 beats for 16 bars
|
|
859
|
-
# sequencer.every(1r, duration: 4r) { puts "tick" }
|
|
860
|
-
# # Executes at 1r, 2r, 3r, 4r, 5r (5 times total)
|
|
885
|
+
# @example No parameters needed
|
|
886
|
+
# seq.every(1, till: 8) { puts "Beat at #{seq.position}" }
|
|
861
887
|
#
|
|
862
|
-
# @example
|
|
863
|
-
#
|
|
888
|
+
# @example Accessing the control object (keyword argument)
|
|
889
|
+
# seq.every(1r, duration: 4r) do |control:|
|
|
890
|
+
# puts "Iteration #{control._execution_counter}"
|
|
891
|
+
# end
|
|
864
892
|
#
|
|
865
893
|
# @example Conditional loop
|
|
866
894
|
# count = 0
|
|
@@ -947,7 +975,16 @@ module Musa
|
|
|
947
975
|
# @param on_stop [Proc, nil] callback when animation stops
|
|
948
976
|
# @param after_bars [Numeric, nil] schedule after completion
|
|
949
977
|
# @param after [Proc, nil] block after completion
|
|
950
|
-
# @yield
|
|
978
|
+
# @yield block executed with interpolated value (via SmartProcBinder — declare only the parameters you need)
|
|
979
|
+
# @yieldparam value [Numeric, Array, Hash] current interpolated value(s) (positional)
|
|
980
|
+
# @yieldparam next_value [Numeric, Array, Hash, nil] next interpolated value(s), nil at end (positional)
|
|
981
|
+
# @yieldparam control [MoveControl] the move control object (keyword, optional)
|
|
982
|
+
# @yieldparam duration [Numeric, Array, Hash] interval duration per component (keyword, optional)
|
|
983
|
+
# @yieldparam quantized_duration [Numeric, Array, Hash] quantized interval duration (keyword, optional)
|
|
984
|
+
# @yieldparam started_ago [Numeric, Array, Hash, nil] time since component last changed (keyword, optional)
|
|
985
|
+
# @yieldparam position_jitter [Numeric, Array, Hash] position rounding error (keyword, optional)
|
|
986
|
+
# @yieldparam duration_jitter [Numeric, Array, Hash] duration rounding error (keyword, optional)
|
|
987
|
+
# @yieldparam right_open [Boolean, Array, Hash] whether final value is excluded (keyword, optional)
|
|
951
988
|
# @return [MoveControl] control object
|
|
952
989
|
#
|
|
953
990
|
# @example Simple pitch glide
|
|
@@ -990,7 +1027,7 @@ module Musa
|
|
|
990
1027
|
# function: proc { |ratio| ratio ** 2 } # Ease-in
|
|
991
1028
|
# ) { |value| puts value }
|
|
992
1029
|
#
|
|
993
|
-
# @example Linear fade
|
|
1030
|
+
# @example Linear fade (only positional value needed)
|
|
994
1031
|
# seq = Musa::Sequencer::BaseSequencer.new(4, 24)
|
|
995
1032
|
#
|
|
996
1033
|
# volume_values = []
|
|
@@ -1001,6 +1038,11 @@ module Musa
|
|
|
1001
1038
|
#
|
|
1002
1039
|
# seq.run
|
|
1003
1040
|
# # Result: volume_values contains [0, 8, 16, 24, ..., 119, 127]
|
|
1041
|
+
#
|
|
1042
|
+
# @example Using keyword parameters
|
|
1043
|
+
# seq.move(from: 60, to: 72, duration: 4r, every: 1/4r) do |value, next_value, control:, duration:|
|
|
1044
|
+
# puts "value=#{value.round} next=#{next_value&.round} dur=#{duration}"
|
|
1045
|
+
# end
|
|
1004
1046
|
def move(every: nil,
|
|
1005
1047
|
from: nil, to: nil, step: nil,
|
|
1006
1048
|
duration: nil, till: nil,
|
data/lib/musa-dsl/version.rb
CHANGED