Class: HeadMusic::Time::EventMap

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/time/event_map.rb

Overview

An ordered list of (position, value) events along a timeline, answering what value is in force at any position.

Everything that changes partway through a flow is this shape -- meter, tempo, key signature, instrument, staff system, clef, staff assignment -- so the mechanism is written once and composed into each typed map rather than inherited by it. The typed maps differ in their first-event policy: a meter map's first event cannot be removed, a staff-assignment map has no stored first event at all and answers a default. A constructor argument expresses that; a superclass would need a hook per policy.

Values are opaque. This class never interprets one, which is why the same class serves a Meter and a Clef.

Examples:

map = HeadMusic::Time::EventMap.new(default: "4/4")
map.add(HeadMusic::Time::MusicalPosition.new(5), "3/4")
map.at(HeadMusic::Time::MusicalPosition.new(7))        # => "3/4"
map.change_at(HeadMusic::Time::MusicalPosition.new(7)) # => nil

Defined Under Namespace

Classes: Event

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(default: nil, removable_first_event: true) ⇒ EventMap

Returns a new instance of EventMap.

Parameters:

  • default (Object, nil) (defaults to: nil) —

    the value in force before the first event

  • removable_first_event (Boolean) (defaults to: true) —

    whether #remove may drop the earliest event, or must preserve it as the map's opening value



48
49
50
51
52
# File 'lib/head_music/time/event_map.rb', line 48

def initialize(default: nil, removable_first_event: true)
  @default = default
  @removable_first_event = removable_first_event
  @events = []
end

Instance Attribute Details

#default ⇒ Object? (readonly)

Returns the value in force before the first event.

Returns:

  • (Object, nil) —

    the value in force before the first event



43
44
45
# File 'lib/head_music/time/event_map.rb', line 43

def default
  @default
end

#events ⇒ Array<Event> (readonly)

Returns the events, earliest first.

Returns:

  • (Array<Event>) —

    the events, earliest first



55
56
57
# File 'lib/head_music/time/event_map.rb', line 55

def events
  @events
end

Class Method Details

.tuple_for(position) ⇒ Array<Integer>

The sort key for a position. Lexical on the four components, which is the ordering MusicalPosition itself uses.

Returns:

  • (Array<Integer>)


161
162
163
# File 'lib/head_music/time/event_map.rb', line 161

def self.tuple_for(position)
  [position.bar, position.count, position.tick, position.subtick]
end

Instance Method Details

#add(position, value) ⇒ Event

Add or replace the event at a position.

Returns:

  • (Event) —

    the event now at that position



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/head_music/time/event_map.rb', line 70

def add(position, value)
  tuple = self.class.tuple_for(position)
  index = events.index { |event| event.at?(tuple) }
  event = Event.new(position, value, tuple)
  if index
    events[index] = event
  else
    events.insert(insertion_index(tuple), event)
  end
  event
end

#at(position) ⇒ Object?

The value in force at a position -- the most recent event at or before it, or the default when there is none.

A binary search rather than a reverse scan: every rendered note takes several map lookups, and position construction itself asks for a meter in a rollover loop.

Returns:

  • (Object, nil) —

    the value in force



110
111
112
# File 'lib/head_music/time/event_map.rb', line 110

def at(position)
  event_at(position)&.value || default
end

#change_at(position) ⇒ Event?

The event starting at a position, as distinct from the one in force there. A writer needs this to decide whether to print a change.

Returns:

  • (Event, nil) —

    the event exactly at this position



118
119
120
121
# File 'lib/head_music/time/event_map.rb', line 118

def change_at(position)
  tuple = self.class.tuple_for(position)
  events.find { |event| event.at?(tuple) }
end

#clear ⇒ self

Remove every event, except the first when the map protects it.

Returns:

  • (self)


97
98
99
100
# File 'lib/head_music/time/event_map.rb', line 97

def clear
  @events = @removable_first_event ? [] : events.first(1)
  self
end

#each_segment(from_position, to_position) {|start_position, end_position, value| ... } ⇒ Object

Yield each [start_position, end_position, value] span between two positions. Positions are yielded as given -- no normalization -- so a caller that needs normalized bounds normalizes them first.

Yields:

  • (start_position, end_position, value)


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/head_music/time/event_map.rb', line 138

def each_segment(from_position, to_position)
  from_tuple = self.class.tuple_for(from_position)
  to_tuple = self.class.tuple_for(to_position)

  current_position = from_position
  current_value = at(from_position)

  events.each do |event|
    next unless event.after?(from_tuple)
    break unless (event.tuple <=> to_tuple) < 0

    yield current_position, event.position, current_value
    current_position = event.position
    current_value = event.value
  end

  yield current_position, to_position, current_value
end

#empty? ⇒ Boolean

Returns true when no event has been added.

Returns:

  • (Boolean) —

    true when no event has been added



63
64
65
# File 'lib/head_music/time/event_map.rb', line 63

def empty?
  events.empty?
end

#event_at(position) ⇒ Event?

The most recent event at or before a position.

Returns:



126
127
128
129
130
131
# File 'lib/head_music/time/event_map.rb', line 126

def event_at(position)
  tuple = self.class.tuple_for(position)
  index = events.bsearch_index { |event| event.after?(tuple) }
  index = index ? index - 1 : events.length - 1
  events[index] if index >= 0
end

#insertion_index(tuple) ⇒ Object (private)



167
168
169
# File 'lib/head_music/time/event_map.rb', line 167

def insertion_index(tuple)
  events.bsearch_index { |event| event.after?(tuple) } || events.length
end

#remove(position) ⇒ Event?

Remove the event at a position, if there is one.

Returns:

  • (Event, nil) —

    the removed event



85
86
87
88
89
90
91
92
# File 'lib/head_music/time/event_map.rb', line 85

def remove(position)
  tuple = self.class.tuple_for(position)
  index = events.index { |event| event.at?(tuple) }
  return if index.nil?
  return if index.zero? && !@removable_first_event

  events.delete_at(index)
end

#values ⇒ Array<Object>

Returns the values, earliest first.

Returns:

  • (Array<Object>) —

    the values, earliest first



58
59
60
# File 'lib/head_music/time/event_map.rb', line 58

def values
  events.map(&:value)
end