Commit 46a033b4 authored by David Reid's avatar David Reid

Initial work on custom loop points and chaining for data sources.

This is locked behind the following compile time macro for now since
it'll involve breaking changes to custom data sources. This will be
removed in version 0.11:

  MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING

Public issue https://github.com/mackron/miniaudio/issues/290
parent 2eb792b7
/*
Demonstrates one way to chain together a number of data sources so they play back seamlessly
without gaps.
This example uses the chaining system built into the `ma_data_source` API. It will take every sound
passed onto the command line in order, and then loop back and start again. When looping a chain of
data sources, you need only link the last data source back to the first one.
To play a chain of data sources, you first need to set up your chain. To set the data source that
should be played after another, you have two options:
* Set a pointer to a specific data source
* Set a callback that will fire when the next data source needs to be retrieved
The first option is good for simple scenarios. The second option is useful if you need to perform
some action when the end of a sound is reached. This example will be using both.
When reading data from a chain, you always read from the head data source. Internally miniaudio
will track a pointer to the data source in the chain that is currently playing. If you don't
consistently read from the head data source this state will become inconsistent and things won't
work correctly. When using a chain, this pointer needs to be reset if you need to play the
chain again from the start:
```c
ma_data_source_set_current(&headDataSource, &headDataSource);
ma_data_source_seek_to_pcm_frame(&headDataSource, 0);
```
The code above is setting the "current" data source in the chain to the head data source, thereby
starting the chain from the start again. It is also seeking the head data source back to the start
so that playback starts from the start as expected. You do not need to seek non-head items back to
the start as miniaudio will do that for you internally.
*/
#define MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING
#define MINIAUDIO_IMPLEMENTATION
#include "../miniaudio.h"
#include <stdio.h>
/*
For simplicity, this example requires the device to use floating point samples.
*/
#define SAMPLE_FORMAT ma_format_f32
#define CHANNEL_COUNT 2
#define SAMPLE_RATE 48000
ma_uint32 g_decoderCount;
ma_decoder* g_pDecoders;
static ma_data_source* next_callback_tail(ma_data_source* pDataSource)
{
MA_ASSERT(g_decoderCount > 0); /* <-- We check for this in main() so should never happen. */
/*
This will be fired when the last item in the chain has reached the end. In this example we want
to loop back to the start, so we need only return a pointer back to the head.
*/
return &g_pDecoders[0];
}
static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
{
/*
We can just read from the first decoder and miniaudio will resolve the chain for us. Note that
if you want to loop the chain, like we're doing in this example, you need to set the `loop`
parameter to false, or else only the current data source will be looped.
*/
ma_data_source_read_pcm_frames(&g_pDecoders[0], pOutput, frameCount, NULL, MA_FALSE);
/* Unused in this example. */
(void)pDevice;
(void)pInput;
}
int main(int argc, char** argv)
{
ma_result result = MA_SUCCESS;
ma_uint32 iDecoder;
ma_decoder_config decoderConfig;
ma_device_config deviceConfig;
ma_device device;
if (argc < 2) {
printf("No input files.\n");
return -1;
}
g_decoderCount = argc-1;
g_pDecoders = (ma_decoder*)malloc(sizeof(*g_pDecoders) * g_decoderCount);
/* In this example, all decoders need to have the same output format. */
decoderConfig = ma_decoder_config_init(SAMPLE_FORMAT, CHANNEL_COUNT, SAMPLE_RATE);
for (iDecoder = 0; iDecoder < g_decoderCount; ++iDecoder) {
result = ma_decoder_init_file(argv[1+iDecoder], &decoderConfig, &g_pDecoders[iDecoder]);
if (result != MA_SUCCESS) {
ma_uint32 iDecoder2;
for (iDecoder2 = 0; iDecoder2 < iDecoder; ++iDecoder2) {
ma_decoder_uninit(&g_pDecoders[iDecoder2]);
}
free(g_pDecoders);
printf("Failed to load %s.\n", argv[1+iDecoder]);
return -1;
}
}
/*
We're going to set up our decoders to run one after the other, but then have the last one loop back
to the first one. For demonstration purposes we're going to use the callback method for the last
data source.
*/
for (iDecoder = 0; iDecoder < g_decoderCount-1; iDecoder += 1) {
ma_data_source_set_next(&g_pDecoders[iDecoder], &g_pDecoders[iDecoder+1]);
}
/*
For the last data source we'll loop back to the start, but for demonstration purposes we'll use a
callback to determine the next data source in the chain.
*/
ma_data_source_set_next_callback(&g_pDecoders[g_decoderCount-1], next_callback_tail);
/*
The data source chain has been established so now we can get the device up and running so we
can listen to it.
*/
deviceConfig = ma_device_config_init(ma_device_type_playback);
deviceConfig.playback.format = SAMPLE_FORMAT;
deviceConfig.playback.channels = CHANNEL_COUNT;
deviceConfig.sampleRate = SAMPLE_RATE;
deviceConfig.dataCallback = data_callback;
deviceConfig.pUserData = NULL;
if (ma_device_init(NULL, &deviceConfig, &device) != MA_SUCCESS) {
printf("Failed to open playback device.\n");
result = -1;
goto done_decoders;
}
if (ma_device_start(&device) != MA_SUCCESS) {
printf("Failed to start playback device.\n");
result = -1;
goto done;
}
printf("Press Enter to quit...");
getchar();
done:
ma_device_uninit(&device);
done_decoders:
for (iDecoder = 0; iDecoder < g_decoderCount; ++iDecoder) {
ma_decoder_uninit(&g_pDecoders[iDecoder]);
}
free(g_pDecoders);
return 0;
}
\ No newline at end of file
......@@ -5930,9 +5930,11 @@ MA_API ma_result ma_data_source_unmap(ma_data_source* pDataSource, ma_uint64 fra
MA_API ma_result ma_data_source_get_data_format(ma_data_source* pDataSource, ma_format* pFormat, ma_uint32* pChannels, ma_uint32* pSampleRate);
MA_API ma_result ma_data_source_get_cursor_in_pcm_frames(ma_data_source* pDataSource, ma_uint64* pCursor);
MA_API ma_result ma_data_source_get_length_in_pcm_frames(ma_data_source* pDataSource, ma_uint64* pLength); /* Returns MA_NOT_IMPLEMENTED if the length is unknown or cannot be determined. Decoders can return this. */
#if MA_VERSION_MINOR >= 11
MA_API ma_result ma_data_source_set_range_in_pcm_frames(ma_data_source* pDataSource, ma_uint64 rangeBeg, ma_uint64 rangeLen);
MA_API ma_result ma_data_source_get_range_in_pcm_frames(ma_data_source* pDataSource, ma_uint64* pRangeBeg, ma_uint64* pRangeLen);
#if defined(MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING)
MA_API ma_result ma_data_source_set_range_in_pcm_frames(ma_data_source* pDataSource, ma_uint64 rangeBeg, ma_uint64 rangeEnd);
MA_API ma_result ma_data_source_set_current(ma_data_source* pDataSource, ma_data_source* pCurrentDataSource);
MA_API ma_result ma_data_source_set_next(ma_data_source* pDataSource, ma_data_source* pNextDataSource);
MA_API ma_result ma_data_source_set_next_callback(ma_data_source* pDataSource, ma_data_source_get_next_proc onGetNext);
#endif
......@@ -43172,7 +43174,7 @@ MA_API ma_result ma_data_source_init(const ma_data_source_config* pConfig, ma_da
pDataSourceBase->vtable = pConfig->vtable;
pDataSourceBase->rangeBegInFrames = 0;
pDataSourceBase->rangeEndInFrames = ~((ma_uint64)0);
pDataSourceBase->pCurrent = NULL;
pDataSourceBase->pCurrent = pDataSource; /* Always read from ourself by default. */
pDataSourceBase->pNext = NULL;
pDataSourceBase->onGetNext = NULL;
......@@ -43196,8 +43198,190 @@ MA_API void ma_data_source_uninit(ma_data_source* pDataSource)
*/
}
#if defined(MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING)
static ma_result ma_data_source_resolve_current(ma_data_source* pDataSource, ma_data_source** ppCurrentDataSource)
{
ma_data_source_base* pCurrentDataSource = (ma_data_source_base*)pDataSource;
MA_ASSERT(pDataSource != NULL);
MA_ASSERT(ppCurrentDataSource != NULL);
if (pCurrentDataSource->pCurrent == NULL) {
/*
The current data source is NULL. If we're using this in the context of a chain we need to return NULL
here so that we don't end up looping. Otherwise we just return the data source itself.
*/
if (pCurrentDataSource->pNext != NULL || pCurrentDataSource->onGetNext != NULL) {
pCurrentDataSource = NULL;
} else {
pCurrentDataSource = (ma_data_source_base*)pDataSource; /* Not being used in a chain. Make sure we just always read from the data source itself at all times. */
}
} else {
pCurrentDataSource = pCurrentDataSource->pCurrent;
}
*ppCurrentDataSource = pCurrentDataSource;
return MA_SUCCESS;
}
static ma_result ma_data_source_read_pcm_frames_within_range(ma_data_source* pDataSource, void* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead)
{
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
if (pDataSourceBase == NULL) {
return MA_AT_END;
}
if (pDataSourceBase->rangeEndInFrames == ~((ma_uint64)0)) {
/* No range is set - just read like normal. The data source itself will tell us when the end is reached. */
return pDataSourceBase->cb.onRead(pDataSourceBase, pFramesOut, frameCount, pFramesRead);
} else {
/* Need to clamp to within the range. */
ma_result result;
ma_uint64 cursor;
ma_uint64 framesRead = 0;
result = ma_data_source_get_cursor_in_pcm_frames(pDataSourceBase, &cursor);
if (result != MA_SUCCESS) {
/* Failed to retrieve the cursor. Cannot read within a range. Just read like normal - this may happen for things like noise data sources where it doesn't really matter. */
return pDataSourceBase->cb.onRead(pDataSourceBase, pFramesOut, frameCount, pFramesRead);
}
/* We have the cursor. We need to make sure we don't read beyond our range. */
if (frameCount > (pDataSourceBase->rangeEndInFrames - cursor)) {
frameCount = (pDataSourceBase->rangeEndInFrames - cursor);
}
result = pDataSourceBase->cb.onRead(pDataSourceBase, pFramesOut, frameCount, &framesRead);
if (pFramesRead != NULL) {
*pFramesRead = framesRead;
}
/* We need to make sure MA_AT_END is returned if we hit the end of the range. */
if (result != MA_AT_END && (cursor + framesRead) == pDataSourceBase->rangeEndInFrames) {
result = MA_AT_END;
}
return result;
}
}
#endif
MA_API ma_result ma_data_source_read_pcm_frames(ma_data_source* pDataSource, void* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead, ma_bool32 loop)
{
#if defined(MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING)
ma_result result = MA_SUCCESS;
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
ma_data_source_base* pCurrentDataSource;
void* pRunningFramesOut = pFramesOut;
ma_uint64 totalFramesProcessed = 0;
ma_format format;
ma_uint32 channels;
if (pFramesRead != NULL) {
*pFramesRead = 0;
}
if (pDataSourceBase == NULL) {
return MA_INVALID_ARGS;
}
/*
We need to know the data format so we can advance the output buffer as we read frames. If this
fails, chaining will not work and we'll just read as much as we can from the current source.
*/
if (ma_data_source_get_data_format(pDataSource, &format, &channels, NULL) != MA_SUCCESS) {
result = ma_data_source_resolve_current(pDataSource, (ma_data_source**)&pCurrentDataSource);
if (result != MA_SUCCESS) {
return result;
}
return ma_data_source_read_pcm_frames_within_range(pCurrentDataSource, pFramesOut, frameCount, pFramesRead);
}
/*
Looping is a bit of a special case. When the `loop` argument is true, chaining will not work and
only the current data source will be read from.
*/
/* Keep reading until we've read as many frames as possible. */
while (totalFramesProcessed < frameCount) {
ma_uint64 framesProcessed;
ma_uint64 framesRemaining = frameCount - totalFramesProcessed;
/* We need to resolve the data source that we'll actually be reading from. */
result = ma_data_source_resolve_current(pDataSource, (ma_data_source**)&pCurrentDataSource);
if (result != MA_SUCCESS) {
break;
}
if (pCurrentDataSource == NULL) {
break;
}
result = ma_data_source_read_pcm_frames_within_range(pCurrentDataSource, pRunningFramesOut, framesRemaining, &framesProcessed);
totalFramesProcessed += framesProcessed;
/*
If we encounted an error from the read callback, make sure it's propagated to the caller. The caller may need to know whether or not MA_BUSY is returned which is
not necessarily considered an error.
*/
if (result != MA_SUCCESS && result != MA_AT_END) {
break;
}
/*
We can determine if we've reached the end by checking the return value of the onRead()
callback. To loop back to the start, all we need to do is seek back to the first frame.
*/
if (result == MA_AT_END) {
/*
We reached the end. If we're looping, we just loop back to the start of the current
data source. If we're not looping we need to check if we have another in the chain, and
if so, switch to it.
*/
if (loop) {
if (ma_data_source_seek_to_pcm_frame(pCurrentDataSource, 0) != MA_SUCCESS) {
break; /* Failed to loop. Abort. */
}
} else {
if (pCurrentDataSource->pNext != NULL) {
pDataSourceBase->pCurrent = pCurrentDataSource->pNext;
} else if (pCurrentDataSource->onGetNext != NULL) {
pDataSourceBase->pCurrent = pCurrentDataSource->onGetNext(pCurrentDataSource);
if (pDataSourceBase->pCurrent == NULL) {
break; /* Our callback did not return a next data source. We're done. */
}
} else {
/* Reached the end of the chain. We're done. */
break;
}
/* The next data source needs to be rewound to ensure data is read in looping scenarios. */
ma_data_source_seek_to_pcm_frame(pDataSourceBase->pCurrent, 0);
/*
We need to make sure we clear the MA_AT_END result so we don't accidentally return
it in the event that we coincidentally ended reading at the exact transition point
of two data sources in a chain.
*/
result = MA_SUCCESS;
}
}
if (pRunningFramesOut != NULL) {
pRunningFramesOut = ma_offset_ptr(pRunningFramesOut, framesProcessed * ma_get_bytes_per_frame(format, channels));
}
}
if (pFramesRead != NULL) {
*pFramesRead = totalFramesProcessed;
}
return result;
#else
ma_data_source_callbacks* pCallbacks = (ma_data_source_callbacks*)pDataSource;
/* Safety. */
......@@ -43265,6 +43449,7 @@ MA_API ma_result ma_data_source_read_pcm_frames(ma_data_source* pDataSource, voi
return result;
}
}
#endif
}
MA_API ma_result ma_data_source_seek_pcm_frames(ma_data_source* pDataSource, ma_uint64 frameCount, ma_uint64* pFramesSeeked, ma_bool32 loop)
......@@ -43274,6 +43459,23 @@ MA_API ma_result ma_data_source_seek_pcm_frames(ma_data_source* pDataSource, ma_
MA_API ma_result ma_data_source_seek_to_pcm_frame(ma_data_source* pDataSource, ma_uint64 frameIndex)
{
#if defined(MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING)
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
if (pDataSourceBase == NULL) {
return MA_SUCCESS;
}
if (pDataSourceBase->cb.onSeek == NULL) {
return MA_NOT_IMPLEMENTED;
}
if (frameIndex > pDataSourceBase->rangeEndInFrames) {
return MA_INVALID_OPERATION; /* Trying to seek to far forward. */
}
return pDataSourceBase->cb.onSeek(pDataSource, pDataSourceBase->rangeBegInFrames + frameIndex);
#else
ma_data_source_callbacks* pCallbacks = (ma_data_source_callbacks*)pDataSource;
if (pCallbacks == NULL) {
return MA_INVALID_ARGS;
......@@ -43284,6 +43486,7 @@ MA_API ma_result ma_data_source_seek_to_pcm_frame(ma_data_source* pDataSource, m
}
return pCallbacks->onSeek(pDataSource, frameIndex);
#endif
}
MA_API ma_result ma_data_source_map(ma_data_source* pDataSource, void** ppFramesOut, ma_uint64* pFrameCount)
......@@ -43362,6 +43565,39 @@ MA_API ma_result ma_data_source_get_data_format(ma_data_source* pDataSource, ma_
MA_API ma_result ma_data_source_get_cursor_in_pcm_frames(ma_data_source* pDataSource, ma_uint64* pCursor)
{
#if defined(MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING)
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
ma_result result;
ma_uint64 cursor;
if (pCursor == NULL) {
return MA_INVALID_ARGS;
}
*pCursor = 0;
if (pDataSourceBase == NULL) {
return MA_SUCCESS;
}
if (pDataSourceBase->cb.onGetCursor == NULL) {
return MA_NOT_IMPLEMENTED;
}
result = pDataSourceBase->cb.onGetCursor(pDataSourceBase, &cursor);
if (result != MA_SUCCESS) {
return result;
}
/* The cursor needs to be made relative to the start of the range. */
if (cursor < pDataSourceBase->rangeBegInFrames) { /* Safety check so we don't return some huge number. */
*pCursor = 0;
} else {
*pCursor = cursor - pDataSourceBase->rangeBegInFrames;
}
return MA_SUCCESS;
#else
ma_data_source_callbacks* pCallbacks = (ma_data_source_callbacks*)pDataSource;
if (pCursor == NULL) {
......@@ -43379,10 +43615,44 @@ MA_API ma_result ma_data_source_get_cursor_in_pcm_frames(ma_data_source* pDataSo
}
return pCallbacks->onGetCursor(pDataSource, pCursor);
#endif
}
MA_API ma_result ma_data_source_get_length_in_pcm_frames(ma_data_source* pDataSource, ma_uint64* pLength)
{
#if defined(MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING)
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
if (pLength == NULL) {
return MA_INVALID_ARGS;
}
*pLength = 0;
if (pDataSourceBase == NULL) {
return MA_INVALID_ARGS;
}
/*
If we have a range defined we'll use that to determine the length. This is one of rare times
where we'll actually trust the caller. If they've set the range, I think it's mostly safe to
assume they've set it based on some higher level knowledge of the structure of the sound bank.
*/
if (pDataSourceBase->rangeEndInFrames != ~((ma_uint64)0)) {
*pLength = pDataSourceBase->rangeEndInFrames - pDataSourceBase->rangeBegInFrames;
return MA_SUCCESS;
}
/*
Getting here means a range is not defined so we'll need to get the data source itself to tell
us the length.
*/
if (pDataSourceBase->cb.onGetLength == NULL) {
return MA_NOT_IMPLEMENTED;
}
return pDataSourceBase->cb.onGetLength(pDataSource, pLength);
#else
ma_data_source_callbacks* pCallbacks = (ma_data_source_callbacks*)pDataSource;
if (pLength == NULL) {
......@@ -43400,9 +43670,83 @@ MA_API ma_result ma_data_source_get_length_in_pcm_frames(ma_data_source* pDataSo
}
return pCallbacks->onGetLength(pDataSource, pLength);
#endif
}
#if defined(MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING)
MA_API ma_result ma_data_source_set_range_in_pcm_frames(ma_data_source* pDataSource, ma_uint64 rangeBeg, ma_uint64 rangeEnd)
{
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
ma_result result;
ma_uint64 cursor;
if (pDataSource == NULL) {
return MA_INVALID_ARGS;
}
if (rangeEnd < rangeBeg) {
return MA_INVALID_ARGS; /* The end of the range must come after the beginning. */
}
pDataSourceBase->rangeBegInFrames = rangeBeg;
pDataSourceBase->rangeEndInFrames = rangeEnd;
/* If the new range is past the current cursor position we need to seek to it. */
result = ma_data_source_get_cursor_in_pcm_frames(pDataSource, &cursor);
if (result == MA_SUCCESS) {
/* Seek to within range. Note that our seek positions here are relative to the new range. */
if (cursor < rangeBeg) {
ma_data_source_seek_to_pcm_frame(pDataSource, 0);
} else if (cursor > rangeEnd) {
ma_data_source_seek_to_pcm_frame(pDataSource, rangeEnd - rangeBeg);
}
} else {
/* We failed to get the cursor position. Probably means the data source has no notion of a cursor such a noise data source. Just pretend the seeking worked. */
}
return MA_SUCCESS;
}
MA_API ma_result ma_data_source_set_current(ma_data_source* pDataSource, ma_data_source* pCurrentDataSource)
{
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
if (pDataSource == NULL) {
return MA_INVALID_ARGS;
}
pDataSourceBase->pCurrent = pCurrentDataSource;
return MA_SUCCESS;
}
MA_API ma_result ma_data_source_set_next(ma_data_source* pDataSource, ma_data_source* pNextDataSource)
{
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
if (pDataSource == NULL) {
return MA_INVALID_ARGS;
}
pDataSourceBase->pNext = pNextDataSource;
return MA_SUCCESS;
}
MA_API ma_result ma_data_source_set_next_callback(ma_data_source* pDataSource, ma_data_source_get_next_proc onGetNext)
{
ma_data_source_base* pDataSourceBase = (ma_data_source_base*)pDataSource;
if (pDataSource == NULL) {
return MA_INVALID_ARGS;
}
pDataSourceBase->onGetNext = onGetNext;
return MA_SUCCESS;
}
#endif
static ma_result ma_audio_buffer_ref__data_source_on_read(ma_data_source* pDataSource, void* pFramesOut, ma_uint64 frameCount, ma_uint64* pFramesRead)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment