Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » [A5] Playing Samples on the Fly?

This thread is locked; no one can reply to it. rss feed Print
[A5] Playing Samples on the Fly?
jmasterx
Member #11,410
October 2009

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
Member #8,592
May 2007
avatar

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
Member #11,410
October 2009

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
Member #8,592
May 2007
avatar

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
Member #476
June 2000
avatar

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

--
Thomas Fjellstrom - [website] - [email] - [Allegro Wiki] - [Allegro TODO]
"If you can't think of a better solution, don't try to make a better solution." -- weapon_S
"The less evidence we have for what we believe is certain, the more violently we defend beliefs against those who don't agree" -- https://twitter.com/neiltyson/status/592870205409353730

Edgar Reynaldo
Member #8,592
May 2007
avatar

SiegeLord
Member #7,827
October 2006
avatar

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.

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

Chris Katko
Member #1,881
January 2002
avatar

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.

-----sig:
“Programs should be written for people to read, and only incidentally for machines to execute.” - Structure and Interpretation of Computer Programs

SiegeLord
Member #7,827
October 2006
avatar

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.

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

Elias
Member #358
May 2000

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

--
"Either help out or stop whining" - Evert

jmasterx
Member #11,410
October 2009

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
Member #8,592
May 2007
avatar

SiegeLord
Member #7,827
October 2006
avatar

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.

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

Gideon Weems
Member #3,925
October 2003

Quote:

ALLEGRO_AUDIO_STREAM

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

Go to: