From f7667aad8dd03c1cdaa2033f7a37815b7dd1f9b4 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 31 Oct 2018 17:04:24 +0100 Subject: [PATCH] Reviewed audio issues - Updated dr_mp3 and implemented it - Reviewed sampleCount vs frameCount - Reviewed XM playing (some weird things...) --- examples/audio/audio_music_stream.c | 2 + src/audio.c | 44 ++-- src/external/dr_mp3.h | 372 +++++++++++++++++++++------- 3 files changed, 314 insertions(+), 104 deletions(-) diff --git a/examples/audio/audio_music_stream.c b/examples/audio/audio_music_stream.c index f9fe23d2..26450a86 100644 --- a/examples/audio/audio_music_stream.c +++ b/examples/audio/audio_music_stream.c @@ -59,6 +59,8 @@ int main() // Get timePlayed scaled to bar dimensions (400 pixels) timePlayed = GetMusicTimePlayed(music)/GetMusicTimeLength(music)*400; + + if (timePlayed > 400) StopMusicStream(music); //---------------------------------------------------------------------------------- // Draw diff --git a/src/audio.c b/src/audio.c index 51023280..5ec23bb2 100644 --- a/src/audio.c +++ b/src/audio.c @@ -214,7 +214,7 @@ typedef enum { AUDIO_BUFFER_USAGE_STATIC = 0, AUDIO_BUFFER_USAGE_STREAM } AudioB // Audio buffer structure // NOTE: Slightly different logic is used when feeding data to the playback device depending on whether or not data is streamed -typedef struct AudioBuffer AudioBuffer; +typedef struct AudioBuffer AudioBuffer; struct AudioBuffer { mal_dsp dsp; // Required for format conversion float volume; @@ -1130,13 +1130,12 @@ Music LoadMusicStream(const char *fileName) TraceLog(LOG_INFO, "[%s] MP3 sample rate: %i", fileName, music->ctxMp3.sampleRate); TraceLog(LOG_INFO, "[%s] MP3 bits per sample: %i", fileName, 32); TraceLog(LOG_INFO, "[%s] MP3 channels: %i", fileName, music->ctxMp3.channels); - TraceLog(LOG_INFO, "[%s] MP3 frames remaining: %i", fileName, (unsigned int)music->ctxMp3.framesRemaining); music->stream = InitAudioStream(music->ctxMp3.sampleRate, 32, music->ctxMp3.channels); // TODO: There is not an easy way to compute the total number of samples available // in an MP3, frames size could be variable... we tried with a 60 seconds music... but crashes... - music->totalSamples = 60*music->ctxMp3.sampleRate*music->ctxMp3.channels; + music->totalSamples = drmp3_get_pcm_frame_count(&music->ctxMp3)*music->ctxMp3.channels; music->samplesLeft = music->totalSamples; music->ctxType = MUSIC_AUDIO_MP3; music->loopCount = -1; // Infinite loop by default @@ -1161,8 +1160,8 @@ Music LoadMusicStream(const char *fileName) music->ctxType = MUSIC_MODULE_XM; music->loopCount = -1; // Infinite loop by default - TraceLog(LOG_DEBUG, "[%s] XM number of samples: %i", fileName, music->totalSamples); - TraceLog(LOG_DEBUG, "[%s] XM track length: %11.6f sec", fileName, (float)music->totalSamples/48000.0f); + TraceLog(LOG_INFO, "[%s] XM number of samples: %i", fileName, music->totalSamples); + TraceLog(LOG_INFO, "[%s] XM track length: %11.6f sec", fileName, (float)music->totalSamples/48000.0f); } else musicLoaded = false; } @@ -1283,7 +1282,7 @@ void StopMusicStream(Music music) case MUSIC_AUDIO_FLAC: /* TODO: Restart FLAC context */ break; #endif #if defined(SUPPORT_FILEFORMAT_MP3) - case MUSIC_AUDIO_MP3: /* TODO: Restart MP3 context */ break; + case MUSIC_AUDIO_MP3: drmp3_seek_to_pcm_frame(&music->ctxMp3, 0); break; #endif #if defined(SUPPORT_FILEFORMAT_XM) case MUSIC_MODULE_XM: /* TODO: Restart XM context */ break; @@ -1306,13 +1305,13 @@ void UpdateMusicStream(Music music) unsigned int subBufferSizeInFrames = ((AudioBuffer *)music->stream.audioBuffer)->bufferSizeInFrames/2; // NOTE: Using dynamic allocation because it could require more than 16KB - void *pcm = calloc(subBufferSizeInFrames*music->stream.sampleSize/8*music->stream.channels, 1); + void *pcm = calloc(subBufferSizeInFrames*music->stream.channels*music->stream.sampleSize/8, 1); int samplesCount = 0; // Total size of data steamed in L+R samples for xm floats, individual L or R for ogg shorts while (IsAudioBufferProcessed(music->stream)) { - if (music->samplesLeft >= subBufferSizeInFrames) samplesCount = subBufferSizeInFrames; + if ((music->samplesLeft/music->stream.channels) >= subBufferSizeInFrames) samplesCount = subBufferSizeInFrames*music->stream.channels; else samplesCount = music->samplesLeft; // TODO: Really don't like ctxType thingy... @@ -1321,27 +1320,31 @@ void UpdateMusicStream(Music music) case MUSIC_AUDIO_OGG: { // NOTE: Returns the number of samples to process (be careful! we ask for number of shorts!) - stb_vorbis_get_samples_short_interleaved(music->ctxOgg, music->stream.channels, (short *)pcm, samplesCount*music->stream.channels); + stb_vorbis_get_samples_short_interleaved(music->ctxOgg, music->stream.channels, (short *)pcm, samplesCount); } break; #if defined(SUPPORT_FILEFORMAT_FLAC) case MUSIC_AUDIO_FLAC: { // NOTE: Returns the number of samples to process - unsigned int numSamplesFlac = (unsigned int)drflac_read_s16(music->ctxFlac, samplesCount*music->stream.channels, (short *)pcm); + unsigned int numSamplesFlac = (unsigned int)drflac_read_s16(music->ctxFlac, samplesCount, (short *)pcm); } break; #endif #if defined(SUPPORT_FILEFORMAT_MP3) case MUSIC_AUDIO_MP3: { - // NOTE: Returns the number of samples to process - unsigned int numSamplesMp3 = (unsigned int)drmp3_read_f32(&music->ctxMp3, samplesCount*music->stream.channels, (float *)pcm); + // NOTE: samplesCount, actually refers to framesCount and returns the number of frames processed + unsigned int numFramesMp3 = (unsigned int)drmp3_read_pcm_frames_f32(&music->ctxMp3, samplesCount/music->stream.channels, (float *)pcm); } break; #endif #if defined(SUPPORT_FILEFORMAT_XM) - case MUSIC_MODULE_XM: jar_xm_generate_samples_16bit(music->ctxXm, pcm, samplesCount); break; + case MUSIC_MODULE_XM: + { + // NOTE: Internally this function considers 2 channels generation, so samplesCount/2 --> WEIRD + jar_xm_generate_samples_16bit(music->ctxXm, (short *)pcm, samplesCount/2); + } break; #endif #if defined(SUPPORT_FILEFORMAT_MOD) case MUSIC_MODULE_MOD: jar_mod_fillbuffer(&music->ctxMod, pcm, samplesCount, 0); break; @@ -1414,7 +1417,7 @@ void SetMusicLoopCount(Music music, int count) // Get music time length (in seconds) float GetMusicTimeLength(Music music) { - float totalSeconds = (float)music->totalSamples/music->stream.sampleRate; + float totalSeconds = (float)music->totalSamples/(music->stream.sampleRate*music->stream.channels); return totalSeconds; } @@ -1425,12 +1428,11 @@ float GetMusicTimePlayed(Music music) float secondsPlayed = 0.0f; unsigned int samplesPlayed = music->totalSamples - music->samplesLeft; - secondsPlayed = (float)samplesPlayed/music->stream.sampleRate; + secondsPlayed = (float)samplesPlayed/(music->stream.sampleRate*music->stream.channels); return secondsPlayed; } - // Init audio stream (to stream audio pcm data) AudioStream InitAudioStream(unsigned int sampleRate, unsigned int sampleSize, unsigned int channels) { @@ -1509,11 +1511,11 @@ void UpdateAudioStream(AudioStream stream, const void *data, int samplesCount) unsigned char *subBuffer = audioBuffer->buffer + ((subBufferSizeInFrames*stream.channels*(stream.sampleSize/8))*subBufferToUpdate); // Does this API expect a whole buffer to be updated in one go? Assuming so, but if not will need to change this logic. - if (subBufferSizeInFrames >= (mal_uint32)samplesCount) + if (subBufferSizeInFrames >= (mal_uint32)samplesCount/stream.channels) { mal_uint32 framesToWrite = subBufferSizeInFrames; - if (framesToWrite > (mal_uint32)samplesCount) framesToWrite = (mal_uint32)samplesCount; + if (framesToWrite > ((mal_uint32)samplesCount/stream.channels)) framesToWrite = (mal_uint32)samplesCount/stream.channels; mal_uint32 bytesToWrite = framesToWrite*stream.channels*(stream.sampleSize/8); memcpy(subBuffer, data, bytesToWrite); @@ -1867,13 +1869,13 @@ static Wave LoadMP3(const char *fileName) Wave wave = { 0 }; // Decode an entire MP3 file in one go - uint64_t totalSampleCount = 0; + uint64_t totalFrameCount = 0; drmp3_config config = { 0 }; - wave.data = drmp3_open_and_decode_file_f32(fileName, &config, &totalSampleCount); + wave.data = drmp3_open_file_and_read_f32(fileName, &config, &totalFrameCount); wave.channels = config.outputChannels; wave.sampleRate = config.outputSampleRate; - wave.sampleCount = (int)totalSampleCount; + wave.sampleCount = (int)totalFrameCount*wave.channels; wave.sampleSize = 32; // NOTE: Only support up to 2 channels (mono, stereo) diff --git a/src/external/dr_mp3.h b/src/external/dr_mp3.h index cd8920a1..070f0c15 100644 --- a/src/external/dr_mp3.h +++ b/src/external/dr_mp3.h @@ -1,5 +1,5 @@ // MP3 audio decoder. Public domain. See "unlicense" statement at the end of this file. -// dr_mp3 - v0.3.2 - 2018-09-11 +// dr_mp3 - v0.4.0 - 2018-xx-xx // // David Reid - mackron@gmail.com // @@ -52,11 +52,6 @@ // // #define DR_MP3_NO_SIMD // Disable SIMD optimizations. -// -// -// LIMITATIONS -// =========== -// - Seeking is extremely inefficient. #ifndef dr_mp3_h #define dr_mp3_h @@ -92,7 +87,8 @@ typedef drmp3_uint32 drmp3_bool32; #define DRMP3_TRUE 1 #define DRMP3_FALSE 0 -#define DRMP3_MAX_SAMPLES_PER_FRAME (1152*2) +#define DRMP3_MAX_PCM_FRAMES_PER_MP3_FRAME 1152 +#define DRMP3_MAX_SAMPLES_PER_FRAME (DRMP3_MAX_PCM_FRAMES_PER_MP3_FRAME*2) // Low Level Push API @@ -214,11 +210,12 @@ typedef struct drmp3_read_proc onRead; drmp3_seek_proc onSeek; void* pUserData; - drmp3_uint32 frameChannels; // The number of channels in the currently loaded MP3 frame. Internal use only. - drmp3_uint32 frameSampleRate; // The sample rate of the currently loaded MP3 frame. Internal use only. - drmp3_uint32 framesConsumed; - drmp3_uint32 framesRemaining; - drmp3_uint8 frames[sizeof(float)*DRMP3_MAX_SAMPLES_PER_FRAME]; // <-- Multipled by sizeof(float) to ensure there's enough room for DR_MP3_FLOAT_OUTPUT. + drmp3_uint32 mp3FrameChannels; // The number of channels in the currently loaded MP3 frame. Internal use only. + drmp3_uint32 mp3FrameSampleRate; // The sample rate of the currently loaded MP3 frame. Internal use only. + drmp3_uint32 pcmFramesConsumedInMP3Frame; + drmp3_uint32 pcmFramesRemainingInMP3Frame; + drmp3_uint8 pcmFrames[sizeof(float)*DRMP3_MAX_SAMPLES_PER_FRAME]; // <-- Multipled by sizeof(float) to ensure there's enough room for DR_MP3_FLOAT_OUTPUT. + drmp3_uint64 currentPCMFrame; // The current PCM frame, globally, based on the output sample rate. Mainly used for seeking. drmp3_src src; size_t dataSize; size_t dataCapacity; @@ -268,12 +265,21 @@ void drmp3_uninit(drmp3* pMP3); // Reads PCM frames as interleaved 32-bit IEEE floating point PCM. // // Note that framesToRead specifies the number of PCM frames to read, _not_ the number of MP3 frames. -drmp3_uint64 drmp3_read_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut); +drmp3_uint64 drmp3_read_pcm_frames_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut); // Seeks to a specific frame. // // Note that this is _not_ an MP3 frame, but rather a PCM frame. -drmp3_bool32 drmp3_seek_to_frame(drmp3* pMP3, drmp3_uint64 frameIndex); +drmp3_bool32 drmp3_seek_to_pcm_frame(drmp3* pMP3, drmp3_uint64 frameIndex); + +// Calculates the total number of PCM frames in the MP3 stream. Cannot be used for infinite streams such as internet +// radio. Runs in linear time. Returns 0 on error. +drmp3_uint64 drmp3_get_pcm_frame_count(drmp3* pMP3); + +// Calculates the total number of MP3 frames in the MP3 stream. Cannot be used for infinite streams such as internet +// radio. Runs in linear time. Returns 0 on error. +drmp3_uint64 drmp3_get_mp3_frame_count(drmp3* pMP3); + // Opens an decodes an entire MP3 stream as a single operation. @@ -281,10 +287,10 @@ drmp3_bool32 drmp3_seek_to_frame(drmp3* pMP3, drmp3_uint64 frameIndex); // pConfig is both an input and output. On input it contains what you want. On output it contains what you got. // // Free the returned pointer with drmp3_free(). -float* drmp3_open_and_decode_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount); -float* drmp3_open_and_decode_memory_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount); +float* drmp3_open_and_read_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount); +float* drmp3_open_memory_and_read_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount); #ifndef DR_MP3_NO_STDIO -float* drmp3_open_and_decode_file_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount); +float* drmp3_open_file_and_read_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount); #endif // Frees any memory that was allocated by a public drmp3 API. @@ -2376,35 +2382,46 @@ drmp3_uint64 drmp3_src_read_frames_linear(drmp3_src* pSRC, drmp3_uint64 frameCou } +static size_t drmp3__on_read(drmp3* pMP3, void* pBufferOut, size_t bytesToRead) +{ + return pMP3->onRead(pMP3->pUserData, pBufferOut, bytesToRead); +} -static drmp3_bool32 drmp3_decode_next_frame(drmp3* pMP3) +static drmp3_bool32 drmp3__on_seek(drmp3* pMP3, int offset, drmp3_seek_origin origin) +{ + drmp3_assert(offset >= 0); + return pMP3->onSeek(pMP3->pUserData, offset, origin); +} + + +static drmp3_uint32 drmp3_decode_next_frame_ex(drmp3* pMP3, drmp3d_sample_t* pPCMFrames) { drmp3_assert(pMP3 != NULL); drmp3_assert(pMP3->onRead != NULL); if (pMP3->atEnd) { - return DRMP3_FALSE; + return 0; } - do - { + drmp3_uint32 pcmFramesRead = 0; + do { // minimp3 recommends doing data submission in 16K chunks. If we don't have at least 16K bytes available, get more. if (pMP3->dataSize < DRMP3_DATA_CHUNK_SIZE) { if (pMP3->dataCapacity < DRMP3_DATA_CHUNK_SIZE) { pMP3->dataCapacity = DRMP3_DATA_CHUNK_SIZE; drmp3_uint8* pNewData = (drmp3_uint8*)drmp3_realloc(pMP3->pData, pMP3->dataCapacity); if (pNewData == NULL) { - return DRMP3_FALSE; // Out of memory. + return 0; // Out of memory. } pMP3->pData = pNewData; } - size_t bytesRead = pMP3->onRead(pMP3->pUserData, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); + size_t bytesRead = drmp3__on_read(pMP3, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); if (bytesRead == 0) { if (pMP3->dataSize == 0) { pMP3->atEnd = DRMP3_TRUE; - return DRMP3_FALSE; // No data. + return 0; // No data. } } @@ -2413,23 +2430,23 @@ static drmp3_bool32 drmp3_decode_next_frame(drmp3* pMP3) if (pMP3->dataSize > INT_MAX) { pMP3->atEnd = DRMP3_TRUE; - return DRMP3_FALSE; // File too big. + return 0; // File too big. } drmp3dec_frame_info info; - drmp3_uint32 samplesRead = drmp3dec_decode_frame(&pMP3->decoder, pMP3->pData, (int)pMP3->dataSize, (drmp3d_sample_t*)pMP3->frames, &info); // <-- Safe size_t -> int conversion thanks to the check above. - if (samplesRead != 0) { + pcmFramesRead = drmp3dec_decode_frame(&pMP3->decoder, pMP3->pData, (int)pMP3->dataSize, pPCMFrames, &info); // <-- Safe size_t -> int conversion thanks to the check above. + if (pcmFramesRead != 0) { size_t leftoverDataSize = (pMP3->dataSize - (size_t)info.frame_bytes); for (size_t i = 0; i < leftoverDataSize; ++i) { pMP3->pData[i] = pMP3->pData[i + (size_t)info.frame_bytes]; } pMP3->dataSize = leftoverDataSize; - pMP3->framesConsumed = 0; - pMP3->framesRemaining = samplesRead; - pMP3->frameChannels = info.channels; - pMP3->frameSampleRate = info.hz; - drmp3_src_set_input_sample_rate(&pMP3->src, pMP3->frameSampleRate); + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = pcmFramesRead; + pMP3->mp3FrameChannels = info.channels; + pMP3->mp3FrameSampleRate = info.hz; + drmp3_src_set_input_sample_rate(&pMP3->src, pMP3->mp3FrameSampleRate); break; } else { // Need more data. minimp3 recommends doing data submission in 16K chunks. @@ -2438,24 +2455,47 @@ static drmp3_bool32 drmp3_decode_next_frame(drmp3* pMP3) pMP3->dataCapacity += DRMP3_DATA_CHUNK_SIZE; drmp3_uint8* pNewData = (drmp3_uint8*)drmp3_realloc(pMP3->pData, pMP3->dataCapacity); if (pNewData == NULL) { - return DRMP3_FALSE; // Out of memory. + return 0; // Out of memory. } pMP3->pData = pNewData; } // Fill in a chunk. - size_t bytesRead = pMP3->onRead(pMP3->pUserData, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); + size_t bytesRead = drmp3__on_read(pMP3, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); if (bytesRead == 0) { pMP3->atEnd = DRMP3_TRUE; - return DRMP3_FALSE; // Error reading more data. + return 0; // Error reading more data. } pMP3->dataSize += bytesRead; } } while (DRMP3_TRUE); - return DRMP3_TRUE; + return pcmFramesRead; +} + +static drmp3_uint32 drmp3_decode_next_frame(drmp3* pMP3) +{ + drmp3_assert(pMP3 != NULL); + return drmp3_decode_next_frame_ex(pMP3, (drmp3d_sample_t*)pMP3->pcmFrames); +} + +static drmp3_uint32 drmp3_seek_next_frame(drmp3* pMP3) +{ + drmp3_assert(pMP3 != NULL); + + drmp3_uint32 pcmFrameCount = drmp3_decode_next_frame_ex(pMP3, NULL); + if (pcmFrameCount == 0) { + return 0; + } + + // We have essentially just skipped past the frame, so just set the remaining samples to 0. + pMP3->currentPCMFrame += pcmFrameCount; + pMP3->pcmFramesConsumedInMP3Frame = pcmFrameCount; + pMP3->pcmFramesRemainingInMP3Frame = 0; + + return pcmFrameCount; } static drmp3_uint64 drmp3_read_src(drmp3_src* pSRC, drmp3_uint64 frameCount, void* pFramesOut, void* pUserData) @@ -2465,64 +2505,64 @@ static drmp3_uint64 drmp3_read_src(drmp3_src* pSRC, drmp3_uint64 frameCount, voi drmp3_assert(pMP3->onRead != NULL); float* pFramesOutF = (float*)pFramesOut; - drmp3_uint32 totalFramesRead = 0; + drmp3_uint64 totalFramesRead = 0; while (frameCount > 0) { // Read from the in-memory buffer first. - while (pMP3->framesRemaining > 0 && frameCount > 0) { - drmp3d_sample_t* frames = (drmp3d_sample_t*)pMP3->frames; + while (pMP3->pcmFramesRemainingInMP3Frame > 0 && frameCount > 0) { + drmp3d_sample_t* frames = (drmp3d_sample_t*)pMP3->pcmFrames; #ifndef DR_MP3_FLOAT_OUTPUT - if (pMP3->frameChannels == 1) { + if (pMP3->mp3FrameChannels == 1) { if (pMP3->channels == 1) { // Mono -> Mono. - pFramesOutF[0] = frames[pMP3->framesConsumed] / 32768.0f; + pFramesOutF[0] = frames[pMP3->pcmFramesConsumedInMP3Frame] / 32768.0f; } else { // Mono -> Stereo. - pFramesOutF[0] = frames[pMP3->framesConsumed] / 32768.0f; - pFramesOutF[1] = frames[pMP3->framesConsumed] / 32768.0f; + pFramesOutF[0] = frames[pMP3->pcmFramesConsumedInMP3Frame] / 32768.0f; + pFramesOutF[1] = frames[pMP3->pcmFramesConsumedInMP3Frame] / 32768.0f; } } else { if (pMP3->channels == 1) { // Stereo -> Mono float sample = 0; - sample += frames[(pMP3->framesConsumed*pMP3->frameChannels)+0] / 32768.0f; - sample += frames[(pMP3->framesConsumed*pMP3->frameChannels)+1] / 32768.0f; + sample += frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+0] / 32768.0f; + sample += frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+1] / 32768.0f; pFramesOutF[0] = sample * 0.5f; } else { // Stereo -> Stereo - pFramesOutF[0] = frames[(pMP3->framesConsumed*pMP3->frameChannels)+0] / 32768.0f; - pFramesOutF[1] = frames[(pMP3->framesConsumed*pMP3->frameChannels)+1] / 32768.0f; + pFramesOutF[0] = frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+0] / 32768.0f; + pFramesOutF[1] = frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+1] / 32768.0f; } } #else - if (pMP3->frameChannels == 1) { + if (pMP3->mp3FrameChannels == 1) { if (pMP3->channels == 1) { // Mono -> Mono. - pFramesOutF[0] = frames[pMP3->framesConsumed]; + pFramesOutF[0] = frames[pMP3->pcmFramesConsumedInMP3Frame]; } else { // Mono -> Stereo. - pFramesOutF[0] = frames[pMP3->framesConsumed]; - pFramesOutF[1] = frames[pMP3->framesConsumed]; + pFramesOutF[0] = frames[pMP3->pcmFramesConsumedInMP3Frame]; + pFramesOutF[1] = frames[pMP3->pcmFramesConsumedInMP3Frame]; } } else { if (pMP3->channels == 1) { // Stereo -> Mono float sample = 0; - sample += frames[(pMP3->framesConsumed*pMP3->frameChannels)+0]; - sample += frames[(pMP3->framesConsumed*pMP3->frameChannels)+1]; + sample += frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+0]; + sample += frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+1]; pFramesOutF[0] = sample * 0.5f; } else { // Stereo -> Stereo - pFramesOutF[0] = frames[(pMP3->framesConsumed*pMP3->frameChannels)+0]; - pFramesOutF[1] = frames[(pMP3->framesConsumed*pMP3->frameChannels)+1]; + pFramesOutF[0] = frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+0]; + pFramesOutF[1] = frames[(pMP3->pcmFramesConsumedInMP3Frame*pMP3->mp3FrameChannels)+1]; } } #endif - pMP3->framesConsumed += 1; - pMP3->framesRemaining -= 1; - frameCount -= 1; + pMP3->pcmFramesConsumedInMP3Frame += 1; + pMP3->pcmFramesRemainingInMP3Frame -= 1; totalFramesRead += 1; + frameCount -= 1; pFramesOutF += pSRC->config.channels; } @@ -2530,11 +2570,11 @@ static drmp3_uint64 drmp3_read_src(drmp3_src* pSRC, drmp3_uint64 frameCount, voi break; } - drmp3_assert(pMP3->framesRemaining == 0); + drmp3_assert(pMP3->pcmFramesRemainingInMP3Frame == 0); // At this point we have exhausted our in-memory buffer so we need to re-fill. Note that the sample rate may have changed // at this point which means we'll also need to update our sample rate conversion pipeline. - if (!drmp3_decode_next_frame(pMP3)) { + if (drmp3_decode_next_frame(pMP3) == 0) { break; } } @@ -2710,7 +2750,9 @@ drmp3_bool32 drmp3_init_file(drmp3* pMP3, const char* filePath, const drmp3_conf void drmp3_uninit(drmp3* pMP3) { - if (pMP3 == NULL) return; + if (pMP3 == NULL) { + return; + } #ifndef DR_MP3_NO_STDIO if (pMP3->onRead == drmp3__on_read_stdio) { @@ -2721,9 +2763,11 @@ void drmp3_uninit(drmp3* pMP3) drmp3_free(pMP3->pData); } -drmp3_uint64 drmp3_read_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut) +drmp3_uint64 drmp3_read_pcm_frames_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut) { - if (pMP3 == NULL || pMP3->onRead == NULL) return 0; + if (pMP3 == NULL || pMP3->onRead == NULL) { + return 0; + } drmp3_uint64 totalFramesRead = 0; @@ -2735,7 +2779,7 @@ drmp3_uint64 drmp3_read_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBuff framesToReadRightNow = framesToRead; } - drmp3_uint64 framesJustRead = drmp3_read_f32(pMP3, framesToReadRightNow, temp); + drmp3_uint64 framesJustRead = drmp3_read_pcm_frames_f32(pMP3, framesToReadRightNow, temp); if (framesJustRead == 0) { break; } @@ -2745,40 +2789,191 @@ drmp3_uint64 drmp3_read_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBuff } } else { totalFramesRead = drmp3_src_read_frames_ex(&pMP3->src, framesToRead, pBufferOut, DRMP3_TRUE); + pMP3->currentPCMFrame += totalFramesRead; } return totalFramesRead; } -drmp3_bool32 drmp3_seek_to_frame(drmp3* pMP3, drmp3_uint64 frameIndex) +drmp3_bool32 drmp3_seek_to_start_of_stream(drmp3* pMP3) { - if (pMP3 == NULL || pMP3->onSeek == NULL) return DRMP3_FALSE; + drmp3_assert(pMP3 != NULL); + drmp3_assert(pMP3->onSeek != NULL); // Seek to the start of the stream to begin with. - if (!pMP3->onSeek(pMP3->pUserData, 0, drmp3_seek_origin_start)) { + if (!drmp3__on_seek(pMP3, 0, drmp3_seek_origin_start)) { return DRMP3_FALSE; } // Clear any cached data. - pMP3->framesConsumed = 0; - pMP3->framesRemaining = 0; + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = 0; + pMP3->currentPCMFrame = 0; pMP3->dataSize = 0; pMP3->atEnd = DRMP3_FALSE; - // TODO: Optimize. - // - // This is inefficient. We simply read frames from the start of the stream. - drmp3_uint64 framesRead = drmp3_read_f32(pMP3, frameIndex, NULL); - if (framesRead != frameIndex) { + return DRMP3_TRUE; +} + +drmp3_bool32 drmp3_seek_to_pcm_frame__brute_force(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + drmp3_assert(pMP3 != NULL); + + if (frameIndex == pMP3->currentPCMFrame) { + return DRMP3_TRUE; + } + + // If we're moving foward we just read from where we're at. Otherwise we need to move back to the start of + // the stream and read from the beginning. + drmp3_uint64 framesToReadAndDiscard; + if (frameIndex >= pMP3->currentPCMFrame) { + // Moving foward. + framesToReadAndDiscard = frameIndex - pMP3->currentPCMFrame; + } else { + // Moving backward. Move to the start of the stream and then move forward. + framesToReadAndDiscard = frameIndex; + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + } + + // MP3 is a bit annoying when it comes to seeking because of the bit reservoir. It basically means that an MP3 frame can possibly + // depend on some of the data of prior frames. This means it's not as simple as seeking to the first byte of the MP3 frame that + // contains the sample because that MP3 frame will need the data from the previous MP3 frame (which we just seeked past!). To + // resolve this we seek past a number of MP3 frames up to a point, and then read-and-discard the remainder. + drmp3_uint64 maxFramesToReadAndDiscard = DRMP3_MAX_PCM_FRAMES_PER_MP3_FRAME * 3; + + // First get rid of anything that's still sitting in the buffer. + if (framesToReadAndDiscard > maxFramesToReadAndDiscard && framesToReadAndDiscard > pMP3->pcmFramesRemainingInMP3Frame) { + framesToReadAndDiscard -= pMP3->pcmFramesRemainingInMP3Frame; + pMP3->currentPCMFrame += pMP3->pcmFramesRemainingInMP3Frame; + pMP3->pcmFramesConsumedInMP3Frame += pMP3->pcmFramesRemainingInMP3Frame; + pMP3->pcmFramesRemainingInMP3Frame = 0; + } + + // Now get rid of leading whole frames. + while (framesToReadAndDiscard > maxFramesToReadAndDiscard) { + drmp3_uint32 pcmFramesSeeked = drmp3_seek_next_frame(pMP3); + if (pcmFramesSeeked == 0) { + break; + } + + framesToReadAndDiscard -= pcmFramesSeeked; + } + + // The last step is to read-and-discard any remaining PCM frames to make it sample-exact. + drmp3_uint64 framesRead = drmp3_read_pcm_frames_f32(pMP3, framesToReadAndDiscard, NULL); + if (framesRead != framesToReadAndDiscard) { return DRMP3_FALSE; } return DRMP3_TRUE; } +drmp3_bool32 drmp3_seek_to_pcm_frame(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + if (pMP3 == NULL || pMP3->onSeek == NULL) { + return DRMP3_FALSE; + } + + // We currently only support brute force seeking. + return drmp3_seek_to_pcm_frame__brute_force(pMP3, frameIndex); +} + +drmp3_uint64 drmp3_get_pcm_frame_count(drmp3* pMP3) +{ + if (pMP3 == NULL) { + return 0; + } + + // The way this works is we move back to the start of the stream, iterate over each MP3 frame and calculate the frame count based + // on our output sample rate, the seek back to the PCM frame we were sitting on before calling this function. + + // The stream must support seeking for this to work. + if (pMP3->onSeek == NULL) { + return 0; + } + + // We'll need to seek back to where we were, so grab the PCM frame we're currently sitting on so we can restore later. + drmp3_uint64 currentPCMFrame = pMP3->currentPCMFrame; + + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return 0; + } + + drmp3_uint64 totalPCMFrameCount = 0; + float totalPCMFrameCountFractionalPart = 0; // <-- With resampling there will be a fractional part to each MP3 frame that we need to accumulate. + for (;;) { + drmp3_uint32 pcmFramesInCurrentMP3FrameIn = drmp3_decode_next_frame_ex(pMP3, NULL); // <-- Passing in NULL here will prevent decoding of the MP3 frame which should save time. + if (pcmFramesInCurrentMP3FrameIn == 0) { + break; + } + + float srcRatio = (float)pMP3->mp3FrameSampleRate / (float)pMP3->sampleRate; + drmp3_assert(srcRatio > 0); + + float pcmFramesInCurrentMP3FrameOutF = totalPCMFrameCountFractionalPart + (pcmFramesInCurrentMP3FrameIn / srcRatio); + drmp3_uint32 pcmFramesInCurrentMP3FrameOut = (drmp3_uint32)pcmFramesInCurrentMP3FrameOutF; + totalPCMFrameCountFractionalPart = pcmFramesInCurrentMP3FrameOutF - pcmFramesInCurrentMP3FrameOut; + totalPCMFrameCount += pcmFramesInCurrentMP3FrameOut; + } + + // Finally, we need to seek back to where we were. + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return 0; + } + + if (!drmp3_seek_to_pcm_frame(pMP3, currentPCMFrame)) { + return 0; + } + + return totalPCMFrameCount; +} + +drmp3_uint64 drmp3_get_mp3_frame_count(drmp3* pMP3) +{ + if (pMP3 == NULL) { + return 0; + } + + // This works the same way as drmp3_get_pcm_frame_count() - move to the start, count MP3 frames, move back to the previous position. + + // The stream must support seeking for this to work. + if (pMP3->onSeek == NULL) { + return 0; + } + + // We'll need to seek back to where we were, so grab the PCM frame we're currently sitting on so we can restore later. + drmp3_uint64 currentPCMFrame = pMP3->currentPCMFrame; + + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return 0; + } + + drmp3_uint64 totalMP3FrameCount = 0; + for (;;) { + drmp3_uint32 pcmFramesInCurrentMP3FrameIn = drmp3_decode_next_frame_ex(pMP3, NULL); + if (pcmFramesInCurrentMP3FrameIn == 0) { + break; + } + + totalMP3FrameCount += 1; + } + + // Finally, we need to seek back to where we were. + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return 0; + } + + if (!drmp3_seek_to_pcm_frame(pMP3, currentPCMFrame)) { + return 0; + } + + return totalMP3FrameCount; +} -float* drmp3__full_decode_and_close_f32(drmp3* pMP3, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +float* drmp3__full_read_and_close_f32(drmp3* pMP3, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) { drmp3_assert(pMP3 != NULL); @@ -2789,7 +2984,7 @@ float* drmp3__full_decode_and_close_f32(drmp3* pMP3, drmp3_config* pConfig, drmp float temp[4096]; for (;;) { drmp3_uint64 framesToReadRightNow = drmp3_countof(temp) / pMP3->channels; - drmp3_uint64 framesJustRead = drmp3_read_f32(pMP3, framesToReadRightNow, temp); + drmp3_uint64 framesJustRead = drmp3_read_pcm_frames_f32(pMP3, framesToReadRightNow, temp); if (framesJustRead == 0) { break; } @@ -2835,35 +3030,35 @@ float* drmp3__full_decode_and_close_f32(drmp3* pMP3, drmp3_config* pConfig, drmp return pFrames; } -float* drmp3_open_and_decode_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +float* drmp3_open_and_read_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) { drmp3 mp3; if (!drmp3_init(&mp3, onRead, onSeek, pUserData, pConfig)) { return NULL; } - return drmp3__full_decode_and_close_f32(&mp3, pConfig, pTotalFrameCount); + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); } -float* drmp3_open_and_decode_memory_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +float* drmp3_open_memory_and_read_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) { drmp3 mp3; if (!drmp3_init_memory(&mp3, pData, dataSize, pConfig)) { return NULL; } - return drmp3__full_decode_and_close_f32(&mp3, pConfig, pTotalFrameCount); + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); } #ifndef DR_MP3_NO_STDIO -float* drmp3_open_and_decode_file_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +float* drmp3_open_file_and_read_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) { drmp3 mp3; if (!drmp3_init_file(&mp3, filePath, pConfig)) { return NULL; } - return drmp3__full_decode_and_close_f32(&mp3, pConfig, pTotalFrameCount); + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); } #endif @@ -2890,7 +3085,18 @@ void drmp3_free(void* p) // REVISION HISTORY -// =============== +// ================ +// +// v0.4.0 - 2018-xx-xx +// - API CHANGE: Rename some APIs: +// - drmp3_read_f32 -> to drmp3_read_pcm_frames_f32 +// - drmp3_seek_to_frame -> drmp3_seek_to_pcm_frame +// - drmp3_open_and_decode_f32 -> drmp3_open_and_read_f32 +// - drmp3_open_and_decode_memory_f32 -> drmp3_open_memory_and_read_f32 +// - drmp3_open_and_decode_file_f32 -> drmp3_open_file_and_read_f32 +// - Add drmp3_get_pcm_frame_count(). +// - Add drmp3_get_mp3_frame_count(). +// - Improve seeking performance. // // v0.3.2 - 2018-09-11 // - Fix a couple of memory leaks.