music-transcription 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.document +3 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -0
  5. data/ChangeLog.rdoc +4 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.txt +20 -0
  8. data/README.rdoc +28 -0
  9. data/Rakefile +54 -0
  10. data/bin/transcribe +176 -0
  11. data/lib/music-transcription.rb +20 -0
  12. data/lib/music-transcription/arrangement.rb +31 -0
  13. data/lib/music-transcription/instrument_config.rb +38 -0
  14. data/lib/music-transcription/interval.rb +66 -0
  15. data/lib/music-transcription/link.rb +115 -0
  16. data/lib/music-transcription/note.rb +156 -0
  17. data/lib/music-transcription/part.rb +128 -0
  18. data/lib/music-transcription/pitch.rb +297 -0
  19. data/lib/music-transcription/pitch_constants.rb +204 -0
  20. data/lib/music-transcription/profile.rb +105 -0
  21. data/lib/music-transcription/program.rb +136 -0
  22. data/lib/music-transcription/score.rb +122 -0
  23. data/lib/music-transcription/tempo.rb +44 -0
  24. data/lib/music-transcription/transition.rb +71 -0
  25. data/lib/music-transcription/value_change.rb +85 -0
  26. data/lib/music-transcription/version.rb +7 -0
  27. data/music-transcription.gemspec +36 -0
  28. data/samples/arrangements/glissando_test.yml +71 -0
  29. data/samples/arrangements/hip.yml +952 -0
  30. data/samples/arrangements/instrument_test.yml +119 -0
  31. data/samples/arrangements/legato_test.yml +237 -0
  32. data/samples/arrangements/make_glissando_test.rb +27 -0
  33. data/samples/arrangements/make_hip.rb +75 -0
  34. data/samples/arrangements/make_instrument_test.rb +34 -0
  35. data/samples/arrangements/make_legato_test.rb +37 -0
  36. data/samples/arrangements/make_missed_connection.rb +72 -0
  37. data/samples/arrangements/make_portamento_test.rb +27 -0
  38. data/samples/arrangements/make_slur_test.rb +37 -0
  39. data/samples/arrangements/make_song1.rb +84 -0
  40. data/samples/arrangements/make_song2.rb +69 -0
  41. data/samples/arrangements/missed_connection.yml +481 -0
  42. data/samples/arrangements/portamento_test.yml +71 -0
  43. data/samples/arrangements/slur_test.yml +237 -0
  44. data/samples/arrangements/song1.yml +640 -0
  45. data/samples/arrangements/song2.yml +429 -0
  46. data/spec/instrument_config_spec.rb +47 -0
  47. data/spec/interval_spec.rb +38 -0
  48. data/spec/link_spec.rb +22 -0
  49. data/spec/musicality_spec.rb +7 -0
  50. data/spec/note_spec.rb +65 -0
  51. data/spec/part_spec.rb +87 -0
  52. data/spec/pitch_spec.rb +139 -0
  53. data/spec/profile_spec.rb +24 -0
  54. data/spec/program_spec.rb +55 -0
  55. data/spec/score_spec.rb +55 -0
  56. data/spec/spec_helper.rb +23 -0
  57. data/spec/transition_spec.rb +13 -0
  58. data/spec/value_change_spec.rb +19 -0
  59. 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