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,316 @@
|
|
|
1
|
+
# Core Extensions - Advanced Metaprogramming
|
|
2
|
+
|
|
3
|
+
**Note for Advanced Users:** This section covers low-level Ruby refinements and metaprogramming utilities that form the foundation of MusaDSL's flexible syntax. These tools are primarily intended for users who want to extend the DSL, create custom builders, or integrate Musa DSL deeply into their own frameworks.
|
|
4
|
+
|
|
5
|
+
Core Extensions provide Ruby refinements and metaprogramming utilities that enable MusaDSL's flexible DSL syntax. These are the building blocks used throughout the framework.
|
|
6
|
+
|
|
7
|
+
## Ruby Refinements & Metaprogramming
|
|
8
|
+
|
|
9
|
+
**Arrayfy & Hashify** - Parameter Normalization:
|
|
10
|
+
|
|
11
|
+
Convert any object to array or hash with specified keys. Essential for flexible DSL method signatures.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'musa-dsl'
|
|
15
|
+
|
|
16
|
+
using Musa::Extension::Arrayfy
|
|
17
|
+
using Musa::Extension::Hashify
|
|
18
|
+
|
|
19
|
+
# Arrayfy: ensure parameter is array
|
|
20
|
+
value = 42
|
|
21
|
+
value.arrayfy # => [42]
|
|
22
|
+
|
|
23
|
+
array = [1, 2, 3]
|
|
24
|
+
array.arrayfy # => [1, 2, 3] (already array, unchanged)
|
|
25
|
+
|
|
26
|
+
# Hashify: convert to hash with specified keys
|
|
27
|
+
data = [60, 1r, 80]
|
|
28
|
+
data.hashify(:pitch, :duration, :velocity)
|
|
29
|
+
# => { pitch: 60, duration: 1r, velocity: 80 }
|
|
30
|
+
|
|
31
|
+
# Works with hashes (validates keys)
|
|
32
|
+
existing = { pitch: 64, duration: 1r }
|
|
33
|
+
existing.hashify(:pitch, :duration, :velocity)
|
|
34
|
+
# => { pitch: 64, duration: 1r, velocity: nil }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**ExplodeRanges** - Range Expansion:
|
|
38
|
+
|
|
39
|
+
Expand Range objects within arrays, useful for parameter generation.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require 'musa-dsl'
|
|
43
|
+
|
|
44
|
+
using Musa::Extension::ExplodeRanges
|
|
45
|
+
|
|
46
|
+
# Expand ranges in arrays
|
|
47
|
+
[0, 2..4, 7].explode_ranges
|
|
48
|
+
# => [0, 2, 3, 4, 7]
|
|
49
|
+
|
|
50
|
+
# Works with nested structures
|
|
51
|
+
[1, 3..5, [10, 12..14]].explode_ranges
|
|
52
|
+
# => [1, 3, 4, 5, [10, 12, 13, 14]]
|
|
53
|
+
|
|
54
|
+
# Useful for pitch collections
|
|
55
|
+
chord = [60, 64..67, 72].explode_ranges
|
|
56
|
+
# => [60, 64, 65, 66, 67, 72]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**DeepCopy** - Deep Object Cloning:
|
|
60
|
+
|
|
61
|
+
Create deep copies of objects with circular reference handling and singleton module preservation.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
require 'musa-dsl'
|
|
65
|
+
|
|
66
|
+
using Musa::Extension::DeepCopy
|
|
67
|
+
|
|
68
|
+
original = { pitch: 60, envelope: { attack: 0.1, decay: 0.2 } }
|
|
69
|
+
copy = original.deep_copy
|
|
70
|
+
|
|
71
|
+
copy[:envelope][:attack] = 0.5
|
|
72
|
+
|
|
73
|
+
original[:envelope][:attack] # => 0.1 (unchanged)
|
|
74
|
+
copy[:envelope][:attack] # => 0.5 (modified)
|
|
75
|
+
|
|
76
|
+
# Preserves singleton modules (dataset types)
|
|
77
|
+
gdv = { grade: 0, duration: 1r }.extend(Musa::Datasets::GDV)
|
|
78
|
+
gdv_copy = gdv.deep_copy
|
|
79
|
+
|
|
80
|
+
gdv_copy.is_a?(Musa::Datasets::GDV) # => true (module preserved)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**SmartProcBinder** - Intelligent Parameter Binding:
|
|
84
|
+
|
|
85
|
+
Automatically match Proc parameters with available values, enabling flexible block signatures in DSL methods.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require 'musa-dsl'
|
|
89
|
+
|
|
90
|
+
# SmartProcBinder is used internally by Series operations
|
|
91
|
+
# to match block parameters flexibly
|
|
92
|
+
|
|
93
|
+
using Musa::Extension::SmartProcBinder
|
|
94
|
+
|
|
95
|
+
# Example: .with operation uses SmartProcBinder
|
|
96
|
+
pitches = S(60, 64, 67)
|
|
97
|
+
durations = S(1r, 1/2r, 1/4r)
|
|
98
|
+
|
|
99
|
+
# Block can request any combination of parameters
|
|
100
|
+
notes = pitches.with(dur: durations) do |p, dur:|
|
|
101
|
+
{ pitch: p, duration: dur }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# SmartProcBinder matches 'p' to pitch value, 'dur:' to duration value
|
|
105
|
+
# regardless of parameter order or naming
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**DynamicProxy** - Lazy Initialization Pattern:
|
|
109
|
+
|
|
110
|
+
Forward method calls to a lazily-initialized target. Used for deferred object creation.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
require 'musa-dsl'
|
|
114
|
+
|
|
115
|
+
# DynamicProxy is used internally for lazy series evaluation
|
|
116
|
+
# and deferred resource allocation
|
|
117
|
+
|
|
118
|
+
# Example: Proxy pattern for expensive resource
|
|
119
|
+
class ExpensiveResource
|
|
120
|
+
def initialize
|
|
121
|
+
puts "Initializing expensive resource..."
|
|
122
|
+
@data = (1..1000000).to_a
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def process
|
|
126
|
+
puts "Processing..."
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Create proxy (doesn't initialize resource yet)
|
|
131
|
+
proxy = Musa::Extension::DynamicProxy::DynamicProxy.new(ExpensiveResource)
|
|
132
|
+
|
|
133
|
+
# Resource is created only when first method is called
|
|
134
|
+
proxy.process # Outputs: "Initializing expensive resource..." then "Processing..."
|
|
135
|
+
proxy.process # Only outputs: "Processing..." (resource already initialized)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**With** - Flexible Block Execution:
|
|
139
|
+
|
|
140
|
+
Execute blocks with flexible context switching (instance_eval vs call with self). Core utility for DSL builders.
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
require 'musa-dsl'
|
|
144
|
+
|
|
145
|
+
using Musa::Extension::With
|
|
146
|
+
|
|
147
|
+
# Used internally by DSL builders to execute configuration blocks
|
|
148
|
+
# Can switch between instance_eval (DSL style) and block.call (parameter style)
|
|
149
|
+
|
|
150
|
+
class Builder
|
|
151
|
+
def initialize(&block)
|
|
152
|
+
@items = []
|
|
153
|
+
# Execute block in builder context using With
|
|
154
|
+
self.with &block
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def item(name)
|
|
158
|
+
@items << name
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def items
|
|
162
|
+
@items
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# DSL-style block (instance_eval)
|
|
167
|
+
builder = Builder.new do
|
|
168
|
+
item "first"
|
|
169
|
+
item "second"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
builder.items # => ["first", "second"]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**AttributeBuilder** - DSL Builder Macros:
|
|
176
|
+
|
|
177
|
+
Metaprogramming macros for creating DSL builder patterns. Automatically generates setter and getter methods.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
require 'musa-dsl'
|
|
181
|
+
|
|
182
|
+
# AttributeBuilder is used internally by MusicXML Builder and other DSL components
|
|
183
|
+
|
|
184
|
+
class SynthConfig
|
|
185
|
+
include Musa::Extension::AttributeBuilder
|
|
186
|
+
|
|
187
|
+
# Define DSL attributes
|
|
188
|
+
attribute :waveform
|
|
189
|
+
attribute :frequency
|
|
190
|
+
attribute :amplitude
|
|
191
|
+
|
|
192
|
+
def initialize(&block)
|
|
193
|
+
self.with &block if block
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Use DSL to configure
|
|
198
|
+
synth = SynthConfig.new do
|
|
199
|
+
waveform :sine
|
|
200
|
+
frequency 440
|
|
201
|
+
amplitude 0.8
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
synth.waveform # => :sine
|
|
205
|
+
synth.frequency # => 440
|
|
206
|
+
synth.amplitude # => 0.8
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Logger - Sequencer-Aware Logging
|
|
210
|
+
|
|
211
|
+
Specialized logger that displays sequencer position alongside log messages. Essential for debugging temporal issues in compositions.
|
|
212
|
+
|
|
213
|
+
**Features:**
|
|
214
|
+
- Automatic sequencer position formatting
|
|
215
|
+
- Configurable position precision (integer and decimal digits)
|
|
216
|
+
- Integration with InspectNice for readable Rational display
|
|
217
|
+
- Standard Ruby Logger levels (DEBUG, INFO, WARN, ERROR, FATAL)
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
require 'musa-dsl'
|
|
221
|
+
|
|
222
|
+
# Create sequencer-aware logger
|
|
223
|
+
sequencer = Musa::Sequencer::Sequencer.new(4, 24)
|
|
224
|
+
|
|
225
|
+
logger = Musa::Logger.new(
|
|
226
|
+
sequencer: sequencer,
|
|
227
|
+
level: :debug,
|
|
228
|
+
position_format_integer_digits: 3, # Position: " 4" instead of "4"
|
|
229
|
+
position_format_decimal_digits: 3 # Position: "4.500" instead of "4.5"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Use logger in sequencer context
|
|
233
|
+
sequencer.at 1 do
|
|
234
|
+
logger.info "Starting melody at bar 1"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
sequencer.at 4.5r do
|
|
238
|
+
logger.debug "Halfway through bar 5"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
sequencer.at 10 do
|
|
242
|
+
logger.warn "Approaching ending"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Run sequencer to see logged output
|
|
246
|
+
sequencer.run
|
|
247
|
+
|
|
248
|
+
# Output:
|
|
249
|
+
# 001.000: [INFO] Starting melody at bar 1
|
|
250
|
+
# 004.500: [DEBUG] Halfway through bar 5
|
|
251
|
+
# 010.000: [WARN] Approaching ending
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Use Cases:**
|
|
255
|
+
- **Temporal Debugging**: Track down timing issues by seeing exact musical position
|
|
256
|
+
- **MIDI Event Monitoring**: Log MIDI note-on/note-off with positions
|
|
257
|
+
- **Composition Development**: Monitor sequencer flow during development
|
|
258
|
+
- **Performance Analysis**: Identify bottlenecks by logging with timestamps
|
|
259
|
+
|
|
260
|
+
## API Reference
|
|
261
|
+
|
|
262
|
+
**Complete API documentation:**
|
|
263
|
+
- [Musa::Extension](https://rubydoc.info/gems/musa-dsl/Musa/Extension) - Ruby refinements and metaprogramming utilities
|
|
264
|
+
- [Musa::Logger](https://rubydoc.info/gems/musa-dsl/Musa/Logger) - Structured logging system
|
|
265
|
+
|
|
266
|
+
**Source code:** `lib/core-ext/` and `lib/logger/`
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
## Documentation
|
|
270
|
+
|
|
271
|
+
Full API documentation is available in YARD format. All files in the project are comprehensively documented with:
|
|
272
|
+
|
|
273
|
+
- Architecture overviews
|
|
274
|
+
- Usage examples
|
|
275
|
+
- Parameter descriptions
|
|
276
|
+
- Return values
|
|
277
|
+
- Integration examples
|
|
278
|
+
|
|
279
|
+
To generate and view the documentation locally:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
yard doc
|
|
283
|
+
yard server
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Then open http://localhost:8808 in your browser.
|
|
287
|
+
|
|
288
|
+
## Examples & Works
|
|
289
|
+
|
|
290
|
+
Listen to compositions created with Musa-DSL: [yeste.studio](https://yeste.studio)
|
|
291
|
+
|
|
292
|
+
## Contributing
|
|
293
|
+
|
|
294
|
+
Contributions are welcome! Please feel free to:
|
|
295
|
+
|
|
296
|
+
1. Fork the repository
|
|
297
|
+
2. Create a feature branch
|
|
298
|
+
3. Make your changes with tests
|
|
299
|
+
4. Submit a pull request
|
|
300
|
+
|
|
301
|
+
**Repository:** https://github.com/javier-sy/musa-dsl
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
Musa-DSL is released under the [LGPL-3.0-or-later](https://www.gnu.org/licenses/lgpl-3.0.html) license.
|
|
306
|
+
|
|
307
|
+
## Acknowledgments
|
|
308
|
+
|
|
309
|
+
- **Author:** Javier Sánchez Yeste ([yeste.studio](https://yeste.studio))
|
|
310
|
+
- **Email:** javier (at) yeste.studio
|
|
311
|
+
|
|
312
|
+
Special thanks to [JetBrains](https://www.jetbrains.com/?from=Musa-DSL) for providing an Open Source project license for RubyMine IDE during several years.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
*Musa-DSL - Algorithmic sound and musical thinking through code*
|