Module: Musa::Series::Constructors

Extended by:
Constructors
Included in:
Musa::Series, Constructors
Defined in:
lib/musa-dsl/series/base-series.rb,
lib/musa-dsl/series/proxy-serie.rb,
lib/musa-dsl/series/queue-serie.rb,
lib/musa-dsl/series/timed-serie.rb,
lib/musa-dsl/series/quantizer-serie.rb,
lib/musa-dsl/series/main-serie-constructors.rb

Overview

Series constructor methods for creating series from various sources.

Provides factory methods for common serie types:

Basic Constructors

  • UNDEFINED - Undefined serie (unresolved state)
  • NIL - Serie that always returns nil
  • S - Serie from array of values
  • E - Serie from evaluation block

Collection Constructors

  • H/HC - Hash of series (hash/combined mode)
  • A/AC - Array of series (array/combined mode)
  • MERGE - Sequential merge of multiple series

Numeric Generators

  • FOR - For-loop style numeric sequence
  • RND - Random values (from array or range)
  • RND1 - Single random value
  • SIN - Sine wave function
  • FIBO - Fibonacci sequence

Musical Generators

  • HARMO - Harmonic note series

Usage Patterns

Array Serie

notes = S(60, 64, 67, 72)
notes.i.next_value  # => 60

Evaluation Block

counter = E(1) { |v, last_value:| last_value + 1 unless last_value == 10 }
counter.i.to_a  # => [1, 2, 3, ..., 10]

Random Values

dice = RND(1, 2, 3, 4, 5, 6)
dice.i.next_value  # => random 1-6

Numeric Sequences

sequence = FOR(from: 0, to: 10, step: 2)
sequence.i.to_a  # => [0, 2, 4, 6, 8, 10]

Combining Series

melody = MERGE(S(60, 64), S(67, 72))
melody.i.to_a  # => [60, 64, 67, 72]

Defined Under Namespace

Classes: FromArray, ProxySerie, QueueSerie, UndefinedSerie

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.A(*series) ⇒ FromArrayOfSeries

Creates array-mode serie from array of series.

Combines multiple series into array-structured values. Returns array of values from respective series. Stops when first serie exhausts.

Examples:

Array of series

a = A(S(1, 2, 3), S(10, 20, 30))
inst = a.i
inst.next_value  # => [1, 10]
inst.next_value  # => [2, 20]

Parameters:

  • series (Array)

    array of series

Returns:

  • (FromArrayOfSeries)

    combined array serie



193
194
195
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 193

def A(*series)
  FromArrayOfSeries.new series, false
end

.AC(*series) ⇒ Object

Combines series of different lengths, cycling the short ones.

When this is the answer

Two materials of different lengths sounding together: a four-note ostinato against a three-note one, a rhythm of five against a melody of seven. They line up again only after the least common multiple of their lengths, and what happens in between -- the same notes meeting different partners -- is the point.

#A stops with the shortest, which is what you want when the series are meant to end together. AC keeps going until every one of them has completed a whole number of cycles, so the result is exactly one full turn of the pattern.

Examples:

Two against three: six pairings before it repeats

AC(S(1, 2), S(10, 20, 30)).i.to_a
# => [[1, 10], [2, 20], [1, 30], [2, 10], [1, 20], [2, 30]]

# 1 meets 10, then 30, then 20, and only then is it back where it began.

A, for comparison: it ends with the shortest

A(S(1, 2), S(10, 20, 30)).i.to_a
# => [[1, 10], [2, 20]]


223
224
225
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 223

def AC(*series)
  FromArrayOfSeries.new series, true
end

.E(*value_args, **key_args) {|value_args, last_value, caller, key_args| ... } ⇒ FromEvalBlockWithParameters

Creates serie from evaluation block.

Calls block repeatedly with parameters and last_value. Block returns next value or nil to stop. Enables stateful generators and algorithms.

Block Parameters

  • value_args: Initial positional parameters
  • last_value: Previous return value (nil on first call)
  • caller: Serie instance (access to parameters attribute)
  • key_args: Initial keyword parameters

When this is the answer

Every other constructor decides its values when it is built. E decides them when it is asked, which is the only way to write a serie whose values depend on something the serie does not own: a variable the piece is changing, an input that has arrived, a decision made elsewhere while the music was already sounding.

If the values are known in advance, S, FOR, FIBO or a transformation of one of them says it better. E is for what is not.

Examples:

Reading state that changes between one value and the next

density = 3
readings = E { |last_value:| density }

i = readings.i
i.next_value   # => 3
density = 8    # somebody else changed it
i.next_value   # => 8

Ending when something outside says so

# Returning nil ends the serie, and nothing else can express "stop when
# this condition, which I cannot see from here, becomes true".
remaining = 3
countdown = E { |last_value:| (remaining -= 1) >= 0 ? remaining : nil }

countdown.i.to_a  # => [2, 1, 0]

Carrying state of its own, in parameters

# last_value is nil on the first call, and the positional arguments are
# the serie's parameters -- handed to every call, not a seed value.
counter = E { |last_value:| (last_value || 0) + 1 unless last_value == 5 }
counter.i.to_a  # => [1, 2, 3, 4, 5]

Parameters:

  • value_args (Array)

    initial positional parameters

  • key_args (Hash)

    initial keyword parameters

Yields:

  • block called for each value

Yield Parameters:

  • value_args (Array)

    current positional parameters

  • last_value (Object, nil)

    previous return value

  • caller (FromEvalBlockWithParameters)

    serie instance

  • key_args (Hash)

    current keyword parameters

Yield Returns:

  • (Object, nil)

    next value or nil to stop

Returns:

  • (FromEvalBlockWithParameters)

    evaluation-based serie



285
286
287
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 285

def E(*value_args, **key_args, &block)
  FromEvalBlockWithParameters.new *value_args, **key_args, &block
end

.FIBO(first = 1, second = 1) ⇒ Fibonacci

Creates a Fibonacci serie: every value is the sum of the two before it.

The two seeds ARE the first two values, so FIBO() yields 1, 1, 2, 3, 5... and any other pair gives a different sequence out of the same machine — not a delayed echo of Fibonacci, a relative of it. Infinite serie.

Examples:

Fibonacci numbers

fib = FIBO()
fib.infinite?  # => true
inst = fib.i
10.times.map { inst.next_value }
# => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Seeded: the sequence including its leading zero

inst = FIBO(0, 1).i
6.times.map { inst.next_value }
# => [0, 1, 1, 2, 3, 5]

Seeded: Fibonacci with its first term removed

inst = FIBO(1, 2).i
6.times.map { inst.next_value }
# => [1, 2, 3, 5, 8, 13]

Lucas numbers

inst = FIBO(2, 1).i
6.times.map { inst.next_value }
# => [2, 1, 3, 4, 7, 11]

Rhythmic proportions

durations = FIBO().i.map { |n| Rational(n, 16) }

A grid position that Fibonacci lands on, turn after turn

positions = FIBO().i.map { |n| n % 32 }

Parameters:

  • first (Numeric) (defaults to: 1)

    the first value (default 1)

  • second (Numeric) (defaults to: 1)

    the second value (default 1)

Returns:

  • (Fibonacci)

    Fibonacci sequence serie



533
534
535
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 533

def FIBO(first = 1, second = 1)
  Fibonacci.new first, second
end

.FOR(from: nil, to: nil, step: nil) ⇒ ForLoop

Creates for-loop style numeric sequence.

Generates sequence from from to to (inclusive) with step increment. Automatically adjusts step sign based on from/to relationship.

Examples:

Ascending sequence

s = FOR(from: 0, to: 10, step: 2)
s.i.to_a  # => [0, 2, 4, 6, 8, 10]

Descending sequence

s = FOR(from: 10, to: 0, step: 2)
s.i.to_a  # => [10, 8, 6, 4, 2, 0]

Infinite sequence

s = FOR(from: 0, step: 1)  # to: nil
s.infinite?  # => true

Parameters:

  • from (Numeric, nil) (defaults to: nil)

    starting value (default: 0)

  • to (Numeric, nil) (defaults to: nil)

    ending value (nil for infinite)

  • step (Numeric, nil) (defaults to: nil)

    increment (default: 1, sign auto-adjusted)

Returns:

  • (ForLoop)

    numeric sequence serie



313
314
315
316
317
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 313

def FOR(from: nil, to: nil, step: nil)
  from ||= 0
  step ||= 1
  ForLoop.new from, to, step
end

.H(**series_hash) ⇒ FromHashOfSeries

Note:

h.i builds a NEW instance every time it is called, each starting from the beginning. Keep the instance to advance through the serie.

Creates hash-mode serie from hash of series.

Combines multiple series into hash-structured values. Returns hash with same keys, values from respective series. Stops when first serie exhausts.

Examples:

Hash of series

h = H(pitch: S(60, 64, 67), velocity: S(96, 80, 64))
inst = h.i
inst.next_value  # => {pitch: 60, velocity: 96}
inst.next_value  # => {pitch: 64, velocity: 80}

Parameters:

  • series_hash (Hash)

    hash of series (key => serie)

Returns:

  • (FromHashOfSeries)

    combined hash serie



154
155
156
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 154

def H(**series_hash)
  FromHashOfSeries.new series_hash, false
end

.HARMO(error: nil, extended: nil) ⇒ HarmonicNotes

Creates a serie of the harmonic series, in semitones over the fundamental.

Yields the interval of each harmonic from a fundamental of 0, approximated to the nearest semitone, and skips any harmonic whose approximation error exceeds the tolerance. Infinite serie; add the fundamental's own pitch to place it. The values do not depend on any input: it starts producing at once.

Parameters

  • error: maximum approximation error, IN SEMITONES, for a harmonic to be accepted (default: 0.5, i.e. accept every harmonic, since no approximation to the nearest semitone can be off by more than half of one)
  • extended: yield { pitch:, error: } instead of the bare pitch, so the approximation error of each harmonic is available. It does NOT add harmonics: the pitches are the same ones.

Examples:

Harmonic series

harmonics = HARMO(error: 0.5)
inst = harmonics.i
8.times.map { inst.next_value }
# => [0, 12, 19, 24, 28, 31, 34, 36]

A tighter tolerance drops the harmonics that fall between semitones

inst = HARMO(error: 0.1).i
8.times.map { inst.next_value }
# => [0, 12, 19, 24, 31, 36, 38, 43]
# the 5th harmonic (28) is 0.137 semitones off, so it is not accepted

Extended: the same pitches, carrying their error

inst = HARMO(error: 0.5, extended: true).i
harmonics = 3.times.map { inst.next_value }

harmonics.map { |h| h[:pitch] }            # => [0, 12, 19]
harmonics.map { |h| h[:error].round(4) }   # => [0.0, 0.0, 0.0196]

# The octaves are exact; the twelfth is not. 3/1 is 0.0196 semitones
# above the tempered fifth, which is why a tolerance below that drops it.

Over a fundamental other than C

over_g = HARMO().i.map { |n| n + 67 }

Parameters:

  • error (Numeric, nil) (defaults to: nil)

    maximum approximation error in semitones (default: 0.5)

  • extended (Boolean, nil) (defaults to: nil)

    yield pitch and error instead of pitch (default: false)

Returns:

  • (HarmonicNotes)

    harmonic series serie



586
587
588
589
590
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 586

def HARMO(error: nil, extended: nil)
  error ||= 0.5
  extended ||= false
  HarmonicNotes.new error, extended
end

.HC(**series_hash) ⇒ FromHashOfSeries

Creates hash-mode combined serie from hash of series.

Like H but cycles all series. When a serie exhausts, it restarts from the beginning, continuing until all series complete their cycles.

Examples:

Combined cycling all series

hc = HC(a: S(1, 2), b: S(10, 20, 30))
hc.max_size(6).i.to_a  # => [{a:1, b:10}, {a:2, b:20}, {a:1, b:30},
                        #     {a:2, b:10}, {a:1, b:20}, {a:2, b:30}]

Parameters:

  • series_hash (Hash)

    hash of series (key => serie)

Returns:

  • (FromHashOfSeries)

    combined hash serie that cycles all series



173
174
175
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 173

def HC(**series_hash)
  FromHashOfSeries.new series_hash, true
end

.MERGE(*series) ⇒ Sequence

Merges multiple series sequentially.

Plays series in sequence: first serie until exhausted, then second, etc. Restarts each serie (except first) before playing.

Examples:

Merge sequences

merged = MERGE(S(1, 2, 3), S(10, 20, 30))
merged.i.to_a  # => [1, 2, 3, 10, 20, 30]

Melodic phrases

phrase1 = S(60, 64, 67)
phrase2 = S(72, 69, 65)
melody = MERGE(phrase1, phrase2)

Parameters:

  • series (Array<Serie>)

    series to merge sequentially

Returns:

  • (Sequence)

    sequential merge serie



399
400
401
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 399

def MERGE(*series)
  Sequence.new(series)
end

.NILNilSerie

Creates serie that always returns nil.

Returns nil on every next_value call. Useful for padding or as placeholder in composite structures.

Examples:

Nil serie

s = NIL().i
s.next_value  # => nil
s.next_value  # => nil

Returns:

  • (NilSerie)

    serie returning nil



108
109
110
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 108

def NIL
  NilSerie.new
end

.PROXY(serie = nil, cyclic: nil) ⇒ ProxySerie

Creates a proxy serie with optional initial source.

Proxy series enable late binding - creating a serie placeholder that will be resolved later. Useful for:

Use Cases

  • Forward references: Reference series before definition
  • Circular structures: Self-referential or mutually referential series
  • Dependency injection: Define structure, inject source later
  • Dynamic routing: Change source serie at runtime

Method Delegation

Proxy delegates all methods to underlying source via method_missing, making it transparent proxy for most operations.

State Resolution

Proxy starts in :undefined state, becomes :prototype/:instance when source is set and resolved.

Cycles

A proxy that points back into the serie it is part of closes a cycle: the material loops instead of ending. This has to be declared with cyclic: true, because a cycle changes what every walk of the graph has to do and should not appear by accident -- a proxy that closes one without having been declared raises ArgumentError. The reverse is fine: declaring a proxy cyclic and pointing it somewhere that does not loop back is exactly the forward reference the declaration exists for.

What a cycle is for is material whose repetition is not decided in advance: a QUEUE fed while the loop is already sounding, an E() reading state that changes between turns. For a serie that is fully known beforehand, .repeat says the same thing without any of this.

Examples:

Forward reference

proxy = PROXY()
proxy.undefined?  # => true

# Define later
proxy.proxy_source = S(1, 2, 3)
proxy.prototype?  # => true

Circular structure

loop_serie = PROXY(cyclic: true)
sequence = S(1, 2, 3).after(loop_serie)
loop_serie.proxy_source = sequence

# The circle closes, and the state of a cycle is the state of what feeds
# it from outside -- here S(1, 2, 3), a prototype.
sequence.state    # => :prototype
loop_serie.state  # => :prototype

# It never runs out, and says so without walking round itself.
sequence.infinite?  # => true

i = sequence.i
9.times.collect { i.next_value }  # => [1, 2, 3, 1, 2, 3, 1, 2, 3]

Two materials calling each other

to_b = PROXY(cyclic: true)
to_a = PROXY(cyclic: true)

a = S(1, 2).after(to_b)
b = S(3, 4).after(to_a)

to_b.proxy_source = b
to_a.proxy_source = a

i = a.i
8.times.collect { i.next_value }  # => [1, 2, 3, 4, 1, 2, 3, 4]

A cycle that comes back empty ends

n = 0
material = E(nil) { n += 1; n <= 3 ? n : nil }

back = PROXY(cyclic: true)
cycle = material.after(back)
back.proxy_source = cycle

# One turn per request: when the turn produces nothing, the loop is over
# instead of spinning forever.
i = cycle.i
5.times.collect { i.next_value }  # => [1, 2, 3, nil, nil]

With initial source

PROXY(S(1, 2, 3)).i.to_a  # => [1, 2, 3]

Parameters:

  • serie (Serie, nil) (defaults to: nil)

    initial source serie (default: nil)

  • cyclic (Boolean, nil) (defaults to: nil)

    whether this proxy may close a cycle

Returns:



103
104
105
# File 'lib/musa-dsl/series/proxy-serie.rb', line 103

def PROXY(serie = nil, cyclic: nil)
  ProxySerie.new(serie, cyclic: cyclic)
end

.QUANTIZE(time_value_serie, reference: nil, step: nil, value_attribute: nil, stops: nil, predictive: nil, left_open: nil, right_open: nil) ⇒ RawQuantizer, PredictiveQuantizer

Turns a continuous ramp into a staircase.

When this is the answer

Something computed as a curve -- a glissando, an envelope, a trajectory out of a matrix -- has to become discrete before it can be played: pitches are semitones, a controller takes integers, a rhythm lands on divisions. Quantizing is that step, and what comes back is not a set of samples taken at the input times but a staircase: one step per boundary crossed, each carrying the time it holds.

The source has to be a serie of timed values -- hashes extended with Datasets::AbsTimed. A bare hash of the right shape is not one and raises "Don't know how to process".

The two modes, and they sound different

Normal changes the step when the ramp reaches it. Predictive changes when the ramp is nearer the next step than the last -- it rounds instead of waiting to arrive, which is what a listener hears as the pitch.

Examples:

A ramp of three semitones over two bars, quantized to semitones

ramp = S({ time: 0r, value: 60.0 }, { time: 2r, value: 63.0 })
       .map { |v| v.extend(Musa::Datasets::AbsTimed) }

ramp.quantize(step: 1).i.to_a.collect { |v| [v[:time], v[:value]] }
# => [[0r, 60r], [2/3r, 61r], [4/3r, 62r]]

# 61 at 2/3 of a bar, which is where the ramp actually arrives at 61.

The same ramp, predictive

ramp.quantize(step: 1, predictive: true).i.to_a.collect { |v| [v[:time], v[:value]] }
# => [[0r, 60r], [1/3r, 61r], [1r, 62r], [5/3r, 63r]]

# 61 at 1/3, halfway -- and it reaches 63, which the other never does.

A coarser step is fewer notes, not smaller ones

ramp.quantize(step: 3).i.to_a.size  # => 1

Parameters:

  • time_value_serie (Serie)

    source timed serie

  • reference (Numeric, nil) (defaults to: nil)

    quantization reference

  • step (Numeric, nil) (defaults to: nil)

    step size

  • value_attribute (Symbol, nil) (defaults to: nil)

    attribute to quantize

  • stops (Boolean, nil) (defaults to: nil)

    include stop points

  • predictive (Boolean, nil) (defaults to: nil)

    use predictive mode

  • left_open (Boolean, nil) (defaults to: nil)

    left boundary open

  • right_open (Boolean, nil) (defaults to: nil)

    right boundary open

Returns:

  • (RawQuantizer, PredictiveQuantizer)

    quantized serie



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/musa-dsl/series/quantizer-serie.rb', line 118

def QUANTIZE(time_value_serie,
             reference: nil, step: nil,
             value_attribute: nil,
             stops: nil,
             predictive: nil,
             left_open: nil,
             right_open: nil)

  reference ||= 0r
  step ||= 1r
  value_attribute ||= :value
  stops ||= false
  predictive ||= false

  if predictive
    raise ArgumentError, "Predictive quantization doesn't allow parameters 'left_open' or 'right_open'" if left_open || right_open

    PredictiveQuantizer.new(reference, step, time_value_serie, value_attribute, stops)
  else
    # By default: left closed and right_open
    # By default 2:
    #   if right_open is true and left_open is nil, left_open will be false
    #   if left_open is true and right_open is nil, right_open will be false

    right_open = right_open.nil? ? !left_open : right_open
    left_open = left_open.nil? ? !right_open : left_open

    RawQuantizer.new(reference, step, time_value_serie, value_attribute, stops, left_open, right_open)
  end
end

.QUEUE(*series) ⇒ QueueSerie

Note:

to_a RESTARTS the instance, so it returns everything queued and not what is left after the next_value above.

Creates queue serie from initial series.

Queue allows adding series dynamically during playback, creating flexible sequential playback with runtime modification.

Features

  • Dynamic addition: Add series with << during playback
  • Sequential playback: Plays series in queue order
  • Method delegation: Delegates methods to current serie
  • Clear: Can clear queue and reset

Use Cases

  • Interactive sequencing with user input
  • Dynamic phrase assembly
  • Playlist-style serie management
  • Reactive composition systems
  • Live coding pattern queuing

Examples:

Basic queue

queue = QUEUE(S(1, 2, 3)).i
queue.next_value  # => 1
queue << S(4, 5, 6).i  # Add dynamically
queue.to_a  # => [1, 2, 3, 4, 5, 6]

Dynamic playlist

melody1 = S(60, 62)
melody2 = S(67, 69)

queue = QUEUE(melody1).i
queue << melody2.i
queue.to_a  # => [60, 62, 67, 69]

A queue that starts with nothing

queue = QUEUE().i
queue.next_value  # => nil

queue << S(1, 2).i
queue.next_value  # => 1
queue.next_value  # => 2

Parameters:

  • series (Array<Serie>)

    initial series in queue

Returns:



59
60
61
# File 'lib/musa-dsl/series/queue-serie.rb', line 59

def QUEUE(*series)
  QueueSerie.new(series)
end

.RND(*_values, values: nil, from: nil, to: nil, step: nil, random: nil) ⇒ RandomValuesFromArray, RandomNumbersFromRange

Creates random value serie from array or range.

Two modes:

  • Array mode: Random values from provided array
  • Range mode: Random numbers from range (from, to, step)

A SHUFFLE, NOT A DIE. Each value is drawn once and removed, so the serie is a random permutation and then ends: six values from RND(1..6) and nil on the seventh. .repeat is what gives sampling with replacement, reshuffling on each pass, and that one is infinite.

Examples:

Shuffling an array

shuffled = RND(1, 2, 3, 4, 5, 6, random: 42)
shuffled.i.to_a       # => [4, 6, 3, 5, 2, 1]
shuffled.infinite?    # => false

Rolling a die -- with replacement, which needs repeat

die = RND(1, 2, 3, 4, 5, 6, random: 42).repeat
die.infinite?  # => true

Random from range

RND(from: 0, to: 10, step: 5, random: 7).i.to_a  # => [0, 10, 5]

With seed

# The same seed is the same sequence, which is what makes a piece the
# same piece.
RND(1, 2, 3, random: 42).i.to_a  # => [3, 2, 1]
RND(1, 2, 3, random: 42).i.to_a  # => [3, 2, 1]

Parameters:

  • _values (Array)

    values to choose from (positional)

  • values (Array, nil) (defaults to: nil)

    values to choose from (named)

  • from (Numeric, nil) (defaults to: nil)

    range start (range mode)

  • to (Numeric, nil) (defaults to: nil)

    range end (range mode, required)

  • step (Numeric, nil) (defaults to: nil)

    range step (default: 1)

  • random (Random, Integer, nil) (defaults to: nil)

    Random instance or seed

Returns:

  • (RandomValuesFromArray, RandomNumbersFromRange)

    random serie

Raises:

  • (ArgumentError)

    if using both positional and named values

  • (ArgumentError)

    if mixing array and range parameters



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 361

def RND(*_values, values: nil, from: nil, to: nil, step: nil, random: nil)
  raise ArgumentError, "Can't use both direct values #{_values} and values named parameter #{values} at the same time." if values && !_values.empty?

  random = Random.new random if random.is_a?(Integer)
  random ||= Random.new

  values ||= _values

  if !values.empty? && from.nil? && to.nil? && step.nil?
    RandomValuesFromArray.new values.explode_ranges, random
  elsif values.empty? && !to.nil?
    from ||= 0
    step ||= 1
    RandomNumbersFromRange.new from, to, step, random
  else
    raise ArgumentError, 'cannot use values and from:/to:/step: together'
  end
end

.RND1(*_values, values: nil, from: nil, to: nil, step: nil, random: nil) ⇒ RandomValueFromArray, RandomNumberFromRange

Creates single random value serie from array or range.

Like RND but returns only one random value then exhausts. Two modes: array mode and range mode.

Examples:

Single random value

rnd = RND1(1, 2, 3, 4, 5)
inst = rnd.i

inst.next_value  # => a Integer   (one of 1..5)
inst.next_value  # => nil         (exhausted: RND1 yields ONE value)

# `inst` and not `rnd.i` twice: every `.i` is a NEW instance, which would
# start again and give a second random value instead of the nil that
# says the serie is done.

Random seed selection

seed = RND1(10, 20, 30, random: 42)

Parameters:

  • _values (Array)

    values to choose from (positional)

  • values (Array, nil) (defaults to: nil)

    values to choose from (named)

  • from (Numeric, nil) (defaults to: nil)

    range start (range mode)

  • to (Numeric, nil) (defaults to: nil)

    range end (range mode, required)

  • step (Numeric, nil) (defaults to: nil)

    range step (default: 1)

  • random (Random, Integer, nil) (defaults to: nil)

    Random instance or seed

Returns:

  • (RandomValueFromArray, RandomNumberFromRange)

    single random value serie

Raises:

  • (ArgumentError)

    if using both positional and named values

  • (ArgumentError)

    if mixing array and range parameters



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 435

def RND1(*_values, values: nil, from: nil, to: nil, step: nil, random: nil)
  raise ArgumentError, "Can't use both direct values #{_values} and values named parameter #{values} at the same time." if values && !_values.empty?

  random = Random.new random if random.is_a?(Integer)
  random ||= Random.new

  values ||= _values

  if !values.empty? && from.nil? && to.nil? && step.nil?
    RandomValueFromArray.new values.explode_ranges, random
  elsif values.empty? && !to.nil?
    from ||= 0
    step ||= 1
    RandomNumberFromRange.new from, to, step, random
  else
    raise ArgumentError, 'cannot use values and from:/to:/step: parameters together'
  end
end

.S(*values) ⇒ FromArray

Creates serie from array of values.

Most common constructor. Values can include ranges which will be expanded automatically via ExplodeRanges extension.

Examples:

Basic array

notes = S(60, 64, 67, 72)
notes.i.to_a  # => [60, 64, 67, 72]

With ranges

scale = S(60..67)
scale.i.to_a  # => [60, 61, 62, 63, 64, 65, 66, 67]

Parameters:

  • values (Array)

    values to iterate (supports ranges)

Returns:



130
131
132
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 130

def S(*values)
  FromArray.new values.explode_ranges
end

.SIN(start_value: nil, steps: nil, amplitude: nil, center: nil) ⇒ SinFunction

Creates sine wave function serie.

Generates values following sine curve. Useful for smooth oscillations, LFO-style modulation, and periodic variations.

Wave Parameters

  • start_value: Initial value (default: center)
  • steps: Period in steps (nil for continuous)
  • amplitude: Wave amplitude, PEAK TO PEAK (default: 1.0). The wave spans center ± amplitude / 2, so center: 70, amplitude: 50 runs from 45 to 95 and not from 20 to 120.
  • center: Center/offset value (default: 0.0)

Wave equation: center + (amplitude / 2) * sin(progress)

Examples:

Basic sine wave

wave = SIN(steps: 8, amplitude: 10, center: 50)
wave.i.to_a  # => oscillates around 50 ± 10

LFO modulation

lfo = SIN(steps: 16, amplitude: 0.5, center: 0.5)
# Use for amplitude modulation

Parameters:

  • start_value (Numeric, nil) (defaults to: nil)

    initial value

  • steps (Numeric, nil) (defaults to: nil)

    full period in steps

  • amplitude (Numeric, nil) (defaults to: nil)

    wave amplitude, peak to peak (default: 1.0)

  • center (Numeric, nil) (defaults to: nil)

    center offset (default: 0.0)

Returns:

  • (SinFunction)

    sine wave serie



486
487
488
489
490
491
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 486

def SIN(start_value: nil, steps: nil, amplitude: nil, center: nil)
  amplitude ||= 1.0
  center ||= 0.0
  start_value ||= center
  SinFunction.new start_value, steps, amplitude, center
end

.TIMED_UNION(*array_of_timed_series, **hash_of_timed_series) ⇒ TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries

Merges multiple timed series by synchronizing events at each time point.

TIMED_UNION combines series with :time attributes, emitting events at each unique time where at least one source has a value. Sources without values at a given time emit nil. Operates in two distinct modes based on input format.

Timed Series Format

Each event is a hash with :time and :value keys, extended with AbsTimed:

{ time: 0r, value: 60, duration: 1r }.extend(Musa::Datasets::AbsTimed)

Additional attributes (:duration, :velocity, etc.) are preserved and synchronized alongside values.

Operating Modes

Array Mode: TIMED_UNION(s1, s2, s3)

  • Anonymous positional sources
  • Output: { time: t, value: [val1, val2, val3] }
  • Use for: Ordered tracks without specific names

Hash Mode: TIMED_UNION(melody: s1, bass: s2)

  • Named sources with keys
  • Output: { time: t, value: { melody: val1, bass: val2 } }
  • Use for: Identified voices/tracks for routing

Value Types and Combination

Direct values (integers, strings, etc.):

s1 = S({ time: 0, value: 60 })
s2 = S({ time: 0, value: 64 })
TIMED_UNION(s1, s2)  # => { time: 0, value: [60, 64] }

Hash values (polyphonic events):

s1 = S({ time: 0, value: { a: 1, b: 2 } })
s2 = S({ time: 0, value: { c: 10 } })
TIMED_UNION(s1, s2)  # => { time: 0, value: { a: 1, b: 2, c: 10 } }

Array values (multi-element events):

s1 = S({ time: 0, value: [1, 2] })
s2 = S({ time: 0, value: [10, 20] })
TIMED_UNION(s1, s2)  # => { time: 0, value: [1, 2, 10, 20] }

Mixed Hash + Direct (advanced):

s1 = S({ time: 0, value: { a: 1, b: 2 } })
s2 = S({ time: 0, value: 100 })
TIMED_UNION(s1, s2)  # => { time: 0, value: { a: 1, b: 2, 0 => 100 } }

Synchronization Behavior

Events are emitted at each unique time point across all sources:

s1 = S({ time: 0r, value: 1 }, { time: 2r, value: 3 })
s2 = S({ time: 1r, value: 10 })
TIMED_UNION(s1, s2).i.to_a
# => [{ time: 0r, value: [1, nil] },
#     { time: 1r, value: [nil, 10] },
#     { time: 2r, value: [3, nil] }]

Extra Attributes

Non-standard attributes (beyond :time, :value) are synchronized:

s1 = S({ time: 0, value: 1, velocity: 80 })
s2 = S({ time: 0, value: 10, duration: 1r })
TIMED_UNION(s1, s2)
# => { time: 0, value: [1, 10], velocity: [80, nil], duration: [nil, 1r] }

Examples:

Array mode with direct values

s1 = S({ time: 0r, value: 1 }, { time: 1r, value: 2 })
s2 = S({ time: 0r, value: 10 }, { time: 2r, value: 20 })

union = TIMED_UNION(s1, s2).i
union.to_a
# => [{ time: 0r, value: [1, 10] },
#     { time: 1r, value: [2, nil] },
#     { time: 2r, value: [nil, 20] }]

Hash mode with named sources

melody = S({ time: 0r, value: 60 }, { time: 1r, value: 64 })
bass = S({ time: 0r, value: 36 }, { time: 2r, value: 40 })

union = TIMED_UNION(melody: melody, bass: bass).i
union.to_a
# => [{ time: 0r, value: { melody: 60, bass: 36 } },
#     { time: 1r, value: { melody: 64, bass: nil } },
#     { time: 2r, value: { melody: nil, bass: 40 } }]

Hash values with polyphonic events

s1 = S({ time: 0r, value: { a: 1, b: 2 } })
s2 = S({ time: 0r, value: { c: 10, d: 20 } })

union = TIMED_UNION(s1, s2).i
union.next_value  # => { time: 0r, value: { a: 1, b: 2, c: 10, d: 20 } }

Extra attributes synchronization

s1 = S({ time: 0r, value: 1, velocity: 80, duration: 1r })
s2 = S({ time: 0r, value: 10, velocity: 90 })

union = TIMED_UNION(s1, s2).i
union.next_value
# => { time: 0r,
#      value: [1, 10],
#      velocity: [80, 90],
#      duration: [1r, nil] }

Key conflict detection

s1 = S({ time: 0r, value: { a: 1, b: 2 } })
s2 = S({ time: 0r, value: { a: 10 } })  # 'a' already used!

union = TIMED_UNION(s1, s2).i
union.next_value  # => RuntimeError: Value: key a already used

Parameters:

  • array_of_timed_series (Array<Serie>)

    timed series (array mode)

  • hash_of_timed_series (Hash{Symbol => Serie})

    named timed series (hash mode)

Returns:

  • (TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries)

    merged serie

Raises:

  • (ArgumentError)

    if mixing array and hash modes

  • (RuntimeError)

    if hash values have duplicate keys across sources

  • (RuntimeError)

    if mixing incompatible value types (Hash with Array)

See Also:

  • Splits compound values into individual timed events
  • Removes events with all-nil values
  • Instance method for union


149
150
151
152
153
154
155
156
157
158
159
# File 'lib/musa-dsl/series/timed-serie.rb', line 149

def TIMED_UNION(*array_of_timed_series, **hash_of_timed_series)
  raise ArgumentError, 'Can\'t union an array of series with a hash of series' if array_of_timed_series.any? && hash_of_timed_series.any?

  if array_of_timed_series.any?
    TimedUnionOfArrayOfTimedSeries.new(array_of_timed_series)
  elsif hash_of_timed_series.any?
    TimedUnionOfHashOfTimedSeries.new(hash_of_timed_series)
  else
    raise ArgumentError, 'Missing argument series'
  end
end

.UNDEFINEDUndefinedSerie

Creates undefined serie.

Returns serie in undefined state. Useful as placeholder that will be resolved later (e.g., in PROXY).

Examples:

Undefined placeholder

proxy = PROXY()  # Uses UNDEFINED internally
proxy.undefined?  # => true

Returns:



91
92
93
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 91

def UNDEFINED
  UndefinedSerie.new
end

Instance Method Details

#A(*series) ⇒ FromArrayOfSeries

Creates array-mode serie from array of series.

Combines multiple series into array-structured values. Returns array of values from respective series. Stops when first serie exhausts.

Examples:

Array of series

a = A(S(1, 2, 3), S(10, 20, 30))
inst = a.i
inst.next_value  # => [1, 10]
inst.next_value  # => [2, 20]

Parameters:

  • series (Array)

    array of series

Returns:

  • (FromArrayOfSeries)

    combined array serie



193
194
195
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 193

def A(*series)
  FromArrayOfSeries.new series, false
end

#AC(*series) ⇒ Object

Combines series of different lengths, cycling the short ones.

When this is the answer

Two materials of different lengths sounding together: a four-note ostinato against a three-note one, a rhythm of five against a melody of seven. They line up again only after the least common multiple of their lengths, and what happens in between -- the same notes meeting different partners -- is the point.

#A stops with the shortest, which is what you want when the series are meant to end together. AC keeps going until every one of them has completed a whole number of cycles, so the result is exactly one full turn of the pattern.

Examples:

Two against three: six pairings before it repeats

AC(S(1, 2), S(10, 20, 30)).i.to_a
# => [[1, 10], [2, 20], [1, 30], [2, 10], [1, 20], [2, 30]]

# 1 meets 10, then 30, then 20, and only then is it back where it began.

A, for comparison: it ends with the shortest

A(S(1, 2), S(10, 20, 30)).i.to_a
# => [[1, 10], [2, 20]]


223
224
225
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 223

def AC(*series)
  FromArrayOfSeries.new series, true
end

#E(*value_args, **key_args) {|value_args, last_value, caller, key_args| ... } ⇒ FromEvalBlockWithParameters

Creates serie from evaluation block.

Calls block repeatedly with parameters and last_value. Block returns next value or nil to stop. Enables stateful generators and algorithms.

Block Parameters

  • value_args: Initial positional parameters
  • last_value: Previous return value (nil on first call)
  • caller: Serie instance (access to parameters attribute)
  • key_args: Initial keyword parameters

When this is the answer

Every other constructor decides its values when it is built. E decides them when it is asked, which is the only way to write a serie whose values depend on something the serie does not own: a variable the piece is changing, an input that has arrived, a decision made elsewhere while the music was already sounding.

If the values are known in advance, S, FOR, FIBO or a transformation of one of them says it better. E is for what is not.

Examples:

Reading state that changes between one value and the next

density = 3
readings = E { |last_value:| density }

i = readings.i
i.next_value   # => 3
density = 8    # somebody else changed it
i.next_value   # => 8

Ending when something outside says so

# Returning nil ends the serie, and nothing else can express "stop when
# this condition, which I cannot see from here, becomes true".
remaining = 3
countdown = E { |last_value:| (remaining -= 1) >= 0 ? remaining : nil }

countdown.i.to_a  # => [2, 1, 0]

Carrying state of its own, in parameters

# last_value is nil on the first call, and the positional arguments are
# the serie's parameters -- handed to every call, not a seed value.
counter = E { |last_value:| (last_value || 0) + 1 unless last_value == 5 }
counter.i.to_a  # => [1, 2, 3, 4, 5]

Parameters:

  • value_args (Array)

    initial positional parameters

  • key_args (Hash)

    initial keyword parameters

Yields:

  • block called for each value

Yield Parameters:

  • value_args (Array)

    current positional parameters

  • last_value (Object, nil)

    previous return value

  • caller (FromEvalBlockWithParameters)

    serie instance

  • key_args (Hash)

    current keyword parameters

Yield Returns:

  • (Object, nil)

    next value or nil to stop

Returns:

  • (FromEvalBlockWithParameters)

    evaluation-based serie



285
286
287
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 285

def E(*value_args, **key_args, &block)
  FromEvalBlockWithParameters.new *value_args, **key_args, &block
end

#FIBO(first = 1, second = 1) ⇒ Fibonacci

Creates a Fibonacci serie: every value is the sum of the two before it.

The two seeds ARE the first two values, so FIBO() yields 1, 1, 2, 3, 5... and any other pair gives a different sequence out of the same machine — not a delayed echo of Fibonacci, a relative of it. Infinite serie.

Examples:

Fibonacci numbers

fib = FIBO()
fib.infinite?  # => true
inst = fib.i
10.times.map { inst.next_value }
# => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Seeded: the sequence including its leading zero

inst = FIBO(0, 1).i
6.times.map { inst.next_value }
# => [0, 1, 1, 2, 3, 5]

Seeded: Fibonacci with its first term removed

inst = FIBO(1, 2).i
6.times.map { inst.next_value }
# => [1, 2, 3, 5, 8, 13]

Lucas numbers

inst = FIBO(2, 1).i
6.times.map { inst.next_value }
# => [2, 1, 3, 4, 7, 11]

Rhythmic proportions

durations = FIBO().i.map { |n| Rational(n, 16) }

A grid position that Fibonacci lands on, turn after turn

positions = FIBO().i.map { |n| n % 32 }

Parameters:

  • first (Numeric) (defaults to: 1)

    the first value (default 1)

  • second (Numeric) (defaults to: 1)

    the second value (default 1)

Returns:

  • (Fibonacci)

    Fibonacci sequence serie



533
534
535
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 533

def FIBO(first = 1, second = 1)
  Fibonacci.new first, second
end

#FOR(from: nil, to: nil, step: nil) ⇒ ForLoop

Creates for-loop style numeric sequence.

Generates sequence from from to to (inclusive) with step increment. Automatically adjusts step sign based on from/to relationship.

Examples:

Ascending sequence

s = FOR(from: 0, to: 10, step: 2)
s.i.to_a  # => [0, 2, 4, 6, 8, 10]

Descending sequence

s = FOR(from: 10, to: 0, step: 2)
s.i.to_a  # => [10, 8, 6, 4, 2, 0]

Infinite sequence

s = FOR(from: 0, step: 1)  # to: nil
s.infinite?  # => true

Parameters:

  • from (Numeric, nil) (defaults to: nil)

    starting value (default: 0)

  • to (Numeric, nil) (defaults to: nil)

    ending value (nil for infinite)

  • step (Numeric, nil) (defaults to: nil)

    increment (default: 1, sign auto-adjusted)

Returns:

  • (ForLoop)

    numeric sequence serie



313
314
315
316
317
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 313

def FOR(from: nil, to: nil, step: nil)
  from ||= 0
  step ||= 1
  ForLoop.new from, to, step
end

#H(**series_hash) ⇒ FromHashOfSeries

Note:

h.i builds a NEW instance every time it is called, each starting from the beginning. Keep the instance to advance through the serie.

Creates hash-mode serie from hash of series.

Combines multiple series into hash-structured values. Returns hash with same keys, values from respective series. Stops when first serie exhausts.

Examples:

Hash of series

h = H(pitch: S(60, 64, 67), velocity: S(96, 80, 64))
inst = h.i
inst.next_value  # => {pitch: 60, velocity: 96}
inst.next_value  # => {pitch: 64, velocity: 80}

Parameters:

  • series_hash (Hash)

    hash of series (key => serie)

Returns:

  • (FromHashOfSeries)

    combined hash serie



154
155
156
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 154

def H(**series_hash)
  FromHashOfSeries.new series_hash, false
end

#HARMO(error: nil, extended: nil) ⇒ HarmonicNotes

Creates a serie of the harmonic series, in semitones over the fundamental.

Yields the interval of each harmonic from a fundamental of 0, approximated to the nearest semitone, and skips any harmonic whose approximation error exceeds the tolerance. Infinite serie; add the fundamental's own pitch to place it. The values do not depend on any input: it starts producing at once.

Parameters

  • error: maximum approximation error, IN SEMITONES, for a harmonic to be accepted (default: 0.5, i.e. accept every harmonic, since no approximation to the nearest semitone can be off by more than half of one)
  • extended: yield { pitch:, error: } instead of the bare pitch, so the approximation error of each harmonic is available. It does NOT add harmonics: the pitches are the same ones.

Examples:

Harmonic series

harmonics = HARMO(error: 0.5)
inst = harmonics.i
8.times.map { inst.next_value }
# => [0, 12, 19, 24, 28, 31, 34, 36]

A tighter tolerance drops the harmonics that fall between semitones

inst = HARMO(error: 0.1).i
8.times.map { inst.next_value }
# => [0, 12, 19, 24, 31, 36, 38, 43]
# the 5th harmonic (28) is 0.137 semitones off, so it is not accepted

Extended: the same pitches, carrying their error

inst = HARMO(error: 0.5, extended: true).i
harmonics = 3.times.map { inst.next_value }

harmonics.map { |h| h[:pitch] }            # => [0, 12, 19]
harmonics.map { |h| h[:error].round(4) }   # => [0.0, 0.0, 0.0196]

# The octaves are exact; the twelfth is not. 3/1 is 0.0196 semitones
# above the tempered fifth, which is why a tolerance below that drops it.

Over a fundamental other than C

over_g = HARMO().i.map { |n| n + 67 }

Parameters:

  • error (Numeric, nil) (defaults to: nil)

    maximum approximation error in semitones (default: 0.5)

  • extended (Boolean, nil) (defaults to: nil)

    yield pitch and error instead of pitch (default: false)

Returns:

  • (HarmonicNotes)

    harmonic series serie



586
587
588
589
590
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 586

def HARMO(error: nil, extended: nil)
  error ||= 0.5
  extended ||= false
  HarmonicNotes.new error, extended
end

#HC(**series_hash) ⇒ FromHashOfSeries

Creates hash-mode combined serie from hash of series.

Like H but cycles all series. When a serie exhausts, it restarts from the beginning, continuing until all series complete their cycles.

Examples:

Combined cycling all series

hc = HC(a: S(1, 2), b: S(10, 20, 30))
hc.max_size(6).i.to_a  # => [{a:1, b:10}, {a:2, b:20}, {a:1, b:30},
                        #     {a:2, b:10}, {a:1, b:20}, {a:2, b:30}]

Parameters:

  • series_hash (Hash)

    hash of series (key => serie)

Returns:

  • (FromHashOfSeries)

    combined hash serie that cycles all series



173
174
175
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 173

def HC(**series_hash)
  FromHashOfSeries.new series_hash, true
end

#MERGE(*series) ⇒ Sequence

Merges multiple series sequentially.

Plays series in sequence: first serie until exhausted, then second, etc. Restarts each serie (except first) before playing.

Examples:

Merge sequences

merged = MERGE(S(1, 2, 3), S(10, 20, 30))
merged.i.to_a  # => [1, 2, 3, 10, 20, 30]

Melodic phrases

phrase1 = S(60, 64, 67)
phrase2 = S(72, 69, 65)
melody = MERGE(phrase1, phrase2)

Parameters:

  • series (Array<Serie>)

    series to merge sequentially

Returns:

  • (Sequence)

    sequential merge serie



399
400
401
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 399

def MERGE(*series)
  Sequence.new(series)
end

#NILNilSerie

Creates serie that always returns nil.

Returns nil on every next_value call. Useful for padding or as placeholder in composite structures.

Examples:

Nil serie

s = NIL().i
s.next_value  # => nil
s.next_value  # => nil

Returns:

  • (NilSerie)

    serie returning nil



108
109
110
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 108

def NIL
  NilSerie.new
end

#PROXY(serie = nil, cyclic: nil) ⇒ ProxySerie

Creates a proxy serie with optional initial source.

Proxy series enable late binding - creating a serie placeholder that will be resolved later. Useful for:

Use Cases

  • Forward references: Reference series before definition
  • Circular structures: Self-referential or mutually referential series
  • Dependency injection: Define structure, inject source later
  • Dynamic routing: Change source serie at runtime

Method Delegation

Proxy delegates all methods to underlying source via method_missing, making it transparent proxy for most operations.

State Resolution

Proxy starts in :undefined state, becomes :prototype/:instance when source is set and resolved.

Cycles

A proxy that points back into the serie it is part of closes a cycle: the material loops instead of ending. This has to be declared with cyclic: true, because a cycle changes what every walk of the graph has to do and should not appear by accident -- a proxy that closes one without having been declared raises ArgumentError. The reverse is fine: declaring a proxy cyclic and pointing it somewhere that does not loop back is exactly the forward reference the declaration exists for.

What a cycle is for is material whose repetition is not decided in advance: a QUEUE fed while the loop is already sounding, an E() reading state that changes between turns. For a serie that is fully known beforehand, .repeat says the same thing without any of this.

Examples:

Forward reference

proxy = PROXY()
proxy.undefined?  # => true

# Define later
proxy.proxy_source = S(1, 2, 3)
proxy.prototype?  # => true

Circular structure

loop_serie = PROXY(cyclic: true)
sequence = S(1, 2, 3).after(loop_serie)
loop_serie.proxy_source = sequence

# The circle closes, and the state of a cycle is the state of what feeds
# it from outside -- here S(1, 2, 3), a prototype.
sequence.state    # => :prototype
loop_serie.state  # => :prototype

# It never runs out, and says so without walking round itself.
sequence.infinite?  # => true

i = sequence.i
9.times.collect { i.next_value }  # => [1, 2, 3, 1, 2, 3, 1, 2, 3]

Two materials calling each other

to_b = PROXY(cyclic: true)
to_a = PROXY(cyclic: true)

a = S(1, 2).after(to_b)
b = S(3, 4).after(to_a)

to_b.proxy_source = b
to_a.proxy_source = a

i = a.i
8.times.collect { i.next_value }  # => [1, 2, 3, 4, 1, 2, 3, 4]

A cycle that comes back empty ends

n = 0
material = E(nil) { n += 1; n <= 3 ? n : nil }

back = PROXY(cyclic: true)
cycle = material.after(back)
back.proxy_source = cycle

# One turn per request: when the turn produces nothing, the loop is over
# instead of spinning forever.
i = cycle.i
5.times.collect { i.next_value }  # => [1, 2, 3, nil, nil]

With initial source

PROXY(S(1, 2, 3)).i.to_a  # => [1, 2, 3]

Parameters:

  • serie (Serie, nil) (defaults to: nil)

    initial source serie (default: nil)

  • cyclic (Boolean, nil) (defaults to: nil)

    whether this proxy may close a cycle

Returns:



103
104
105
# File 'lib/musa-dsl/series/proxy-serie.rb', line 103

def PROXY(serie = nil, cyclic: nil)
  ProxySerie.new(serie, cyclic: cyclic)
end

#QUANTIZE(time_value_serie, reference: nil, step: nil, value_attribute: nil, stops: nil, predictive: nil, left_open: nil, right_open: nil) ⇒ RawQuantizer, PredictiveQuantizer

Turns a continuous ramp into a staircase.

When this is the answer

Something computed as a curve -- a glissando, an envelope, a trajectory out of a matrix -- has to become discrete before it can be played: pitches are semitones, a controller takes integers, a rhythm lands on divisions. Quantizing is that step, and what comes back is not a set of samples taken at the input times but a staircase: one step per boundary crossed, each carrying the time it holds.

The source has to be a serie of timed values -- hashes extended with Datasets::AbsTimed. A bare hash of the right shape is not one and raises "Don't know how to process".

The two modes, and they sound different

Normal changes the step when the ramp reaches it. Predictive changes when the ramp is nearer the next step than the last -- it rounds instead of waiting to arrive, which is what a listener hears as the pitch.

Examples:

A ramp of three semitones over two bars, quantized to semitones

ramp = S({ time: 0r, value: 60.0 }, { time: 2r, value: 63.0 })
       .map { |v| v.extend(Musa::Datasets::AbsTimed) }

ramp.quantize(step: 1).i.to_a.collect { |v| [v[:time], v[:value]] }
# => [[0r, 60r], [2/3r, 61r], [4/3r, 62r]]

# 61 at 2/3 of a bar, which is where the ramp actually arrives at 61.

The same ramp, predictive

ramp.quantize(step: 1, predictive: true).i.to_a.collect { |v| [v[:time], v[:value]] }
# => [[0r, 60r], [1/3r, 61r], [1r, 62r], [5/3r, 63r]]

# 61 at 1/3, halfway -- and it reaches 63, which the other never does.

A coarser step is fewer notes, not smaller ones

ramp.quantize(step: 3).i.to_a.size  # => 1

Parameters:

  • time_value_serie (Serie)

    source timed serie

  • reference (Numeric, nil) (defaults to: nil)

    quantization reference

  • step (Numeric, nil) (defaults to: nil)

    step size

  • value_attribute (Symbol, nil) (defaults to: nil)

    attribute to quantize

  • stops (Boolean, nil) (defaults to: nil)

    include stop points

  • predictive (Boolean, nil) (defaults to: nil)

    use predictive mode

  • left_open (Boolean, nil) (defaults to: nil)

    left boundary open

  • right_open (Boolean, nil) (defaults to: nil)

    right boundary open

Returns:

  • (RawQuantizer, PredictiveQuantizer)

    quantized serie



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/musa-dsl/series/quantizer-serie.rb', line 118

def QUANTIZE(time_value_serie,
             reference: nil, step: nil,
             value_attribute: nil,
             stops: nil,
             predictive: nil,
             left_open: nil,
             right_open: nil)

  reference ||= 0r
  step ||= 1r
  value_attribute ||= :value
  stops ||= false
  predictive ||= false

  if predictive
    raise ArgumentError, "Predictive quantization doesn't allow parameters 'left_open' or 'right_open'" if left_open || right_open

    PredictiveQuantizer.new(reference, step, time_value_serie, value_attribute, stops)
  else
    # By default: left closed and right_open
    # By default 2:
    #   if right_open is true and left_open is nil, left_open will be false
    #   if left_open is true and right_open is nil, right_open will be false

    right_open = right_open.nil? ? !left_open : right_open
    left_open = left_open.nil? ? !right_open : left_open

    RawQuantizer.new(reference, step, time_value_serie, value_attribute, stops, left_open, right_open)
  end
end

#QUEUE(*series) ⇒ QueueSerie

Note:

to_a RESTARTS the instance, so it returns everything queued and not what is left after the next_value above.

Creates queue serie from initial series.

Queue allows adding series dynamically during playback, creating flexible sequential playback with runtime modification.

Features

  • Dynamic addition: Add series with << during playback
  • Sequential playback: Plays series in queue order
  • Method delegation: Delegates methods to current serie
  • Clear: Can clear queue and reset

Use Cases

  • Interactive sequencing with user input
  • Dynamic phrase assembly
  • Playlist-style serie management
  • Reactive composition systems
  • Live coding pattern queuing

Examples:

Basic queue

queue = QUEUE(S(1, 2, 3)).i
queue.next_value  # => 1
queue << S(4, 5, 6).i  # Add dynamically
queue.to_a  # => [1, 2, 3, 4, 5, 6]

Dynamic playlist

melody1 = S(60, 62)
melody2 = S(67, 69)

queue = QUEUE(melody1).i
queue << melody2.i
queue.to_a  # => [60, 62, 67, 69]

A queue that starts with nothing

queue = QUEUE().i
queue.next_value  # => nil

queue << S(1, 2).i
queue.next_value  # => 1
queue.next_value  # => 2

Parameters:

  • series (Array<Serie>)

    initial series in queue

Returns:



59
60
61
# File 'lib/musa-dsl/series/queue-serie.rb', line 59

def QUEUE(*series)
  QueueSerie.new(series)
end

#RND(*_values, values: nil, from: nil, to: nil, step: nil, random: nil) ⇒ RandomValuesFromArray, RandomNumbersFromRange

Creates random value serie from array or range.

Two modes:

  • Array mode: Random values from provided array
  • Range mode: Random numbers from range (from, to, step)

A SHUFFLE, NOT A DIE. Each value is drawn once and removed, so the serie is a random permutation and then ends: six values from RND(1..6) and nil on the seventh. .repeat is what gives sampling with replacement, reshuffling on each pass, and that one is infinite.

Examples:

Shuffling an array

shuffled = RND(1, 2, 3, 4, 5, 6, random: 42)
shuffled.i.to_a       # => [4, 6, 3, 5, 2, 1]
shuffled.infinite?    # => false

Rolling a die -- with replacement, which needs repeat

die = RND(1, 2, 3, 4, 5, 6, random: 42).repeat
die.infinite?  # => true

Random from range

RND(from: 0, to: 10, step: 5, random: 7).i.to_a  # => [0, 10, 5]

With seed

# The same seed is the same sequence, which is what makes a piece the
# same piece.
RND(1, 2, 3, random: 42).i.to_a  # => [3, 2, 1]
RND(1, 2, 3, random: 42).i.to_a  # => [3, 2, 1]

Parameters:

  • _values (Array)

    values to choose from (positional)

  • values (Array, nil) (defaults to: nil)

    values to choose from (named)

  • from (Numeric, nil) (defaults to: nil)

    range start (range mode)

  • to (Numeric, nil) (defaults to: nil)

    range end (range mode, required)

  • step (Numeric, nil) (defaults to: nil)

    range step (default: 1)

  • random (Random, Integer, nil) (defaults to: nil)

    Random instance or seed

Returns:

  • (RandomValuesFromArray, RandomNumbersFromRange)

    random serie

Raises:

  • (ArgumentError)

    if using both positional and named values

  • (ArgumentError)

    if mixing array and range parameters



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 361

def RND(*_values, values: nil, from: nil, to: nil, step: nil, random: nil)
  raise ArgumentError, "Can't use both direct values #{_values} and values named parameter #{values} at the same time." if values && !_values.empty?

  random = Random.new random if random.is_a?(Integer)
  random ||= Random.new

  values ||= _values

  if !values.empty? && from.nil? && to.nil? && step.nil?
    RandomValuesFromArray.new values.explode_ranges, random
  elsif values.empty? && !to.nil?
    from ||= 0
    step ||= 1
    RandomNumbersFromRange.new from, to, step, random
  else
    raise ArgumentError, 'cannot use values and from:/to:/step: together'
  end
end

#RND1(*_values, values: nil, from: nil, to: nil, step: nil, random: nil) ⇒ RandomValueFromArray, RandomNumberFromRange

Creates single random value serie from array or range.

Like RND but returns only one random value then exhausts. Two modes: array mode and range mode.

Examples:

Single random value

rnd = RND1(1, 2, 3, 4, 5)
inst = rnd.i

inst.next_value  # => a Integer   (one of 1..5)
inst.next_value  # => nil         (exhausted: RND1 yields ONE value)

# `inst` and not `rnd.i` twice: every `.i` is a NEW instance, which would
# start again and give a second random value instead of the nil that
# says the serie is done.

Random seed selection

seed = RND1(10, 20, 30, random: 42)

Parameters:

  • _values (Array)

    values to choose from (positional)

  • values (Array, nil) (defaults to: nil)

    values to choose from (named)

  • from (Numeric, nil) (defaults to: nil)

    range start (range mode)

  • to (Numeric, nil) (defaults to: nil)

    range end (range mode, required)

  • step (Numeric, nil) (defaults to: nil)

    range step (default: 1)

  • random (Random, Integer, nil) (defaults to: nil)

    Random instance or seed

Returns:

  • (RandomValueFromArray, RandomNumberFromRange)

    single random value serie

Raises:

  • (ArgumentError)

    if using both positional and named values

  • (ArgumentError)

    if mixing array and range parameters



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 435

def RND1(*_values, values: nil, from: nil, to: nil, step: nil, random: nil)
  raise ArgumentError, "Can't use both direct values #{_values} and values named parameter #{values} at the same time." if values && !_values.empty?

  random = Random.new random if random.is_a?(Integer)
  random ||= Random.new

  values ||= _values

  if !values.empty? && from.nil? && to.nil? && step.nil?
    RandomValueFromArray.new values.explode_ranges, random
  elsif values.empty? && !to.nil?
    from ||= 0
    step ||= 1
    RandomNumberFromRange.new from, to, step, random
  else
    raise ArgumentError, 'cannot use values and from:/to:/step: parameters together'
  end
end

#S(*values) ⇒ FromArray

Creates serie from array of values.

Most common constructor. Values can include ranges which will be expanded automatically via ExplodeRanges extension.

Examples:

Basic array

notes = S(60, 64, 67, 72)
notes.i.to_a  # => [60, 64, 67, 72]

With ranges

scale = S(60..67)
scale.i.to_a  # => [60, 61, 62, 63, 64, 65, 66, 67]

Parameters:

  • values (Array)

    values to iterate (supports ranges)

Returns:



130
131
132
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 130

def S(*values)
  FromArray.new values.explode_ranges
end

#SIN(start_value: nil, steps: nil, amplitude: nil, center: nil) ⇒ SinFunction

Creates sine wave function serie.

Generates values following sine curve. Useful for smooth oscillations, LFO-style modulation, and periodic variations.

Wave Parameters

  • start_value: Initial value (default: center)
  • steps: Period in steps (nil for continuous)
  • amplitude: Wave amplitude, PEAK TO PEAK (default: 1.0). The wave spans center ± amplitude / 2, so center: 70, amplitude: 50 runs from 45 to 95 and not from 20 to 120.
  • center: Center/offset value (default: 0.0)

Wave equation: center + (amplitude / 2) * sin(progress)

Examples:

Basic sine wave

wave = SIN(steps: 8, amplitude: 10, center: 50)
wave.i.to_a  # => oscillates around 50 ± 10

LFO modulation

lfo = SIN(steps: 16, amplitude: 0.5, center: 0.5)
# Use for amplitude modulation

Parameters:

  • start_value (Numeric, nil) (defaults to: nil)

    initial value

  • steps (Numeric, nil) (defaults to: nil)

    full period in steps

  • amplitude (Numeric, nil) (defaults to: nil)

    wave amplitude, peak to peak (default: 1.0)

  • center (Numeric, nil) (defaults to: nil)

    center offset (default: 0.0)

Returns:

  • (SinFunction)

    sine wave serie



486
487
488
489
490
491
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 486

def SIN(start_value: nil, steps: nil, amplitude: nil, center: nil)
  amplitude ||= 1.0
  center ||= 0.0
  start_value ||= center
  SinFunction.new start_value, steps, amplitude, center
end

#TIMED_UNION(*array_of_timed_series, **hash_of_timed_series) ⇒ TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries

Merges multiple timed series by synchronizing events at each time point.

TIMED_UNION combines series with :time attributes, emitting events at each unique time where at least one source has a value. Sources without values at a given time emit nil. Operates in two distinct modes based on input format.

Timed Series Format

Each event is a hash with :time and :value keys, extended with AbsTimed:

{ time: 0r, value: 60, duration: 1r }.extend(Musa::Datasets::AbsTimed)

Additional attributes (:duration, :velocity, etc.) are preserved and synchronized alongside values.

Operating Modes

Array Mode: TIMED_UNION(s1, s2, s3)

  • Anonymous positional sources
  • Output: { time: t, value: [val1, val2, val3] }
  • Use for: Ordered tracks without specific names

Hash Mode: TIMED_UNION(melody: s1, bass: s2)

  • Named sources with keys
  • Output: { time: t, value: { melody: val1, bass: val2 } }
  • Use for: Identified voices/tracks for routing

Value Types and Combination

Direct values (integers, strings, etc.):

s1 = S({ time: 0, value: 60 })
s2 = S({ time: 0, value: 64 })
TIMED_UNION(s1, s2)  # => { time: 0, value: [60, 64] }

Hash values (polyphonic events):

s1 = S({ time: 0, value: { a: 1, b: 2 } })
s2 = S({ time: 0, value: { c: 10 } })
TIMED_UNION(s1, s2)  # => { time: 0, value: { a: 1, b: 2, c: 10 } }

Array values (multi-element events):

s1 = S({ time: 0, value: [1, 2] })
s2 = S({ time: 0, value: [10, 20] })
TIMED_UNION(s1, s2)  # => { time: 0, value: [1, 2, 10, 20] }

Mixed Hash + Direct (advanced):

s1 = S({ time: 0, value: { a: 1, b: 2 } })
s2 = S({ time: 0, value: 100 })
TIMED_UNION(s1, s2)  # => { time: 0, value: { a: 1, b: 2, 0 => 100 } }

Synchronization Behavior

Events are emitted at each unique time point across all sources:

s1 = S({ time: 0r, value: 1 }, { time: 2r, value: 3 })
s2 = S({ time: 1r, value: 10 })
TIMED_UNION(s1, s2).i.to_a
# => [{ time: 0r, value: [1, nil] },
#     { time: 1r, value: [nil, 10] },
#     { time: 2r, value: [3, nil] }]

Extra Attributes

Non-standard attributes (beyond :time, :value) are synchronized:

s1 = S({ time: 0, value: 1, velocity: 80 })
s2 = S({ time: 0, value: 10, duration: 1r })
TIMED_UNION(s1, s2)
# => { time: 0, value: [1, 10], velocity: [80, nil], duration: [nil, 1r] }

Examples:

Array mode with direct values

s1 = S({ time: 0r, value: 1 }, { time: 1r, value: 2 })
s2 = S({ time: 0r, value: 10 }, { time: 2r, value: 20 })

union = TIMED_UNION(s1, s2).i
union.to_a
# => [{ time: 0r, value: [1, 10] },
#     { time: 1r, value: [2, nil] },
#     { time: 2r, value: [nil, 20] }]

Hash mode with named sources

melody = S({ time: 0r, value: 60 }, { time: 1r, value: 64 })
bass = S({ time: 0r, value: 36 }, { time: 2r, value: 40 })

union = TIMED_UNION(melody: melody, bass: bass).i
union.to_a
# => [{ time: 0r, value: { melody: 60, bass: 36 } },
#     { time: 1r, value: { melody: 64, bass: nil } },
#     { time: 2r, value: { melody: nil, bass: 40 } }]

Hash values with polyphonic events

s1 = S({ time: 0r, value: { a: 1, b: 2 } })
s2 = S({ time: 0r, value: { c: 10, d: 20 } })

union = TIMED_UNION(s1, s2).i
union.next_value  # => { time: 0r, value: { a: 1, b: 2, c: 10, d: 20 } }

Extra attributes synchronization

s1 = S({ time: 0r, value: 1, velocity: 80, duration: 1r })
s2 = S({ time: 0r, value: 10, velocity: 90 })

union = TIMED_UNION(s1, s2).i
union.next_value
# => { time: 0r,
#      value: [1, 10],
#      velocity: [80, 90],
#      duration: [1r, nil] }

Key conflict detection

s1 = S({ time: 0r, value: { a: 1, b: 2 } })
s2 = S({ time: 0r, value: { a: 10 } })  # 'a' already used!

union = TIMED_UNION(s1, s2).i
union.next_value  # => RuntimeError: Value: key a already used

Parameters:

  • array_of_timed_series (Array<Serie>)

    timed series (array mode)

  • hash_of_timed_series (Hash{Symbol => Serie})

    named timed series (hash mode)

Returns:

  • (TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries)

    merged serie

Raises:

  • (ArgumentError)

    if mixing array and hash modes

  • (RuntimeError)

    if hash values have duplicate keys across sources

  • (RuntimeError)

    if mixing incompatible value types (Hash with Array)

See Also:

  • Splits compound values into individual timed events
  • Removes events with all-nil values
  • Instance method for union


149
150
151
152
153
154
155
156
157
158
159
# File 'lib/musa-dsl/series/timed-serie.rb', line 149

def TIMED_UNION(*array_of_timed_series, **hash_of_timed_series)
  raise ArgumentError, 'Can\'t union an array of series with a hash of series' if array_of_timed_series.any? && hash_of_timed_series.any?

  if array_of_timed_series.any?
    TimedUnionOfArrayOfTimedSeries.new(array_of_timed_series)
  elsif hash_of_timed_series.any?
    TimedUnionOfHashOfTimedSeries.new(hash_of_timed_series)
  else
    raise ArgumentError, 'Missing argument series'
  end
end

#UNDEFINEDUndefinedSerie

Creates undefined serie.

Returns serie in undefined state. Useful as placeholder that will be resolved later (e.g., in PROXY).

Examples:

Undefined placeholder

proxy = PROXY()  # Uses UNDEFINED internally
proxy.undefined?  # => true

Returns:



91
92
93
# File 'lib/musa-dsl/series/main-serie-constructors.rb', line 91

def UNDEFINED
  UndefinedSerie.new
end