I am working on a music game for PC. A song plays, notes fall down the screen, the player hits corresponding buttons at the correct time. Standard music game stuff. Some notes are meant to produce effects on the music, for which I am using fmod DSPs. The issue I am having is when I produce a stutter effect. So the user holds a button, and while that button is held the track stutters with a frequency determined by its tempo and a few other parameters. Everything works fine except sometimes, even when I provide identical parameters for FMOD_DSP_TREMOLO_PHASE, the phase is a little off by what appears to be a random amount.
So for example let’s say the song is 60bpm and there is one of these stutter notes at the 4th beat into song, and it lasts for 4 beats, and it is meant to stutter at 16th note speed. This stutter wave is square. The main update loop is processing and I received a signal that the user has began holding one of these stutter buttons. So now I need to activate the tremolo DSP. Some relevant code:
[code:30y9bu5z]double freq = (m_bpm / SECONDS_PER_MINUTE) * m_beat;
if(freq < 0.1) freq = 0.1;
if(freq > 20.0) freq = 20.0;
double stutterStartTime = m_pObject->getTime() + SoundManager::getInstance()->getSongStartTime();
double currentTime = WKDXApplication::getInstance()->getAbsoluteTimeS();
double phase = (1.0 – m_duty) * 0.5;
phase += (currentTime – stutterStartTime) * freq;
while(phase >= 1.0) phase -= 1.0;
while(phase < 0.0) phase += 1.0;
result = m_pDSP->setParameterFloat(FMOD_DSP_TREMOLO_PHASE, (float)phase);
Here m_bpm would be 60.0, SECONDS_PER_MINUTE is 60.0, m_beat is 4.0 (since a 16th note divides a beat in 4/4 4 times). m_pObject is an object with data for the button that was pressed so getTime() returns the time that it started (not the time the user hit it, but the time into the song that it should have began, so in this case 4.0 since it is the 4th beat into the song at 60bpm). getSongStartTime() returns a timestamp of the time I told fmod to play the song. getAbsoluteTimeS() returns the timestamp now. Both of these times are in seconds and computed using QueryPerformanceCounter. I use QueryPerformanceCounter to determine elapsed time before calling my main update loop etc.
So originally I set the phase to (1.0 – m_duty) * 0.5; This is because it is a square wave and I want the beginning of the "on" part of that square to start immediately (as opposed to having that square peak centered within the period of the tremolo). But since this is an update loop, we have overshot the actual time that this button was supposed to have started, so we need to shift the phase so that the stutter does not start late. Hence this line: phase += (currentTime – stutterStartTime) * freq; Now the phase should be exactly on. I’ve logged extensive information to the console about all these values while playing back the song and all the values always look correct. I am as confident I can be that the phase I am providing fmod is extremely accurate at the time I am providing it. However, sometimes when providing the same phase, the stutter is completely off.
How do I know it is off? I log the wav output to a file and compare it against a pre-built track I have that stutters perfectly the entire time. It is very easy to tell if the stutters line up. I have a test song that just turns the stutter on and off every measure. Some measures it is dead on, some it is off. Even when checking the console output, and comparing that to the output waveform I can tell that near identical phases are producing drastically different results in the resulting sound.
So a question you might be thinking now is, why am I setting the phase when the button is pressed instead of just setting it once in the beginning of the song when I create the DSP? By the way, doing it that way works perfectly. It sounds amazing, and when I compare the waveforms they are in sync the entire time. But I cannot do it this way because in my game users will be creating their own songs and charts (the patters of the notes, and the parameters for the effects they trigger). Songs can have tempo changes throughout. So the same 16th stutter at time A could be at a completely different phase than one at time B if there are tempo changes between A and B. I know, this sounds silly, or like abusive user behavior but I have been an avid member of the music game scene for a very long time, and I make charts for other games like this and I can personally attest to that it is extremely popular to make charts that have gimmicks in them using tempo modification (so the track appears to wobble in a dubstep song etc.). It is not uncommon for a song to have hundreds of tempo changes throughout. I absolutely need to support this.
Also, I am NOT going to support changing parameters to a stutter effect once it has started. For example if a user puts a 16th stutter on at 60 bpm then the tempo doubles while the stutter is running I am not going to increase the frequency of the stutter. It stays what it was when it was supposed to have started.
So given my requirements the best solution seems to be setting the phase when the button is pressed since I know the time now and the time the button was supposed to have started. The fmod documentation says setting the phase should be "instantaneous" but all my results point to that it is not actually instantaneous. fmod internally seems to waiting some short indeterminate amount of time before it actually applies that phase modification. I can’t in any other way explain how the phase seems to fluctuate randomly with multiple calls with the same phase.
So my question: Is the method I am using a viable solution with fmod, or do you see something I am doing incorrectly? BTW I have tried many kludgy work-arounds that don’t seem to do anything (calling update() on the fmod System before and after setting the phase, trying to turn on and off the tremolo DSP using setBypass, trying to turn on and off the tremolo DSP by just leaving it running and changing the duty to 1.0 to ‘turn it off’ the putting it back to the real value to ‘turn it on’). Again, I have logged lots of information for the timing values I use and the values I am providing to fmod and everything looks great on my end.
Any help would be greatly appreciated. I am really impressed with fmod and I’ve been able to add a lot of functionality to my project really easily using it. This is the only hiccup I’ve had so far.
- Daki asked 5 years ago
I have implemented a work-around to this issue where I compute up front all the phases a certain song will require for its stutter effects based on all the BPM changes and where the stutter notes fall, then I create a tremolo DSP for each unique phase and start them all at the same time along with the song, then turn them on and off as needed. This is pretty kludgy but seems to be the only solution, and I have to imagine tremolo DSPs are computationally very cheap.
I still feel like the behavior I described previously is a bug with fmod and if anyone with insight could provide some comments I would appreciate the feedback.
- Daki answered 5 years ago
Tremelo DSP parameter changes are applied at the start of the next mix block. The FMOD mixer is running asynchronously in it’s own thread, waking periodically to refill the output buffer as it is consumed by the OS audio driver.
It you wish to better synchronize with the mixer try using the functions System::lockDSP() and System::unlockDSP().
Thanks for the reply (and for reading my wall of text)!
What you are saying makes perfect sense and I understand that fmod internally needs to wait until it is prompted to refill the buffer. I would have thought though that since I tell fmod at time t1 to set the phase to x and then fmod at time t2 applies the phase and refills the buffer it could shift the phase in accordance with the delay, so at t2 instead of setting the phase to x it would set it to x + (t2 – t1) * frequency, since fmod could get both t1 and t2 and the frequency is known since it is a property of the DSP. This way the phase provided at t1 would actually be instantaneous.
- Daki answered 4 years ago
Please login first to submit.