Question

I'm calculating programatically the frequencies of given musical notes.

Quick intro:

  • The musical note A4 has a frequency of 440Hz
  • Notes one octave higher are double the frequency of the same note in a lower octave (so A3 is 220Hz, A2 is 110Hz)
  • The difference between one note, to the next semitone is log 2 to the base of a 12th.
  • C3 = B3 x 2 ^ (1/12)

Writing this formula in Ruby, I came up with the following:

# Starting from the note A4 (440Hz)
A4 = 440.to_f
# I want the frequencies of each note over the next 3 octaves
number_of_octaves = 3
# There are 12 semitones per octave
SEMITIONES_PER_OCTAVE = 12

current_freq = A4
(number_of_octaves * SEMITIONES_PER_OCTAVE).times do |i|
  puts "-----" if i % 12 == 0 # separate each octave with dashes
  puts current_freq
  current_freq = current_freq * 2 ** Rational('1/12')
end

The results I'm getting back are not perfect though. The A notes seem to have rounded a little higher than expected:

-----
440.0
466.1637615180899
493.8833012561241
523.2511306011974
554.3652619537443
587.3295358348153
622.253967444162
659.2551138257401
698.456462866008
739.988845423269
783.9908719634989
830.6093951598906
-----
880.0000000000003
932.3275230361802
987.7666025122486
1046.502261202395
1108.7305239074888
1174.6590716696307
1244.5079348883241
1318.5102276514804
1396.9129257320162
1479.9776908465383
1567.981743926998
1661.2187903197814
-----
1760.000000000001
1864.6550460723606
1975.5332050244976
2093.0045224047904
2217.4610478149784
2349.3181433392624
2489.0158697766497
2637.020455302962
2793.825851464034
2959.9553816930784
3135.963487853998
3322.4375806395647

Note the A frequencies - instead of being 880, 1760, they are slightly higher.

I thought Ruby's Rational was supposed to give accurate calculations and avoid the rounding errors from using floats.

Can anybody explain:

  1. Why is this result inaccurate?
  2. How can I improve the above code to obtain a truly accurate result?
Was it helpful?

Solution

It's not clear to me whether in this expression: current_freq * 2 ** Rational('1/12') that Ruby is keeping the entire calculation in the Rational realm. Within Ruby, you get:

2.0.0p195 :001 > current_freq = 440
 => 440
2.0.0p195 :002 >  current_freq * 2 ** Rational('1/12')
 => 466.1637615180899

The calculation produces a float, not a Rational. If we kept it Rational, it would look like:

2.0.0p195 :005 > Rational( current_freq * 2 ** Rational('1/12'))
 => (4100419809895505/8796093022208)

Even if you do this:

2.0.0p195 :010 > Rational(2) ** Rational(1,12)
 => 1.0594630943592953

Ruby goes from Rational to float. The Ruby doc on Rational doesn't describe this clearly, but the examples given show this when taking a rational to a fractional exponent that's not an integer. This makes sense, since when you take a rational number to a rational (fractional, non-integer) exponent, chances are you're going to get an irrational number. 2**(1/12) is one of those cases.

So to keep accuracy, you'd need to keep everything in the Rational realm throughout which isn't really possible once you hit an irrational number. You might, as Scott Hunter suggests, be able to narrow the field with some custom functions to control the inaccuracy. It's unclear whether that would be worth the effort in this situation.

OTHER TIPS

While I'm sure it is representing 1/12 exactly (as a fraction), once you use it as an exponent, you're back in floating point, and the potential for round-off returns.

I suppose you write your own power function, which checks to see if the exponent is an integer and uses multiplication explicitly; that would take care of your A's at least.

To answer the second part of your question:

How can I improve the above code to obtain a truly accurate result?

You could calculate the frequencies with f = 2n/12 × 440:

def freq(n)
  2 ** (n/12.0) * 440
end

puts (0..12).map { |n| freq(n) }

Output:

440.0
466.1637615180899
493.8833012561241
523.2511306011972
554.3652619537442
587.3295358348151
622.2539674441618
659.2551138257398
698.4564628660078
739.9888454232688
783.9908719634985
830.6093951598903
880.0
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top