Class: Musa::Datasets::Score Private

Inherits:
Object
  • Object
show all
Includes:
Enumerable, AbsD, Queriable, Render, ToMXML
Defined in:
lib/musa-dsl/datasets/score.rb,
lib/musa-dsl/datasets/score/render.rb,
lib/musa-dsl/datasets/score/queriable.rb,
lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb,
lib/musa-dsl/datasets/score/to-mxml/process-ps.rb,
lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb,
lib/musa-dsl/datasets/score/to-mxml/process-time.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Time-indexed container for musical events.

Score organizes musical events along a timeline, storing them at specific time points and providing efficient queries for time intervals. Implements Enumerable for iteration over time slots.

Purpose

Score provides:

  • Time-indexed storage: Events organized by start time (Rational)
  • Interval queries: Find events in time ranges (#between, #changes_between)
  • Duration tracking: Automatically tracks event durations
  • Export formats: MusicXML export via ToMXML
  • Rendering: MIDI rendering via Render
  • Filtering: Create subsets via #subset

Structure

Internally maintains two structures:

  • @score: Hash mapping time → Array of events
  • @indexer: Array of { start, finish, dataset } for interval queries

Event Requirements

Events must:

  • Extend Abs (absolute values, not deltas)
  • Have a :duration key (from AbsD)

Time Representation

All times are stored as Rational numbers for exact arithmetic:

score.at(0r, add: event)    # At time 0
score.at(1/4r, add: event)  # a quarter of a bar in

Examples:

Create empty score

score = Musa::Datasets::Score.new

Create from hash

score = Score.new({
  0r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
  1r => [{ pitch: 64, duration: 1.0 }.extend(PDV)]
})

Add events

score = Score.new
gdv1 = { grade: 0, duration: 1.0 }.extend(GDV)
gdv2 = { grade: 2, duration: 1.0 }.extend(GDV)
score.at(0r, add: gdv1)
score.at(1r, add: gdv2)

Query time interval

score = Score.new({ 0r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
                    1r => [{ pitch: 64, duration: 1.0 }.extend(PDV)] })

events = score.between(0r, 2r)
events.map { |e| e[:dataset][:pitch] }  # => [60, 64]

# What comes back is not the dataset but a reading of it against the
# interval asked for:
events.first
# => { start: (0/1), finish: (1/1),
#      start_in_interval: (0/1), finish_in_interval: (1/1),
#      dataset: { pitch: 60, duration: 1.0 } }

Filter events

pitched = Score.new({ 0r => [{ pitch: 60, duration: 1r }.extend(PDV)],
                      1r => [{ pitch: 64, duration: 1r }.extend(PDV)] })
high_notes = pitched.subset { |event| event[:pitch] > 60 }
high_notes.positions  # => [1r]

Get all positions

score.positions  # => [0r, 1r, 2r, ...]

Get duration

score.duration  # => Latest finish time - 1r

See Also:

Defined Under Namespace

Modules: Queriable, Render, ToMXML

Constant Summary collapse

NaturalKeys =
NaturalKeys.freeze

Instance Method Summary collapse

Constructor Details

#initialize(hash = nil) ⇒ Score

Creates new score.

Examples:

Empty score

score = Score.new

With initial events

score = Score.new({
  0r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
  1r => [{ pitch: 64, duration: 1.0 }.extend(PDV)]
})

Parameters:

  • hash (Hash{Rational => Array<Abs>}, nil) (defaults to: nil)

    optional initial events Hash mapping times to arrays of events

Raises:

  • (ArgumentError)

    if hash values aren't Arrays



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/musa-dsl/datasets/score.rb', line 122

def initialize(hash = nil)
  raise ArgumentError, "'hash' parameter should be a Hash with time and events information" unless hash.nil? || hash.is_a?(Hash)

  @score = {}
  @indexer = []

  if hash
    hash.sort.each do |k, v|
      raise ArgumentError, "'hash' values for time #{k} should be an Array of events" unless v.is_a?(Array)

      v.each do |vv|
        at(k, add: vv)
      end
    end
  end
end

Instance Method Details

#at(time, add: nil) ⇒ Array<Abs>?

Adds event at time or gets time slot.

Without add parameter, returns array of events at that time. With add parameter, adds event to that time slot.

Examples:

Add event

gdv = { grade: 0, duration: 1.0 }.extend(GDV)
score.at(0r, add: gdv)

Get time slot

events = score.at(0r)  # => Array of events at time 0

Multiple events at same time (chord)

chord = Score.new
chord.at(0r, add: { pitch: 60, duration: 1.0 }.extend(PDV))
chord.at(0r, add: { pitch: 64, duration: 1.0 }.extend(PDV))
chord.at(0r).size  # => 2

Parameters:

  • time (Numeric)

    time position (converted to Rational)

  • add (Abs, nil) (defaults to: nil)

    event to add (must extend Abs and have :duration)

Returns:

  • (Array<Abs>, nil)

    time slot if no add, nil if adding

Raises:

  • (ArgumentError)

    if add is not an Abs dataset



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/musa-dsl/datasets/score.rb', line 215

def at(time, add: nil)
  time = time.rationalize

  if add
    raise ArgumentError, "#{add} is not a Abs dataset" unless add&.is_a?(Musa::Datasets::Abs)

    slot = @score[time] ||= [].extend(QueryableByTimeSlot)

    slot << add

    @indexer << { start: time,
                  finish: time + add.duration.rationalize,
                  dataset: add }

    nil
  else
    @score[time] ||= [].extend(QueryableByTimeSlot)
  end
end

#between(closed_interval_start, open_interval_finish) ⇒ Array<Hash>

Queries events overlapping time interval.

Returns events that are active (playing) during the interval [start, finish). Interval uses closed start (included) and open finish (excluded).

Events are included if they:

  • Start before interval finish AND finish after interval start
  • OR are instant events (start == finish) at interval instant

Examples:

Query bar

score = Score.new({ 0r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
                    1r => [{ pitch: 64, duration: 1.0 }.extend(PDV)],
                    3r => [{ pitch: 67, duration: 1.0 }.extend(PDV)] })

score.between(0r, 4r).size  # => 3

Long note spans interval

score = Score.new
score.at(0r, add: { duration: 10.0 }.extend(AbsD))

score.between(2r, 4r)
# => [{ start: (0/1), finish: (10/1),
#       start_in_interval: (2/1), finish_in_interval: (4/1),
#       dataset: { duration: 10.0 } }]

# The event is included although it starts outside the interval, and the
# two _in_interval keys are the part of it that falls inside. That is the
# difference between asking "what begins here" and "what sounds here".

Parameters:

  • closed_interval_start (Rational)

    interval start (included)

  • open_interval_finish (Rational)

    interval finish (excluded)

Returns:

  • (Array<Hash>)

    array of event info hashes with:

    • :start: Event start time
    • :finish: Event finish time
    • :start_in_interval: Effective start within interval
    • :finish_in_interval: Effective finish within interval
    • :dataset: The event dataset


338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/musa-dsl/datasets/score.rb', line 338

def between(closed_interval_start, open_interval_finish)
  @indexer
    .select { |i| i[:start] < open_interval_finish && i[:finish] > closed_interval_start ||
                  closed_interval_start == open_interval_finish &&
                      i[:start] == i[:finish] &&
                      i[:start] == closed_interval_start }
    .sort_by { |i| i[:start] }
    .collect { |i| { start: i[:start],
                     finish: i[:finish],
                     start_in_interval: i[:start] > closed_interval_start ? i[:start] : closed_interval_start,
                     finish_in_interval: i[:finish] < open_interval_finish ? i[:finish] : open_interval_finish,
                     dataset: i[:dataset] } }.extend(QueryableByDataset)
end

#changes_between(closed_interval_start, open_interval_finish) ⇒ Array<Hash>

Queries start/finish change events in interval.

Returns timeline of note-on/note-off style events for the interval. Useful for real-time rendering or event-based processing.

Returns events sorted by time, with :finish events before :start events at the same time (to avoid gaps).

Examples:

Get all changes in bar

score = Score.new({ 1r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
                    2r => [{ pitch: 64, duration: 1.0 }.extend(PDV)] })

changes = score.changes_between(0r, 4r)
changes.map { |c| [c[:time], c[:change]] }
# => [[(1/1), :start], [(2/1), :finish], [(2/1), :start], [(3/1), :finish]]

# At 2 the :finish comes BEFORE the :start. Two notes meeting end to end
# would otherwise open the second before closing the first, and whatever
# is listening would hear one note where there are two.

changes.each do |change|
  case change[:change]
  when :start
    puts "Note ON at #{change[:time]}"
  when :finish
    puts "Note OFF at #{change[:time]}"
  end
end

Parameters:

  • closed_interval_start (Rational)

    interval start (included)

  • open_interval_finish (Rational)

    interval finish (excluded)

Returns:

  • (Array<Hash>)

    array of change event hashes with:

    • :change: :start or :finish
    • :time: When change occurs
    • :start: Event start time
    • :finish: Event finish time
    • :start_in_interval: Effective start within interval
    • :finish_in_interval: Effective finish within interval
    • :time_in_interval: Effective change time within interval
    • :dataset: The event dataset


395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/musa-dsl/datasets/score.rb', line 395

def changes_between(closed_interval_start, open_interval_finish)
  (
    #
    # we have a start event if the element
    # begins between queried interval start (included) and interval finish (excluded)
    #
    @indexer
      .select { |i| i[:start] >= closed_interval_start && i[:start] < open_interval_finish }
      .collect { |i| i.clone.merge({ change: :start, time: i[:start] }) } +

    #
    # we have a finish event if the element interval finishes
    # between queried interval start (excluded) and queried interval finish (included) or
    # element interval finishes exactly on queried interval start
    # but the element interval started before queried interval start
    # (the element is not an instant)
    #
    @indexer
      .select { |i| ( i[:finish] > closed_interval_start ||
                      i[:finish] == closed_interval_start && i[:finish] == i[:start])   &&
                    ( i[:finish] < open_interval_finish ||
                      i[:finish] == open_interval_finish && i[:start] < open_interval_finish) }
      .collect { |i| i.clone.merge({ change: :finish, time: i[:finish] }) } +

    #
    # when the queried interval has no duration (it's an instant) we have a start and a finish event
    # if the element also is an instant exactly coincident with the queried interval
    #
    @indexer
      .select { |i| ( closed_interval_start == open_interval_finish &&
                      i[:start] == closed_interval_start &&
                      i[:finish] == open_interval_finish) }
      .collect { |i| [i.clone.merge({ change: :start, time: i[:start] }),
                      i.clone.merge({ change: :finish, time: i[:finish] })] }
      .flatten(1)
  )
    .sort_by { |i| [ i[:time],
                     i[:start] < i[:finish] && i[:change] == :finish ? 0 : 1] }
    .collect { |i| { change: i[:change],
                     time: i[:time],
                     start: i[:start],
                     finish: i[:finish],
                     start_in_interval: i[:start] > closed_interval_start ? i[:start] : closed_interval_start,
                     finish_in_interval: i[:finish] < open_interval_finish ? i[:finish] : open_interval_finish,
                     time_in_interval: if i[:time] < closed_interval_start
                                         closed_interval_start
                                       elsif i[:time] > open_interval_finish
                                         open_interval_finish
                                       else
                                         i[:time]
                                       end,
                     dataset: i[:dataset] } }.extend(QueryableByDataset)
end

#durationRational

Returns total duration of score.

Calculated as finish time minus 1.

Examples:

Duration calculation

score.at(0r, add: { duration: 2.0 }.extend(AbsD))
score.duration  # => 1r (finish 2r - 1r)

Returns:



187
188
189
# File 'lib/musa-dsl/datasets/score.rb', line 187

def duration
  (finish || 1r) - 1r
end

#each {|time, events| ... } ⇒ void

This method returns an undefined value.

Iterates over time slots in order.

Yields [time, events] pairs sorted by time. Implements Enumerable.

Examples:

Iterate over time slots

score = Score.new({ 0r => [{ pitch: 60, duration: 1.0 }.extend(PDV)],
                    1r => [{ pitch: 64, duration: 1.0 }.extend(PDV)] })

score.each do |time, events|
  puts "At #{time}: #{events.size} event(s)"
end

score.map { |time, events| [time, events.size] }  # => [[(0/1), 1], [(1/1), 1]]

# Sorted by time, whatever order they were added in: `each` sorts, and
# everything Enumerable gives the class comes through it.

Yield Parameters:



285
286
287
# File 'lib/musa-dsl/datasets/score.rb', line 285

def each(&block)
  @score.sort.each(&block)
end

#finishRational?

Returns latest finish time of all events.

Examples:

Finish time

score.at(0r, add: { duration: 2.0 }.extend(AbsD))
score.finish  # => 2r

Returns:

  • (Rational, nil)

    latest finish time, or nil if score is empty



174
175
176
# File 'lib/musa-dsl/datasets/score.rb', line 174

def finish
  @indexer.collect { |i| i[:finish] }.max
end

#forward_durationNumeric Originally defined in module AbsD

Returns forward duration (time until next event).

Defaults to :duration if :forward_duration not specified. This is the value play waits on in :wait mode, so it is what advances a serie.

Examples:

{ pitch: 60, duration: 1.0 }.extend(AbsD).forward_duration  # => 1.0

The fallback runs one way only

{ forward_duration: 1/2r }.extend(AbsD).forward_duration  # => (1/2)
{ forward_duration: 1/2r }.extend(AbsD).duration          # => nil

Returns:

  • (Numeric)

    forward duration

#get(key) ⇒ Object? Also known as: []

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Gets attribute value.

Supports accessing natural keys like :duration, :finish.

Parameters:

  • key (Symbol)

    attribute name

Returns:

  • (Object, nil)

    attribute value



159
160
161
162
163
# File 'lib/musa-dsl/datasets/score.rb', line 159

def get(key)
  if NaturalKeys.include?(key) && self.respond_to?(key)
    self.send(key)
  end
end

#inspectString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns formatted string representation.

Produces multiline representation suitable for inspection.

Returns:

  • (String)

    formatted score representation



536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/musa-dsl/datasets/score.rb', line 536

def inspect
  s = StringIO.new

  first_level1 = true

  s.write "Musa::Datasets::Score.new({\n"

  @score.each do |k, v|
    s.write "#{ ", \n" unless first_level1 }  #{ k.inspect } => [\n"
    first_level1 = false
    first_level2 = true

    v.each do |vv|
      s.write "#{ ", \n" unless first_level2 }\t#{ vv }"
      first_level2 = false
    end

    s.write  " ]"
  end
  s.write "\n})"

  s.string
end

#note_durationNumeric Originally defined in module AbsD

Returns actual note duration.

Defaults to :duration if :note_duration not specified.

Examples:

{ pitch: 60, duration: 1.0, note_duration: 0.5 }.extend(AbsD).note_duration  # => 0.5

Returns:

  • (Numeric)

    note duration

#positionsArray<Rational>

Returns all time positions sorted.

Examples:

Positions sorted

placed = Score.new
placed.at(1r, add: { pitch: 60, duration: 1r }.extend(PDV))
placed.at(0r, add: { pitch: 64, duration: 1r }.extend(PDV))
placed.positions  # => [0r, 1r]

Returns:



259
260
261
# File 'lib/musa-dsl/datasets/score.rb', line 259

def positions
  @score.keys.sort
end

#render(on:) {|event| ... } ⇒ nil Originally defined in module Render

Renders score on sequencer.

Schedules all events in the score on the sequencer, calling the block for each event at its scheduled time. Score times are converted to sequencer wait times (score_time - 1).

Supports nested scores recursively.

Examples:

MIDI output

require 'midi-communications'

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 1.0, velocity: 64 }.extend(Musa::Datasets::PDV))

midi_out = MIDICommunications::Output.gets
sequencer = Musa::Sequencer::Sequencer.new(4, 24)

score.render(on: sequencer) do |event|
  if event[:pitch]
    midi_out.puts(0x90, event[:pitch], event[:velocity] || 64)
    sequencer.at event[:duration] do
      midi_out.puts(0x80, event[:pitch], event[:velocity] || 64)
    end
  end
end

sequencer.run

Console output

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))

seq = Musa::Sequencer::Sequencer.new(4, 24)
score.render(on: seq) do |event|
  puts "Time #{seq.position}: #{event.inspect}"
end
seq.run
# => "Time 95/96: {pitch: 60, duration: 1.0}"

Nested score rendering

inner = Musa::Datasets::Score.new
inner.at(1r, add: { pitch: 67 }.extend(Musa::Datasets::PDV))

outer = Musa::Datasets::Score.new
outer.at(1r, add: { pitch: 60 }.extend(Musa::Datasets::PDV))
outer.at(2r, add: inner)

seq = Musa::Sequencer::Sequencer.new(4, 24)
outer.render(on: seq) do |event|
  puts "Event: #{event[:pitch]}"
end
seq.run
# Inner scores automatically rendered at their scheduled times

Parameters:

Yield Parameters:

  • event (Abs)

    each event to process Block is called at the scheduled time with the event dataset

Returns:

  • (nil)

Raises:

  • (ArgumentError)

    if element is not Abs or Score

#resetvoid

This method returns an undefined value.

Clears all events from score.

Examples:

Reset score

score.reset
score.size  # => 0


146
147
148
149
# File 'lib/musa-dsl/datasets/score.rb', line 146

def reset
  @score.clear
  @indexer.clear
end

#sizeInteger

Returns number of time positions.

Examples:

Size counting

sized = Score.new
sized.at(0r, add: { pitch: 60, duration: 1r }.extend(PDV))
sized.at(0r, add: { pitch: 64, duration: 1r }.extend(PDV))  # Same time
sized.at(1r, add: { pitch: 67, duration: 1r }.extend(PDV))  # Different time
sized.size  # => 2
# Two time positions, not three events.

Returns:

  • (Integer)

    number of distinct time positions



246
247
248
# File 'lib/musa-dsl/datasets/score.rb', line 246

def size
  @score.keys.size
end

#subset {|dataset| ... } ⇒ Score

Creates filtered subset of score.

Returns new Score containing only events matching the condition.

Examples:

Filter by pitch

score = Score.new({ 0r => [{ pitch: 60, duration: 1r }.extend(PDV),
                           { pitch: 64, duration: 1r, staccato: true }.extend(PDV)],
                    1r => [{ pitch: 67, duration: 1r }.extend(PDV)] })

high_notes = score.subset { |event| event[:pitch] > 60 }

high_notes.class  # => Musa::Datasets::Score
high_notes.map { |time, events| [time, events.map { |e| e[:pitch] }] }
# => [[(0/1), [64]], [(1/1), [67]]]

# A Score and not an array of events: the times survive the filter, so
# what comes back can be rendered, queried and filtered again. A slot
# left with nothing in it is dropped rather than kept empty.

Filter by attribute presence

staccato_notes = score.subset { |event| event[:staccato] }
staccato_notes.map { |time, events| [time, events.map { |e| e[:pitch] }] }
# => [[(0/1), [64]]]

Filter by grade

tonic_notes = score.subset { |event| event[:grade] == 0 }
tonic_notes.size  # => 0

# Nothing raises for a key these events do not have: `nil == 0` is
# simply false. A subset over the wrong dataset kind comes back empty,
# not broken.

Yield Parameters:

  • dataset (Abs)

    each event dataset

Yield Returns:

  • (Boolean)

    true to include event

Returns:

  • (Score)

    new filtered score

Raises:

  • (ArgumentError)

    if no block given



515
516
517
518
519
520
521
522
523
524
525
526
527
# File 'lib/musa-dsl/datasets/score.rb', line 515

def subset
  raise ArgumentError, "subset needs a block with the inclusion condition on the dataset" unless block_given?

  filtered_score = Score.new

  @score.each_pair do |time, datasets|
    datasets.each do |dataset|
      filtered_score.at time, add: dataset if yield(dataset)
    end
  end

  filtered_score
end

#to_hHash{Rational => Array<Abs>}

Converts to hash representation.

Examples:

Convert to hash

hash = score.to_h
# => { 0r => [event1, event2], 1r => [event3] }

Returns:



296
297
298
# File 'lib/musa-dsl/datasets/score.rb', line 296

def to_h
  @score.sort.to_h
end

#to_mxml(beats_per_bar, ticks_per_beat, beat_type: nil, bpm: nil, title: nil, creators: nil, encoding_date: nil, parts:, logger: nil, do_log: nil) ⇒ Musa::MusicXML::Builder::ScorePartwise Originally defined in module ToMXML

Converts score to MusicXML.

Creates complete MusicXML document with metadata, parts, measures, notes, rests, and dynamics markings.

Examples:

Simple piano score

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))

mxml = score.to_mxml(
  4, 24,
  bpm: 120,
  title: 'Invention',
  creators: { composer: 'J.S. Bach' },
  parts: { piano: { name: 'Piano', clefs: { g: 2, f: 4 } } }
)

String quartet

score = Musa::Datasets::Score.new
score.at(1r, add: { instrument: :vln1, pitch: 67, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { instrument: :vln2, pitch: 64, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { instrument: :vla, pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))
score.at(1r, add: { instrument: :vc, pitch: 48, duration: 1.0 }.extend(Musa::Datasets::PDV))

mxml = score.to_mxml(
  4, 24,
  parts: {
    vln1: { name: 'Violin I', abbreviation: 'Vln. I', clefs: { g: 2 } },
    vln2: { name: 'Violin II', abbreviation: 'Vln. II', clefs: { g: 2 } },
    vla: { name: 'Viola', abbreviation: 'Vla.', clefs: { c: 3 } },
    vc: { name: 'Cello', abbreviation: 'Vc.', clefs: { f: 4 } }
  }
)

Export to file

score = Musa::Datasets::Score.new
score.at(1r, add: { pitch: 60, duration: 1.0 }.extend(Musa::Datasets::PDV))

mxml = score.to_mxml(4, 24, parts: { piano: { name: 'Piano' } })
File.write('output.musicxml', mxml.to_xml.string)

Parameters:

  • beats_per_bar (Integer)

    time signature numerator (e.g., 4 for 4/4)

  • ticks_per_beat (Integer)

    resolution per beat (typically 24)

  • beat_type (Integer) (defaults to: nil)

    time signature denominator: what figure the beat is (4 for a quarter, 8 for an eighth). Defaults to 4. The sequencer does not know this and does not need to -- it counts bars and ticks -- but the notation cannot be written without it: it is what makes 6/8 a 6/8 and not a 6/4, and what names every figure.

  • bpm (Integer) (defaults to: nil)

    tempo in beats per minute (default: 90)

  • title (String) (defaults to: nil)

    work title (default: 'Untitled')

  • creators (Hash{Symbol => String}) (defaults to: nil)

    creator roles and names (default: { composer: 'Unknown' })

  • encoding_date (DateTime, nil) (defaults to: nil)

    encoding date for metadata

  • parts (Hash{Symbol => Hash})

    part definitions Each part: { name: String, abbreviation: String, clefs: Hash } Clefs: { clef_sign: line_number } (e.g., { g: 2, f: 4 } for piano)

  • logger (Musa::Logger::Logger, nil) (defaults to: nil)

    logger for debugging

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

    enable logging output

Returns:

Raises:

  • (ArgumentError)

#valid?Boolean Originally defined in module E

Checks if event is valid.

Base implementation always returns true. Subclasses should override to implement specific validation logic.

Examples:

event.valid?  # => true

Returns:

  • (Boolean)

    true if valid

#validate!void Originally defined in module E

This method returns an undefined value.

Validates event, raising if invalid.

Examples:

event.validate!  # Raises if invalid

Raises:

  • (RuntimeError)

    if event is not valid

#values_of(attribute) ⇒ Set

Collects all values for an attribute.

Returns set of all unique values across all events.

Examples:

Get all pitches

score = Score.new({ 0r => [{ pitch: 60, duration: 1r }.extend(PDV)],
                    1r => [{ pitch: 64, duration: 1r }.extend(PDV)] })

score.values_of(:pitch)  # => #<Set: {60, 64}>

An attribute the events do not carry

score.values_of(:grade)  # => #<Set: {nil}>

# A set holding nil, not an empty set: every event contributes its value
# for the attribute and a missing one is nil. Asking a score of PDVs for
# grades answers "all of them have none" rather than "there are none".

Parameters:

  • attribute (Symbol)

    attribute key

Returns:

  • (Set)

    set of unique values



469
470
471
472
473
474
475
# File 'lib/musa-dsl/datasets/score.rb', line 469

def values_of(attribute)
  values = Set[]
  @score.each_value do |slot|
    slot.each { |dataset| values << dataset[attribute] }
  end
  values
end