mathiasvr / bluejay

:bird: Digital ESC firmware for controlling brushless motors in multirotors
GNU General Public License v3.0
479 stars 50 forks source link

feat: Initial implementation of making startup tunes user configurable #8

Closed saidinesh5 closed 3 years ago

saidinesh5 commented 3 years ago

Right now we represent a startup tune using:

Eep_Pgm_Startup_Tune - An array of 128 bytes to store the startup melody

Bluejay Startup Melody has a structure like:

[bpm] [default octave] [default duration] [62 (Temp4, Temp3) values]
2 bytes 1 byte 1 byte 124 bytes

In general,

We then blindly loop through these pulses and call the wait/beep subroutine.

This should give us 62 musical notes, which I think should be enough for a start up routine.

The big advantage of doing it this way is that the bluejay configurator app does most of the logic of converting music to these tuples, and the whole implementation will be possible in under 200 bytes of firmware space

The main question now is: are 62 notes enough?

Fixes https://github.com/mathiasvr/bluejay/issues/3

Requires: https://github.com/mathiasvr/bluejay-configurator/pull/13

mathiasvr commented 3 years ago

This is not important right now but I will just share here that I think it would be nice if we could use an existing "melody syntax" in the configurator.

@stylesuxx introduced me to RTTTL and I see there is also MML.

saidinesh5 commented 3 years ago

@mathiasvr Yup. Already started extending https://github.com/adamonsoon/rtttl-parse to spit out Temp3, Temp4 values. Also I fixed the issues i was having with my React component. I was trying to base it on the Number component which uses:

    handleChange: function (component, value){}

and value was always undefined.

So just went with something from react documentation and everything started working.

handleChange: function(e) {
        this.props.onChange(e.target.name, parseInt(e.target.value));
}

Without further ado, the obligatory screenshot:

image

Now, do you happen to know any quick formula for me to convert a sound frequency value to the Temp3 register value of beep subroutine? Will save me a lot of time with my rtttl-parse functions.

Basically I have 2 problems left to solve now:

1) Parse rtttl notes into Eep_Pgm_Startup_Tune array 2) Parse Eep_Pgm_Startup_Tune into rtttl notes for easy editing. The latter seems to be tricky because there is a many to one mapping of rtttl notes -> Eep_Pgm_Startup_Tune array, because of having to split up values > 255.

mathiasvr commented 3 years ago

@saidinesh5 Nice work, looks good.

Now, do you happen to know any quick formula for me to convert a sound frequency value to the Temp3 register value of beep subroutine? Will save me a lot of time with my rtttl-parse functions.

I can try to see if I can come up with something.

Basically I have 2 problems left to solve now:

  1. Parse rtttl notes into Eep_Pgm_Startup_Tune array
  2. Parse Eep_Pgm_Startup_Tune into rtttl notes for easy editing. The latter seems to be tricky because there is a many to one mapping of rtttl notes -> Eep_Pgm_Startup_Tune array, because of having to split up values > 255.

What do you mean by splitting up values?

mathiasvr commented 3 years ago

Based on some old data I had, I made this very crude approximation:

Temp3 = Math.round(328687 / freq**1.3746)

Maybe it can be used as a starting point.

saidinesh5 commented 3 years ago

@mathiasvr wow. That's a surprisingly good approximation. Needs a little tuning but can do that later by brute forcing all possible Temp3 values and map them to the note frequencies. Shouldn't be too difficult, since we are dealing with an 8 bit word. And splitting up values as in, this is the algorithm I am using to convert rtttl to the startup array:

/**
 * Parse RTTTL to Bluejay ESC startup tone data
 *
 * @param {string} rtttl - RTTTL String
 * @returns an array of (Number of pulses, Pulse width) tuples
 */
static parseToBluejayStartupTone(rtttl, startupToneLength) {
  let parsedData = Rtttl.parse(rtttl);
  startupToneLength = startupToneLength || 128;

  const MAX_ITEM_VALUE = 2**8;
  // Melody is basically an array of [{ duration(in ms): number, frequency (in Hz): number }]
  let melody = parsedData.melody;  
  let result = new Uint8Array(startupToneLength);

  function frequencyToBluejayTemp3(freq) {
  // TODO: Later on make this more accurate using brute force
    return Math.round(328687/freq**1.3746);
  }

  var currentResultIndex = 0;
  var currentMelodyIndex = 0;

  while(currentMelodyIndex < melody.length && currentResultIndex < result.length) {
    var item = melody[currentMelodyIndex];
    if (item.frequency != 0) {
        // temp3 is a measure of wavelength of the sound
        let temp3 = frequencyToBluejayTemp3(item.frequency);

        if (temp3 > 0 && temp3 < MAX_ITEM_VALUE) {
            let duration_per_pulse_ms = 1000/item.frequency; //(25 + temp3*200*25/150)/1000;
            let pulses_needed = Math.round(item.duration / duration_per_pulse_ms);

            while (pulses_needed > 0 && currentResultIndex < result.length) {
                result[currentResultIndex++] = pulses_needed%MAX_ITEM_VALUE;
                result[currentResultIndex++] = temp3;
                pulses_needed = Math.floor(pulses_needed/MAX_ITEM_VALUE);
            }
        } else {
            console.warn("Skipping note of frequency: ", item.frequency)
        }
    } else {
        // Can wait from 1-255ms for each (Temp3, Temp4) tuple
        // So split up a single silent note, if we have to
        let duration = Math.round(item.duration);

        while (duration > 0 && currentResultIndex < result.length) {
            result[currentResultIndex++] = duration%MAX_ITEM_VALUE;
            result[currentResultIndex++] = 0;
            duration = Math.floor(duration/MAX_ITEM_VALUE);
        }
    }

    currentMelodyIndex++;
  }

  if (currentMelodyIndex < melody.length) {
      console.warn("Only " + currentMelodyIndex + " notes out of " + melody.length + " fit in the startup sequence");
  }

  return result
}

If the duration can't fit in one Temp4 variable, we split a single rtttl note into multiple (Temp4, Temp3) tuples. That means we lose the original Rtttl data during this conversion process. On top of that, if a given 1/4th note duration can be achieved by a tempo of x bpm, we can create the same note duration using a 1/8th note and double the tempo.

So right now my inverse function looks like:

static parseBluejayStartupToneToRtttl(startupTone) {
    // First glob together items we had to split up due to the 8 bit limit

    // Convert the array of [(Temp4, Temp3)] to an array of [{duration: number, frequency: number}] . This is kind of like the coin change problem now. But have to first find out what the coins are, before we can go down the path of a greedy approach

    //return rttf format as a string: 'Name:Defaults:rtttl'
}
mathiasvr commented 3 years ago

@saidinesh5 It's possible to change the range of Temp3/Temp4 if preferable, I already did this once before. We could also encode the duration differently to avoid duplicating notes? Anyway, looks good so far, do you know what is the typical range of frequencies/notes and durations we need to support?

I can also make a more accurate formula by measuring the frequency produced by the beep routine. This should be easier than brute force even though i'm not entirely sure what you had in mind.

Also, I shipped a version of the configurator that uses eeprom revision 202 for something else, my bad.

saidinesh5 commented 3 years ago

Oh that's interesting. Originally i was trying to write the decoder completely in assembly but it got annoying very quickly to look up frequencies and calculate note durations in assembly, so thought we can do it nicely in configurator, with proper error checking - that way the ESC can blindly play what configurator gives it.

These are the tempos (in beats per minute) rtttl tones usually use:

[
      '25', '28', '31', '35', '40', '45', '50', '56', '63', '70', '80', '90', '100',
      '112', '125', '140', '160', '180', '200', '225', '250', '285', '320', '355',
      '400', '450', '500', '565', '635', '715', '800', '900'
]

So the shortest note is: 1/32 * (60000/900) ~ 2ms And the longest note is: 3/2 * (60000/25) = 3600ms

As for revision 202 - no worries. Can change mine to 203. Have to squash this branch and rename commits to make this branch mergable anyway.

I opened a pull request of the configurator and added some TODOs there in case you want to look into this: https://github.com/mathiasvr/bluejay-configurator/pull/13

mathiasvr commented 3 years ago

I have looked more into rtttl and I think the range of allowed durations should be calculated like this:

Shortest: 60000 / 900 * 4/32 = 8.3ms Longest: 60000 / 25 * 4 = 9.6s

I might try to update the beep routine to better accommodate musical notes to simplify how we process and store notes/timings.