music-transcription 0.3.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.
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