TCastleTimer with Sound

Hi,

I am trying to create a simple metronome app for Android. It should play a short sound with very short intervals.
Max it should be 250 sound plays per minute.
Hence the shortest play duration is 60/250 = 0.24s.
I created a WAV file with beat sound. It’s duration: 0.15s.

The problem is that SoundEngine is playing sounds with latencies.
Thus when I press play should I hear the the beats are not accurate.
What I tried:
I tried to create TCastleTimer with CounteractDelays := true;
Also I tried it with CounteractDelays := false; the results are the same.
To play sounds I tried both:
SoundEngine.PlaySound(Buffer);
and
SoundEngine.Sound(SoundType);

Also I tried it without TCastleTimer:
On WindowUpdate I analyze Timeout and then fire play sound event.

Thus I think probably the TCastleTimer is accurate. But playing sound has some starting latency. Is there any way to improve it?

On Windows all variants/combinations of PlaySound/Sound, TCastleTimer/WindowUpdate are working fine. I hear the very accurate beats.
But I tried two Android phones: one is old and slow and another is new and fast and both phones plays sounds with not precise intervals.

I think to implement precise timing (as in metronome app) you should use something better than TCastleTimer - I’ll try to put together an example, but I’m busy right now. TCastleTimer is designed for implementing game logic or recurring events, not precise time intervals with accuracy maybe higher than the current FPS.

First of all, make sure that WAV file doesn’t have an internal delay (that’s the problem I most often have myself :)). As soon as you’ve created it yourself it shouldn’t have, but just make sure.

Normally I’d expect TCastleTimer to fail maintain difference between 119 and 120 bpm, but not an audible delay over 0.03ms.

I think about pre-rendering sound. For example, I may need to create 1 minute of pre-rendered beats.

Is there any way to append/concatenate several sound in TSoundBuffer? Or append: beat+silence, beat+silence etc.

Or do you know any good Pascal libs where I could do sound manipulations like joining/cutting etc? And do it in-memory.
And then simply play the rendered sound.

I don’t think that’s a good idea, neither it’ll solve the problem (as you can’t pre-render an infinite sequence of sounds and will face the same problem when the pre-rendered sequence ended). I’ll see what can be done here in around 6-10 hours.

The overall idea that I’ll try to follow would be this way:

  1. Create a TCastleUserInterface derivative.
  2. Override Update event.
  3. Observe if NextBeatTime is between two Update events - and play sound there + calculate NextBeatTime for the next beat.
  4. Maybe predict next frame length to double the accuracy.

This should provide no additive errors in beat times with average accuracy around 0.5/FPS → 0.25/FPS (if approach 4 would work).

As I wrote: I also tried some things with Window Update event. Maybe there should be a better prediction algo. But I could not get good results. Thus I will try to move to “pre-rendering” direction.

Be sure to set CounteractDelays as true on TCastleTimer. This is a ready mechanism for dealing with delays :slight_smile: You can see examples/user_interface/timer_test/timer_test.lpr, TCastleTimer (with CounteractDelays) can be very accurate so I would encourage to use it.

What you use are proper way to play sound without delays ( SoundEngine.PlaySound(Buffer) and SoundEngine.Sound(SoundType)). Hm, that said, I never tried to make a metronome application, so I didn’t test whether there’s something problematic/slow on Android.

Possibly OpenAL is not just performing fast enough on Android, in which case there’s little we could do on CGE. Maybe using FMOD ( FMOD · castle-engine/castle-engine Wiki · GitHub ) would be a solution (albeit we don’t yet have it ready on Android, but it would be trivial to fix).

I said I tried to use CounteractDelays as true and CounteractDelays as false without any difference. Probably it’s a bad idea to start PlaySound() or Sound() about 2 times a second. So I will try with sound pre-rendering and then playing it in a loop.

Did a test for metronome. metronome.zip (7.7 KB)
APK: https://drive.google.com/file/d/1R9GGeT-0HpcBTZKppnvCTmwh3gLpISMP/view?usp=sharing

Yeah, it works like a charm on Desktop, but fails miserably on Android 4.2.2. The metronome beats are off-sync with render and come at uneven intervals.

Hm, this would suggest a delay in OpenAL. Since CGE display is regular on Android as far as I understand (I didn’t test on Android now, only saw it is cool on desktop). 2 ideas:

  1. Please test what happens in TOpenALSoundSourceBackend.Play. There’s a line

    while CompleteBuffer.ALBuffer <> alGetSource1ui(ALSource, AL_BUFFER) do
      Sleep(10);
    

    It was a workaround for Apple OpenAL implementation, and usually this loop should not execute even once… but if it does, it would explain some delay. Please add some WritelnLog to log when this Sleep(10) happens and see on Android.

  2. Otherwise, my only idea is to just blame OpenAL and try FMOD. This will require adding fmod integration on Android, as an Android service. FMOD · castle-engine/castle-engine Wiki · GitHub

Eugene,
Thank you for sharing the code with interesting algorithm. I confirm that on Windows it works fine. But my code with simple TCastleTimer also worked very good on Windows.
Oon Android the performance of your code is good, but it’s not ideal. There are still some delays. You may compare on Google Play, for example, B’Metronome which sounds ideally. It plays ideally even on 220 BPM.

I trying to comment this code and then recompiled Eugene’s code for Android. I did not notice any significant improvements. Maybe FMOD will be better. But maybe the sound kernel on Android is organized in such way that it’s not possible to accurately call playing sound functions twice a second or something.

There are a few things to try. Quick duckduckgoogling reveals that this is a known issue of OpenAL (at least it looks like so) and there are workarounds this problem. But that needs experimenting, so I’m not sure how quickly I or somebody else would be able to do the tests and come up with a nice solution.