Class: HeadMusic::Content::Composition

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/content/composition.rb

Overview

A composition is musical content.

Defined Under Namespace

Classes: HashDeserializer

Constant Summary collapse

SCHEMA_VERSION =
1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name: nil, key_signature: nil, meter: nil, composer: nil, origin: nil, comments: nil) ⇒ Composition

Returns a new instance of Composition.



18
19
20
21
22
23
24
# File 'lib/head_music/content/composition.rb', line 18

def initialize(name: nil, key_signature: nil, meter: nil, composer: nil, origin: nil, comments: nil)
  ensure_attributes(name, key_signature, meter)
  @composer = composer
  @origin = origin
  @voices = []
  @comments = Array(comments).map { |text| HeadMusic::Content::Comment.new(self, text) }
end

Instance Attribute Details

#commentsObject (readonly)

Returns the value of attribute comments.



8
9
10
# File 'lib/head_music/content/composition.rb', line 8

def comments
  @comments
end

#composerObject (readonly)

Returns the value of attribute composer.



8
9
10
# File 'lib/head_music/content/composition.rb', line 8

def composer
  @composer
end

#key_signatureObject (readonly)

Returns the value of attribute key_signature.



8
9
10
# File 'lib/head_music/content/composition.rb', line 8

def key_signature
  @key_signature
end

#meterObject (readonly)

Returns the value of attribute meter.



8
9
10
# File 'lib/head_music/content/composition.rb', line 8

def meter
  @meter
end

#nameObject (readonly)

Returns the value of attribute name.



8
9
10
# File 'lib/head_music/content/composition.rb', line 8

def name
  @name
end

#originObject (readonly)

Returns the value of attribute origin.



8
9
10
# File 'lib/head_music/content/composition.rb', line 8

def origin
  @origin
end

#voicesObject (readonly)

Returns the value of attribute voices.



8
9
10
# File 'lib/head_music/content/composition.rb', line 8

def voices
  @voices
end

Class Method Details

.from_h(hash) ⇒ Object



10
11
12
# File 'lib/head_music/content/composition.rb', line 10

def self.from_h(hash)
  HashDeserializer.new(hash).composition
end

.from_json(json) ⇒ Object



14
15
16
# File 'lib/head_music/content/composition.rb', line 14

def self.from_json(json)
  from_h(JSON.parse(json))
end

Instance Method Details

#add_comment(text, position = nil) ⇒ Object



31
32
33
34
# File 'lib/head_music/content/composition.rb', line 31

def add_comment(text, position = nil)
  @comments << HeadMusic::Content::Comment.new(self, text, position)
  @comments.last
end

#add_voice(role: nil) ⇒ Object



26
27
28
29
# File 'lib/head_music/content/composition.rb', line 26

def add_voice(role: nil)
  @voices << HeadMusic::Content::Voice.new(composition: self, role: role)
  @voices.last
end

#bars(last = latest_bar_number) ⇒ Object



46
47
48
49
50
51
52
53
# File 'lib/head_music/content/composition.rb', line 46

def bars(last = latest_bar_number)
  @bars ||= []
  first = [earliest_bar_number, last].min
  (first..last).each do |bar_number|
    @bars[bar_number] ||= HeadMusic::Content::Bar.new(self)
  end
  @bars[first..last]
end

#bars_to_hObject (private)

Iterates the raw sparse array (not the public #bars slice, which loses the number offset), pairing each non-default bar with its number.



135
136
137
138
139
140
141
142
143
144
# File 'lib/head_music/content/composition.rb', line 135

def bars_to_h
  (@bars || []).each_with_index.filter_map do |bar, number|
    next if bar.nil?

    bar_hash = bar.to_h
    next if bar_hash.empty?

    {"number" => number}.merge(bar_hash)
  end
end

#cantus_firmus_voiceObject



71
72
73
# File 'lib/head_music/content/composition.rb', line 71

def cantus_firmus_voice
  voices.detect(&:cantus_firmus?)
end

#change_key_signature(bar_number, key_signature) ⇒ Object



55
56
57
# File 'lib/head_music/content/composition.rb', line 55

def change_key_signature(bar_number, key_signature)
  bars(bar_number).last.key_signature = key_signature
end

#change_meter(bar_number, meter) ⇒ Object



59
60
61
# File 'lib/head_music/content/composition.rb', line 59

def change_meter(bar_number, meter)
  bars(bar_number).last.meter = meter
end

#counterpoint_voiceObject



75
76
77
# File 'lib/head_music/content/composition.rb', line 75

def counterpoint_voice
  voices.reject(&:cantus_firmus?).first
end

#earliest_bar_numberObject



63
64
65
# File 'lib/head_music/content/composition.rb', line 63

def earliest_bar_number
  [voices.map(&:earliest_bar_number), first_allocated_bar_number, 1].flatten.compact.min
end

#ensure_attributes(name, key_signature, meter) ⇒ Object (private)



117
118
119
120
121
122
# File 'lib/head_music/content/composition.rb', line 117

def ensure_attributes(name, key_signature, meter)
  @name = name || "Composition"
  @key_signature = HeadMusic::Rudiment::KeySignature.get(key_signature) if key_signature
  @key_signature ||= HeadMusic::Rudiment::KeySignature.default
  @meter = meter ? HeadMusic::Rudiment::Meter.get(meter) : HeadMusic::Rudiment::Meter.default
end

#first_allocated_bar_numberObject (private)

Bars can be allocated below the voices’ earliest bar (e.g. a key or meter change in a pickup bar), so the earliest bar reflects those allocations too.



113
114
115
# File 'lib/head_music/content/composition.rb', line 113

def first_allocated_bar_number
  (@bars || []).index { |bar| !bar.nil? }
end

#key_signature_at(bar_number) ⇒ Object



41
42
43
44
# File 'lib/head_music/content/composition.rb', line 41

def key_signature_at(bar_number)
  key_signature_change = last_key_signature_change(bar_number)
  key_signature_change ? key_signature_change.key_signature : key_signature
end

#last_key_signature_change(bar_number) ⇒ Object (private)



129
130
131
# File 'lib/head_music/content/composition.rb', line 129

def last_key_signature_change(bar_number)
  bars(bar_number)[earliest_bar_number..bar_number].reverse.detect(&:key_signature)
end

#last_meter_change(bar_number) ⇒ Object (private)



124
125
126
127
# File 'lib/head_music/content/composition.rb', line 124

def last_meter_change(bar_number)
  bar_number = [bar_number, earliest_bar_number].max
  bars(bar_number)[earliest_bar_number..bar_number].reverse.detect(&:meter)
end

#latest_bar_numberObject



67
68
69
# File 'lib/head_music/content/composition.rb', line 67

def latest_bar_number
  [voices.map(&:latest_bar_number), 1].flatten.max
end

#meter_at(bar_number) ⇒ Object



36
37
38
39
# File 'lib/head_music/content/composition.rb', line 36

def meter_at(bar_number)
  meter_change = last_meter_change(bar_number)
  meter_change ? meter_change.meter : meter
end

#to_abc(**options) ⇒ Object



83
84
85
# File 'lib/head_music/content/composition.rb', line 83

def to_abc(**options)
  HeadMusic::Notation::ABC.render(self, **options)
end

#to_hObject



91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/head_music/content/composition.rb', line 91

def to_h
  {
    "schema_version" => SCHEMA_VERSION,
    "name" => name,
    "key_signature" => key_signature.name,
    "meter" => meter.to_s,
    "composer" => composer&.to_s,
    "origin" => origin&.to_s,
    "voices" => voices.map(&:to_h),
    "bars" => bars_to_h,
    "comments" => comments.map(&:to_h)
  }
end

#to_json(*_args) ⇒ Object



105
106
107
# File 'lib/head_music/content/composition.rb', line 105

def to_json(*_args)
  to_h.to_json
end

#to_musicxmlObject



87
88
89
# File 'lib/head_music/content/composition.rb', line 87

def to_musicxml
  HeadMusic::Notation::MusicXML.render(self)
end

#to_sObject



79
80
81
# File 'lib/head_music/content/composition.rb', line 79

def to_s
  "#{name}#{voices.count} #{(voices.count == 1) ? "voice" : "voices"}"
end