[A5] Playing Samples on the Fly?
jmasterx

In my game, I have lots of short sounds that will be event triggered. However, I don't want to load them all into memory, instead, I just want to load, play, and destroy them after they are done playing.

Is there a way to do this? Mainly, is there an event I can receive when a sample has finished playing?

I can do it via polling but that feels like a big hack.

Thanks

Edgar Reynaldo

Not currently, no.

Which part of Allegro would fire off the audio events? The mixer, or the voice? I suppose it would have to be the mixer. It should have a list of SAMPLE_IDS that it is playing, and it should be able to detect when that sample is done playing. But when is the event fired? During the buffer update? That could lead to destroying a sample before it is finished mixing. Remember the sample that quit and post it to a queue of events to fire off before the next mixer update?

I'm sure allegro could be hacked to do this, but I haven't looked at the actual code. There's probably a mixer callback controlled by a timer in a separate thread.

I personally would like to see this happen too. No promises, but I'll try to take a look at the code to at least see how feasible this is, and what would need to be done.

jmasterx

Yeah, I guess it's a bit trickier the way Allegro is designed. When I coded on ios a few months ago, every AVAudioPlayer object can attach a listener (AVAudioDelegate) and you get a callback when the player finishes playing.

But that doesn't involve a mixer and causes things like global gain to be trickier to implement.

it would be nice to have something like:

al_register_sample_instance_callback((void* callback(ALLEGRO_SAMPLE_INSTANCE* spl, ALLEGRO_MIXER* mixer, bool finished));

So if the sample gets destroyed, it could dispatch the callback with finished = false. But this probably would create all kinds of problems I'm not considering.

Edgar Reynaldo

I think the magic happens in MAKE_MIXER, specifically in fix_looped_position.

kcm_mixer.c#SelectExpand
280#define MAKE_MIXER(NAME, NEXT_SAMPLE_VALUE, TYPE) \ 281static void NAME(void *source, void **vbuf, unsigned int *samples, \ 282 ALLEGRO_AUDIO_DEPTH buffer_depth, size_t dest_maxc) \ 283{ \ 284 ALLEGRO_SAMPLE_INSTANCE *spl = (ALLEGRO_SAMPLE_INSTANCE *)source; \ 285 TYPE *buf = *vbuf; \ 286 size_t maxc = al_get_channel_count(spl->spl_data.chan_conf); \ 287 size_t samples_l = *samples; \ 288 size_t c; \ 289 int delta, delta_error; \ 290 SAMP_BUF samp_buf; \ 291 \ 292 BRESENHAM; \ 293 \ 294 if (!spl->is_playing) \ 295 return; \ 296 \ 297 while (samples_l > 0) { \ 298 const TYPE *s; \ 299 int old_step = spl->step; \ 300 \ 301 if (!fix_looped_position(spl)) \ 302 return; \ 303 if (old_step != spl->step) { \ 304 BRESENHAM; \ 305 } \ 306 \ 307 /* It might be worth preparing multiple sample values at once. */ \ 308 s = (TYPE *) NEXT_SAMPLE_VALUE(&samp_buf, spl, maxc); \ 309 \ 310 for (c = 0; c < dest_maxc; c++) { \ 311 ALLEGRO_STATIC_ASSERT(kcm_mixer, ALLEGRO_MAX_CHANNELS == 8); \ 312 switch (maxc) { \ 313 /* Each case falls through. */ \ 314 case 8: *buf += s[7] * spl->matrix[c*maxc + 7]; \ 315 case 7: *buf += s[6] * spl->matrix[c*maxc + 6]; \ 316 case 6: *buf += s[5] * spl->matrix[c*maxc + 5]; \ 317 case 5: *buf += s[4] * spl->matrix[c*maxc + 4]; \ 318 case 4: *buf += s[3] * spl->matrix[c*maxc + 3]; \ 319 case 3: *buf += s[2] * spl->matrix[c*maxc + 2]; \ 320 case 2: *buf += s[1] * spl->matrix[c*maxc + 1]; \ 321 case 1: *buf += s[0] * spl->matrix[c*maxc + 0]; \ 322 default: break; \ 323 } \ 324 buf++; \ 325 } \ 326 \ 327 spl->pos += delta; \ 328 spl->pos_bresenham_error += delta_error; \ 329 if (spl->pos_bresenham_error >= spl->step_denom) { \ 330 spl->pos++; \ 331 spl->pos_bresenham_error -= spl->step_denom; \ 332 } \ 333 samples_l--; \ 334 } \ 335 fix_looped_position(spl); \ 336 (void)buffer_depth; \ 337} 338 339MAKE_MIXER(read_to_mixer_point_float_32, point_spl32, float) 340MAKE_MIXER(read_to_mixer_linear_float_32, linear_spl32, float) 341MAKE_MIXER(read_to_mixer_cubic_float_32, cubic_spl32, float) 342MAKE_MIXER(read_to_mixer_point_int16_t_16, point_spl16, int16_t) 343MAKE_MIXER(read_to_mixer_linear_int16_t_16, linear_spl16, int16_t) 344 345#undef MAKE_MIXER

kcm_mixer.c#SelectExpand
165/* fix_looped_position: 166 * When a stream loops, this will fix up the position and anything else to 167 * allow it to safely continue playing as expected. Returns false if it 168 * should stop being mixed. 169 */ 170static bool fix_looped_position(ALLEGRO_SAMPLE_INSTANCE *spl) 171{ 172 bool is_empty; 173 ALLEGRO_AUDIO_STREAM *stream; 174 175 /* Looping! Should be mostly self-explanatory */ 176 switch (spl->loop) { 177 case ALLEGRO_PLAYMODE_LOOP: 178 if (spl->loop_end - spl->loop_start != 0) { 179 if (spl->step > 0) { 180 while (spl->pos >= spl->loop_end) { 181 spl->pos -= (spl->loop_end - spl->loop_start); 182 } 183 } 184 else if (spl->step < 0) { 185 while (spl->pos < spl->loop_start) { 186 spl->pos += (spl->loop_end - spl->loop_start); 187 } 188 } 189 } 190 return true; 191 192 case ALLEGRO_PLAYMODE_BIDIR: 193 /* When doing bi-directional looping, you need to do a follow-up 194 * check for the opposite direction if a loop occurred, otherwise 195 * you could end up misplaced on small, high-step loops. 196 */ 197 if (spl->loop_end - spl->loop_start != 0) { 198 if (spl->step >= 0) { 199 check_forward: 200 if (spl->pos >= spl->loop_end) { 201 spl->step = -spl->step; 202 spl->pos = spl->loop_end - (spl->pos - spl->loop_end) - 1; 203 goto check_backward; 204 } 205 } 206 else { 207 check_backward: 208 if (spl->pos < spl->loop_start || spl->pos >= spl->loop_end) { 209 spl->step = -spl->step; 210 spl->pos = spl->loop_start + (spl->loop_start - spl->pos); 211 goto check_forward; 212 } 213 } 214 } 215 return true; 216 217 case ALLEGRO_PLAYMODE_ONCE: 218 if (spl->pos < spl->spl_data.len) { 219 return true; 220 } 221 spl->pos = 0; 222 spl->is_playing = false; 223 return false; 224 225 case _ALLEGRO_PLAYMODE_STREAM_ONCE: 226 case _ALLEGRO_PLAYMODE_STREAM_ONEDIR: 227 if (spl->pos < spl->spl_data.len) { 228 return true; 229 } 230 stream = (ALLEGRO_AUDIO_STREAM *)spl; 231 is_empty = !_al_kcm_refill_stream(stream); 232 if (is_empty && stream->is_draining) { 233 stream->spl.is_playing = false; 234 } 235 236 _al_kcm_emit_stream_events(stream); 237 238 return !(is_empty); 239 } 240 241 ASSERT(false); 242 return false; 243}

In the case of ALLEGRO_PLAYMODE_ONCE fix_looped_position returns false. This could be used as a signal by MAKE_MIXER to fire a sample over event. You would also have to account for ALLEGRO_PLAYMODE_LOOP and BIDIR too, but fix_looped_position returns true for those, since they haven't stopped playing yet.

Thomas Fjellstrom

I've always thought that the audio add-on needed to fire off events. But that never happened.

Edgar Reynaldo

Well, I think it makes sense to emit events when a sample / stream is over / loops.

SiegeLord

My problem with using events for this is that it satisfies this use case only, and no other use cases. E.g. what if you wanted to play a second sample as soon as the first ended? You can't use an event for that, as there will a gap between when the sample ends and when the event is processed.

So from my point of view, this is where you pull out the good old ALLEGRO_AUDIO_STREAM and stream the samples yourself.

EDIT: The backends do have a finite buffer that provides you some latency during which this can be done, but there's no API to configure its size... that's a failing of the current system, I think. In principle, if you didn't mind the latency and had a dedicated thread for audio processing, events might work ok.

Chris Katko
SiegeLord said:

You can't use an event for that, as there will a gap between when the sample ends and when the event is processed.

Allow firing the event some X time before it ends, allowing the handler time to be ready to cue up the next sample.

Disclaimer: I've not used Allegro 5 much.

SiegeLord

Allow firing the event some X time before it ends, allowing the handler time to be ready to cue up the next sample.

The delay is still non-deterministic (the audio runs on a separate thread, there's no synchronization between it and the main thread). There is also no mechanism to do 'conditional playback' of a sample.

One final alternative to all this is to add a yet another audio source, that is based on callbacks. It'd be very similar to ALLEGRO_AUDIO_STREAM but would have no internal buffering or anything else of the sort.

Elias
SiegeLord said:

So from my point of view, this is where you pull out the good old ALLEGRO_AUDIO_STREAM and stream the samples yourself.

+1

jmasterx

For my use case, having a delay from the time the sample ends to the time the event is sent is perfectly okay. My use case was strictly for memory management purposes; the case when you have 100's of samples and one of them can be played at any given time, and you want to avoid loading all 100 samples into memory. I could easily stop a prior sample before starting another, but that would not sound right.

So I'll probably instead poll and check if my samples are done and destroy them as they finish playing.

Edgar Reynaldo

What kind of latency are we talking about here? How big is the voice buffer, and how often is it refilled?

SiegeLord

What kind of latency are we talking about here? How big is the voice buffer, and how often is it refilled?

This is backend dependent, which is the crux of the issue here: it's unpredictable. I haven't looked if this is just an API oversight, or some backends don't have a meaningful number you can change/query.

Gideon Weems
Quote:

ALLEGRO_AUDIO_STREAM

When two devs and Boobuigi recommend the same thing, you should take note.

Thread #614729. Printed from Allegro.cc