music-transcription 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +3 -0
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.rdoc +4 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +54 -0
- data/bin/transcribe +176 -0
- data/lib/music-transcription.rb +20 -0
- data/lib/music-transcription/arrangement.rb +31 -0
- data/lib/music-transcription/instrument_config.rb +38 -0
- data/lib/music-transcription/interval.rb +66 -0
- data/lib/music-transcription/link.rb +115 -0
- data/lib/music-transcription/note.rb +156 -0
- data/lib/music-transcription/part.rb +128 -0
- data/lib/music-transcription/pitch.rb +297 -0
- data/lib/music-transcription/pitch_constants.rb +204 -0
- data/lib/music-transcription/profile.rb +105 -0
- data/lib/music-transcription/program.rb +136 -0
- data/lib/music-transcription/score.rb +122 -0
- data/lib/music-transcription/tempo.rb +44 -0
- data/lib/music-transcription/transition.rb +71 -0
- data/lib/music-transcription/value_change.rb +85 -0
- data/lib/music-transcription/version.rb +7 -0
- data/music-transcription.gemspec +36 -0
- data/samples/arrangements/glissando_test.yml +71 -0
- data/samples/arrangements/hip.yml +952 -0
- data/samples/arrangements/instrument_test.yml +119 -0
- data/samples/arrangements/legato_test.yml +237 -0
- data/samples/arrangements/make_glissando_test.rb +27 -0
- data/samples/arrangements/make_hip.rb +75 -0
- data/samples/arrangements/make_instrument_test.rb +34 -0
- data/samples/arrangements/make_legato_test.rb +37 -0
- data/samples/arrangements/make_missed_connection.rb +72 -0
- data/samples/arrangements/make_portamento_test.rb +27 -0
- data/samples/arrangements/make_slur_test.rb +37 -0
- data/samples/arrangements/make_song1.rb +84 -0
- data/samples/arrangements/make_song2.rb +69 -0
- data/samples/arrangements/missed_connection.yml +481 -0
- data/samples/arrangements/portamento_test.yml +71 -0
- data/samples/arrangements/slur_test.yml +237 -0
- data/samples/arrangements/song1.yml +640 -0
- data/samples/arrangements/song2.yml +429 -0
- data/spec/instrument_config_spec.rb +47 -0
- data/spec/interval_spec.rb +38 -0
- data/spec/link_spec.rb +22 -0
- data/spec/musicality_spec.rb +7 -0
- data/spec/note_spec.rb +65 -0
- data/spec/part_spec.rb +87 -0
- data/spec/pitch_spec.rb +139 -0
- data/spec/profile_spec.rb +24 -0
- data/spec/program_spec.rb +55 -0
- data/spec/score_spec.rb +55 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/transition_spec.rb +13 -0
- data/spec/value_change_spec.rb +19 -0
- metadata +239 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
# Defines a relationship (tie, slur, legato, etc.) to a note with a certain pitch.
|
5
|
+
#
|
6
|
+
# @author James Tunnell
|
7
|
+
#
|
8
|
+
# @!attribute [rw] target_pitch
|
9
|
+
# @return [Pitch] The pitch of the note which is being connected to.
|
10
|
+
#
|
11
|
+
# @!attribute [rw] relationship
|
12
|
+
# @return [Symbol] The relationship between the current note and a consecutive
|
13
|
+
# note. Valid values are RELATIONSHIP_NONE, RELATIONSHIP_TIE,
|
14
|
+
# RELATIONSHIP_SLUR, RELATIONSHIP_LEGATO, RELATIONSHIP_GLISSANDO,
|
15
|
+
# and RELATIONSHIP_PORTAMENTO.
|
16
|
+
#
|
17
|
+
class Link
|
18
|
+
include Hashmake::HashMakeable
|
19
|
+
|
20
|
+
# no relationship with the following note
|
21
|
+
RELATIONSHIP_NONE = :none
|
22
|
+
# tie to the following note
|
23
|
+
RELATIONSHIP_TIE = :tie
|
24
|
+
# play notes continuously and don't rearticulate
|
25
|
+
RELATIONSHIP_SLUR = :slur
|
26
|
+
# play notes continuously and do rearticulate
|
27
|
+
RELATIONSHIP_LEGATO = :legato
|
28
|
+
# play an uninterrupted slide through a series of consecutive tones to the next note.
|
29
|
+
RELATIONSHIP_GLISSANDO = :glissando
|
30
|
+
# play an uninterrupted glide to the next note.
|
31
|
+
RELATIONSHIP_PORTAMENTO = :portamento
|
32
|
+
|
33
|
+
# a list of valid note relationships
|
34
|
+
RELATIONSHIPS = [
|
35
|
+
RELATIONSHIP_NONE,
|
36
|
+
RELATIONSHIP_TIE,
|
37
|
+
RELATIONSHIP_SLUR,
|
38
|
+
RELATIONSHIP_LEGATO,
|
39
|
+
RELATIONSHIP_GLISSANDO,
|
40
|
+
RELATIONSHIP_PORTAMENTO
|
41
|
+
]
|
42
|
+
|
43
|
+
# hashed-arg specs (for hash-makeable idiom)
|
44
|
+
ARG_SPECS = {
|
45
|
+
:target_pitch => arg_spec(:reqd => false, :type => Pitch, :default => ->(){ Pitch.new }),
|
46
|
+
:relationship => arg_spec(:reqd => false, :type => Symbol, :default => RELATIONSHIP_NONE, :validator => ->(a){ RELATIONSHIPS.include?(a)}),
|
47
|
+
}
|
48
|
+
|
49
|
+
attr_reader :target_pitch, :relationship
|
50
|
+
|
51
|
+
# A new instance of Link.
|
52
|
+
# @param [Hash] args Hashed arguments. See ARG_SPECS for details about valid keys.
|
53
|
+
def initialize args={}
|
54
|
+
hash_make args
|
55
|
+
end
|
56
|
+
|
57
|
+
# Produce an identical Link object.
|
58
|
+
def clone
|
59
|
+
Link.new(:target_pitch => @target_pitch.clone, :relationship => @relationship)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Compare equality of two Link objects.
|
63
|
+
def ==(other)
|
64
|
+
return (@target_pitch == other.target_pitch) && (@relationship == other.relationship)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Set the pitch of the note being connected to.
|
68
|
+
# @param [Pitch] target_pitch The pitch of the note being connected to.
|
69
|
+
# @raise [ArgumentError] if target_pitch is not a Pitch.
|
70
|
+
def target_pitch= target_pitch
|
71
|
+
ARG_SPECS[:target_pitch].validate_value target_pitch
|
72
|
+
@target_pitch = target_pitch
|
73
|
+
end
|
74
|
+
|
75
|
+
# Set the note relationship.
|
76
|
+
# @param [Symbol] relationship The relationship of the note to the following
|
77
|
+
# note (if applicable). Valid relationship are given by the
|
78
|
+
# RELATIONSHIPS constant.
|
79
|
+
# @raise [ArgumentError] if relationship is not a valid relationship.
|
80
|
+
def relationship= relationship
|
81
|
+
ARG_SPECS[:relationship].validate_value relationship
|
82
|
+
@relationship = relationship
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
module_function
|
88
|
+
|
89
|
+
# helper method to create a Link object with GLISSANDO relationship.
|
90
|
+
def glissando pitch
|
91
|
+
Link.new(:target_pitch => pitch, :relationship => Link::RELATIONSHIP_GLISSANDO)
|
92
|
+
end
|
93
|
+
|
94
|
+
# helper method to create a Link object with LEGATO relationship.
|
95
|
+
def legato pitch
|
96
|
+
Link.new(:target_pitch => pitch, :relationship => Link::RELATIONSHIP_LEGATO)
|
97
|
+
end
|
98
|
+
|
99
|
+
# helper method to create a Link object with PORTAMENTO relationship.
|
100
|
+
def portamento pitch
|
101
|
+
Link.new(:target_pitch => pitch, :relationship => Link::RELATIONSHIP_PORTAMENTO)
|
102
|
+
end
|
103
|
+
|
104
|
+
# helper method to create a Link object with SLUR relationship.
|
105
|
+
def slur pitch
|
106
|
+
Link.new(:target_pitch => pitch, :relationship => Link::RELATIONSHIP_SLUR)
|
107
|
+
end
|
108
|
+
|
109
|
+
# helper method to create a Link object with TIE relationship.
|
110
|
+
def tie pitch
|
111
|
+
Link.new(:target_pitch => pitch, :relationship => Link::RELATIONSHIP_TIE)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
# Abstraction of a musical note. The note can contain multiple intervals
|
5
|
+
# (at different pitches). Each interval can also contain a link to an interval
|
6
|
+
# in a following note. Contains values for attack, sustain, and separation,
|
7
|
+
# which will be used to form the envelope profile for the note.
|
8
|
+
#
|
9
|
+
# @author James Tunnell
|
10
|
+
#
|
11
|
+
# @!attribute [rw] duration
|
12
|
+
# @return [Numeric] The duration (in, say note length or time), greater than 0.0.
|
13
|
+
#
|
14
|
+
# @!attribute [rw] intervals
|
15
|
+
# @return [Numeric] The intervals that define which pitches are part of the
|
16
|
+
# note and can link to intervals in a following note.
|
17
|
+
#
|
18
|
+
# @!attribute [rw] attack
|
19
|
+
# @return [Numeric] The amount of attack, from 0.0 (less) to 1.0 (more).
|
20
|
+
# Attack controls how quickly a note's loudness increases
|
21
|
+
# at the start.
|
22
|
+
#
|
23
|
+
# @!attribute [rw] sustain
|
24
|
+
# @return [Numeric] The amount of sustain, from 0.0 (less) to 1.0 (more).
|
25
|
+
# Sustain controls how much the note's loudness is
|
26
|
+
# sustained after the attack.
|
27
|
+
#
|
28
|
+
# @!attribute [rw] separation
|
29
|
+
# @return [Numeric] Shift the note release towards or away the beginning
|
30
|
+
# of the note. From 0.0 (towards end of the note) to
|
31
|
+
# 1.0 (towards beginning of the note).
|
32
|
+
#
|
33
|
+
class Note
|
34
|
+
include Hashmake::HashMakeable
|
35
|
+
attr_reader :duration, :intervals, :sustain, :attack, :separation
|
36
|
+
|
37
|
+
# hashed-arg specs (for hash-makeable idiom)
|
38
|
+
ARG_SPECS = {
|
39
|
+
:duration => arg_spec(:type => Numeric, :reqd => true, :validator => ->(a){ a > 0 } ),
|
40
|
+
:intervals => arg_spec_array(:type => Interval, :reqd => false),
|
41
|
+
:sustain => arg_spec(:type => Numeric, :reqd => false, :validator => ->(a){ a.between?(0.0,1.0)}, :default => 0.5),
|
42
|
+
:attack => arg_spec(:type => Numeric, :reqd => false, :validator => ->(a){ a.between?(0.0,1.0)}, :default => 0.5),
|
43
|
+
:separation => arg_spec(:type => Numeric, :reqd => false, :validator => ->(a){ a.between?(0.0,1.0)}, :default => 0.5),
|
44
|
+
}
|
45
|
+
|
46
|
+
# A new instance of Note.
|
47
|
+
# @param [Hash] args Hashed arguments. See Note::ARG_SPECS for details.
|
48
|
+
def initialize args={}
|
49
|
+
hash_make args
|
50
|
+
end
|
51
|
+
|
52
|
+
# Compare the equality of another Note object.
|
53
|
+
def == other
|
54
|
+
return (@duration == other.duration) &&
|
55
|
+
(@intervals == other.intervals) &&
|
56
|
+
(@sustain == other.sustain) &&
|
57
|
+
(@attack == other.attack) &&
|
58
|
+
(@separation == other.separation)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set the note duration.
|
62
|
+
# @param [Numeric] duration The duration to use.
|
63
|
+
# @raise [ArgumentError] if duration is not greater than 0.
|
64
|
+
def duration= duration
|
65
|
+
ARG_SPECS[:duration].validate_value duration
|
66
|
+
@duration = duration
|
67
|
+
end
|
68
|
+
|
69
|
+
# Set the note sustain.
|
70
|
+
# @param [Numeric] sustain The sustain of the note.
|
71
|
+
# @raise [ArgumentError] if sustain is not a Numeric.
|
72
|
+
# @raise [RangeError] if sustain is outside the range 0.0..1.0.
|
73
|
+
def sustain= sustain
|
74
|
+
ARG_SPECS[:sustain].validate_value sustain
|
75
|
+
@sustain = sustain
|
76
|
+
end
|
77
|
+
|
78
|
+
# Set the note attack.
|
79
|
+
# @param [Numeric] attack The attack of the note.
|
80
|
+
# @raise [ArgumentError] if attack is not a Numeric.
|
81
|
+
# @raise [RangeError] if attack is outside the range 0.0..1.0.
|
82
|
+
def attack= attack
|
83
|
+
ARG_SPECS[:attack].validate_value attack
|
84
|
+
@attack = attack
|
85
|
+
end
|
86
|
+
|
87
|
+
# Set the note separation.
|
88
|
+
# @param [Numeric] separation The separation of the note.
|
89
|
+
# @raise [ArgumentError] if separation is not a Numeric.
|
90
|
+
# @raise [RangeError] if separation is outside the range 0.0..1.0.
|
91
|
+
def separation= separation
|
92
|
+
ARG_SPECS[:separation].validate_value separation
|
93
|
+
@separation = separation
|
94
|
+
end
|
95
|
+
|
96
|
+
# Produce an identical Note object.
|
97
|
+
def clone
|
98
|
+
Marshal.load(Marshal.dump(self))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Remove any duplicate intervals (occuring on the same pitch), removing
|
102
|
+
# all but the last occurance. Remove any duplicate links (links to the
|
103
|
+
# same interval), removing all but the last occurance.
|
104
|
+
def remove_duplicates
|
105
|
+
# in case of duplicate notes
|
106
|
+
intervals_to_remove = Set.new
|
107
|
+
for i in (0...@intervals.count).entries.reverse
|
108
|
+
@intervals.each_index do |j|
|
109
|
+
if j < i
|
110
|
+
if @intervals[i].pitch == @intervals[j].pitch
|
111
|
+
intervals_to_remove.add @intervals[j]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
@intervals.delete_if { |interval| intervals_to_remove.include? interval}
|
117
|
+
|
118
|
+
# in case of duplicate links
|
119
|
+
for i in (0...@intervals.count).entries.reverse
|
120
|
+
@intervals.each_index do |j|
|
121
|
+
if j < i
|
122
|
+
if @intervals[i].linked? && @intervals[j].linked? && @intervals[i].link.target_pitch == @intervals[j].link.target_pitch
|
123
|
+
@intervals[j].link = Link.new
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def transpose pitch_diff
|
131
|
+
self.clone.transpose! pitch_diff
|
132
|
+
end
|
133
|
+
|
134
|
+
def transpose! pitch_diff
|
135
|
+
@intervals.each do |interval|
|
136
|
+
interval.pitch += pitch_diff
|
137
|
+
interval.link.target_pitch += pitch_diff
|
138
|
+
end
|
139
|
+
return self
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_s
|
143
|
+
"#{duration}:#{intervals.map{|i| i.pitch}.inspect}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
module_function
|
148
|
+
|
149
|
+
def note duration, intervals = [], other_args = {}
|
150
|
+
Note.new(
|
151
|
+
{ :duration => duration, :intervals => intervals }.merge other_args
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Music
|
4
|
+
module Transcription
|
5
|
+
|
6
|
+
# Abstraction of a musical part. Contains start offset, notes, and loudness_profile settings.
|
7
|
+
#
|
8
|
+
# @author James Tunnell
|
9
|
+
#
|
10
|
+
# @!attribute [rw] offset
|
11
|
+
# @return [Numeric] The offset where the part begins.
|
12
|
+
#
|
13
|
+
# @!attribute [rw] notes
|
14
|
+
# @return [Array] The notes to be played.
|
15
|
+
#
|
16
|
+
# @!attribute [rw] loudness_profile
|
17
|
+
# @return [Profile] The parts loudness_profile profile.
|
18
|
+
#
|
19
|
+
class Part
|
20
|
+
include Hashmake::HashMakeable
|
21
|
+
attr_reader :offset, :loudness_profile, :notes
|
22
|
+
|
23
|
+
# hashed-arg specs (for hash-makeable idiom)
|
24
|
+
ARG_SPECS = {
|
25
|
+
:offset => arg_spec(:reqd => false, :type => Numeric, :default => 0),
|
26
|
+
:loudness_profile => arg_spec(:reqd => false, :type => Profile, :validator => ->(a){ a.values_between?(0.0,1.0) }, :default => ->(){ Profile.new(:start_value => 0.5) }),
|
27
|
+
:notes => arg_spec_array(:reqd => false, :type => Note),
|
28
|
+
}
|
29
|
+
|
30
|
+
# A new instance of Part.
|
31
|
+
# @param [Hash] args Hashed arguments. Valid optional keys are :loudness_profile,
|
32
|
+
# :notes, and :offset.
|
33
|
+
def initialize args = {}
|
34
|
+
hash_make args, Part::ARG_SPECS
|
35
|
+
end
|
36
|
+
|
37
|
+
# Produce an exact copy of the current object
|
38
|
+
def clone
|
39
|
+
Marshal.load(Marshal.dump(self))
|
40
|
+
end
|
41
|
+
|
42
|
+
# Compare the equality of another Part object.
|
43
|
+
def ==(other)
|
44
|
+
return (@offset == other.offset) &&
|
45
|
+
(@loudness_profile == other.loudness_profile) &&
|
46
|
+
(@notes == other.notes)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set the start offset of the part.
|
50
|
+
# @param [Numeric] offset The start offset of the part.
|
51
|
+
# @raise [ArgumentError] unless offset is a Numeric.
|
52
|
+
def offset= offset
|
53
|
+
ARG_SPECS[:offset].validate_value offset
|
54
|
+
@offset = offset
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set the loudness_profile Profile.
|
58
|
+
# @param [Tempo] loudness_profile The Profile for part loudness_profile.
|
59
|
+
# @raise [ArgumentError] if loudness_profile is not a Profile.
|
60
|
+
def loudness_profile= loudness_profile
|
61
|
+
ARG_SPECS[:loudness_profile].validate_value loudness_profile
|
62
|
+
@loudness_profile = loudness_profile
|
63
|
+
end
|
64
|
+
|
65
|
+
# Duration of part notes.
|
66
|
+
def duration
|
67
|
+
total_duration = @notes.inject(0) { |sum, note| sum + note.duration }
|
68
|
+
return @offset + total_duration
|
69
|
+
end
|
70
|
+
|
71
|
+
# offset where part begins
|
72
|
+
def start
|
73
|
+
return @offset
|
74
|
+
end
|
75
|
+
|
76
|
+
# offset where part ends
|
77
|
+
def end
|
78
|
+
return @offset + duration
|
79
|
+
end
|
80
|
+
|
81
|
+
def transpose pitch_diff
|
82
|
+
self.clone.transpose! pitch_diff
|
83
|
+
end
|
84
|
+
|
85
|
+
def transpose! pitch_diff
|
86
|
+
@notes.each do |note|
|
87
|
+
note.transpose! pitch_diff
|
88
|
+
end
|
89
|
+
return self
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class PartFile < Part
|
94
|
+
include Hashmake::HashMakeable
|
95
|
+
attr_reader :file_path
|
96
|
+
|
97
|
+
# hashed-arg specs (for hash-makeable idiom)
|
98
|
+
ARG_SPECS = {
|
99
|
+
:file_path => arg_spec(:reqd => true, :type => String, :validator => ->(a){ File.exist? a })
|
100
|
+
}
|
101
|
+
|
102
|
+
# A new instance of Part.
|
103
|
+
# @param [Hash] args Hashed arguments. Only valid keys is :file_path.
|
104
|
+
def initialize args
|
105
|
+
hash_make args, PartFile::ARG_SPECS
|
106
|
+
|
107
|
+
unless @file_path.nil?
|
108
|
+
obj = YAML.load_file @file_path
|
109
|
+
|
110
|
+
if obj.is_a?(Part)
|
111
|
+
super(:offset => obj.offset, :notes => obj.notes, :loudness_profile => obj.loudness_profile)
|
112
|
+
elsif obj.is_a?(Hash)
|
113
|
+
super(obj)
|
114
|
+
else
|
115
|
+
raise ArgumentError, "Expected a Hash or Part object"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Produce an exact copy of the current object
|
121
|
+
def clone
|
122
|
+
Marshal.load(Marshal.dump(self))
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
# Abstraction of a musical pitch. Contains values for octave, semitone,
|
5
|
+
# and cent. These values are useful because they allow simple mapping to
|
6
|
+
# both the abstract (musical scales) and concrete (audio data).
|
7
|
+
#
|
8
|
+
# Fundamentally, pitch can be considered a ratio to some base number.
|
9
|
+
# For music, this is a base frequency. The pitch frequency can be
|
10
|
+
# determined by multiplying the base frequency by the pitch ratio. For
|
11
|
+
# the standard musical scale, the base frequency of C0 is 16.35 Hz.
|
12
|
+
#
|
13
|
+
# Octaves represent the largest means of differing two pitches. Each
|
14
|
+
# octave added will double the ratio. At zero octaves, the ratio is
|
15
|
+
# 1.0. At one octave, the ratio will be 2.0. Each semitone and cent
|
16
|
+
# is an increment of less-than-power-of-two.
|
17
|
+
#
|
18
|
+
# Semitones are the primary steps between octaves. By default, the
|
19
|
+
# number of semitones per octave is 12, corresponding to the twelve-tone equal
|
20
|
+
# temperment tuning system. The number of semitones per octave can be
|
21
|
+
# modified at runtime by overriding the Pitch::SEMITONES_PER_OCTAVE
|
22
|
+
# constant.
|
23
|
+
#
|
24
|
+
# Cents are the smallest means of differing two pitches. By default, the
|
25
|
+
# number of cents per semitone is 100 (hence the name cent, as in per-
|
26
|
+
# cent). This number can be modified at runtime by overriding the
|
27
|
+
# Pitch::CENTS_PER_SEMITONE constant.
|
28
|
+
#
|
29
|
+
# @author James Tunnell
|
30
|
+
#
|
31
|
+
# @!attribute [r] octave
|
32
|
+
# @return [Fixnum] The pitch octave.
|
33
|
+
# @!attribute [r] semitone
|
34
|
+
# @return [Fixnum] The pitch semitone.
|
35
|
+
# @!attribute [r] cent
|
36
|
+
# @return [Fixnum] The pitch cent.
|
37
|
+
# @!attribute [r] cents_per_octave
|
38
|
+
# @return [Fixnum] The number of cents per octave. Default is 1200
|
39
|
+
# (12 x 100). If a different scale is required,
|
40
|
+
# modify CENTS_PER_SEMITONE (default 12) and/or
|
41
|
+
# SEMITONES_PER_OCTAVE (default 100).
|
42
|
+
# @!attribute [r] base_freq
|
43
|
+
# @return [Numeric] Multiplied with pitch ratio to determine the final frequency
|
44
|
+
# of the pitch. Defaults to DEFAULT_BASE_FREQ, but can be set
|
45
|
+
# during initialization to something else using the :base_freq key.
|
46
|
+
#
|
47
|
+
class Pitch
|
48
|
+
include Comparable
|
49
|
+
include Hashmake::HashMakeable
|
50
|
+
attr_reader :cents_per_octave, :base_freq, :octave, :semitone, :cent
|
51
|
+
|
52
|
+
#The default number of semitones per octave is 12, corresponding to
|
53
|
+
# the twelve-tone equal temperment tuning system.
|
54
|
+
SEMITONES_PER_OCTAVE = 12
|
55
|
+
|
56
|
+
#The default number of cents per semitone is 100 (hence the name cent,
|
57
|
+
# as in percent).
|
58
|
+
CENTS_PER_SEMITONE = 100
|
59
|
+
|
60
|
+
# The default base ferquency is C0
|
61
|
+
DEFAULT_BASE_FREQ = 16.351597831287414
|
62
|
+
|
63
|
+
# hashed-arg specs (for hash-makeable idiom)
|
64
|
+
ARG_SPECS = {
|
65
|
+
:octave => arg_spec(:reqd => false, :type => Fixnum, :default => 0),
|
66
|
+
:semitone => arg_spec(:reqd => false, :type => Fixnum, :default => 0),
|
67
|
+
:cent => arg_spec(:reqd => false, :type => Fixnum, :default => 0),
|
68
|
+
:base_freq => arg_spec(:reqd => false, :type => Numeric, :validator => ->(a){ a > 0.0 }, :default => DEFAULT_BASE_FREQ)
|
69
|
+
}
|
70
|
+
|
71
|
+
# A new instance of Pitch.
|
72
|
+
# @param [Hash] args Hashed args. See ARG_SPECS for details.
|
73
|
+
# @raise [ArgumentError] if any of :octave, :semitone, or :cent is
|
74
|
+
# not a Fixnum.
|
75
|
+
def initialize args={}
|
76
|
+
@cents_per_octave = CENTS_PER_SEMITONE * SEMITONES_PER_OCTAVE
|
77
|
+
hash_make args
|
78
|
+
normalize!
|
79
|
+
end
|
80
|
+
|
81
|
+
# Set @base_freq, which is used with the pitch ratio to produce the
|
82
|
+
# pitch frequency.
|
83
|
+
def base_freq= base_freq
|
84
|
+
ARG_SPECS[:base_freq].validate_value base_freq
|
85
|
+
@base_freq = base_freq
|
86
|
+
end
|
87
|
+
|
88
|
+
# Set @octave.
|
89
|
+
def octave= octave
|
90
|
+
ARG_SPECS[:octave].validate_value octave
|
91
|
+
@octave = octave
|
92
|
+
end
|
93
|
+
|
94
|
+
# Set semitone.
|
95
|
+
def semitone= semitone
|
96
|
+
ARG_SPECS[:semitone].validate_value semitone
|
97
|
+
@semitone = semitone
|
98
|
+
end
|
99
|
+
|
100
|
+
# Set @cent.
|
101
|
+
def cent= cent
|
102
|
+
ARG_SPECS[:cent].validate_value cent
|
103
|
+
@cent = cent
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return the pitch's frequency, which is determined by multiplying the base
|
107
|
+
# frequency and the pitch ratio. Base frequency defaults to DEFAULT_BASE_FREQ,
|
108
|
+
# but can be set during initialization to something else by specifying the
|
109
|
+
# :base_freq key.
|
110
|
+
def freq
|
111
|
+
return self.ratio() * @base_freq
|
112
|
+
end
|
113
|
+
|
114
|
+
# Set the pitch according to the given frequency. Uses the current base_freq
|
115
|
+
# to determine what the pitch ratio should be, and sets it accordingly.
|
116
|
+
def freq= freq
|
117
|
+
self.ratio = freq / @base_freq
|
118
|
+
end
|
119
|
+
|
120
|
+
# Calculate the total cent count. Converts octave and semitone count
|
121
|
+
# to cent count before adding to existing cent count.
|
122
|
+
# @return [Fixnum] total cent count
|
123
|
+
def total_cent
|
124
|
+
return (@octave * @cents_per_octave) +
|
125
|
+
(@semitone * CENTS_PER_SEMITONE) + @cent
|
126
|
+
end
|
127
|
+
|
128
|
+
# Set the Pitch ratio according to a total number of cents.
|
129
|
+
# @param [Fixnum] cent The total number of cents to use.
|
130
|
+
# @raise [ArgumentError] if cent is not a Fixnum
|
131
|
+
def total_cent= cent
|
132
|
+
raise ArgumentError, "cent is not a Fixnum" if !cent.is_a?(Fixnum)
|
133
|
+
@octave, @semitone, @cent = 0, 0, cent
|
134
|
+
normalize!
|
135
|
+
end
|
136
|
+
|
137
|
+
# Calculate the pitch ratio. Raises 2 to the power of the total cent
|
138
|
+
# count divided by cents-per-octave.
|
139
|
+
# @return [Float] ratio
|
140
|
+
def ratio
|
141
|
+
2.0**(self.total_cent.to_f / @cents_per_octave)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Represent the Pitch ratio according to a ratio.
|
145
|
+
# @param [Numeric] ratio The ratio to represent.
|
146
|
+
# @raise [RangeError] if ratio is less than or equal to 0.0
|
147
|
+
def ratio= ratio
|
148
|
+
raise RangeError, "ratio #{ratio} is less than or equal to 0.0" if ratio <= 0.0
|
149
|
+
|
150
|
+
x = Math.log2 ratio
|
151
|
+
self.total_cent = (x * @cents_per_octave).round
|
152
|
+
end
|
153
|
+
|
154
|
+
# Round to the nearest semitone.
|
155
|
+
def round
|
156
|
+
self.clone.round!
|
157
|
+
end
|
158
|
+
|
159
|
+
# Round to the nearest semitone.
|
160
|
+
def round!
|
161
|
+
if @cent >= (CENTS_PER_SEMITONE / 2)
|
162
|
+
@semitone += 1
|
163
|
+
end
|
164
|
+
@cent = 0
|
165
|
+
normalize!
|
166
|
+
return self
|
167
|
+
end
|
168
|
+
|
169
|
+
# Calculates the number of semitones which would represent the pitch's
|
170
|
+
# octave and semitone count. Excludes cents.
|
171
|
+
def total_semitone
|
172
|
+
return (@octave * SEMITONES_PER_OCTAVE) + @semitone
|
173
|
+
end
|
174
|
+
|
175
|
+
# Override default hash method.
|
176
|
+
def hash
|
177
|
+
return self.total_cent
|
178
|
+
end
|
179
|
+
|
180
|
+
# Compare pitch equality using total cent
|
181
|
+
def ==(other)
|
182
|
+
self.total_cent == other.total_cent
|
183
|
+
end
|
184
|
+
|
185
|
+
# Compare pitches. A higher ratio or total cent is considered larger.
|
186
|
+
# @param [Pitch] other The pitch object to compare.
|
187
|
+
def <=> (other)
|
188
|
+
self.total_cent <=> other.total_cent
|
189
|
+
end
|
190
|
+
|
191
|
+
# Add pitches by adding the total cent count of each.
|
192
|
+
# @param [Pitch] other The pitch object to add.
|
193
|
+
def + (other)
|
194
|
+
self.class.new :octave => (@octave + other.octave), :semitone => (@semitone + other.semitone), :cent => (@cent + other.cent)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Add pitches by subtracting the total cent count.
|
198
|
+
# @param [Pitch] other The pitch object to subtract.
|
199
|
+
def - (other)
|
200
|
+
self.class.new :octave => (@octave - other.octave), :semitone => (@semitone - other.semitone), :cent => (@cent - other.cent)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Produce an identical Pitch object.
|
204
|
+
def clone
|
205
|
+
Marshal.load(Marshal.dump(self))
|
206
|
+
end
|
207
|
+
|
208
|
+
# Balance out the octave, semitone, and cent count.
|
209
|
+
def normalize!
|
210
|
+
centTotal = (@octave * @cents_per_octave) + (@semitone * CENTS_PER_SEMITONE) + @cent
|
211
|
+
|
212
|
+
@octave = centTotal / @cents_per_octave
|
213
|
+
centTotal -= @octave * @cents_per_octave
|
214
|
+
|
215
|
+
@semitone = centTotal / CENTS_PER_SEMITONE
|
216
|
+
centTotal -= @semitone * CENTS_PER_SEMITONE
|
217
|
+
|
218
|
+
@cent = centTotal
|
219
|
+
return self
|
220
|
+
end
|
221
|
+
|
222
|
+
# Produce a string representation of a pitch (e.g. "C2")
|
223
|
+
def to_s
|
224
|
+
if @cents_per_octave != 1200
|
225
|
+
raise "Don't know how to produce a string representation since cents_per_octave is not 1200."
|
226
|
+
end
|
227
|
+
|
228
|
+
semitone_str = case @semitone
|
229
|
+
when 0 then "C"
|
230
|
+
when 1 then "Db"
|
231
|
+
when 2 then "D"
|
232
|
+
when 3 then "Eb"
|
233
|
+
when 4 then "E"
|
234
|
+
when 5 then "F"
|
235
|
+
when 6 then "Gb"
|
236
|
+
when 7 then "G"
|
237
|
+
when 8 then "Ab"
|
238
|
+
when 9 then "A"
|
239
|
+
when 10 then "Bb"
|
240
|
+
when 11 then "B"
|
241
|
+
end
|
242
|
+
|
243
|
+
return semitone_str + @octave.to_s
|
244
|
+
end
|
245
|
+
|
246
|
+
def self.make_from_freq(freq, base_freq = DEFAULT_BASE_FREQ)
|
247
|
+
pitch = Pitch.new()
|
248
|
+
pitch.ratio = freq / base_freq
|
249
|
+
return pitch
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
class String
|
257
|
+
# Create a Pitch object from a string (e.g. "C2"). String can contain a letter (A-G),
|
258
|
+
# to indicate the semitone, followed by an optional sharp/flat (#/b) and then the
|
259
|
+
# octave number (non-negative integer).
|
260
|
+
def to_pitch
|
261
|
+
string = self
|
262
|
+
if string =~ /[AaBbCcDdEeFfGg][#b][\d]+/
|
263
|
+
semitone = letter_to_semitone string[0]
|
264
|
+
semitone = case string[1]
|
265
|
+
when "#" then semitone + 1
|
266
|
+
when "b" then semitone - 1
|
267
|
+
else raise ArgumentError, "unexpected symbol found"
|
268
|
+
end
|
269
|
+
octave = string[2..-1].to_i
|
270
|
+
return Music::Transcription::Pitch.new(:octave => octave, :semitone => semitone)
|
271
|
+
elsif string =~ /[AaBbCcDdEeFfGg][\d]+/
|
272
|
+
semitone = letter_to_semitone string[0]
|
273
|
+
octave = string[1..-1].to_i
|
274
|
+
return Music::Transcription::Pitch.new(:octave => octave, :semitone => semitone)
|
275
|
+
else
|
276
|
+
raise ArgumentError, "string #{string} cannot be converted to a pitch"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
|
282
|
+
def letter_to_semitone letter
|
283
|
+
semitone = case letter
|
284
|
+
when /[Cc]/ then 0
|
285
|
+
when /[Dd]/ then 2
|
286
|
+
when /[Ee]/ then 4
|
287
|
+
when /[Ff]/ then 5
|
288
|
+
when /[Gg]/ then 7
|
289
|
+
when /[Aa]/ then 9
|
290
|
+
when /[Bb]/ then 11
|
291
|
+
else raise ArgumentError, "invalid letter \"#{letter}\" given"
|
292
|
+
end
|
293
|
+
|
294
|
+
return semitone
|
295
|
+
end
|
296
|
+
|
297
|
+
end
|