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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88eb7916fe255e3b5d72520d25d4057252c41f24926975659712668a43c0cc41
4
- data.tar.gz: 7fbe17e6453b42f4729104e47392c7b4819436de0bf0e64fd28505f0e936e76f
3
+ metadata.gz: '08cad9ebc734d1e470cbf691738f1f08b9042796a4cf01adadb0bb2d126ef121'
4
+ data.tar.gz: f5b9a5ec33e63d511507fa27f42f64b7b126e9104c0d14c747c95445f953dc13
5
5
  SHA512:
6
- metadata.gz: 25514b23a820ff8ec8f2f99c6e607dcec8879db85d030e3d3e19398a26ea63f3c81752d7e839eefb27e6a640b355aa0b58fb0a43b6f076f79b59e2303bf00e8d
7
- data.tar.gz: f89fbbf88a3f62f455dcf6cb342f47f324c1c4c93c225fcc45ba86f766e31a3300cacd7a3ff379698e97a201594a0e1610357cd20e843a4d797997c551b9c758
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
- ## 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
 
@@ -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.42.7'.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.42.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javier Sánchez Yeste