Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Apr 4, 2021

Conversation

saidinesh5
Copy link
Contributor

@saidinesh5 saidinesh5 commented Feb 14, 2021

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,

  • The first 4 bytes of the byte array are considered metadata and are ignored by the parer in the ESC firmware.
  • Temp3 is the pulse duration - a measure of the frequency of the note. Temp 3 == 0 => This is a pause note.
  • Temp4 is the number of pulses - a measure of the duration of the note. Temp4 == 0 => Terminate the startup melody loop

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 #3

Requires: mathiasvr/bluejay-configurator#13

@saidinesh5 saidinesh5 mentioned this pull request Feb 14, 2021
@saidinesh5 saidinesh5 changed the title Initial implementation of making startup tunes user configurable feat: Initial implementation of making startup tunes user configurable Feb 14, 2021
Bluejay.asm Outdated Show resolved Hide resolved
@mathiasvr
Copy link
Owner

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
Copy link
Contributor Author

saidinesh5 commented Mar 13, 2021

@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
Copy link
Owner

@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
Copy link
Owner

mathiasvr commented Mar 13, 2021

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
Copy link
Contributor Author

@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
Copy link
Owner

@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
Copy link
Contributor Author

saidinesh5 commented Mar 15, 2021

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: mathiasvr/bluejay-configurator#13

We now support startup tunes of length 64 notes*
Each note is a tuple of (number of pulses, pulse duration)
i.e (Temp4, Temp3) register values of beep subroutine

An array of 128 values is reserved at CSEG 1A70 for this feature
The array is configurable via. Bluejay configurator - which does
most of the heavy lifting
Bluejay.asm Outdated Show resolved Hide resolved
@mathiasvr
Copy link
Owner

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.

saidinesh5 and others added 6 commits March 29, 2021 05:27
Reserve the first 4 bytes of Eep_Pgm_Startup_Tune for metadata.
This reduces the maximum number of notes to 62, but will make editing
experience a lot more plesant as we no longer have to guess tempo to
reconstruct the Rtttl.
@saidinesh5 saidinesh5 marked this pull request as ready for review April 3, 2021 23:46
@mathiasvr mathiasvr merged commit 3b355fb into mathiasvr:main Apr 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Custom beep melodies
2 participants