Core Extensions - Advanced Metaprogramming

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.

Core Extensions provide Ruby refinements and metaprogramming utilities that enable MusaDSL's flexible DSL syntax. These are the building blocks used throughout the framework.

When is this the answer

These are not tools for writing music. They are what the DSL is made of, and you reach for them when you are extending it -- a builder of your own, a verb that takes parameters as loosely as the framework's do, a structure that has to survive being copied.

By the shape of the problem:

You have You want This
a parameter that may be one thing or many to stop writing x.is_a?(Array) ? x : [x] arrayfy
a parameter that may be one value or one per voice velocity: 80 and velocity: [80, 90] to mean the same thing hashify -- a single value broadcasts to every key
60..67 written where a list was expected the range opened out explode_ranges
a nested structure about to be handed to somebody who mutates a copy that shares nothing dup(deep: true), or clone(deep: true) if it is a dataset
a block whose body should read as a DSL item 'first' to resolve against your object With
a reference to something that does not exist yet to hand it out now and decide later DynamicProxy
a class with many add_x / xs pairs to write not to write them AttributeBuilder
a block that receives who-knows-what to declare only the parameters you use SmartProcBinder

When it is NOT the answer. If you are placing notes in time, choosing material, or shaping a phrase, nothing here is what you want -- see series, sequencer and neumas. These extensions have no musical meaning: they are about Ruby, and they are here because the musical layer is built on them.

The two that bite. dup drops the singleton class and clone keeps it, so copying a GDV with dup(deep: true) gives back a plain hash that works until something calls to_pdv on it. And explode_ranges only opens the top level: a range inside a nested array stays a range.

Ruby Refinements & Metaprogramming

Arrayfy & Hashify - Parameter Normalization:

Convert any object to array or hash with specified keys. Essential for flexible DSL method signatures.

require 'musa-dsl'

using Musa::Extension::Arrayfy
using Musa::Extension::Hashify

# Arrayfy: ensure parameter is array
value = 42
value.arrayfy  # => [42]

array = [1, 2, 3]
array.arrayfy  # => [1, 2, 3] (already array, unchanged)

# Hashify: convert to hash with the given keys, which go in `keys:`
data = [60, 1r, 80]
data.hashify(keys: [:pitch, :duration, :velocity])
# => { pitch: 60, duration: 1r, velocity: 80 }

# A hash keeps what it has and gains what it lacks
existing = { pitch: 64, duration: 1r }
existing.hashify(keys: [:pitch, :duration, :velocity])
# => { pitch: 64, duration: 1r, velocity: nil }

# And a single value is BROADCAST to every key, which is the whole point:
# it is what lets `velocity: 80` and `velocity: [80, 90, 100]` be written
# the same way at the call site
80.hashify(keys: [:soprano, :alto, :bass])
# => { soprano: 80, alto: 80, bass: 80 }

# `default:` fills the gaps instead of leaving them nil
{ pitch: 64 }.hashify(keys: [:pitch, :velocity], default: 80)
# => { pitch: 64, velocity: 80 }

ExplodeRanges - Range Expansion:

Expand Range objects within arrays, useful for parameter generation.

require 'musa-dsl'

using Musa::Extension::ExplodeRanges

# Expand ranges in arrays
[0, 2..4, 7].explode_ranges
# => [0, 2, 3, 4, 7]

# Only at the top level: a range inside a nested array is left alone.
[1, 3..5, [10, 12..14]].explode_ranges
# => [1, 3, 4, 5, [10, 12..14]]

# Useful for pitch collections
chord = [60, 64..67, 72].explode_ranges
# => [60, 64, 65, 66, 67, 72]

DeepCopy - Deep Object Cloning:

Create deep copies of objects with circular reference handling and singleton module preservation.

The refinement adds a deep: option to dup and clone; there is no deep_copy method on the object.

require 'musa-dsl'

using Musa::Extension::DeepCopy

original = { pitch: 60, envelope: { attack: 0.1, decay: 0.2 } }
copy = original.dup(deep: true)

copy[:envelope][:attack] = 0.5

original[:envelope][:attack]  # => 0.1
copy[:envelope][:attack]      # => 0.5

dup or clone decides whether the dataset survives, and it decides it the same way plain Ruby does: dup drops the singleton class, clone keeps it. A GDV, a PDV, an AbsI — every dataset in this framework is a Hash or an Array with a module extended into its singleton class, so this is not a detail:

gdv = { grade: 0, duration: 1r }.extend(Musa::Datasets::GDV)

gdv.clone(deep: true).is_a?(Musa::Datasets::GDV)  # => true
gdv.dup(deep: true).is_a?(Musa::Datasets::GDV)    # => false

Copy a dataset with dup and what comes back is a plain hash with the right keys, which will go on working until something asks it to_pdv and finds no such method.

Freezing follows Object#clone's three rules, at every node. freeze: true freezes the whole copy, false freezes nothing, and the default — nil — gives each node the state its own original had, so a frozen tree comes back frozen and a mixed one comes back mixed:

frozen = { a: { b: 1 }.freeze }.freeze
frozen.clone(deep: true).frozen?       # => true
frozen.clone(deep: true)[:a].frozen?   # => true

frozen.clone(deep: true, freeze: false).frozen?  # => false

It also handles circular graphs: a structure that refers to itself is copied once and the copy refers to itself in the same shape.

SmartProcBinder - Intelligent Parameter Binding:

Automatically match Proc parameters with available values, enabling flexible block signatures in DSL methods.

require 'musa-dsl'

# SmartProcBinder is used internally by Series operations
# to match block parameters flexibly

using Musa::Extension::SmartProcBinder

# Example: .with operation uses SmartProcBinder
pitches = S(60, 64, 67)
durations = S(1r, 1/2r, 1/4r)

# Block can request any combination of parameters
notes = pitches.with(dur: durations) do |p, dur:|
  { pitch: p, duration: dur }
end

# SmartProcBinder matches 'p' to pitch value, 'dur:' to duration value
# regardless of parameter order or naming

DynamicProxy - A Reference Before There Is Anything To Refer To:

An object that forwards everything to a receiver you set later. It does not create anything by itself: what it buys is being able to hand out a reference now and decide what it points at afterwards.

require 'musa-dsl'

proxy = Musa::Extension::DynamicProxy::DynamicProxy.new

proxy.receiver = [1, 2, 3]
proxy.size   # => 3

proxy.receiver = 'Hello'
proxy.size   # => 5

Called before its receiver is set, it says so rather than failing obscurely:

begin
  Musa::Extension::DynamicProxy::DynamicProxy.new.size
rescue NoMethodError => e
  e.message
end
# => "Method 'size' is unknown because self is a DynamicProxy with undefined receiver"

With - Flexible Block Execution:

Run a block either in the caller's context or in the object's, which is what lets a DSL read as a DSL. include Musa::Extension::With and the object gains with.

require 'musa-dsl'

class Builder
  include Musa::Extension::With

  attr_reader :items

  def initialize(&block)
    @items = []
    with(&block) if block
  end

  def item(name)
    @items << name
    self
  end
end

Builder.new { item 'first'; item 'second' }.items
# => ["first", "second"]

Inside the block, item resolves against the Builder: the block was instance_evaled. Write a parameter named _ and it flips — the block runs in the caller's context and the object arrives as that parameter, which is what you want when the block needs the surrounding scope more than the object's verbs.

AttributeBuilder - DSL Builder Macros:

Generates the adder/getter pairs that the Score classes are built from. It is extended, not included, and the macros are named after the shape of what they build -- there is no generic attribute:

Macro Generates
attr_simple_builder :name name(value) sets, name reads
attr_tuple_adder_to_hash :item, Klass add_item(id, value), items
attr_tuple_adder_to_array :item, Klass add_item(...), items
attr_complex_adder_to_array :item, Klass the same, with a block-built value
attr_complex_adder_to_custom :item the same, with your own constructor
require 'musa-dsl'

Track = Struct.new(:id, :name)

class Arrangement
  extend Musa::Extension::AttributeBuilder

  def initialize
    @tracks = {}
  end

  attr_tuple_adder_to_hash :track, Track
end

arrangement = Arrangement.new
arrangement.add_track :piano, 'Piano I'
arrangement.tracks[:piano].name  # => "Piano I"

The plural is derived from the singular (tracktracks) unless plural: says otherwise, and the adder builds Klass.new(id, value), so the class has to take both.

Logger - Sequencer-Aware Logging

Specialized logger that displays sequencer position alongside log messages. Essential for debugging temporal issues in compositions.

Features:

  • Automatic sequencer position formatting
  • Configurable position precision (integer and decimal digits)
  • Integration with InspectNice for readable Rational display
  • Standard Ruby Logger levels (DEBUG, INFO, WARN, ERROR, FATAL)
require 'musa-dsl'

# Create sequencer-aware logger
sequencer = Musa::Sequencer::Sequencer.new(4, 24)

# The position format is ONE number: integer digits before the point, decimal
# digits after it. 3.3 prints bar 4.5 as "  4.500".
logger = Musa::Logger::Logger.new(sequencer: sequencer, position_format: 3.3)
logger.level = Logger::DEBUG

# Use logger in sequencer context
sequencer.at 1 do
  logger.info "Starting melody at bar 1"
end

sequencer.at 4.5r do
  logger.debug "Halfway through bar 5"
end

sequencer.at 10 do
  logger.warn "Approaching ending"
end

# Run sequencer to see logged output
sequencer.run

# Output:
#   001.000: [INFO] Starting melody at bar 1
#   004.500: [DEBUG] Halfway through bar 5
#   010.000: [WARN] Approaching ending

Use Cases:

  • Temporal Debugging: Track down timing issues by seeing exact musical position
  • MIDI Event Monitoring: Log MIDI note-on/note-off with positions
  • Composition Development: Monitor sequencer flow during development
  • Performance Analysis: Identify bottlenecks by logging with timestamps

API Reference

Complete API documentation:

Source code: lib/musa-dsl/core-ext/ and lib/musa-dsl/logger/