Module: Musa::Series::Operations
- 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/buffer-serie.rb,
lib/musa-dsl/series/quantizer-serie.rb,
lib/musa-dsl/series/series-composer.rb,
lib/musa-dsl/series/main-serie-operations.rb,
lib/musa-dsl/series/hash-or-array-serie-splitter.rb
Overview
Series transformation operations for composing and modifying series.
Provides methods for transforming, combining, and controlling series flow. All operations return new series (functional/immutable style).
Categories
Mapping & Transformation
- map - Transform values via block
- with - Combine multiple series for mapping
- process_with - Generic processor with parameters
- hashify - Convert array values to hash
- shift - Shift values by offset
Filtering & Selection
- select - Keep values matching condition
- remove - Remove values matching condition
- skip - Skip first N values
- max_size - Limit to N values
- cut - Cut into chunks
Flow Control
- repeat - Repeat series N times or conditionally
- autorestart - Auto-restart when exhausted
- flatten - Flatten nested series
- merge - Merge serie of series
- after / + - Append series sequentially
Switching & Multiplexing
- switch - Switch between series based on selector
- multiplex - Multiplex series based on selector
- switch_serie - Switch to different series entirely
Structural Operations
- reverse - Reverse values
- randomize - Shuffle values randomly
- lock - Lock serie (prevent changes)
- flatten - Flatten nested series
Timing Operations
- anticipate - Evaluate block one step ahead
- lazy - Delay evaluation to next step
Usage Patterns
Mapping
notes = S(60, 64, 67).map { |n| n + 12 } # Transpose octave
notes.i.to_a # => [72, 76, 79]
Filtering
evens = S(1, 2, 3, 4, 5, 6).select { |n| n.even? }
evens.i.to_a # => [2, 4, 6]
Combining
pitches = S(60, 64, 67)
velocities = S(96, 80, 64)
notes = pitches.with(velocities) { |p, v| {pitch: p, velocity: v} }
Repeating
pattern = S(1, 2, 3).repeat(3)
pattern.i.to_a # => [1, 2, 3, 1, 2, 3, 1, 2, 3]
Chaining Operations
result = S(1, 2, 3, 4, 5)
.select { |n| n.even? }
.map { |n| n * 10 }
.repeat(2)
result.i.to_a # => [20, 40, 20, 40]
Defined Under Namespace
Classes: ComposerAsOperationSerie, Processor
Instance Method Summary collapse
-
#+(other) ⇒ Sequence
Appends another serie (operator alias for after).
-
#after(*series) ⇒ Sequence
Appends series sequentially.
-
#anticipate {|current, next_value| ... } ⇒ Anticipate
Evaluates block one step ahead (anticipate).
-
#autorestart ⇒ Autorestart
Auto-restarts serie when exhausted.
-
#buffered(sync: false) ⇒ BufferSerie, SyncBufferSerie
Creates a buffered serie allowing multiple independent iterations over same source.
-
#compact_timed ⇒ TimedCompacter
Removes timed events where all values are nil.
-
#composer { ... } ⇒ ComposerAsOperationSerie
Creates a composer transformation pipeline for complex multi-stage transformations.
-
#cut(length) ⇒ Cutter
Cuts serie into chunks of specified length.
-
#flatten ⇒ Flattener
Flattens nested series into single level.
-
#flatten_timed ⇒ TimedFlattener
Splits compound timed values into individual timed events.
-
#hashify(*keys) ⇒ HashFromSeriesArray
Converts array values to hash with specified keys.
-
#lazy {|previous| ... } ⇒ LazySerieEval
Delays evaluation to next step (lazy evaluation).
-
#lock ⇒ Locker
Locks serie preventing further modifications.
-
#map(isolate_values: nil) {|value| ... } ⇒ ProcessWith
Maps values via transformation block.
-
#max_size(length) ⇒ LengthLimiter
Limits serie to maximum number of values.
-
#merge ⇒ MergeSerieOfSeries
Merges serie of series into single serie.
-
#multiplex(*indexed_series, **hash_series) ⇒ MultiplexSelector
Advances every serie and gives back the value of the one selected.
-
#process_with(**parameters) {|value, parameters| ... } ⇒ Processor
Transforms each value with a block whose parameters can change later.
-
#proxy ⇒ Object
TODO add test case.
-
#quantize(reference: nil, step: nil, value_attribute: nil, stops: nil, predictive: nil, left_open: nil, right_open: nil) ⇒ RawQuantizer, PredictiveQuantizer
Quantizes time-value serie to discrete steps.
-
#queued ⇒ QueueSerie
Wraps this serie in a queue.
-
#randomize(random: nil) ⇒ Randomizer
Randomizes order of values.
-
#remove(block = nil) {|value| ... } ⇒ Remover
Removes values matching condition.
-
#repeat(times = nil, condition: nil) { ... } ⇒ Repeater, InfiniteRepeater
Repeats serie multiple times or conditionally.
-
#reverse ⇒ Reverser
Reverses order of values.
-
#select(block = nil) {|value| ... } ⇒ Selector
Selects values matching condition.
-
#shift(shift) ⇒ Shifter
Rotates serie elements circularly.
-
#skip(length) ⇒ Skipper
Skips first N values.
-
#split ⇒ Splitter
Serie splitter for decomposing hash/array values into component series.
-
#switch(*indexed_series, **hash_series) ⇒ Switcher
Reads one value from whichever serie the selector names.
-
#switch_serie(*indexed_series, **hash_series) ⇒ SwitchFullSerie
Switches to entirely different series based on selector.
-
#union_timed(*other_timed_series, key: nil, **other_key_timed_series) ⇒ TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries
Combines this timed serie with others via TIMED_UNION.
-
#with(*with_series, on_restart: nil, isolate_values: nil, **with_key_series) {|main_value, with_values, with_key_values| ... } ⇒ With
(also: #eval)
Combines multiple series for mapping.
Instance Method Details
#+(other) ⇒ Sequence
Appends another serie (operator alias for after).
503 504 505 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 503 def +(other) Musa::Series::Constructors.MERGE self, other end |
#after(*series) ⇒ Sequence
Appends series sequentially.
Alias for MERGE - plays this serie, then others in sequence.
488 489 490 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 488 def after(*series) Musa::Series::Constructors.MERGE self, *series end |
#anticipate {|current, next_value| ... } ⇒ Anticipate
Evaluates block one step ahead (anticipate).
Block receives current value and NEXT value (peeked). Enables look-ahead transformations and transitions.
The block gets THREE values -- the previous one, the current one and the next -- and either end is nil where there is nothing.
672 673 674 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 672 def anticipate(&block) Anticipate.new self, &block end |
#autorestart ⇒ Autorestart
The serie ENDS at each pass -- yielding nil -- and restarts on the
next call. That nil is the signal that a pass finished, which is what
makes the repetition countable; it also means infinite? is false and
that to_a stops at the end of the first pass. For a serie with no
seam in it, use repeat.
Auto-restarts serie when exhausted.
Creates an infinite serie from an original serie that automatically restarts from beginning when it reaches the end.
116 117 118 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 116 def autorestart Autorestart.new self end |
#buffered(sync: false) ⇒ BufferSerie, SyncBufferSerie
Creates a buffered serie allowing multiple independent iterations over same source.
Provides buffering mechanism enabling multiple "readers" to independently iterate over the same serie source without interfering with each other.
Buffering Modes
- Async (default): Buffers fill independently, each progresses at own pace
- Sync: All buffers synchronized, restart affects all
Use Cases
- Multiple voices reading same melodic sequence at different speeds
- Polyphonic playback from single source
- Canonic structures (rounds, fugues)
- Independent transformations of same base material
Memory Management
History is automatically cleaned when all buffers have progressed past old values, preventing unbounded memory growth.
57 58 59 60 61 62 63 |
# File 'lib/musa-dsl/series/buffer-serie.rb', line 57 def buffered(sync: false) if sync SyncBufferSerie.new(self) else BufferSerie.new(self) end end |
#compact_timed ⇒ TimedCompacter
Removes timed events where all values are nil.
Filters out temporal "gaps" where no sources have active values. Useful after union operations that create nil placeholders, or for cleaning sparse sequences.
Removal logic:
- Direct nil:
{ time: t, value: nil }→ removed - All-nil Hash:
{ time: t, value: { a: nil, b: nil } }→ removed - Partial Hash:
{ time: t, value: { a: 1, b: nil } }→ kept (has non-nil) - All-nil Array:
{ time: t, value: [nil, nil] }→ removed - Partial Array:
{ time: t, value: [1, nil] }→ kept (has non-nil)
580 581 582 |
# File 'lib/musa-dsl/series/timed-serie.rb', line 580 def compact_timed TimedCompacter.new(self) end |
#composer { ... } ⇒ ComposerAsOperationSerie
Creates a composer transformation pipeline for complex multi-stage transformations.
Composer provides declarative DSL for building transformation pipelines with multiple inputs, outputs, and intermediate processing stages.
Composer Concepts
- Pipelines: Named transformation chains
- Inputs: Named input series (proxied and buffered)
- Outputs: Named output series
- Operations: Transformation steps in pipeline
- Auto-commit: Automatic finalization of pipelines
DSL Structure
A pipeline is declared by NAMING it and listing its operations; the names
are then wired together with route. Operations are symbols (reverse)
or hashes ({ map: ... }, { skip: 1 }, { S: [...] }). input and
output are pipelines like any other — they are simply the ones the
Composer creates for you, and can be renamed or multiplied through the
inputs: and outputs: keywords.
composer do
stage1 operation1, operation2
stage2 operation3
route input, to: stage1
route stage1, to: stage2
route stage2, to: output
end
Musical Applications
- Complex multi-voice processing
- Effect chains and routing
- Algorithmic composition pipelines
- Multi-stage transformations
- Modular synthesis-style routing
89 90 91 |
# File 'lib/musa-dsl/series/series-composer.rb', line 89 def composer(&block) ComposerAsOperationSerie.new(self, &block) end |
#cut(length) ⇒ Cutter
Cuts serie into chunks of specified length.
Returns a serie of SERIES, each yielding length values.
The chunks are series and not arrays, which is what keeps the whole thing
lazy -- nothing is materialised until each chunk is consumed. Ask for the
arrays explicitly with to_a(recursive: true).
524 525 526 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 524 def cut(length) Cutter.new self, length end |
#flatten ⇒ Flattener
Flattens nested series into single level.
Recursively consumes series elements that are themselves series.
205 206 207 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 205 def flatten Flattener.new self end |
#flatten_timed ⇒ TimedFlattener
Splits compound timed values into individual timed events.
Converts events with Hash or Array values into separate timed events per element, preserving time and extra attributes. Direct values pass through unchanged.
Hash values → Hash of timed events (keyed by original keys):
{ time: 0, value: { a: 1, b: 2 }, velocity: { a: 80, b: 90 } }
# becomes:
{ a: { time: 0, value: 1, velocity: 80 },
b: { time: 0, value: 2, velocity: 90 } }
Array values → Array of timed events (indexed):
{ time: 0, value: [1, 2], velocity: [80, 90] }
# becomes:
[{ time: 0, value: 1, velocity: 80 },
{ time: 0, value: 2, velocity: 90 }]
Direct values → Pass through unchanged (already flat)
Use Cases
- Separate polyphonic events into individual voices
- Split multi-track sequences for independent processing
- Prepare for voice-specific routing via
split - Enable per-voice filtering with
compact_timed
529 530 531 |
# File 'lib/musa-dsl/series/timed-serie.rb', line 529 def flatten_timed TimedFlattener.new(self) end |
#hashify(*keys) ⇒ HashFromSeriesArray
Converts array values to hash with specified keys.
Takes array-valued serie and converts to hash using provided keys.
256 257 258 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 256 def hashify(*keys) HashFromSeriesArray.new self, keys end |
#lazy {|previous| ... } ⇒ LazySerieEval
Delays evaluation to next step (lazy evaluation).
Block receives previous value and evaluates for current step. Enables state-dependent transformations.
691 692 693 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 691 def lazy(&block) LazySerieEval.new self, &block end |
#lock ⇒ Locker
Locks serie preventing further modifications.
Returns locked copy that cannot be transformed further.
301 302 303 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 301 def lock Locker.new self end |
#map(isolate_values: nil) {|value| ... } ⇒ ProcessWith
Maps values via transformation block.
Simplest and most common transformation. Applies block to each value.
Shorthand for with without additional series.
637 638 639 640 641 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 637 def map(isolate_values: nil, &block) isolate_values ||= isolate_values.nil? ? false : isolate_values ProcessWith.new self, isolate_values: isolate_values, &block end |
#max_size(length) ⇒ LengthLimiter
Limits serie to maximum number of values.
Stops after N values regardless of source length.
173 174 175 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 173 def max_size(length) LengthLimiter.new self, length end |
#merge ⇒ MergeSerieOfSeries
Merges serie of series into single serie.
Flattens one level: consumes serie where each element is itself a serie, merging them sequentially.
541 542 543 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 541 def merge MergeSerieOfSeries.new self end |
#multiplex(*indexed_series, **hash_series) ⇒ MultiplexSelector
Advances every serie and gives back the value of the one selected.
All of them move, all of the time. What the selector chooses is which one is heard, not which one runs -- so this is a window onto several simultaneous streams, and looking away from one does not pause it. A crossfade, a texture where one layer surfaces at a time, an instrument that changes what it is doubling.
Compare #switch, where the unselected series wait.
457 458 459 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 457 def multiplex(*indexed_series, **hash_series) MultiplexSelector.new self, indexed_series, hash_series end |
#process_with(**parameters) {|value, parameters| ... } ⇒ Processor
Transforms each value with a block whose parameters can change later.
When this is the answer, and not .map
.map { |v| v + 7 } freezes the 7 in the block. process_with keeps
it on the serie, where it can be changed while the serie is running
-- so the transformation is under control instead of being decided once.
A transposition that moves mid-phrase, a scaling driven from outside, a
depth that a controller sets: anything where the amount is itself a
parameter of the piece.
If the amount is known and fixed, .map says it with less.
239 240 241 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 239 def process_with(**parameters, &processor) Processor.new self, parameters, &processor end |
#proxy ⇒ Object
TODO add test case
260 261 262 |
# File 'lib/musa-dsl/series/proxy-serie.rb', line 260 def proxy Series::ProxySerie.new(self) end |
#quantize(reference: nil, step: nil, value_attribute: nil, stops: nil, predictive: nil, left_open: nil, right_open: nil) ⇒ RawQuantizer, PredictiveQuantizer
Quantizes time-value serie to discrete steps.
Quantization Modes
- Raw: Rounds values to nearest step, interpolates between points
- Predictive: Predicts crossings of quantization boundaries
Applications
- Quantize MIDI controller data to discrete values
- Convert continuous pitch bends to semitones
- Snap timing to grid
- Generate stepped automation curves
- Convert analog input to digital steps
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/musa-dsl/series/quantizer-serie.rb', line 48 def quantize(reference: nil, step: nil, value_attribute: nil, stops: nil, predictive: nil, left_open: nil, right_open: nil) # TODO remove debugging puts, intermediate hash comments on :info and InspectNice Series::Constructors.QUANTIZE(self, reference: reference, step: step, value_attribute: value_attribute, stops: stops, predictive: predictive, left_open: left_open, right_open: right_open) end |
#queued ⇒ QueueSerie
Wraps this serie in a queue.
190 191 192 |
# File 'lib/musa-dsl/series/queue-serie.rb', line 190 def queued Series::Constructors.QUEUE(self) end |
#randomize(random: nil) ⇒ Randomizer
Randomizes order of values.
Shuffles values randomly. Requires finite serie.
340 341 342 343 344 345 346 347 348 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 340 def randomize(random: nil) # Without this the Integer reached Randomizer intact and only failed # when the serie was consumed, with a traceback pointing far away from # the call that caused it (issue #87). random = Random.new(random) if random.is_a?(Integer) random ||= Random.new Randomizer.new self, random end |
#remove(block = nil) {|value| ... } ⇒ Remover
Removes values matching condition.
Filters out values where block returns true.
366 367 368 369 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 366 def remove(block = nil, &yield_block) block ||= yield_block Remover.new self, &block end |
#repeat(times = nil, condition: nil) { ... } ⇒ Repeater, InfiniteRepeater
Repeats serie multiple times or conditionally.
Three modes:
- times: Repeat exact number of times
- condition: Repeat while condition true
- neither: Infinite repetition
150 151 152 153 154 155 156 157 158 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 150 def repeat(times = nil, condition: nil, &condition_block) condition ||= condition_block if times || condition Repeater.new self, times, &condition else InfiniteRepeater.new self end end |
#reverse ⇒ Reverser
Reverses order of values.
Consumes entire serie and returns values in reverse order. Requires finite serie.
317 318 319 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 317 def reverse Reverser.new self end |
#select(block = nil) {|value| ... } ⇒ Selector
Selects values matching condition.
Keeps only values where block returns true.
387 388 389 390 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 387 def select(block = nil, &yield_block) block ||= yield_block Selector.new self, &block end |
#shift(shift) ⇒ Shifter
Rotates serie elements circularly.
Performs circular rotation of elements:
- Negative values rotate left (first elements move to end)
- Positive values rotate right (last elements move to beginning)
- Zero performs no rotation
Note: Right rotation (positive values) requires finite series as the entire serie must be loaded into memory for rotation.
290 291 292 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 290 def shift(shift) Shifter.new self, shift end |
#skip(length) ⇒ Skipper
Skips first N values.
Discards first length values, returns rest.
190 191 192 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 190 def skip(length) Skipper.new self, length end |
#split ⇒ Splitter
Serie splitter for decomposing hash/array values into component series.
Splits series of hash or array values into individual component series, enabling independent access to each component.
Splitting Modes
- Hash mode: Split
{pitch: 60, velocity: 96}into separate series for:pitchand:velocity - Array mode: Split
[60, 96]into separate series for indices 0, 1
Component Access
- Hash:
splitter[:pitch],splitter[:velocity] - Array:
splitter[0],splitter[1] - Enumerable:
splitter.each { |component| ... }
Use Cases
- Separate polyphonic voices from single source
- Independent processing of musical parameters
- Extract specific components (pitch, duration, velocity, etc.)
- Multi-track decomposition
45 46 47 |
# File 'lib/musa-dsl/series/hash-or-array-serie-splitter.rb', line 45 def split Splitter.new(self) end |
#switch(*indexed_series, **hash_series) ⇒ Switcher
Reads one value from whichever serie the selector names.
The unselected series do not advance. Each one waits where it was and continues from there next time it is chosen, which is what makes this a dialogue between materials rather than a window onto them: two voices taking turns, each picking up its own thread.
Compare #multiplex, where everything advances and only one value is heard.
420 421 422 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 420 def switch(*indexed_series, **hash_series) Switcher.new self, indexed_series, hash_series end |
#switch_serie(*indexed_series, **hash_series) ⇒ SwitchFullSerie
Switches to entirely different series based on selector.
Changes which serie is being consumed entirely.
471 472 473 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 471 def switch_serie(*indexed_series, **hash_series) SwitchFullSerie.new self, indexed_series, hash_series end |
#union_timed(*other_timed_series, key: nil, **other_key_timed_series) ⇒ TimedUnionOfArrayOfTimedSeries, TimedUnionOfHashOfTimedSeries
Combines this timed serie with others via TIMED_UNION.
Convenience method for unioning series, supporting both array and hash modes. Calls Constructors#TIMED_UNION constructor with appropriate parameters.
Array mode: s1.union_timed(s2, s3)
Hash mode: s1.union_timed(key: :melody, bass: s2, drums: s3)
618 619 620 621 622 623 624 625 626 627 628 |
# File 'lib/musa-dsl/series/timed-serie.rb', line 618 def union_timed(*other_timed_series, key: nil, **other_key_timed_series) if key && other_key_timed_series.any? Series::Constructors.TIMED_UNION(key => self, **other_key_timed_series) elsif other_timed_series.any? && other_key_timed_series.empty? Series::Constructors.TIMED_UNION(self, *other_timed_series) else raise ArgumentError, 'Can\'t union an array of series with a hash of series' end end |
#with(*with_series, on_restart: nil, isolate_values: nil, **with_key_series) {|main_value, with_values, with_key_values| ... } ⇒ With Also known as: eval
Combines multiple series for mapping.
Synchronously iterates multiple series, passing all values to block. Enables multi-voice transformations and combinations.
Parameters
- with_series: Positional series (passed as array to block)
- with_key_series: Named series (passed as keywords to block)
- on_restart: Block called on restart
- isolate_values: Clone values to prevent mutation
Block Parameters
Block receives:
- Main serie value (first argument)
- Positional with_series values (array)
- Keyword with_key_series values (keywords)
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 |
# File 'lib/musa-dsl/series/main-serie-operations.rb', line 596 def with(*with_series, on_restart: nil, isolate_values: nil, **with_key_series, &block) if with_series.any? && with_key_series.any? raise ArgumentError, 'Can\'t use extra parameters series and key named parameters series' end extra_series = if with_series.any? with_series elsif with_key_series.any? with_key_series end isolate_values ||= isolate_values.nil? ? true : isolate_values ProcessWith.new self, extra_series, on_restart, isolate_values: isolate_values, &block end |