Module: Musa::Datasets::P

Includes:
Dataset
Defined in:
lib/musa-dsl/datasets/p.rb

Overview

Point series: sequential points in time with durations.

P (Point series) represents sequential points in time as arrays with alternating structure: [point, duration, point, duration, point, ...].

Structure

The array alternates between points and durations:

[point₀, duration₀, point₁, duration₁, point₂]
  • Points (odd positions): Any data (numbers, hashes, complex structures, etc.)
  • Durations (even positions): Time between points (numbers)
  • Last point: Final point has no duration (sequence end)

This compact format efficiently represents timed sequences without repeating time information.

Conversions

P can be converted to two different representations:

1. Timed Series (to_timed_serie)

Converts to series of AbsTimed events with absolute time and value. Each value gets a timestamp based on cumulative durations.

p = [60, 4, 64, 8, 67].extend(P)
serie = p.to_timed_serie
# Yields:
# { time: 0, value: 60 }
# { time: 1.0, value: 64 }  (4 * base_duration = 1.0)
# { time: 3.0, value: 67 }  (8 * base_duration = 2.0)

2. Parameter Segment Series (to_ps_serie)

Converts to series of PS (Parameter Segment) objects representing continuous changes between consecutive points.

p = [60, 4, 64, 8, 67].extend(P)
serie = p.to_ps_serie
# Yields PS objects:
# { from: 60, to: 64, duration: 1.0, right_open: true }
# { from: 64, to: 67, duration: 2.0, right_open: false }

Point Transformation

The #map method transforms points while preserving durations:

p = [60, 4, 64, 8, 67].extend(P)
p2 = p.map { |point| point + 12 }
# => [72, 4, 76, 8, 79]
# Durations unchanged, points transformed

Examples:

Basic point series (MIDI pitches)

# MIDI pitches with durations in quarter notes
p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
# 60 (C4) for 4 quarters → 64 (E4) for 8 quarters → 67 (G4)

Hash points (complex data structures)

p = [
  { pitch: 60, velocity: 64 }, 4,
  { pitch: 64, velocity: 80 }, 8,
  { pitch: 67, velocity: 64 }
].extend(Musa::Datasets::P)

p.to_timed_serie(base_duration: 1/4r).i.to_a
# => [{ time: (0/1), value: { pitch: 60, velocity: 64 } },
#     { time: (1/1), value: { pitch: 64, velocity: 80 } },
#     { time: (3/1), value: { pitch: 67, velocity: 64 } }]

# A point can be anything; only the odd positions are read as points, and
# nothing looks inside them.

Convert to timed serie

p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
p.to_timed_serie(base_duration: 1/4r).i.to_a
# => [{ time: 0r, value: 60 }, { time: 1r, value: 64 }, { time: 3r, value: 67 }]

# The 4 and the 8 between the pitches are durations in base_durations, so
# the third point lands at 3r and not at 2r.

Start at specific time

p = [60, 4, 64].extend(Musa::Datasets::P)
p.to_timed_serie(base_duration: 1/4r, time_start: 10r).i.to_a
# => [{ time: 10r, value: 60 }, { time: 11r, value: 64 }]

Start time from component

p = [{ time: 100, pitch: 60 }, 4, { time: 200, pitch: 64 }].extend(P)
p.to_timed_serie(time_start_component: :time).i.to_a
# => [{ time: (100/1), value: { time: 100, pitch: 60 } },
#     { time: (101/1), value: { time: 200, pitch: 64 } }]

# Only the FIRST point's component sets the clock. The 200 in the second
# point travels as data and is not read as a time: the second event is at
# 101, one duration after the first.

Transform points

p = [60, 4, 64, 8, 67].extend(Musa::Datasets::P)
p.map { |point| point + 12 }  # => [72, 4, 76, 8, 79]

# Only the points: the 4 and the 8 are durations and come out untouched,
# so a transposition can be written without guarding against them.

See Also:

Defined Under Namespace

Classes: PtoTimedSerie

Instance Method Summary collapse

Instance Method Details

#map {|point| ... } ⇒ P

Maps over points, preserving durations.

Transforms each point (odd positions) using the block while keeping durations (even positions) unchanged.

Examples:

Transform points (e.g., transpose pitches)

p = [60, 4, 64, 8, 67].extend(P)
p.map { |point| point + 12 }
# => [72, 4, 76, 8, 79]

Transform hash points

p = [{ pitch: 60 }, 4, { pitch: 64 }].extend(P)
p.map { |point| point.merge(velocity: 80) }
# => [{ pitch: 60, velocity: 80 }, 4, { pitch: 64, velocity: 80 }]

# The 4 between them is a duration, and the block is never called on it.

Yield Parameters:

  • point (Object)

    each point in the series

Yield Returns:

  • (Object)

    transformed point

Returns:

  • (P)

    new P with transformed points



220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/musa-dsl/datasets/p.rb', line 220

def map(&block)
  i = 0
  clone.map! do |element|
    # Process with block only the points (points are the alternating elements because P
    # structure is <point> <duration> <point> <duration> <point>)
    #
    if (i += 1) % 2 == 1
      block.call(element)
    else
      element
    end
  end
end

#to_ps_serie(base_duration: nil) ⇒ Musa::Series::Serie<PS>

Converts to series of parameter segments.

Creates Musa::Datasets::PS objects representing continuous changes from each point to the next. Useful for glissandi, parameter sweeps, or any continuous interpolation between points.

Examples:

Create parameter segments

line = [60, 4, 64, 8, 67].extend(P)
serie = line.to_ps_serie.i  # a prototype cannot be consumed
serie.next_value
# => { from: 60, to: 64, duration: 1r, right_open: true }

Parameters:

  • base_duration (Rational) (defaults to: nil)

    duration unit multiplier (default: 1/4r) Durations in P are multiplied by this to get actual time

Returns:



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/musa-dsl/datasets/p.rb', line 134

def to_ps_serie(base_duration: nil)
  # A quarter of a bar. Not "a quarter note": that is what it lasts in 4/4
  # and nowhere else -- durations are fractions of a bar throughout the
  # library (see {Musa::Datasets::AbsD}), and the two readings part company
  # as soon as the meter is not 4/4.
  base_duration ||= 1/4r

  # TODO if instead of using clone (needed because of p.shift) we use index counter the P elements would be evaluated on the last moment

  Musa::Series::Constructors.E(clone, base_duration) do |p, base_duration|
    (p.size >= 3) ?
      { from: p.shift,
        duration: p.shift * base_duration,
        to: p.first,
        right_open: (p.length > 1) }.extend(PS).tap { |_| _.base_duration = base_duration } : nil
  end
end

#to_timed_serie(time_start: nil, time_start_component: nil, base_duration: nil) ⇒ PtoTimedSerie

Converts to series of timed events (AbsTimed).

Creates series yielding AbsTimed events with absolute time and value. Each value is emitted at its calculated time point based on cumulative durations.

Examples:

Basic timed serie

line = [60, 4, 64, 8, 67].extend(P)
serie = line.to_timed_serie.i  # a prototype cannot be consumed
serie.next_value  # => { time: 0r, value: 60 }
serie.next_value  # => { time: 1r, value: 64 }
serie.next_value  # => { time: 3r, value: 67 }

Custom start time

line = [60, 4, 64, 8, 67].extend(P)
line.to_timed_serie(time_start: 10r).i.to_a
# => [{ time: (10/1), value: 60 },
#     { time: (11/1), value: 64 },
#     { time: (13/1), value: 67 }]

# The start shifts the whole serie; the distances between points are the
# durations and do not move.

Start time from component

p = [{ time: 100, pitch: 60 }, 4, { pitch: 64 }].extend(P)
p.to_timed_serie(time_start_component: :time).i.to_a
# => [{ time: (100/1), value: { time: 100, pitch: 60 } },
#     { time: (101/1), value: { pitch: 64 } }]

# Only the first point needs the component: the rest are placed by the
# durations between them.

Parameters:

  • time_start (Numeric) (defaults to: nil)

    starting time offset (default: 0)

  • time_start_component (Symbol) (defaults to: nil)

    key in first value to use as time offset If provided, adds first to time_start

  • base_duration (Rational) (defaults to: nil)

    duration unit multiplier (default: 1/4r)

Returns:



189
190
191
192
193
194
195
196
197
# File 'lib/musa-dsl/datasets/p.rb', line 189

def to_timed_serie(time_start: nil, time_start_component: nil, base_duration: nil)
  time_start ||= 0r
  time_start += self.first[time_start_component] if time_start_component

  # A quarter of a bar; see the note in #to_ps_serie above.
  base_duration ||= 1/4r

  PtoTimedSerie.new(self, base_duration, time_start)
end