Class: Musa::Datasets::Score
- 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
Overview
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) # At quarter note
Defined Under Namespace
Modules: Queriable, Render, ToMXML
Constant Summary collapse
- NaturalKeys =
NaturalKeys.freeze
Instance Method Summary collapse
-
#[](key) ⇒ Object?
private
Gets attribute value.
-
#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).
-
#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, 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.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/musa-dsl/datasets/score.rb', line 109 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
#[](key) ⇒ Object?
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.
146 147 148 149 150 |
# File 'lib/musa-dsl/datasets/score.rb', line 146 def [](key) if NaturalKeys.include?(key) && self.respond_to?(key) self.send(key) end end |
#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.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/musa-dsl/datasets/score.rb', line 199 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
301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/musa-dsl/datasets/score.rb', line 301 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).
348 349 350 351 352 353 354 355 356 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 |
# File 'lib/musa-dsl/datasets/score.rb', line 348 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.
172 173 174 |
# File 'lib/musa-dsl/datasets/score.rb', line 172 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.
258 259 260 |
# File 'lib/musa-dsl/datasets/score.rb', line 258 def each(&block) @score.sort.each(&block) end |
#finish ⇒ Rational?
Returns latest finish time of all events.
159 160 161 |
# File 'lib/musa-dsl/datasets/score.rb', line 159 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.
#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.
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 |
# File 'lib/musa-dsl/datasets/score.rb', line 465 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.
240 241 242 |
# File 'lib/musa-dsl/datasets/score.rb', line 240 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.
133 134 135 136 |
# File 'lib/musa-dsl/datasets/score.rb', line 133 def reset @score.clear @indexer.clear end |
#size ⇒ Integer
Returns number of time positions.
228 229 230 |
# File 'lib/musa-dsl/datasets/score.rb', line 228 def size @score.keys.size end |
#subset {|dataset| ... } ⇒ Score
Creates filtered subset of score.
Returns new Score containing only events matching the condition.
444 445 446 447 448 449 450 451 452 453 454 455 456 |
# File 'lib/musa-dsl/datasets/score.rb', line 444 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.
269 270 271 |
# File 'lib/musa-dsl/datasets/score.rb', line 269 def to_h @score.sort.to_h end |
#to_mxml(beats_per_bar, ticks_per_beat, 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.
417 418 419 420 421 422 423 |
# File 'lib/musa-dsl/datasets/score.rb', line 417 def values_of(attribute) values = Set[] @score.each_value do |slot| slot.each { |dataset| values << dataset[attribute] } end values end |