nathanRamaNoodles / Noodle-Synth

A User-friendly Arduino/Teensy Library to play RTTL/MIDI with infinite polyphonic notes; it has full control over volume, pitch, and music. No shields needed(Just a speaker).
https://create.arduino.cc/projecthub/nathan_ramanathan/play-music-without-delay-40e51b
MIT License
139 stars 14 forks source link

Pull song contents from I2C EEPROM #17

Closed trentdye closed 4 years ago

trentdye commented 4 years ago

Hi, this library is one of the best sound generating libraries for Arduino that I have come across -- I especially get a kick out of the RAMP sound and how closely it can mimic an old-school gaming device.

I am looking to fork this library so that it will pull data from an i2c eeprom (the 24LC256) instead of PROGMEM. My goal is to place that i2c eeprom on a removable cassette, so that physically switching the cassette would change the song. To achieve this, I only expect to have to change a few things in order to get this working:

  1. Change all instances of pgm_read_byte_near() to request data from the i2c eeprom instead.
  2. Change all instances of strlen_P() to request the length of data.
  3. Change the musicWithoutDelay constructor and the newSong() function minimally, so that you can pass the integer byte location of the song in memory instead of passing a pointer to a char array.

Are there other functions within the library that touch PROGMEM or core PROGMEM functions, other than the ones that I highlighted?

Thanks a bunch, Trent

nathanRamaNoodles commented 4 years ago

Yes, that seems to be all of the related PROGMEM calls. Before I used PROGMEM, I used a simple char array and used an integer to track location similar to your third requirement. I think that version is v2.0.0 of this library.

But anyway, I'm sure it will translate over to your cassette idea. Have fun :)

Also, I've been delaying one key feature of this library; reading a midi array. MIDI is more universal than RTTL and making MIDI songs are way easier than RTTL.

trentdye commented 4 years ago

Awesome, thank you :) Good luck with the MIDI integration!

trentdye commented 4 years ago

Sorry for closing and opening and closing this again -- not very skilled at Github. I have an issue related to skipsolver() but I'm trying to gain as much information as I can before asking.

nathanRamaNoodles commented 4 years ago

Hey Nathan, I was able to get data streaming from the I2C EEPROM, and it can do so quickly enough to play the Zelda example song at its defined speed. I am currently debugging an issue where the newSong function takes roughly 30 seconds to execute, and calls the pgm_read_byte_near() function about 100,400 times. I was wondering wh

I had this problem a long time ago, and I can tell you it's definitely the header being fed in incorrectly. Create a fork and share the link to the code. Maybe I can guide you...

trentdye commented 4 years ago

What do you mean by header? I just did some sleuthing and figured out that we're getting stuck in this while loop: https://github.com/nathanRamaNoodles/Noodle-Synth/blob/619c0f85cb464fcf651a27271455619d1d7b8bbc/src/MusicWithoutDelay.cpp#L720 and it keeps calling the read-progmem/eeprom command until the argument goes past 32,768. I can't quite understand what we're trying to scan for in this loop, especially the last condition pgm_read_byte_near(mySong+skipCount)!=strlen_P(mySong) but the expression seems to keep evaluating true over and over again.

When I'm not getting stuck in that loop, I'm getting stuck in some of those adjacent while loops. I'd love to fool around with SkipSolver but I'm not actually sure what the purpose of this function and these while loops are.

I'm trying to play 2 tracks, and my memory device has 32Kbytes of storage, so I'm storing one track as a bunch of char's starting at memory location 2 and the other at 16002. As such, I don't call newSong using a pointer -- I changed newSong to take a uint16_t argument for where I define the song tracks to begin in memory. All of the other bytes in the memory device are 0x00. I'm not sure how skipSolver acts when it encounters 0x00 -- is there a better choice of byte that would better evade these infinite loops?

Many thanks for your help Nathan. I will fork my code but I've made a lot of changes so I'm worried that you won't be able to get to the bottom of what actually might be causing the problem.

trentdye commented 4 years ago

OK my code is up at https://github.com/trentdye/Noodle-Synth

nathanRamaNoodles commented 4 years ago

The header is the settings for the rttl string. Like ":o=4:c, b, d" will play a "c, b" and "d" note at the 4th octave. The header is decrypted by the newSong function.

A little background on skipSolver:

The skipSolver() is not necessary for the library to work. It is a helper function for skipTo(). Skipto is a complex function I wrote when I was first learning about doing stuff at the same time without blocking other code operations. These days we have more powerful microcontrollers that use FreeRTOS that allow multi - threading functions. Unfortunately, a arduino uno can't use FreeRtos reliably and efficiently, so I decided to do it the old fashioned way. "Current time minus the previous time ". After I got that mastered, I wanted to sync all instruments together and play at the same location of the song without latency. It was a huge task but I pulled some things from my pause() function and eventually got it working.

Anyway, story time aside, the while loop you mentioned is adding every note until the calculated time is >= to the time we want to skip to. And if we input a time greater than the entire total time of the song, we simply skip to the end. You can get rid of the skipto function if you want. A possible problem could be that the length of the song is incorrect? The length of the song is the end minus the setting's header. So, ":o=4:c, b, d" the length is 5 which is "c, b, d".

nathanRamaNoodles commented 4 years ago

Oh yeah, skipsolver also gives the total time of the song since skipping to the end is basically the whole song in milliseconds

trentdye commented 4 years ago

Thank you for the explanation, that clears a lot up! Getting rid of skipSolver and skipTo entirely is tempting (I removed it and it fixed the problem immediately and took about 1/10 of the time for newSong() to complete), but would require removing the calls to it in pause(), play() and reverse().

It's a tentative solution, but I'd still love to utilize pause and reverse for what I'm trying to do.

trentdye commented 4 years ago

A possible problem could be that the length of the song is incorrect? The length of the song is the end minus the setting's header. So, ":o=4:c, b, d" the length is 5 which is "c, b, d".

Might be it! Do you know where this is in the code? I discovered that my custom function to replace strlen_P does not quite work in the same way as strlenP. If newSong is the pointer to the char array, strlen_P(newSong) will return the length of the entire character array, whereas strlen_P(newSong+1) will return the length of the character array minus one. Is this where the problem stems from? The snag is that I couldn't find anywhere where you called strlen_P with any argument other than mySong, and I couldn't find anywhere where you moved the mySong pointer.

nathanRamaNoodles commented 4 years ago

pLoc is used as a location reference to the "actual" start of the song.

  1. https://github.com/nathanRamaNoodles/Noodle-Synth/blob/master/src/MusicWithoutDelay.cpp#L134
  2. https://github.com/nathanRamaNoodles/Noodle-Synth/blob/master/src/MusicWithoutDelay.cpp#L196

Also, how does readEEPROM work? Have you tried testing with smaller char arrays to see if the length corresponds to your getLength function?

Also, to make debugging easier, I would suggest adding a boolean variable to the class called, EEPROM_MODE and then in your readEEPROM function, if EEPROM_MODE is true, it will use your getLength and Wire library, otherwise it will use my pgm_read_near_byte and strlen_P methods. This way if you want to test whether the locations are right, you have an example already.

nathanRamaNoodles commented 4 years ago

Also, after looking at the code I believe strlen_P is the actual length including the header. So ":o=4:c, b, d" the length is 10. The only magic of knowing the start of the first note is the pLoc location.

trentdye commented 4 years ago

Hi Nathan, I was able to solve the problem by changing my definition of getLength (which is called to determine the length of the char array) to more closely resemble the strlen_P function. I did this by creating a new sketch where I parsed my EEPROM (programmed with the Zelda song) and the same song programmed into PROGMEM, byte by byte, saw that they were == identical (validating the ReadEEPROM function I wrote -- so the problem must be in getLength), and then looking at what strlen_P returns for values greater than newSong -- it turns out that it returns the number of characters between this memory location and the end of the char array. I still don't know where strlen_P is used in this dynamic sort of fashion, but I stopped getting stuck in while loops.

Thanks for all the help on this issue.

nathanRamaNoodles commented 4 years ago

Congrats! Could you show a video of it working. I'm kinda curious about the result. :)

trentdye commented 4 years ago

I'm rushing to get these cassette player PCBs out before christmas, once that's all done I'll be sure to document this in a video for you. One thing about this i2c eeprom implementation is that you can definitely see how read speed affects the functionality. From running the Zelda demo, I was under the impression that newSong wasn't doing any heavy lifting at all, but from literally counting all the i2c reads that are done, there's a lot going on in that function -- a lot of blocking before you get the non-blocking functionality back! :-) I've done all the necessary things like choosing the proper pullup and setting the i2c frequency to 400K, and they've done a little to help me out.

I'm still working out some little quirks -- for example going from forwards to rewind and back again causes skipSolver pauses and sometimes causes the whole engine to crash -- but I'm pretty sure those are all the fault of the i2c reads or this 3.3V ATMEGA328P being fairly slow. I'm so glad the big problem was resolved though!