musicality 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|