flac.cpp
1 /* 2 Copyright (C) 2009 Jonathon Fowler <jf@jonof.id.au> 3 4 This program is free software; you can redistribute it and/or 5 modify it under the terms of the GNU General Public License 6 as published by the Free Software Foundation; either version 2 7 of the License, or (at your option) any later version. 8 9 This program is distributed in the hope that it will be useful, 10 but WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 13 See the GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with this program; if not, write to the Free Software 17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 19 */ 20 21 /** 22 * FLAC source support for MultiVoc 23 */ 24 25 #include "compat.h" 26 27 #ifdef HAVE_FLAC 28 29 #define FLAC__NO_DLL 30 31 #if defined(__APPLE__) || defined(__linux__) 32 #include <FLAC/all.h> 33 #else 34 #include "FLAC/all.h" 35 #endif 36 37 #include "_multivc.h" 38 #include "multivoc.h" 39 #include "pitch.h" 40 #include "pragmas.h" 41 42 typedef struct 43 { 44 void *ptr; 45 size_t length; 46 size_t pos; 47 48 FLAC__StreamDecoder *stream; 49 FLAC__uint64 sample_pos; 50 51 char *block; 52 size_t blocksize; 53 54 VoiceNode *owner; 55 } flac_data; 56 57 // callbacks, round 1 58 59 static size_t read_flac(void *ptr, size_t size, size_t nmemb, FLAC__IOHandle datasource) 60 { 61 flac_data *flac = (flac_data *)datasource; 62 size_t nread = 0; 63 size_t bytes; 64 65 errno = 0; 66 67 if (flac->length == flac->pos) 68 { 69 return 0; 70 } 71 72 for (; nmemb > 0; nmemb--, nread++) 73 { 74 bytes = flac->length - flac->pos; 75 if (size < bytes) 76 { 77 bytes = size; 78 } 79 80 memcpy(ptr, (uint8_t *)flac->ptr + flac->pos, bytes); 81 flac->pos += bytes; 82 ptr = (uint8_t *)ptr + bytes; 83 84 if (flac->length == flac->pos) 85 { 86 nread++; 87 break; 88 } 89 } 90 91 return nread; 92 } 93 94 static size_t write_flac(const void *ptr, size_t size, size_t nmemb, FLAC__IOHandle datasource) 95 { 96 UNREFERENCED_PARAMETER(ptr); 97 UNREFERENCED_PARAMETER(size); 98 UNREFERENCED_PARAMETER(nmemb); 99 UNREFERENCED_PARAMETER(datasource); 100 101 return 0; 102 } 103 104 static int seek_flac(FLAC__IOHandle datasource, FLAC__int64 offset, int whence) 105 { 106 flac_data *flac = (flac_data *)datasource; 107 108 switch (whence) 109 { 110 case SEEK_SET: flac->pos = 0; break; 111 case SEEK_CUR: break; 112 case SEEK_END: flac->pos = flac->length; break; 113 } 114 115 flac->pos += offset; 116 117 if (flac->pos > flac->length) 118 { 119 flac->pos = flac->length; 120 } 121 122 return 0; 123 } 124 125 static FLAC__int64 tell_flac(FLAC__IOHandle datasource) 126 { 127 flac_data *flac = (flac_data *)datasource; 128 129 return flac->pos; 130 } 131 132 static FLAC__int64 length_flac(FLAC__IOHandle datasource) 133 { 134 flac_data *flac = (flac_data *)datasource; 135 136 return flac->length; 137 } 138 139 static int eof_flac(FLAC__IOHandle datasource) 140 { 141 flac_data *flac = (flac_data *)datasource; 142 143 return (flac->pos == flac->length); 144 } 145 146 static int close_flac(FLAC__IOHandle datasource) 147 { 148 UNREFERENCED_PARAMETER(datasource); 149 return 0; 150 } 151 152 static FLAC__IOCallbacks flac_callbacks = { 153 read_flac, write_flac, seek_flac, tell_flac, eof_flac, close_flac, 154 }; 155 156 157 // callbacks, round 2 158 159 FLAC__StreamDecoderReadStatus read_flac_stream(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, 160 void *client_data) 161 { 162 UNREFERENCED_PARAMETER(decoder); 163 if (*bytes > 0) 164 { 165 *bytes = read_flac(buffer, sizeof(FLAC__byte), *bytes, client_data); 166 if (errno) 167 return FLAC__STREAM_DECODER_READ_STATUS_ABORT; 168 else if (*bytes == 0) 169 return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; 170 else 171 return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; 172 } 173 else 174 return FLAC__STREAM_DECODER_READ_STATUS_ABORT; 175 } 176 177 FLAC__StreamDecoderSeekStatus seek_flac_stream(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, 178 void *client_data) 179 { 180 UNREFERENCED_PARAMETER(decoder); 181 if (seek_flac(client_data, absolute_byte_offset, SEEK_SET) < 0) 182 return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; 183 else 184 return FLAC__STREAM_DECODER_SEEK_STATUS_OK; 185 } 186 187 FLAC__StreamDecoderTellStatus tell_flac_stream(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, 188 void *client_data) 189 { 190 UNREFERENCED_PARAMETER(decoder); 191 *absolute_byte_offset = tell_flac(client_data); 192 return FLAC__STREAM_DECODER_TELL_STATUS_OK; 193 } 194 195 FLAC__StreamDecoderLengthStatus length_flac_stream(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, 196 void *client_data) 197 { 198 UNREFERENCED_PARAMETER(decoder); 199 *stream_length = length_flac(client_data); 200 return FLAC__STREAM_DECODER_LENGTH_STATUS_OK; 201 } 202 203 FLAC__bool eof_flac_stream(const FLAC__StreamDecoder *decoder, void *client_data) 204 { 205 UNREFERENCED_PARAMETER(decoder); 206 return eof_flac(client_data); 207 } 208 209 FLAC__StreamDecoderWriteStatus write_flac_stream(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, 210 const FLAC__int32 *const ibuffer[], void *client_data) 211 { 212 flac_data *fd = (flac_data *)client_data; 213 VoiceNode *voice = fd->owner; 214 FLAC__uint64 samples = frame->header.blocksize; 215 216 UNREFERENCED_PARAMETER(decoder); 217 218 voice->channels = frame->header.channels; 219 voice->bits = frame->header.bits_per_sample; 220 voice->SamplingRate = frame->header.sample_rate; 221 222 if (frame->header.number_type == FLAC__FRAME_NUMBER_TYPE_FRAME_NUMBER) 223 fd->sample_pos = frame->header.number.frame_number; 224 else if (frame->header.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER) 225 fd->sample_pos = frame->header.number.sample_number; 226 227 if ((FLAC__uint64)(uintptr_t)voice->Loop.End > 0 && 228 fd->sample_pos + samples >= (FLAC__uint64)(uintptr_t)voice->Loop.End) 229 { 230 samples = (FLAC__uint64)(uintptr_t)voice->Loop.End - fd->sample_pos; 231 if (!FLAC__stream_decoder_seek_absolute(fd->stream, (FLAC__uint64)(uintptr_t)voice->Loop.Start)) 232 LOG_F(ERROR, "write_flac_stream: error in FLAC__stream_decoder_seek_absolute (LOOP_START %" PRIu64 ", LOOP_END %" PRIu64 ")", 233 (FLAC__uint64)(uintptr_t)voice->Loop.Start, (FLAC__uint64)(uintptr_t)voice->Loop.End); 234 } 235 236 size_t const size = samples * voice->channels * (voice->bits >> 3); 237 238 voice->position = 0; 239 // CODEDUP multivoc.c MV_SetVoicePitch 240 voice->RateScale = divideu64((uint64_t)voice->SamplingRate * voice->PitchScale, MV_MixRate); 241 voice->FixedPointBufferSize = (voice->RateScale * MV_MIXBUFFERSIZE) - voice->RateScale; 242 MV_SetVoiceMixMode(voice); 243 244 char * block = fd->block; 245 246 if (size > fd->blocksize) 247 { 248 block = (char *)Xaligned_alloc(16, size); 249 250 if (block == nullptr) 251 return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; 252 } 253 254 { 255 char *obuffer = block; 256 FLAC__uint64 sample; 257 uint8_t channel; 258 259 // this loop is adapted from code in ov_read_filter() in vorbisfile.c in libvorbis 260 for (sample = 0; sample < samples; ++sample) 261 for (channel = 0; channel < frame->header.channels; ++channel) 262 { 263 int8_t byte; 264 FLAC__int32 val = ibuffer[channel][sample]; 265 if (val > (1 << (voice->bits - 1)) - 1) 266 val = (1 << (voice->bits - 1)) - 1; 267 else if (val < -(1 << (voice->bits - 1))) 268 val = -(1 << (voice->bits - 1)); 269 for (byte = 0; byte < voice->bits; byte += 8) *obuffer++ = ((val >> byte) & 0x000000FF); 270 } 271 } 272 273 voice->sound = block; 274 voice->length = samples << 16; 275 276 if (block != fd->block) 277 { 278 char * oldblock = fd->block; 279 fd->block = block; 280 fd->blocksize = size; 281 ALIGNED_FREE_AND_NULL(oldblock); 282 } 283 284 return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; 285 } 286 287 void error_flac_stream(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) 288 { 289 // flac_data * fd = (flac_data *) client_data; 290 UNREFERENCED_PARAMETER(client_data); 291 UNREFERENCED_PARAMETER(decoder); 292 LOG_F(ERROR, "%s", FLAC__StreamDecoderErrorStatusString[status]); 293 // FLAC__stream_decoder_flush(fd->stream); 294 } 295 296 int MV_GetFLACPosition(VoiceNode *voice) 297 { 298 FLAC__uint64 position = 0; 299 flac_data *fd = (flac_data *)voice->rawdataptr; 300 301 FLAC__stream_decoder_get_decode_position(fd->stream, &position); 302 303 return position; 304 } 305 306 void MV_SetFLACPosition(VoiceNode *voice, int position) 307 { 308 flac_data *fd = (flac_data *)voice->rawdataptr; 309 310 FLAC__stream_decoder_seek_absolute(fd->stream, position); 311 } 312 313 /*--------------------------------------------------------------------- 314 Function: MV_GetNextFLACBlock 315 316 Controls playback of FLAC data 317 ---------------------------------------------------------------------*/ 318 319 static playbackstatus MV_GetNextFLACBlock(VoiceNode *voice) 320 321 { 322 flac_data *fd = (flac_data *)voice->rawdataptr; 323 FLAC__StreamDecoderState decode_state; 324 // FLAC__bool decode_status; 325 326 if ((FLAC__uint64)(uintptr_t)voice->Loop.End > 0 && fd->sample_pos >= (FLAC__uint64)(uintptr_t)voice->Loop.End) 327 if (!FLAC__stream_decoder_seek_absolute(fd->stream, (FLAC__uint64)(uintptr_t)voice->Loop.Start)) 328 LOG_F(ERROR, "MV_GetNextFLACBlock: error in FLAC__stream_decoder_seek_absolute (LOOP_START %" PRIu64 ", LOOP_END %" PRIu64 ")", 329 (FLAC__uint64)(uintptr_t)voice->Loop.Start, (FLAC__uint64)(uintptr_t)voice->Loop.End); 330 331 /*decode_status =*/FLAC__stream_decoder_process_single(fd->stream); 332 decode_state = FLAC__stream_decoder_get_state(fd->stream); 333 334 /* 335 if (!decode_status) 336 { 337 LOG_F(INFO, "MV_GetNextFLACBlock: %s", FLAC__StreamDecoderStateString[decode_state]); 338 return NoMoreData; 339 } 340 */ 341 342 if (decode_state == FLAC__STREAM_DECODER_SEEK_ERROR) 343 { 344 FLAC__stream_decoder_flush(fd->stream); 345 decode_state = FLAC__stream_decoder_get_state(fd->stream); 346 } 347 348 if (decode_state == FLAC__STREAM_DECODER_END_OF_STREAM) 349 { 350 if (voice->Loop.Size > 0) 351 { 352 if (!FLAC__stream_decoder_seek_absolute(fd->stream, (FLAC__uint64)(uintptr_t)voice->Loop.Start)) 353 LOG_F(ERROR, "MV_GetNextFLACBlock: error in FLAC__stream_decoder_seek_absolute (LOOP_START %" PRIu64 ")", 354 (FLAC__uint64)(uintptr_t)voice->Loop.Start); 355 } 356 else 357 return NoMoreData; 358 } 359 360 #if 0 361 // unnecessary: duplicated in write_flac_stream() 362 voice->channels = FLAC__stream_decoder_get_channels(fd->stream); 363 voice->bits = FLAC__stream_decoder_get_bits_per_sample(fd->stream); 364 voice->SamplingRate = FLAC__stream_decoder_get_sample_rate(fd->stream); 365 // CODEDUP multivoc.c MV_SetVoicePitch 366 voice->RateScale = ( voice->SamplingRate * voice->PitchScale ) / MV_MixRate; 367 voice->FixedPointBufferSize = ( voice->RateScale * MV_MIXBUFFERSIZE ) - voice->RateScale; 368 MV_SetVoiceMixMode( voice ); 369 #endif 370 371 return KeepPlaying; 372 } 373 374 375 /*--------------------------------------------------------------------- 376 Function: MV_PlayFLAC3D 377 378 Begin playback of sound data at specified angle and distance 379 from listener. 380 ---------------------------------------------------------------------*/ 381 382 int MV_PlayFLAC3D(char *ptr, uint32_t length, int loophow, int pitchoffset, int angle, int distance, int priority, fix16_t volume, intptr_t callbackval) 383 { 384 int left; 385 int right; 386 int mid; 387 int vol; 388 int status; 389 390 if (!MV_Installed) 391 return MV_SetErrorCode(MV_NotInstalled); 392 393 if (distance < 0) 394 { 395 distance = -distance; 396 angle += MV_NUMPANPOSITIONS / 2; 397 } 398 399 vol = MIX_VOLUME(distance); 400 401 // Ensure angle is within 0 - 127 402 angle &= MV_MAXPANPOSITION; 403 404 left = MV_PanTable[angle][vol].left; 405 right = MV_PanTable[angle][vol].right; 406 mid = max(0, 255 - distance); 407 408 status = MV_PlayFLAC(ptr, length, loophow, -1, pitchoffset, mid, left, right, priority, volume, callbackval); 409 410 return status; 411 } 412 413 414 /*--------------------------------------------------------------------- 415 Function: MV_PlayFLAC 416 417 Begin playback of sound data with the given sound levels and 418 priority. 419 ---------------------------------------------------------------------*/ 420 421 int MV_PlayFLAC(char *ptr, uint32_t length, int loopstart, int loopend, int pitchoffset, int vol, int left, int right, int priority, fix16_t volume, intptr_t callbackval) 422 { 423 UNREFERENCED_PARAMETER(loopend); 424 425 if (!MV_Installed) 426 return MV_SetErrorCode(MV_NotInstalled); 427 428 // Request a voice from the voice pool 429 auto voice = MV_AllocVoice(priority, sizeof(flac_data)); 430 if (voice == nullptr) 431 return MV_SetErrorCode(MV_NoVoices); 432 433 auto fd = (flac_data *)voice->rawdataptr; 434 435 fd->owner = voice; 436 fd->ptr = ptr; 437 fd->pos = 0; 438 fd->blocksize = 0; 439 fd->length = length; 440 fd->block = nullptr; 441 fd->stream = FLAC__stream_decoder_new(); 442 fd->sample_pos = 0; 443 444 FLAC__stream_decoder_set_metadata_ignore_all(fd->stream); 445 446 if (FLAC__stream_decoder_init_stream(fd->stream, read_flac_stream, seek_flac_stream, tell_flac_stream, 447 length_flac_stream, eof_flac_stream, write_flac_stream, 448 /*metadata_flac_stream*/ nullptr, error_flac_stream, 449 (void *)fd) != FLAC__STREAM_DECODER_INIT_STATUS_OK) 450 { 451 LOG_F(ERROR, "MV_PlayFLAC: error in FLAC__stream_decoder_init_stream: %s", FLAC__stream_decoder_get_resolved_state_string(fd->stream)); 452 ALIGNED_FREE_AND_NULL(fd); 453 return MV_SetErrorCode(MV_InvalidFile); 454 } 455 456 voice->wavetype = FMT_FLAC; 457 voice->GetSound = MV_GetNextFLACBlock; 458 voice->NextBlock = fd->block; 459 voice->PitchScale = PITCH_GetScale(pitchoffset); 460 voice->priority = priority; 461 voice->callbackval = callbackval; 462 463 voice->Loop = { nullptr, nullptr, 0, loopstart >= 0 }; 464 465 // parse metadata 466 // loop parsing designed with multiple repetitions in mind 467 // In retrospect, it may be possible to MV_GetVorbisCommentLoops(voice, (vorbis_comment *) 468 // &tags->data.vorbis_comment) 469 // but libvorbisfile may be confused by the signedness of char* vs FLAC__byte* and this code does not depend on 470 // HAVE_VORBIS. 471 auto metadata_chain = FLAC__metadata_chain_new(); 472 if (metadata_chain != nullptr) 473 { 474 if (FLAC__metadata_chain_read_with_callbacks(metadata_chain, fd, flac_callbacks)) 475 { 476 FLAC__Metadata_Iterator *metadata_iterator = FLAC__metadata_iterator_new(); 477 if (metadata_iterator != nullptr) 478 { 479 char *vc_loopstart = nullptr; 480 char *vc_loopend = nullptr; 481 char *vc_looplength = nullptr; 482 483 FLAC__metadata_iterator_init(metadata_iterator, metadata_chain); 484 485 do 486 { 487 FLAC__StreamMetadata *tags = FLAC__metadata_iterator_get_block(metadata_iterator); 488 489 if (tags->type == FLAC__METADATA_TYPE_STREAMINFO) 490 { 491 const FLAC__StreamMetadata_StreamInfo *info = &tags->data.stream_info; 492 493 if (info->channels != 1 && info->channels != 2) 494 { 495 FLAC__metadata_object_delete(tags); 496 FLAC__metadata_iterator_delete(metadata_iterator); 497 // FLAC__metadata_chain_delete(metadata_chain); 498 FLAC__stream_decoder_finish(fd->stream); 499 FLAC__stream_decoder_delete(fd->stream); 500 ALIGNED_FREE_AND_NULL(fd); 501 return MV_SetErrorCode(MV_InvalidFile); 502 } 503 504 voice->channels = info->channels; 505 voice->bits = info->bits_per_sample; 506 voice->SamplingRate = info->sample_rate; 507 } 508 509 // load loop tags from metadata 510 if (tags->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) 511 { 512 FLAC__uint32 comment; 513 for (comment = 0; comment < tags->data.vorbis_comment.num_comments; ++comment) 514 { 515 const char *entry = (const char *)tags->data.vorbis_comment.comments[comment].entry; 516 if (entry != nullptr && entry[0] != '\0') 517 { 518 const char *value = strchr(entry, '='); 519 const size_t field = value - entry; 520 value += 1; 521 522 for (int t = 0; t < loopStartTagCount && vc_loopstart == nullptr; ++t) 523 { 524 char const * const tag = loopStartTags[t]; 525 if (field == strlen(tag) && Bstrncasecmp(entry, tag, field) == 0) 526 vc_loopstart = Xstrdup(value); 527 } 528 529 for (int t = 0; t < loopEndTagCount && vc_loopend == nullptr; ++t) 530 { 531 char const * const tag = loopEndTags[t]; 532 if (field == strlen(tag) && Bstrncasecmp(entry, tag, field) == 0) 533 vc_loopend = Xstrdup(value); 534 } 535 536 for (int t = 0; t < loopLengthTagCount && vc_looplength == nullptr; ++t) 537 { 538 char const * const tag = loopLengthTags[t]; 539 if (field == strlen(tag) && Bstrncasecmp(entry, tag, field) == 0) 540 vc_looplength = Xstrdup(value); 541 } 542 } 543 } 544 } 545 546 FLAC__metadata_object_delete( 547 tags); // If it were not for this, I would assign pointers instead of strdup(). 548 } while (FLAC__metadata_iterator_next(metadata_iterator)); 549 550 if (vc_loopstart != nullptr) 551 { 552 { 553 const FLAC__int64 flac_loopstart = atol(vc_loopstart); 554 if (flac_loopstart >= 0) // a loop starting at 0 is valid 555 { 556 voice->Loop.Start = (const char *)(intptr_t)flac_loopstart; 557 voice->Loop.Size = 1; 558 } 559 } 560 Xfree(vc_loopstart); 561 } 562 if (vc_loopend != nullptr) 563 { 564 if (voice->Loop.Size > 0) 565 { 566 const FLAC__int64 flac_loopend = atol(vc_loopend); 567 if (flac_loopend > 0) // a loop ending at 0 is invalid 568 voice->Loop.End = (const char *)(intptr_t)flac_loopend; 569 } 570 Xfree(vc_loopend); 571 } 572 if (vc_looplength != nullptr) 573 { 574 if (voice->Loop.Size > 0 && voice->Loop.End == 0) 575 { 576 const FLAC__int64 flac_looplength = atol(vc_looplength); 577 if (flac_looplength > 0) // a loop of length 0 is invalid 578 voice->Loop.End = (const char *)((intptr_t)flac_looplength + (intptr_t)voice->Loop.Start); 579 } 580 Xfree(vc_looplength); 581 } 582 583 FLAC__metadata_iterator_delete(metadata_iterator); 584 } 585 else 586 LOG_F(ERROR, "MV_PlayFLAC: error in FLAC__metadata_iterator_new"); 587 } 588 else 589 LOG_F(ERROR, "MV_PlayFLAC: error in FLAC__metadata_chain_read_with_callbacks: %s", FLAC__Metadata_ChainStatusString[FLAC__metadata_chain_status(metadata_chain)]); 590 591 // FLAC__metadata_chain_delete(metadata_chain); // when run with GDB, this throws SIGTRAP about freed heap 592 // memory being modified 593 } 594 else 595 LOG_F(ERROR, "MV_PlayFLAC: error in FLAC__metadata_chain_new"); 596 597 // CODEDUP multivoc.c MV_SetVoicePitch 598 voice->RateScale = divideu64((uint64_t)voice->SamplingRate * voice->PitchScale, MV_MixRate); 599 voice->FixedPointBufferSize = (voice->RateScale * MV_MIXBUFFERSIZE) - voice->RateScale; 600 MV_SetVoiceMixMode(voice); 601 602 MV_SetVoiceVolume(voice, vol, left, right, volume); 603 MV_PlayVoice(voice); 604 605 return voice->handle; 606 } 607 608 609 void MV_ReleaseFLACVoice(VoiceNode *voice) 610 { 611 Bassert(voice->wavetype == FMT_FLAC && voice->rawdataptr != nullptr && voice->rawdatasiz == sizeof(flac_data)); 612 613 flac_data *fd = (flac_data *)voice->rawdataptr; 614 voice->rawdataptr = nullptr; 615 voice->rawdatasiz = 0; 616 617 if (fd->stream != nullptr) 618 { 619 auto stream = fd->stream; 620 fd->stream = nullptr; 621 622 FLAC__stream_decoder_finish(stream); 623 FLAC__stream_decoder_delete(stream); 624 } 625 626 auto block = fd->block; 627 fd->block = nullptr; 628 629 ALIGNED_FREE_AND_NULL(block); 630 ALIGNED_FREE_AND_NULL(fd); 631 } 632 #else 633 #include "_multivc.h" 634 635 int MV_PlayFLAC(char *, uint32_t, int, int, int, int, int, int, int, fix16_t, intptr_t) 636 { 637 LOG_F(ERROR, "MV_PlayFLAC: FLAC support not included in this binary."); 638 return -1; 639 } 640 641 int MV_PlayFLAC3D(char *, uint32_t, int, int, int, int, int, fix16_t, intptr_t) 642 { 643 LOG_F(ERROR, "MV_PlayFLAC: FLAC support not included in this binary."); 644 return -1; 645 } 646 #endif // HAVE_FLAC