comparison Isolated/SoundDecoder.c @ 38:71b465ff0622

Added support files.
author Eric Wing <ewing@anscamobile.com>
date Thu, 28 Apr 2011 16:22:30 -0700
parents
children 5c9eaf0cc385
comparison
equal deleted inserted replaced
37:b346b6608eab 38:71b465ff0622
1 #ifndef ALMIXER_COMPILED_WITH_SDL
2
3 #include "SoundDecoder.h"
4 #include "SoundDecoder_Internal.h"
5 #include "tErrorLib.h"
6 #include "LinkedList.h"
7 #include <stdlib.h>
8 #include <string.h>
9 #include <assert.h>
10
11 #ifdef ANDROID_NDK
12 #include <android/log.h>
13 #endif
14
15 /* A partial shim reimplementation of SDL_sound to work around the LGPL issues.
16 * This implementation is more limited than SDL_sound.
17 * For example, there is no generic software conversion routines.
18 * Functions are also not necessarily thread safe.
19 * This implementation relies on the back-end decoder much more heavily
20 * than SDL_sound. (For example, I bypass the internal->buffer.)
21 */
22
23 static LinkedList* s_listOfLoadedSamples = NULL;
24
25 static signed char s_isInitialized = 0;
26 static TErrorPool* s_errorPool = NULL;
27
28 static const SoundDecoder_DecoderInfo** s_availableDecoders = NULL;
29
30 #ifdef __APPLE__
31 //extern const SoundDecoder_DecoderFunctions __SoundDecoder_DecoderFunctions_CoreAudio;
32 extern const Sound_DecoderFunctions __Sound_DecoderFunctions_CoreAudio;
33 #endif
34 #ifdef ANDROID_NDK
35 #ifdef SOUND_SUPPORTS_WAV
36 extern const Sound_DecoderFunctions __Sound_DecoderFunctions_WAV;
37 #endif
38 #ifdef SOUND_SUPPORTS_MPG123
39 extern const Sound_DecoderFunctions __Sound_DecoderFunctions_MPG123;
40 #endif
41 #ifdef SOUND_SUPPORTS_OGG
42 extern const Sound_DecoderFunctions __Sound_DecoderFunctions_OGG;
43 #endif
44 #endif
45
46 typedef struct
47 {
48 int available;
49 const SoundDecoder_DecoderFunctions* funcs;
50 } SoundElement;
51
52 static SoundElement s_linkedDecoders[] =
53 {
54 #if defined(__APPLE__)
55 { 0, &__Sound_DecoderFunctions_CoreAudio },
56 #endif
57 #if defined(ANDROID_NDK)
58 #ifdef SOUND_SUPPORTS_WAV
59 { 0, &__Sound_DecoderFunctions_WAV },
60 #endif
61 #ifdef SOUND_SUPPORTS_MPG123
62 { 0, &__Sound_DecoderFunctions_MPG123 },
63 #endif
64 #ifdef SOUND_SUPPORTS_OGG
65 { 0, &__Sound_DecoderFunctions_OGG },
66 #endif
67 #endif
68 { 0, NULL }
69 };
70
71
72 #include <ctype.h>
73 int SoundDecoder_strcasecmp(const char* str1, const char* str2)
74 {
75 int the_char1;
76 int the_char2;
77 int i = 0;
78 if(str1 == str2)
79 {
80 return 0;
81 }
82 if(NULL == str1)
83 {
84 return -1;
85 }
86 if(NULL == str2)
87 {
88 return 1;
89 }
90
91 do
92 {
93 the_char1 = tolower(str1[i]);
94 the_char2 = tolower(str2[i]);
95 if(the_char1 < the_char2)
96 {
97 return -1;
98 }
99 else if(the_char1 > the_char2)
100 {
101 return 1;
102 }
103 i++;
104 } while( (0 != the_char1) && (0 != the_char2) );
105
106 return 0;
107 }
108
109
110 #ifdef ANDROID_NDK
111 #include <stdarg.h>
112 #include <android/log.h>
113 int SoundDecoder_DebugPrint(const char* format, ...)
114 {
115 va_list arg_list;
116 int ret_val;
117
118 va_start(arg_list, format);
119 ret_val = __android_log_vprint(ANDROID_LOG_INFO, "SoundDecoder", format, arg_list);
120 va_end(arg_list);
121 return ret_val;
122 }
123 #endif
124
125 const char* SoundDecoder_GetError()
126 {
127 const char* error_string = NULL;
128 if(NULL == s_errorPool)
129 {
130 return "Error: You should not call SoundDecoder_GetError while Sound is not initialized";
131 }
132 error_string = TError_GetLastErrorStr(s_errorPool);
133 /* SDL returns empty strings instead of NULL */
134 if(NULL == error_string)
135 {
136 return "";
137 }
138 else
139 {
140 return error_string;
141 }
142 }
143
144 void SoundDecoder_ClearError()
145 {
146 if(NULL == s_errorPool)
147 {
148 return;
149 }
150 TError_SetError(s_errorPool, 0, NULL);
151 }
152
153 void SoundDecoder_SetError(const char* err_str, ...)
154 {
155 if(NULL == s_errorPool)
156 {
157 fprintf(stderr, "Error: You should not call SoundDecoder_SetError while Sound is not initialized\n");
158 return;
159 }
160 va_list argp;
161 va_start(argp, err_str);
162 // SDL_SetError which I'm emulating has no number parameter.
163 TError_SetErrorv(s_errorPool, 1, err_str, argp);
164 va_end(argp);
165 #ifdef ANDROID_NDK
166 __android_log_print(ANDROID_LOG_INFO, "SoundDecoder_SetError", TError_GetLastErrorStr(s_errorPool));
167 #endif
168 }
169
170
171 void SoundDecoder_GetLinkedVersion(SoundDecoder_Version* the_version)
172 {
173 if(NULL != the_version)
174 {
175 the_version->major = SOUNDDECODER_VER_MAJOR;
176 the_version->minor = SOUNDDECODER_VER_MINOR;
177 the_version->patch = SOUNDDECODER_VER_PATCH;
178 }
179 }
180
181
182 const SoundDecoder_DecoderInfo** SoundDecoder_AvailableDecoders()
183 {
184 return(s_availableDecoders);
185 }
186
187 int SoundDecoder_Init()
188 {
189 size_t total_number_of_decoders;
190 size_t i;
191 size_t current_pos = 0;
192 if(1 == s_isInitialized)
193 {
194 return 1;
195 }
196 if(NULL == s_errorPool)
197 {
198 s_errorPool = TError_CreateErrorPool();
199 }
200 if(NULL == s_errorPool)
201 {
202 return 0;
203 }
204
205 total_number_of_decoders = sizeof(s_linkedDecoders) / sizeof(s_linkedDecoders[0]);
206 s_availableDecoders = (const SoundDecoder_DecoderInfo **)malloc((total_number_of_decoders) * sizeof(SoundDecoder_DecoderInfo*));
207 if(NULL == s_availableDecoders)
208 {
209 SoundDecoder_SetError(ERR_OUT_OF_MEMORY);
210 return 0;
211 }
212
213 /* Allocate memory for linked list of sound samples. */
214 s_listOfLoadedSamples = LinkedList_Create();
215 if(NULL == s_listOfLoadedSamples)
216 {
217 free(s_availableDecoders);
218 s_availableDecoders = NULL;
219 SoundDecoder_SetError(ERR_OUT_OF_MEMORY);
220 return 0;
221 }
222
223 for(i = 0; s_linkedDecoders[i].funcs != NULL; i++)
224 {
225 s_linkedDecoders[i].available = s_linkedDecoders[i].funcs->init();
226 if(s_linkedDecoders[i].available)
227 {
228 s_availableDecoders[current_pos] = &(s_linkedDecoders[i].funcs->info);
229 current_pos++;
230 }
231 }
232
233 s_availableDecoders[current_pos] = NULL;
234 s_isInitialized = 1;
235 return 1;
236 }
237
238 void SoundDecoder_Quit()
239 {
240 size_t i;
241 if(0 == s_isInitialized)
242 {
243 return;
244 }
245
246 /*
247 * SDL_sound actually embeds the linked list in the internal data structure.
248 * So any sample can potentially reach any other sample.
249 * But I'm keeping my own separate list.
250 */
251 while(LinkedList_Size(s_listOfLoadedSamples) > 0)
252 {
253 SoundDecoder_Sample* sound_sample = (SoundDecoder_Sample*)LinkedList_PopBack(s_listOfLoadedSamples);
254 SoundDecoder_FreeSample(sound_sample);
255 }
256 LinkedList_Free(s_listOfLoadedSamples);
257 s_listOfLoadedSamples = NULL;
258
259
260 for(i = 0; s_linkedDecoders[i].funcs != NULL; i++)
261 {
262 if (s_linkedDecoders[i].available)
263 {
264 s_linkedDecoders[i].funcs->quit();
265 s_linkedDecoders[i].available = 0;
266 }
267 }
268
269 if(NULL != s_availableDecoders)
270 {
271 free(s_availableDecoders);
272 }
273 s_availableDecoders = NULL;
274
275
276 /* Remember: ALmixer_SetError/GetError calls will not work while this is gone. */
277 TError_FreeErrorPool(s_errorPool);
278 s_errorPool = NULL;
279
280 s_isInitialized = 0;
281 }
282
283
284 void SoundDecoder_FreeSample(SoundDecoder_Sample* sound_sample)
285 {
286 SoundDecoder_SampleInternal* sample_internal;
287
288 /* Quit unloads all samples, so it is not possible to free a sample
289 * when not initialized.
290 */
291 if(0 == s_isInitialized)
292 {
293 return;
294 }
295
296 if(sound_sample == NULL)
297 {
298 return;
299 }
300
301 /* SDL_sound keeps a linked list of all the loaded samples.
302 * We want to remove the current sample from that list.
303 */
304 LinkedListNode* the_node = LinkedList_Find(s_listOfLoadedSamples, sound_sample, NULL);
305 if(NULL == the_node)
306 {
307 SoundDecoder_SetError("SoundDecoder_FreeSample: Internal Error, sample does not exist in linked list.");
308 return;
309 }
310 LinkedList_Remove(s_listOfLoadedSamples, the_node);
311
312 sample_internal = (SoundDecoder_SampleInternal*)sound_sample->opaque;
313
314 /* Ugh...SDL_sound has a lot of pointers.
315 * I hope I didn't miss any dynamic memory.
316 */
317
318 /* Call close on the decoder */
319 sample_internal->funcs->close(sound_sample);
320
321 if(NULL != sample_internal->rw)
322 {
323 sample_internal->rw->close(sample_internal->rw);
324 }
325
326 /* Ooops. The public buffer might be shared with the internal buffer.
327 * Make sure to not accidentally double delete.
328 */
329 if((NULL != sample_internal->buffer)
330 && (sample_internal->buffer != sound_sample->buffer)
331 )
332 {
333 free(sample_internal->buffer);
334 }
335 free(sample_internal);
336
337 if(NULL != sound_sample->buffer)
338 {
339 free(sound_sample->buffer);
340 }
341 free(sound_sample);
342 }
343
344
345
346 static int Internal_LoadSample(const SoundDecoder_DecoderFunctions *funcs,
347 SoundDecoder_Sample* sound_sample, const char *ext
348 )
349 {
350 SoundDecoder_SampleInternal* internal_sample = (SoundDecoder_SampleInternal*)sound_sample->opaque;
351 long current_file_position = internal_sample->rw->seek(internal_sample->rw, 0, SEEK_CUR);
352
353 /* fill in the funcs for this decoder... */
354 sound_sample->decoder = &funcs->info;
355 internal_sample->funcs = funcs;
356 if (!funcs->open(sound_sample, ext))
357 {
358 internal_sample->rw->seek(internal_sample->rw, current_file_position, SEEK_SET);
359 return 0;
360 }
361
362 /* we found a compatible decoder */
363
364 /* SDL_sound normally goes on to setup a bunch of things to
365 * support format conversion via SDL APIs.
366 * I am not porting any of that stuff.
367 * My goal is to simply setup the struct properties to values
368 * that will not cause any confusion with other parts of the implementation.
369 */
370
371
372 if(0 == sound_sample->desired.format)
373 {
374 sound_sample->desired.format = sound_sample->actual.format;
375 }
376 if(0 == sound_sample->desired.channels)
377 {
378 sound_sample->desired.channels = sound_sample->actual.channels;
379 }
380 if(0 == sound_sample->desired.rate)
381 {
382 sound_sample->desired.rate = sound_sample->actual.rate;
383 }
384
385 /* I'm a little confused at the difference between the internal
386 * public buffer. I am going to make them the same.
387 * I assume I already allocated the public buffer.
388 */
389 internal_sample->buffer = sound_sample->buffer;
390 internal_sample->buffer_size = sound_sample->buffer_size;
391
392 /* Insert the new sample into the linked list of samples. */
393 LinkedList_PushBack(s_listOfLoadedSamples, sound_sample);
394
395 return 1;
396 }
397
398
399
400 SoundDecoder_Sample* SoundDecoder_NewSampleFromFile(const char* file_name,
401 SoundDecoder_AudioInfo* desired_format,
402 size_t buffer_size)
403 {
404
405 const char* file_extension;
406 ALmixer_RWops* rw_ops;
407
408
409 if(0 == s_isInitialized)
410 {
411 SoundDecoder_SetError(ERR_NOT_INITIALIZED);
412 return NULL;
413 }
414 if(NULL == file_name)
415 {
416 SoundDecoder_SetError("No file specified");
417 return NULL;
418 }
419
420 file_extension = strrchr(file_name, '.');
421 if(NULL != file_extension)
422 {
423 file_extension++;
424 }
425
426 rw_ops = ALmixer_RWFromFile(file_name, "rb");
427
428 return SoundDecoder_NewSample(rw_ops, file_extension, desired_format, buffer_size);
429 }
430
431
432 SoundDecoder_Sample* SoundDecoder_NewSample(ALmixer_RWops* rw_ops, const char* file_extension, SoundDecoder_AudioInfo* desired_format, size_t buffer_size)
433 {
434 SoundDecoder_Sample* new_sample;
435 SoundDecoder_SampleInternal* internal_sample;
436 SoundElement* current_decoder;
437
438 if(0 == s_isInitialized)
439 {
440 SoundDecoder_SetError(ERR_NOT_INITIALIZED);
441 return NULL;
442 }
443 if(NULL == rw_ops)
444 {
445 SoundDecoder_SetError("No file specified");
446 return NULL;
447 }
448
449 new_sample = (SoundDecoder_Sample*)calloc(1, sizeof(SoundDecoder_Sample));
450 if(NULL == new_sample)
451 {
452 SoundDecoder_SetError(ERR_OUT_OF_MEMORY);
453 return NULL;
454 }
455 internal_sample = (SoundDecoder_SampleInternal*)calloc(1, sizeof(SoundDecoder_SampleInternal));
456 if(NULL == internal_sample)
457 {
458 free(new_sample);
459 SoundDecoder_SetError(ERR_OUT_OF_MEMORY);
460 return NULL;
461 }
462 new_sample->buffer = calloc(1, buffer_size);
463 if(NULL == new_sample->buffer)
464 {
465 free(internal_sample);
466 free(new_sample);
467 SoundDecoder_SetError(ERR_OUT_OF_MEMORY);
468 return NULL;
469 }
470
471 new_sample->buffer_size = buffer_size;
472
473 if(NULL != desired_format)
474 {
475 memcpy(&new_sample->desired, desired_format, sizeof(SoundDecoder_AudioInfo));
476 }
477
478 internal_sample->rw = rw_ops;
479 new_sample->opaque = internal_sample;
480
481
482 if(NULL != file_extension)
483 {
484 for(current_decoder = &s_linkedDecoders[0]; current_decoder->funcs != NULL; current_decoder++)
485 {
486 if(current_decoder->available)
487 {
488 const char** decoder_file_extension = current_decoder->funcs->info.extensions;
489 while(*decoder_file_extension)
490 {
491 if(0 == (SoundDecoder_strcasecmp(*decoder_file_extension, file_extension)))
492 {
493 if(Internal_LoadSample(current_decoder->funcs, new_sample, file_extension))
494 {
495 return(new_sample);
496 }
497 break; /* go to the next decoder */
498 }
499 decoder_file_extension++;
500 }
501 }
502 }
503 }
504
505 /* no direct file_extensionension match? Try everything we've got... */
506 for(current_decoder = &s_linkedDecoders[0]; current_decoder->funcs != NULL; current_decoder++)
507 {
508 if(current_decoder->available)
509 {
510 int already_tried_decoder = 0;
511 const char** decoder_file_extension = current_decoder->funcs->info.extensions;
512
513 /* skip decoders we already tried from the above loop */
514 while(*decoder_file_extension)
515 {
516 if(SoundDecoder_strcasecmp(*decoder_file_extension, file_extension) == 0)
517 {
518 already_tried_decoder = 1;
519 break;
520 }
521 decoder_file_extension++;
522 }
523
524 if(0 == already_tried_decoder)
525 {
526 if (Internal_LoadSample(current_decoder->funcs, new_sample, file_extension))
527 {
528 return new_sample;
529 }
530 }
531 }
532 }
533
534 /* could not find a decoder */
535 SoundDecoder_SetError(ERR_UNSUPPORTED_FORMAT);
536 /* clean up the memory */
537 free(new_sample->opaque);
538 if(NULL != new_sample->buffer)
539 {
540 free(new_sample->buffer);
541 }
542 free(new_sample);
543
544 rw_ops->close(rw_ops);
545 return NULL;
546 }
547
548
549 int SoundDecoder_SetBufferSize(SoundDecoder_Sample* sound_sample, size_t new_buffer_size)
550 {
551 SoundDecoder_SampleInternal* internal_sample = NULL;
552 void* new_buffer_ptr = NULL;
553
554 BAIL_IF_MACRO(!s_isInitialized, ERR_NOT_INITIALIZED, 0);
555 BAIL_IF_MACRO(NULL == sound_sample, ERR_NULL_SAMPLE, 0);
556
557 internal_sample = ((SoundDecoder_SampleInternal*)sound_sample->opaque);
558
559
560 new_buffer_ptr = realloc(sound_sample->buffer, new_buffer_size);
561 BAIL_IF_MACRO(NULL == new_buffer_ptr, ERR_OUT_OF_MEMORY, 0);
562
563 sound_sample->buffer = new_buffer_ptr;
564 sound_sample->buffer_size = new_buffer_size;
565 internal_sample->buffer = sound_sample->buffer;
566 internal_sample->buffer_size = sound_sample->buffer_size;
567
568 return 1;
569 }
570
571
572 size_t SoundDecoder_Decode(SoundDecoder_Sample* sound_sample)
573 {
574 SoundDecoder_SampleInternal* internal_sample = NULL;
575 size_t bytes_read = 0;
576
577 BAIL_IF_MACRO(!s_isInitialized, ERR_NOT_INITIALIZED, 0);
578 BAIL_IF_MACRO(NULL == sound_sample, ERR_NULL_SAMPLE, 0);
579 BAIL_IF_MACRO(sound_sample->flags & SOUND_SAMPLEFLAG_ERROR, ERR_PREVIOUS_SAMPLE_ERROR, 0);
580 BAIL_IF_MACRO(sound_sample->flags & SOUND_SAMPLEFLAG_EOF, ERR_ALREADY_AT_EOF_ERROR, 0);
581
582 internal_sample = (SoundDecoder_SampleInternal*)sound_sample->opaque;
583
584 assert(sound_sample->buffer != NULL);
585 assert(sound_sample->buffer_size > 0);
586 assert(internal_sample->buffer != NULL);
587 assert(internal_sample->buffer_size > 0);
588
589 /* reset EAGAIN. Decoder can flip it back on if it needs to. */
590 sound_sample->flags &= ~SOUND_SAMPLEFLAG_EAGAIN;
591 bytes_read = internal_sample->funcs->read(sound_sample);
592 return bytes_read;
593 }
594
595
596 size_t SoundDecoder_DecodeAll(SoundDecoder_Sample* sound_sample)
597 {
598 SoundDecoder_SampleInternal* internal_sample = NULL;
599 void* data_buffer = NULL;
600 size_t updated_buffer_size = 0;
601
602 BAIL_IF_MACRO(!s_isInitialized, ERR_NOT_INITIALIZED, 0);
603 BAIL_IF_MACRO(NULL == sound_sample, ERR_NULL_SAMPLE, 0);
604
605 /* My original thought was to call SetBufferSize and resize to
606 * the size needed to hold the entire decoded file utilizing total_time,
607 * but it appears SDL_sound simply loops on SoundDecoder_Decode.
608 * I suppose it is possible to partially decode or seek a file, and then
609 * call DecodeAll so you won't have the whole thing in which case
610 * my idea would waste memory.
611 */
612 while( (0 == (sound_sample->flags & SOUND_SAMPLEFLAG_EOF) )
613 && (0 == (sound_sample->flags & SOUND_SAMPLEFLAG_ERROR))
614 )
615 {
616 size_t bytes_decoded = SoundDecoder_Decode(sound_sample);
617 void* realloced_ptr = realloc(data_buffer, updated_buffer_size + bytes_decoded);
618 if(NULL == realloced_ptr)
619 {
620 sound_sample->flags |= SOUND_SAMPLEFLAG_ERROR;
621 SoundDecoder_SetError(ERR_OUT_OF_MEMORY);
622 if(NULL != data_buffer)
623 {
624 free(data_buffer);
625 }
626 return bytes_decoded;
627 }
628 data_buffer = realloced_ptr;
629 /* copy the chunk of decoded PCM to the end of our new data buffer */
630 memcpy( ((char*)data_buffer) + updated_buffer_size, sound_sample->buffer, bytes_decoded );
631 updated_buffer_size += bytes_decoded;
632 }
633
634 internal_sample = (SoundDecoder_SampleInternal*)sound_sample->opaque;
635 if(internal_sample->buffer != sound_sample->buffer)
636 {
637 free(internal_sample->buffer);
638 }
639 free(sound_sample->buffer);
640
641 sound_sample->buffer = data_buffer;
642 sound_sample->buffer_size = updated_buffer_size;
643 internal_sample->buffer = sound_sample->buffer;
644 internal_sample->buffer_size = sound_sample->buffer_size;
645
646 return sound_sample->buffer_size;
647 }
648
649
650 int SoundDecoder_Rewind(SoundDecoder_Sample* sound_sample)
651 {
652 SoundDecoder_SampleInternal* internal_sample;
653 int ret_val;
654
655 BAIL_IF_MACRO(!s_isInitialized, ERR_NOT_INITIALIZED, 0);
656 BAIL_IF_MACRO(NULL == sound_sample, ERR_NULL_SAMPLE, 0);
657
658 internal_sample = (SoundDecoder_SampleInternal*)sound_sample->opaque;
659 ret_val = internal_sample->funcs->rewind(sound_sample);
660 if(0 == ret_val)
661 {
662 sound_sample->flags |= SOUND_SAMPLEFLAG_ERROR;
663 SoundDecoder_SetError("Rewind failed");
664 return 0;
665 }
666 /* reset flags */
667 sound_sample->flags &= ~SOUND_SAMPLEFLAG_EAGAIN;
668 sound_sample->flags &= ~SOUND_SAMPLEFLAG_ERROR;
669 sound_sample->flags &= ~SOUND_SAMPLEFLAG_EOF;
670
671 return 1;
672 }
673
674
675 int SoundDecoder_Seek(SoundDecoder_Sample* sound_sample, size_t ms)
676 {
677 SoundDecoder_SampleInternal* internal_sample;
678 int ret_val;
679
680 BAIL_IF_MACRO(!s_isInitialized, ERR_NOT_INITIALIZED, 0);
681 BAIL_IF_MACRO(NULL == sound_sample, ERR_NULL_SAMPLE, 0);
682
683 BAIL_IF_MACRO(!(sound_sample->flags & SOUND_SAMPLEFLAG_CANSEEK), "Sound sample is not seekable", 0);
684
685 internal_sample = (SoundDecoder_SampleInternal*)sound_sample->opaque;
686 ret_val = internal_sample->funcs->seek(sound_sample, ms);
687 if(0 == ret_val)
688 {
689 sound_sample->flags |= SOUND_SAMPLEFLAG_ERROR;
690 SoundDecoder_SetError("Seek failed");
691 return 0;
692 }
693 /* reset flags */
694 sound_sample->flags &= ~SOUND_SAMPLEFLAG_EAGAIN;
695 sound_sample->flags &= ~SOUND_SAMPLEFLAG_ERROR;
696 sound_sample->flags &= ~SOUND_SAMPLEFLAG_EOF;
697
698 return 1;
699 }
700
701
702 ptrdiff_t SoundDecoder_GetDuration(SoundDecoder_Sample* sound_sample)
703 {
704 SoundDecoder_SampleInternal* internal_sample;
705 BAIL_IF_MACRO(!s_isInitialized, ERR_NOT_INITIALIZED, -1);
706 BAIL_IF_MACRO(NULL == sound_sample, ERR_NULL_SAMPLE, 0);
707 internal_sample = (SoundDecoder_SampleInternal*)sound_sample->opaque;
708 return internal_sample->total_time;
709 }
710
711 #endif