musa-dsl 0.42.6 → 0.43.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88eb7916fe255e3b5d72520d25d4057252c41f24926975659712668a43c0cc41
4
- data.tar.gz: 7fbe17e6453b42f4729104e47392c7b4819436de0bf0e64fd28505f0e936e76f
3
+ metadata.gz: fa34fee0b26abe335f3288cc4593230f4f9e76b0b8f198594d5880c3055038f6
4
+ data.tar.gz: 7836f199fc7a81192269391d4e26d68e99de85d116750e30c32bf39df421ed39
5
5
  SHA512:
6
- metadata.gz: 25514b23a820ff8ec8f2f99c6e607dcec8879db85d030e3d3e19398a26ea63f3c81752d7e839eefb27e6a640b355aa0b58fb0a43b6f076f79b59e2303bf00e8d
7
- data.tar.gz: f89fbbf88a3f62f455dcf6cb342f47f324c1c4c93c225fcc45ba86f766e31a3300cacd7a3ff379698e97a201594a0e1610357cd20e843a4d797997c551b9c758
6
+ metadata.gz: 296f17a87edaf39c519c3054bfc790445aa45aa5295aee76b978c831465a6e4a312b2c541fc9d99424e7365ff9726055e05537ddb6b7b362ee5fb69bb5b3e83d
7
+ data.tar.gz: 571a82f51bc2e5d61c77f214629d727c8c47ed1c7a8d0ba6c4ac7c3019ccd4ba93f7a6f85b81e7041fdb773aa11607ab1606a7a2f9adbee252ccc72e8e516a76
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
- ## Installation
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
 
@@ -9,11 +9,40 @@ Editor → MusaLCE Client → TCP (port 1327) → REPL Server → DSL Context
9
9
  Results/Errors
10
10
  ```
11
11
 
12
- **Available MusaLCE Clients:**
13
- - **MusaLCEClientForVSCode**: Visual Studio Code extension
14
- - **MusaLCEClientForAtom**: Atom editor plugin
15
- - **MusaLCEforBitwig**: Bitwig Studio integration
16
- - **MusaLCEforLive**: Ableton Live integration
12
+ The REPL is **only** the TCP eval channel between editor and server. Anything else a live-coding session needs (DAW transport, MIDI routing, OSC surface relay, etc.) lives outside this subsystem.
13
+
14
+ ## Two scenarios
15
+
16
+ ### Case 1 Standalone live coding (you bring your own REPL)
17
+
18
+ This is the scenario this document covers. You build your own `main.rb` (sequencer, voices, clock, transport, your own helpers) and start `Musa::REPL::REPL.new(binding)` inside the sequencer's DSL context. Your editor connects to it on `localhost:1327` and you send Ruby fragments live.
19
+
20
+ Maximum control. Useful when:
21
+
22
+ - You drive non-DAW targets — SuperCollider, Max/MSP, OSC apps, OS voice synthesis, custom hardware.
23
+ - You're prototyping a personal live-coding DSL (helpers, Tidal-Cycles-style API).
24
+ - You want to keep the dependency footprint to musa-dsl alone.
25
+
26
+ A complete worked example with a Tidal-Cycles-style `d(n)` / `hush` / `solo` API: [`musadsl-demo/_demo-13-live-coding`](https://github.com/javier-sy/musadsl-demo).
27
+
28
+ ### Case 2 — Suite workflow (musalce-server handles everything)
29
+
30
+ When the target is **Ableton Live** or **Bitwig Studio**, the [musalce-server](https://github.com/javier-sy/musalce-server) gem packages this REPL plus a sequencer, a clock, a transport, a DAW handler (OSC over UDP to the per-DAW extension) and a surface for Stream Deck integration via Pulso. Internally case 2 is a **specialization** of case 1 — `musalce-server` opens `Musa::REPL::REPL.new(binding)` after pre-building all the boilerplate, and exposes a `daw.*` API in the DSL context.
31
+
32
+ Documented separately in the suite's architecture reference: [musalce-server/docs/architecture.md](https://github.com/javier-sy/musalce-server/blob/master/docs/architecture.md).
33
+
34
+ ## Components
35
+
36
+ **REPL clients** (talk to the REPL server over TCP 1327 — same shape in both scenarios):
37
+
38
+ - [MusaLCEClientForVSCode](https://github.com/javier-sy/MusaLCEClientForVSCode) — Visual Studio Code extension
39
+ - [MusaLCEClientForAtom](https://github.com/javier-sy/MusaLCEClientForAtom) — Atom editor plugin (discontinued, December 2022)
40
+
41
+ **Suite-only components** (only used in case 2 — see musalce-server architecture doc):
42
+
43
+ - [musalce-server](https://github.com/javier-sy/musalce-server) — packages REPL + sequencer + DAW handler + surface
44
+ - [MusaLCEforBitwig](https://github.com/javier-sy/MusaLCEforBitwig) — Bitwig Studio controller extension (Java)
45
+ - [MusaLCEforLive](https://github.com/javier-sy/MusaLCEforLive) — Ableton Live MIDI Remote Script (Python)
17
46
 
18
47
  ## Communication Protocol
19
48
 
@@ -55,34 +84,41 @@ Server → Client:
55
84
  Starting composition...
56
85
  ```
57
86
 
58
- ## Server Setup
59
-
60
- **Basic REPL Server:**
87
+ ## Server Setup (Case 1 — canonical pattern)
61
88
 
62
89
  ```ruby
63
90
  require 'musa-dsl'
91
+ require 'midi-communications' # or any other MIDI/OSC layer you prefer
92
+
64
93
  include Musa::All
65
94
 
66
- # Create sequencer and transport
95
+ # 1. MIDI output of your choice
96
+ output = MIDICommunications::Output.gets
97
+
98
+ # 2. Clock + transport
67
99
  clock = TimerClock.new(bpm: 120, ticks_per_beat: 24)
68
100
  transport = Transport.new(clock, 4, 24)
69
101
 
70
- # Start REPL server bound to sequencer context
71
- # The REPL will execute code in the sequencer's DSL context
102
+ # 3. Sequencer DSL context anything you define inside `with` becomes
103
+ # available in the REPL (instance methods, accessors, helpers).
72
104
  transport.sequencer.with do
73
- # DSL methods available in REPL
74
- def note(pitch:, duration:)
75
- puts "Playing pitch #{pitch} for #{duration} bars"
105
+ voices = MIDIVoices.new(sequencer: transport.sequencer, output: output, channels: [0, 1])
106
+
107
+ # Define whatever DSL surface you want exposed to the REPL.
108
+ def my_note(pitch:, duration:)
109
+ voices.voices[0].note(pitch, duration: duration)
76
110
  end
77
111
 
78
- # Create REPL server (port 1327 by default)
112
+ # 4. Start the REPL server (binds to TCP/1327 in this DSL context)
79
113
  @repl = Musa::REPL::REPL.new(binding)
80
114
  end
81
115
 
82
- # Start playback (REPL runs in background thread)
116
+ # 5. Start playback (REPL runs in background thread)
83
117
  transport.start
84
118
  ```
85
119
 
120
+ If you instead want the **suite workflow** (case 2 — Bitwig or Live with DAW handler + Stream Deck surface), don't write the above by hand: install [musalce-server](https://github.com/javier-sy/musalce-server) and run `musalce-server bitwig|live`. It opens this same REPL with a richer DSL context (`daw.*`, `surface[:event]`, …).
121
+
86
122
  **File Path Injection:**
87
123
 
88
124
  When a client sends a file path via `#path`, the REPL injects it as `@user_pathname` (Pathname object). This enables relative requires based on the editor's current file location:
@@ -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 [value] block executed for each serie value
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 [value] block for each value
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
- # @yield [position] block executed each interval
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) { |pos| puts "Beat #{pos}" }
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 Every beat until position 10
863
- # sequencer.every(1r, till: 10r) { |control| puts control.position }
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 [value] block executed with interpolated value
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,
@@ -1,3 +1,3 @@
1
1
  module Musa
2
- VERSION = '0.42.6'.freeze
2
+ VERSION = '0.43.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: musa-dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.42.6
4
+ version: 0.43.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Sánchez Yeste