Tone() function to generate float resolution frequencies

Hi Everyone,

Happy arduino user here, and also first post on this forum, so if it's not in the correct section i will gladly modify it.

I recently wrote my own code to make music with a stepper motor, using tone() function.

I'm quite happy with it, but the pitch resolution gets lower and lower as the frequency goes down (there are only 31 values between 31 and 62 Hz).

The result is the stepper motor sounding more out of tune as he gets to its bass register, either too sharp of too flat, depending on the frequency value rounding result.

My questions are : is it possible to modify the Tone() function to accept float as frequency input and output the correct frequency ?

Is it enough to just replace this :
void tone(uint8_t _pin, unsigned int frequency, unsigned long duration)
by this ?
void tone(uint8_t _pin, float frequency, unsigned long duration)

I make progress, but the tone() code is currently too complex for me to understand well.

I don't think the hardware clock, either 8 or 16 mhz will be an issue, as it already provides good resolution for higher notes.

Is it simpler to write another function from scratch ? I was thinking of doing it using micros().

As tone() is used in many projects, and as i didn't find any good replacement, I think it would be a great improvement to be able to generate precise frequencies in the bass register.

Thanks for reading !
Paul

The tone function signal is generated by a hardware timer, which accepts only integer parameters.

However, 31 values between 31 and 62 Hz is significantly higher resolution than the western 12 tone per octave scale, so I don't see the problem.

Let's take C1 (32.703 Hz) as an example. The nearest whole frequency is 33 Hz. That's about 15 cents too sharp. The "Just Noticeable Difference" for tones is about 10 cents, so this frequency error may be noticeable (almost surely noticeable to musical ears). C#1 is worse—about 17.5 cents sharp (35 vs 34.648 Hz).

1 Like

Of course. Most people may not ever notice.

For accurate tone generation, Direct Digital Synthesis is the way to go.

1 Like

Most people wouldn't be expecting perfect concert pitch from a whiney CPU output. It's a toy.

Also the frequency is only as accurate as the resonator part that Arduino uses for timing. It's not very accurate and it's temperature sensitive.

A good API would allow you to specify the period, instead of, or as well as, the frequency... that would solve the problem.

Thanks for your replies.

So using micros() to switch on and off an arduino output at audio rate will not get me there? it seems feasible to me but I don't know much.

is it possible to write a working code that converts a given float that represents frequency into a set of instructions, like "turn on output, wait for so many microseconds, then change output state"?

the advantage of micros() over tone() is that the lower the frequency, the longer the period, so it should gain pitch precision at lower frequencies.

Yes, you could do that. Personally, I would consider using scaled integers vs. float. Just my preference.

Nobody will notice that without a reference Most musicians don't have perfect pitch and perfect pitch isn't "perfect" and someone with perfect pitch will usually be identified as the nearest valid note.

Many regular consumer soundcards are off by enough that a musician can't play in-tune but nobody notices it until they try to play-along a properly-tuned instrument.

...I'm going to guess that you don't really get 1Hz resolution with the Arduino. Do you always hear a change with every 1Hz change?

I use this in a band context to provide synth-bass frequencies. When listening in solo i don't hear any out-of-tuneness, but played along other instruments it can be noticeable.

As one note might be rounded down and the next up, the relative difference between them becomes quite big and the tuning less harmonic in itself.

Out of tune bass also has the power to make everything else sound out of tune while sounding in tune itself.

I also would like to implement glissando, pitch bend effects and so on, that require good pitch accuracy.

Cool ! gonna try anyway. Sorry for the noob question, What is a scaled integer ? I don't find it in arduino data types reference.

Scaled integer has to do with the interpretation not the storage type. For example how do I handle fractional dollars? Do I need float for a quantity like $3.14? No, because I can represent pennies instead, 314 cents. To use this method, you multiply by x before storing it, and divide by x when retrieving it. In that example, x = 100.

Floats could be used directly, the only issue is efficiency since the float calculations take longer, even if it's only an immediate conversion to some internal integer value.

The feature set you are aiming for, seems beyond what tone() can help you with. So you can have some fun with this using micros().

On the ESP32 you can generate different tones with +/- 1 Hz resolution, but it would be difficult to tell the difference.

Here's an example that plays all possible notes (non-blocking) where there's more than 30Hz difference between notes.

It is a relative thing so at 30 Hz it is a difference of 3%.
You can really hear that.
Bot most users will use a 2 inch speaker to play a melody. The lowest freq the speaker can generate will 110 Hz (now down to 1%), and the lowest note of a melody may be 220 Hz...
By the way, if you have two strings (of a guitar, one at 440 Hz and the other at 441 Hz) you can easily hear the interference (tone gets louder and softer each second).

1 Like

One should be able to modify the function source code to use float.

I haven't verified this, but "void Tone::play(uint16_t frequency, uint32_t duration)" would become "void Tone::play(float frequency, uint32_t duration)". I would explicitly round all assignments to "ocr" in that method rather than casting to an integer which will truncate.

The header file and calls in your code would be modified to have frequency as a float, matching the function source code.

This should improve frequency accuracy, particularly for the lower frequencies, but there is still going to be a frequency error due to the discrete divide values available from the timer.

The 'tone()' function seems to use Timer2 which is only an 8-bit timer. I think 31 Hz is the lower limit. I would use the 16-bit timer (Timer1) which can go down to about 0.25 Hz.

Here is an example I wrote of generating audio frequencies using Timer1. You type in a frequency as a floating-point value and it will give you the closest it can generate.

// Generating Variable-Frequency PWM on Timer1 of an Arduino UNO (Pins 9 and/or 10)
// Good for frequencies from 1 Hz to over 65 kHz.
// Written June 19th, 2020 by John Wasser

uint16_t TimerTOP = 0xFFFF;

bool SetTimer1Frequency(float frequency)
{
  byte prescaleBits; // 1, 2, 3, 4, 5
  uint16_t prescaleFactor;  // 1, 8, 64, 256, 1024
  uint32_t top32;

  // Clear the three clock select bits to stop the timer
  TCCR1B &= ~((1 << CS12) | (1 << CS11) | (1 << CS10));

  // Find the smallest prescale factor that will fit the TOP value within 16 bits.
  // frequency = F_CPU / (prescale * (TOP + 1))
  // TOP = (F_CPU / (prescale * frequency)) - 1;

  prescaleBits = 1;
  prescaleFactor = 1;  // Used for 123-65535 Hz
  top32 = (F_CPU / (prescaleFactor * frequency)) - 0.5;  // Round to next lower integer
  if (top32 < 1)
    return false;

  if (top32 > 65535UL) // Too many clocks to count in 16 bits?
  {
    prescaleBits = 2;
    prescaleFactor = 8;  // Used for 16-122 Hz
    top32 = (F_CPU / (prescaleFactor * frequency)) + 0.5;  // Round to nearest integer
    if (top32 > 65535UL) // Too many clocks to count in 16 bits?
    {
      prescaleBits = 3;
      prescaleFactor = 64;  // Used for 2-15 Hz
      top32 = (F_CPU / (prescaleFactor * frequency)) + 0.5;  // Round to nearest integer
      if (top32 > 65535UL) // Too many clocks to count in 16 bits?
      {
        prescaleBits = 4;
        prescaleFactor = 256; // Only used for 1 Hz
        top32 = (F_CPU / (prescaleFactor * frequency)) + 0.5;  // Round to nearest integer
        if (top32 > 65535UL) // Too many clocks to count in 16 bits?
        {
          prescaleBits = 5;
          prescaleFactor = 1024;
          top32 = (F_CPU / (prescaleFactor * frequency)) + 0.5;  // Round to nearest integer
          if (top32 > 65535UL) // Too many clocks to count in 16 bits?
          {
            return false;
          }
        }
      }
    }
  }

  float actualFrequency = F_CPU / float((top32 + 1) * prescaleFactor);

  Serial.print("Freq: ");
  Serial.print(frequency);
  Serial.print(" actual: ");
  Serial.print(actualFrequency);
  Serial.print(" prescale: ");
  Serial.print(prescaleFactor);
  Serial.print(" TOP: ");
  Serial.println(top32);

  TimerTOP = top32;

  OCR1A = TimerTOP / 2;
  OCR1B = TimerTOP / 2;

  ICR1 = TimerTOP;

  TCCR1B |= prescaleBits; // Set clock prescale bits to start the timer
  return true;
}

void setup()
{
  Serial.begin(115200);
  delay(200);

  digitalWrite(9, LOW);
  pinMode(9, OUTPUT);
  digitalWrite(10, LOW);
  pinMode(10, OUTPUT);

  // Stop Timer/Counter1
  TCCR1A = 0;  // Timer/Counter1 Control Register A
  TCCR1B = 0;  // Timer/Counter1 Control Register B
  TIMSK1 = 0;  // Timer/Counter1 Interrupt Mask Register

  // Set Timer/Counter1 to Waveform Generation Mode 14: Fast PWM with TOP set by ICR1
  TCCR1A |= (1 << WGM11);  // WGM=14
  TCCR1B |= (1 << WGM13) | (1 << WGM12);  // WGM=14
  TCCR1A |= (1 << COM1A1);  // Normal PWM on Pin 9
  TCCR1A |= (1 << COM1B1);  // Normal PWM on Pin 10

  SetTimer1Frequency(1000.0);  // Default to 1 kHz
}

void loop()
{
  if (Serial.available())
  {
    float frequency = Serial.parseFloat();
    if (!SetTimer1Frequency(frequency))
    {
      Serial.print("Frequency Set Failed: ");
      Serial.println(frequency);
    }

    delay(100);
    while (Serial.available())
    {
      Serial.read();
      delay(100);
    }
  }
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.