Class: Musa::Datasets::Score Private
- 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:
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
Defined Under Namespace
Modules: Queriable, Render, ToMXML
Constant Summary collapse
- NaturalKeys =
NaturalKeys.freeze
Instance Method Summary collapse
-
#at(time, add: nil) ⇒ Array<Abs>?
Adds event at time or gets time slot.
-
#between(closed_interval_start, open_interval_finish) ⇒ Array<Hash>
Queries events overlapping time interval.
-
#changes_between(closed_interval_start, open_interval_finish) ⇒ Array<Hash>
Queries start/finish change events in interval.
-
#duration ⇒ Rational
Returns total duration of score.
-
#each {|time, events| ... } ⇒ void
Iterates over time slots in order.
-
#finish ⇒ Rational?
Returns latest finish time of all events.
-
#forward_duration ⇒ Numeric
included
from AbsD
Returns forward duration (time until next event).
-
#get(key) ⇒ Object?
(also: #[])
private
Gets attribute value.
-
#initialize(hash = nil) ⇒ Score
constructor
Creates new score.
-
#inspect ⇒ String
private
Returns formatted string representation.
-
#note_duration ⇒ Numeric
included
from AbsD
Returns actual note duration.
-
#positions ⇒ Array<Rational>
Returns all time positions sorted.
-
#render(on:) {|event| ... } ⇒ nil
included
from Render
Renders score on sequencer.
-
#reset ⇒ void
Clears all events from score.
-
#size ⇒ Integer
Returns number of time positions.
-
#subset {|dataset| ... } ⇒ Score
Creates filtered subset of score.
-
#to_h ⇒ Hash{Rational => Array<Abs>}
Converts to hash representation.
-
#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
included
from ToMXML
Converts score to MusicXML.
-
#valid? ⇒ Boolean
included
from E
Checks if event is valid.
-
#validate! ⇒ void
included
from E
Validates event, raising if invalid.
-
#values_of(attribute) ⇒ Set
Collects all values for an attribute.
Constructor Details
#initialize(hash = nil) ⇒ Score
Creates new score.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/musa-dsl/datasets/score.rb', line 112 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.
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/musa-dsl/datasets/score.rb', line 205 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
310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/musa-dsl/datasets/score.rb', line 310 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).
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/musa-dsl/datasets/score.rb', line 357 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 |
#duration ⇒ Rational
Returns total duration of score.
Calculated as finish time minus 1.
177 178 179 |
# File 'lib/musa-dsl/datasets/score.rb', line 177 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.
267 268 269 |
# File 'lib/musa-dsl/datasets/score.rb', line 267 def each(&block) @score.sort.each(&block) end |
#finish ⇒ Rational?
Returns latest finish time of all events.
164 165 166 |
# File 'lib/musa-dsl/datasets/score.rb', line 164 def finish @indexer.collect { |i| i[:finish] }.max end |
#forward_duration ⇒ Numeric 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.
#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.
149 150 151 152 153 |
# File 'lib/musa-dsl/datasets/score.rb', line 149 def get(key) if NaturalKeys.include?(key) && self.respond_to?(key) self.send(key) end end |
#inspect ⇒ String
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.
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 |
# File 'lib/musa-dsl/datasets/score.rb', line 474 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_duration ⇒ Numeric Originally defined in module AbsD
Returns actual note duration.
Defaults to :duration if :note_duration not specified.
#positions ⇒ Array<Rational>
Returns all time positions sorted.
249 250 251 |
# File 'lib/musa-dsl/datasets/score.rb', line 249 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.
#reset ⇒ void
This method returns an undefined value.
Clears all events from score.
136 137 138 139 |
# File 'lib/musa-dsl/datasets/score.rb', line 136 def reset @score.clear @indexer.clear end |
#size ⇒ Integer
Returns number of time positions.
236 237 238 |
# File 'lib/musa-dsl/datasets/score.rb', line 236 def size @score.keys.size end |
#subset {|dataset| ... } ⇒ Score
Creates filtered subset of score.
Returns new Score containing only events matching the condition.
453 454 455 456 457 458 459 460 461 462 463 464 465 |
# File 'lib/musa-dsl/datasets/score.rb', line 453 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_h ⇒ Hash{Rational => Array<Abs>}
Converts to hash representation.
278 279 280 |
# File 'lib/musa-dsl/datasets/score.rb', line 278 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.
#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.
#validate! ⇒ void Originally defined in module E
This method returns an undefined value.
Validates event, raising if invalid.
#values_of(attribute) ⇒ Set
Collects all values for an attribute.
Returns set of all unique values across all events.
426 427 428 429 430 431 432 |
# File 'lib/musa-dsl/datasets/score.rb', line 426 def values_of(attribute) values = Set[] @score.each_value do |slot| slot.each { |dataset| values << dataset[attribute] } end values end |