musicality 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +65 -0
- data/bin/midify +78 -0
- data/examples/hip.rb +32 -0
- data/examples/missed_connection.rb +26 -0
- data/examples/song1.rb +33 -0
- data/examples/song2.rb +32 -0
- data/lib/musicality/errors.rb +9 -0
- data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
- data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
- data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
- data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
- data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
- data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
- data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
- data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
- data/lib/musicality/notation/model/articulations.rb +13 -0
- data/lib/musicality/notation/model/change.rb +62 -0
- data/lib/musicality/notation/model/dynamics.rb +12 -0
- data/lib/musicality/notation/model/link.rb +73 -0
- data/lib/musicality/notation/model/meter.rb +54 -0
- data/lib/musicality/notation/model/meters.rb +9 -0
- data/lib/musicality/notation/model/note.rb +120 -0
- data/lib/musicality/notation/model/part.rb +54 -0
- data/lib/musicality/notation/model/pitch.rb +163 -0
- data/lib/musicality/notation/model/pitches.rb +21 -0
- data/lib/musicality/notation/model/program.rb +53 -0
- data/lib/musicality/notation/model/score.rb +132 -0
- data/lib/musicality/notation/packing/change_packing.rb +46 -0
- data/lib/musicality/notation/packing/part_packing.rb +31 -0
- data/lib/musicality/notation/packing/program_packing.rb +16 -0
- data/lib/musicality/notation/packing/score_packing.rb +108 -0
- data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
- data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
- data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
- data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
- data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
- data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
- data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
- data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
- data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
- data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/note_node.rb +40 -0
- data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
- data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/parseable.rb +30 -0
- data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
- data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
- data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
- data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
- data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
- data/lib/musicality/notation/util/interpolation.rb +16 -0
- data/lib/musicality/notation/util/piecewise_function.rb +122 -0
- data/lib/musicality/notation/util/value_computer.rb +170 -0
- data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
- data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
- data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
- data/lib/musicality/performance/conversion/score_collator.rb +126 -0
- data/lib/musicality/performance/midi/midi_events.rb +34 -0
- data/lib/musicality/performance/midi/midi_util.rb +31 -0
- data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
- data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
- data/lib/musicality/performance/model/note_attacks.rb +19 -0
- data/lib/musicality/performance/model/note_sequence.rb +111 -0
- data/lib/musicality/performance/util/note_linker.rb +28 -0
- data/lib/musicality/performance/util/optimization.rb +31 -0
- data/lib/musicality/validatable.rb +38 -0
- data/lib/musicality/version.rb +3 -0
- data/lib/musicality.rb +81 -0
- data/musicality.gemspec +30 -0
- data/spec/musicality_spec.rb +7 -0
- data/spec/notation/conversion/change_conversion_spec.rb +40 -0
- data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
- data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
- data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
- data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
- data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
- data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
- data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
- data/spec/notation/model/change_spec.rb +90 -0
- data/spec/notation/model/link_spec.rb +83 -0
- data/spec/notation/model/meter_spec.rb +97 -0
- data/spec/notation/model/note_spec.rb +183 -0
- data/spec/notation/model/part_spec.rb +69 -0
- data/spec/notation/model/pitch_spec.rb +180 -0
- data/spec/notation/model/program_spec.rb +50 -0
- data/spec/notation/model/score_spec.rb +211 -0
- data/spec/notation/packing/change_packing_spec.rb +153 -0
- data/spec/notation/packing/part_packing_spec.rb +66 -0
- data/spec/notation/packing/program_packing_spec.rb +33 -0
- data/spec/notation/packing/score_packing_spec.rb +301 -0
- data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
- data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
- data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
- data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
- data/spec/notation/parsing/link_nodes_spec.rb +30 -0
- data/spec/notation/parsing/link_parsing_spec.rb +13 -0
- data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
- data/spec/notation/parsing/note_node_spec.rb +87 -0
- data/spec/notation/parsing/note_parsing_spec.rb +46 -0
- data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
- data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
- data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
- data/spec/notation/parsing/pitch_node_spec.rb +38 -0
- data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
- data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
- data/spec/notation/util/value_computer_spec.rb +146 -0
- data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
- data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
- data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
- data/spec/performance/conversion/score_collator_spec.rb +183 -0
- data/spec/performance/midi/midi_util_spec.rb +110 -0
- data/spec/performance/midi/part_sequencer_spec.rb +40 -0
- data/spec/performance/midi/score_sequencer_spec.rb +50 -0
- data/spec/performance/model/note_sequence_spec.rb +147 -0
- data/spec/performance/util/note_linker_spec.rb +68 -0
- data/spec/performance/util/optimization_spec.rb +73 -0
- data/spec/spec_helper.rb +43 -0
- metadata +323 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Tempo
|
4
|
+
class QNPM
|
5
|
+
def self.to_bpm qnpm, beat_dur
|
6
|
+
Rational(qnpm,4*beat_dur)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.to_nps qnpm
|
10
|
+
Rational(qnpm,240)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class BPM
|
15
|
+
def self.to_qnpm bpm, beat_dur
|
16
|
+
4*beat_dur*bpm
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.to_nps bpm, beat_dur
|
20
|
+
Rational(bpm*beat_dur,60)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Score
|
4
|
+
class Unmeasured < TempoBased
|
5
|
+
# Convert to unmeasured score by converting measure-based offsets to
|
6
|
+
# note-based offsets, and eliminating the use of meters. Also, tempo is
|
7
|
+
# coverted from beats-per-minute to quarter-notes per minute.
|
8
|
+
def to_timed tempo_sample_rate
|
9
|
+
UnmeasuredScoreConverter.new(self,tempo_sample_rate).convert_score
|
10
|
+
end
|
11
|
+
|
12
|
+
def note_time_map tempo_sample_rate
|
13
|
+
tempo_computer = ValueComputer.new(@start_tempo, @tempo_changes)
|
14
|
+
ntc = NoteTimeConverter.new(tempo_computer, tempo_sample_rate)
|
15
|
+
ntc.note_time_map(note_offsets)
|
16
|
+
end
|
17
|
+
|
18
|
+
def note_offsets
|
19
|
+
noffs = Set.new([0.to_r])
|
20
|
+
|
21
|
+
@tempo_changes.each do |noff,change|
|
22
|
+
noffs += change.offsets(noff)
|
23
|
+
end
|
24
|
+
|
25
|
+
@parts.values.each do |part|
|
26
|
+
noff = 0.to_r
|
27
|
+
part.notes.each do |note|
|
28
|
+
noff += note.duration
|
29
|
+
noffs.add(noff)
|
30
|
+
end
|
31
|
+
|
32
|
+
part.dynamic_changes.each do |noff,change|
|
33
|
+
noffs += change.offsets(noff)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@program.segments.each do |seg|
|
38
|
+
noffs.add(seg.first)
|
39
|
+
noffs.add(seg.last)
|
40
|
+
end
|
41
|
+
|
42
|
+
return noffs.sort
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Musicality
|
4
|
+
|
5
|
+
# Converts unmeasured score to timed score, by converting note-based offsets
|
6
|
+
# and durations to time-based, and eliminating the use of tempo.
|
7
|
+
class UnmeasuredScoreConverter
|
8
|
+
|
9
|
+
def initialize score, tempo_sample_rate
|
10
|
+
unless score.valid?
|
11
|
+
raise NotValidError, "The given score can not be converted because \
|
12
|
+
it is invalid, with these errors: #{score.errors}"
|
13
|
+
end
|
14
|
+
|
15
|
+
@score = score
|
16
|
+
@note_time_map = score.note_time_map(tempo_sample_rate)
|
17
|
+
end
|
18
|
+
|
19
|
+
def convert_score
|
20
|
+
Score::Timed.new(parts: convert_parts, program: convert_program)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Convert note-based offsets & durations to time-based.
|
24
|
+
def convert_parts
|
25
|
+
Hash[ @score.parts.map do |name,part|
|
26
|
+
offset = 0.to_r
|
27
|
+
|
28
|
+
new_notes = part.notes.map do |note|
|
29
|
+
starttime = @note_time_map[offset]
|
30
|
+
endtime = @note_time_map[offset + note.duration]
|
31
|
+
offset += note.duration
|
32
|
+
newnote = note.clone
|
33
|
+
newnote.duration = endtime - starttime
|
34
|
+
newnote
|
35
|
+
end
|
36
|
+
|
37
|
+
new_dcs = Hash[ part.dynamic_changes.map do |noff,change|
|
38
|
+
case change
|
39
|
+
when Change::Immediate
|
40
|
+
[@note_time_map[noff],change.clone]
|
41
|
+
when Change::Gradual
|
42
|
+
toff1, toff2, toff3, toff4 = change.offsets(noff).map {|x| @note_time_map[x] }
|
43
|
+
[toff2, Change::Gradual.new(change.value,
|
44
|
+
toff3-toff2, toff2-toff1, toff4-toff3)]
|
45
|
+
end
|
46
|
+
end ]
|
47
|
+
|
48
|
+
[name, Part.new(part.start_dynamic,
|
49
|
+
notes: new_notes, dynamic_changes: new_dcs)]
|
50
|
+
end]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert note-based offsets & durations to time-based.
|
54
|
+
def convert_program
|
55
|
+
newsegments = @score.program.segments.map do |segment|
|
56
|
+
first = @note_time_map[segment.first]
|
57
|
+
last = @note_time_map[segment.last]
|
58
|
+
first...last
|
59
|
+
end
|
60
|
+
Program.new(newsegments)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Change
|
4
|
+
attr_reader :value, :duration
|
5
|
+
|
6
|
+
def initialize value, duration
|
7
|
+
@value = value
|
8
|
+
@duration = duration
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
self.class == other.class &&
|
13
|
+
self.value == other.value &&
|
14
|
+
self.duration == other.duration
|
15
|
+
end
|
16
|
+
|
17
|
+
class Immediate < Change
|
18
|
+
def initialize value
|
19
|
+
super(value,0)
|
20
|
+
end
|
21
|
+
|
22
|
+
def clone
|
23
|
+
Immediate.new(@value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Gradual < Change
|
28
|
+
attr_reader :elapsed, :impending, :remaining, :total_duration
|
29
|
+
|
30
|
+
def initialize value, impending, elapsed=0, remaining=0
|
31
|
+
if elapsed < 0
|
32
|
+
raise NegativeError, "elapsed (#{elapsed}) is < 0"
|
33
|
+
end
|
34
|
+
|
35
|
+
if impending <= 0
|
36
|
+
raise NonPositiveError, "impending (#{impending}) is <= 0"
|
37
|
+
end
|
38
|
+
|
39
|
+
if remaining < 0
|
40
|
+
raise NegativeError, "remaining #{remaining} is < 0"
|
41
|
+
end
|
42
|
+
|
43
|
+
@total_duration = elapsed + impending + remaining
|
44
|
+
@elapsed = elapsed
|
45
|
+
@impending = impending
|
46
|
+
@remaining = remaining
|
47
|
+
super(value,impending)
|
48
|
+
end
|
49
|
+
|
50
|
+
def ==(other)
|
51
|
+
super(other) &&
|
52
|
+
@elapsed == other.elapsed &&
|
53
|
+
@remaining == other.remaining
|
54
|
+
end
|
55
|
+
|
56
|
+
def clone
|
57
|
+
Gradual.new(@value, @impending, @elapsed, @remaining)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
# Connect one note pitch to the target pitch of the next note, via slur, legato, etc.
|
4
|
+
#
|
5
|
+
# @!attribute [rw] target_pitch
|
6
|
+
# @return [Pitch] The pitch of the note which is being connected to.
|
7
|
+
#
|
8
|
+
class Link
|
9
|
+
def clone
|
10
|
+
Marshal.load(Marshal.dump(self))
|
11
|
+
end
|
12
|
+
|
13
|
+
class Tie < Link
|
14
|
+
def initialize; end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
self.class == other.class
|
18
|
+
end
|
19
|
+
|
20
|
+
def transpose diff
|
21
|
+
self.clone.transpose! diff
|
22
|
+
end
|
23
|
+
|
24
|
+
def transpose! diff
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s; "="; end
|
29
|
+
end
|
30
|
+
|
31
|
+
class TargetedLink < Link
|
32
|
+
attr_accessor :target_pitch
|
33
|
+
|
34
|
+
def initialize target_pitch
|
35
|
+
@target_pitch = target_pitch
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
self.class == other.class && @target_pitch == other.target_pitch
|
40
|
+
end
|
41
|
+
|
42
|
+
def transpose diff
|
43
|
+
self.clone.transpose! diff
|
44
|
+
end
|
45
|
+
|
46
|
+
def transpose! diff
|
47
|
+
@target_pitch = @target_pitch.transpose(diff)
|
48
|
+
return self
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
link_char + @target_pitch.to_s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Glissando < TargetedLink
|
57
|
+
def link_char; "~"; end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Portamento < TargetedLink
|
61
|
+
def link_char; "/"; end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Slur < TargetedLink
|
65
|
+
def link_char; "="; end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Legato < TargetedLink
|
69
|
+
def link_char; "|"; end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Meter
|
4
|
+
include Validatable
|
5
|
+
|
6
|
+
attr_reader :measure_duration, :beat_duration, :beats_per_measure
|
7
|
+
|
8
|
+
def initialize beats_per_measure, beat_duration
|
9
|
+
@beats_per_measure = beats_per_measure
|
10
|
+
@beat_duration = beat_duration
|
11
|
+
@measure_duration = beats_per_measure * beat_duration
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_methods
|
15
|
+
[ :check_beats_per_measure, :check_beat_duration ]
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_beats_per_measure
|
19
|
+
unless @beats_per_measure > 0
|
20
|
+
raise NonPositiveError, "beats per measure #{@beats_per_measure} is not positive"
|
21
|
+
end
|
22
|
+
|
23
|
+
unless @beats_per_measure.is_a?(Integer)
|
24
|
+
raise NonIntegerError, "beats per measure #{@beats_per_measure} is not an integer"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def check_beat_duration
|
29
|
+
unless @beat_duration > 0
|
30
|
+
raise NonPositiveError, "beat duration #{@beat_duration} is not positive"
|
31
|
+
end
|
32
|
+
|
33
|
+
unless @beat_duration > 0
|
34
|
+
raise NonRationalError, "beat duration #{@beat_duration} is a rational"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
return (@beats_per_measure == other.beats_per_measure &&
|
40
|
+
@beat_duration == other.beat_duration)
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
if beat_duration.numerator == 1
|
45
|
+
num = beats_per_measure * beat_duration.numerator
|
46
|
+
den = beat_duration.denominator
|
47
|
+
"#{num}/#{den}"
|
48
|
+
else
|
49
|
+
"#{beats_per_measure}*#{beat_duration}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
class Note
|
6
|
+
include Validatable
|
7
|
+
|
8
|
+
attr_reader :pitches, :links
|
9
|
+
attr_accessor :articulation, :duration, :accented
|
10
|
+
|
11
|
+
DEFAULT_ARTICULATION = Articulations::NORMAL
|
12
|
+
|
13
|
+
def initialize duration, pitches = [], articulation: DEFAULT_ARTICULATION, accented: false, links: {}
|
14
|
+
@duration = duration
|
15
|
+
if !pitches.is_a? Enumerable
|
16
|
+
pitches = [ pitches ]
|
17
|
+
end
|
18
|
+
@pitches = Set.new(pitches).sort
|
19
|
+
@articulation = articulation
|
20
|
+
@accented = accented
|
21
|
+
@links = links
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_methods
|
25
|
+
[ :ensure_positive_duration ]
|
26
|
+
end
|
27
|
+
|
28
|
+
def ensure_positive_duration
|
29
|
+
unless @duration > 0
|
30
|
+
raise NonPositiveError, "duration #{@duration} is not positive"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def == other
|
35
|
+
return (@duration == other.duration) &&
|
36
|
+
(self.pitches == other.pitches) &&
|
37
|
+
(@links.to_a.sort == other.links.to_a.sort) &&
|
38
|
+
(@articulation == other.articulation) &&
|
39
|
+
(@accented == other.accented)
|
40
|
+
end
|
41
|
+
|
42
|
+
def clone
|
43
|
+
Marshal.load(Marshal.dump(self))
|
44
|
+
end
|
45
|
+
|
46
|
+
def transpose diff
|
47
|
+
self.clone.transpose! diff
|
48
|
+
end
|
49
|
+
|
50
|
+
def transpose! diff
|
51
|
+
@pitches = @pitches.map {|pitch| pitch.transpose(diff) }
|
52
|
+
@links = Hash[ @links.map do |k,v|
|
53
|
+
[ k.transpose(diff), v.transpose(diff) ]
|
54
|
+
end ]
|
55
|
+
return self
|
56
|
+
end
|
57
|
+
|
58
|
+
def stretch ratio
|
59
|
+
self.clone.stretch! ratio
|
60
|
+
end
|
61
|
+
|
62
|
+
def stretch! ratio
|
63
|
+
@duration *= ratio
|
64
|
+
return self
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
d = @duration.to_r
|
69
|
+
if d.denominator == 1
|
70
|
+
dur_str = "#{d.numerator}"
|
71
|
+
elsif d.numerator == 1
|
72
|
+
dur_str = "/#{d.denominator}"
|
73
|
+
else
|
74
|
+
dur_str = d.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
art_str = case @articulation
|
78
|
+
when Articulations::SLUR then "="
|
79
|
+
when Articulations::LEGATO then "|"
|
80
|
+
when Articulations::TENUTO then "_"
|
81
|
+
when Articulations::PORTATO then "%"
|
82
|
+
when Articulations::STACCATO then "."
|
83
|
+
when Articulations::STACCATISSIMO then "'"
|
84
|
+
else ""
|
85
|
+
end
|
86
|
+
|
87
|
+
pitch_links_str = @pitches.map do |p|
|
88
|
+
if @links.has_key?(p)
|
89
|
+
p.to_s + @links[p].to_s
|
90
|
+
else
|
91
|
+
p.to_s
|
92
|
+
end
|
93
|
+
end.join(",")
|
94
|
+
|
95
|
+
acc_str = @accented ? "!" : ""
|
96
|
+
return dur_str + art_str + pitch_links_str + acc_str
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.add_note_method(name, dur)
|
100
|
+
self.class.send(:define_method,name.to_sym) do |pitches = [], articulation: DEFAULT_ARTICULATION, links: {}, accented: false|
|
101
|
+
Note.new(dur, pitches, articulation: articulation, links: links, accented: accented)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
{
|
106
|
+
:sixteenth => Rational(1,16),
|
107
|
+
:dotted_SIXTEENTH => Rational(3,32),
|
108
|
+
:eighth => Rational(1,8),
|
109
|
+
:dotted_eighth => Rational(3,16),
|
110
|
+
:quarter => Rational(1,4),
|
111
|
+
:dotted_quarter => Rational(3,8),
|
112
|
+
:half => Rational(1,2),
|
113
|
+
:dotted_half => Rational(3,4),
|
114
|
+
:whole => Rational(1),
|
115
|
+
}.each do |meth_name, dur|
|
116
|
+
add_note_method meth_name, dur
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Musicality
|
4
|
+
|
5
|
+
class Part
|
6
|
+
include Validatable
|
7
|
+
|
8
|
+
attr_accessor :start_dynamic, :dynamic_changes, :notes
|
9
|
+
|
10
|
+
def initialize start_dynamic, notes: [], dynamic_changes: {}
|
11
|
+
@notes = notes
|
12
|
+
@start_dynamic = start_dynamic
|
13
|
+
@dynamic_changes = dynamic_changes
|
14
|
+
|
15
|
+
yield(self) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_methods
|
19
|
+
[:ensure_start_dynamic, :ensure_dynamic_change_values_range ]
|
20
|
+
end
|
21
|
+
|
22
|
+
def validatables
|
23
|
+
@notes
|
24
|
+
end
|
25
|
+
|
26
|
+
def clone
|
27
|
+
Marshal.load(Marshal.dump(self))
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
return (@notes == other.notes) &&
|
32
|
+
(@start_dynamic == other.start_dynamic) &&
|
33
|
+
(@dynamic_changes == other.dynamic_changes)
|
34
|
+
end
|
35
|
+
|
36
|
+
def duration
|
37
|
+
return @notes.inject(0) { |sum, note| sum + note.duration }
|
38
|
+
end
|
39
|
+
|
40
|
+
def ensure_start_dynamic
|
41
|
+
unless @start_dynamic.between?(0,1)
|
42
|
+
raise RangeError, "start dynamic #{@start_dynamic} is not between 0 and 1"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def ensure_dynamic_change_values_range
|
47
|
+
outofrange = @dynamic_changes.values.select {|v| !v.value.between?(0,1) }
|
48
|
+
if outofrange.any?
|
49
|
+
raise RangeError, "dynamic change values #{outofrange} are not between 0 and 1"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
# Abstraction of a musical pitch. Contains values for octave and semitone.
|
4
|
+
#
|
5
|
+
# Octaves represent the largest means of differing two pitches. Each
|
6
|
+
# octave added will double the ratio. At zero octaves, the ratio is
|
7
|
+
# 1.0. At one octave, the ratio will be 2.0. Each semitone is an increment
|
8
|
+
# of less-than-power-of-two.
|
9
|
+
#
|
10
|
+
# Semitones are the primary steps between octaves. The number of
|
11
|
+
# semitones per octave is 12.
|
12
|
+
|
13
|
+
# @author James Tunnell
|
14
|
+
#
|
15
|
+
# @!attribute [r] octave
|
16
|
+
# @return [Fixnum] The pitch octave.
|
17
|
+
# @!attribute [r] semitone
|
18
|
+
# @return [Fixnum] The pitch semitone.
|
19
|
+
#
|
20
|
+
class Pitch
|
21
|
+
include Comparable
|
22
|
+
attr_reader :octave, :semitone, :cent, :total_cents
|
23
|
+
|
24
|
+
#The default number of semitones per octave is 12, corresponding to
|
25
|
+
# the twelve-tone equal temperment tuning system.
|
26
|
+
SEMITONES_PER_OCTAVE = 12
|
27
|
+
CENTS_PER_SEMITONE = 100
|
28
|
+
CENTS_PER_OCTAVE = SEMITONES_PER_OCTAVE * CENTS_PER_SEMITONE
|
29
|
+
|
30
|
+
# The base ferquency is C0
|
31
|
+
BASE_FREQ = 16.351597831287414
|
32
|
+
|
33
|
+
def initialize octave:0, semitone:0, cent: 0
|
34
|
+
raise NonIntegerError, "octave #{octave} is not an integer" unless octave.is_a?(Integer)
|
35
|
+
raise NonIntegerError, "semitone #{semitone} is not an integer" unless semitone.is_a?(Integer)
|
36
|
+
raise NonIntegerError, "cent #{cent} is not an integer" unless cent.is_a?(Integer)
|
37
|
+
|
38
|
+
@octave = octave
|
39
|
+
@semitone = semitone
|
40
|
+
@cent = cent
|
41
|
+
@total_cents = (@octave*SEMITONES_PER_OCTAVE + @semitone)*CENTS_PER_SEMITONE + @cent
|
42
|
+
balance!
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the pitch's frequency, which is determined by multiplying the base
|
46
|
+
# frequency and the pitch ratio. Base frequency defaults to DEFAULT_BASE_FREQ,
|
47
|
+
# but can be set during initialization to something else by specifying the
|
48
|
+
# :base_freq key.
|
49
|
+
def freq
|
50
|
+
return self.ratio() * BASE_FREQ
|
51
|
+
end
|
52
|
+
|
53
|
+
# Calculate the pitch ratio. Raises 2 to the power of the total cent
|
54
|
+
# count divided by cents-per-octave.
|
55
|
+
# @return [Float] ratio
|
56
|
+
def ratio
|
57
|
+
2.0**(@total_cents.to_f / CENTS_PER_OCTAVE)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Override default hash method.
|
61
|
+
def hash
|
62
|
+
return @total_cents
|
63
|
+
end
|
64
|
+
|
65
|
+
# Compare pitch equality using total semitone
|
66
|
+
def ==(other)
|
67
|
+
return (self.class == other.class &&
|
68
|
+
@total_cents == other.total_cents)
|
69
|
+
end
|
70
|
+
|
71
|
+
def eql?(other)
|
72
|
+
self == other
|
73
|
+
end
|
74
|
+
|
75
|
+
# Compare pitches. A higher ratio or total semitone is considered larger.
|
76
|
+
# @param [Pitch] other The pitch object to compare.
|
77
|
+
def <=> (other)
|
78
|
+
@total_cents <=> other.total_cents
|
79
|
+
end
|
80
|
+
|
81
|
+
# rounds to the nearest semitone
|
82
|
+
def round
|
83
|
+
if @cent == 0
|
84
|
+
self.clone
|
85
|
+
else
|
86
|
+
Pitch.new(semitone: (@total_cents / CENTS_PER_SEMITONE.to_f).round)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# diff in (rounded) semitones
|
91
|
+
def diff other
|
92
|
+
Rational(@total_cents - other.total_cents, CENTS_PER_SEMITONE)
|
93
|
+
end
|
94
|
+
|
95
|
+
def transpose semitones
|
96
|
+
Pitch.new(cent: (@total_cents + semitones * CENTS_PER_SEMITONE).round)
|
97
|
+
end
|
98
|
+
|
99
|
+
def total_semitones
|
100
|
+
Rational(@total_cents, CENTS_PER_SEMITONE)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.from_semitones semitones
|
104
|
+
Pitch.new(cent: (semitones * CENTS_PER_SEMITONE).round)
|
105
|
+
end
|
106
|
+
|
107
|
+
def clone
|
108
|
+
Pitch.new(cent: @total_cents)
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_s(sharpit = false)
|
112
|
+
letter = case semitone
|
113
|
+
when 0 then "C"
|
114
|
+
when 1 then sharpit ? "C#" : "Db"
|
115
|
+
when 2 then "D"
|
116
|
+
when 3 then sharpit ? "D#" : "Eb"
|
117
|
+
when 4 then "E"
|
118
|
+
when 5 then "F"
|
119
|
+
when 6 then sharpit ? "F#" : "Gb"
|
120
|
+
when 7 then "G"
|
121
|
+
when 8 then sharpit ? "G#" : "Ab"
|
122
|
+
when 9 then "A"
|
123
|
+
when 10 then sharpit ? "A#" : "Bb"
|
124
|
+
when 11 then "B"
|
125
|
+
end
|
126
|
+
|
127
|
+
if @cent == 0
|
128
|
+
return letter + octave.to_s
|
129
|
+
elsif @cent > 0
|
130
|
+
return letter + octave.to_s + "+" + @cent.to_s
|
131
|
+
else
|
132
|
+
return letter + octave.to_s + @cent.to_s
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.from_ratio ratio
|
137
|
+
raise NonPositiveError, "ratio #{ratio} is not > 0" unless ratio > 0
|
138
|
+
x = Math.log2 ratio
|
139
|
+
new(cent: (x * CENTS_PER_OCTAVE).round)
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.from_freq freq
|
143
|
+
from_ratio(freq / BASE_FREQ)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
# Balance out the octave and semitone count.
|
149
|
+
def balance!
|
150
|
+
centsTotal = @total_cents
|
151
|
+
|
152
|
+
@octave = centsTotal / CENTS_PER_OCTAVE
|
153
|
+
centsTotal -= @octave * CENTS_PER_OCTAVE
|
154
|
+
|
155
|
+
@semitone = centsTotal / CENTS_PER_SEMITONE
|
156
|
+
centsTotal -= @semitone * CENTS_PER_SEMITONE
|
157
|
+
|
158
|
+
@cent = centsTotal
|
159
|
+
return self
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|