You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dr_libs/old/dr_audio.h

5308 lines
171 KiB
C

// Audio playback, recording and mixing. Public domain. See "unlicense" statement at the end of this file.
// dr_audio - v0.0 (unversioned) - Release Date TBD
//
// David Reid - mackron@gmail.com
// !!!!! THIS IS WORK IN PROGRESS !!!!!
// USAGE
//
// dr_audio is a single-file library. To use it, do something like the following in one .c file.
// #define DR_AUDIO_IMPLEMENTATION
// #include "dr_audio.h"
//
// You can then #include this file in other parts of the program as you would with any other header file.
//
// dr_audio supports loading and decoding of WAV, FLAC and Vorbis streams via dr_wav, dr_flac and stb_vorbis respectively. To enable
// these all you need to do is #include "dr_audio.h" _after_ #include "dr_wav.h", #include "dr_flac.h" and #include "stb_vorbis.c" in
// the implementation file, like so:
//
// #define DR_WAV_IMPLEMENTATION
// #include "dr_wav.h"
//
// #define DR_FLAC_IMPLEMENTATION
// #include "dr_flac.h"
//
// #define STB_VORBIS_IMPLEMENTATION
// #include "stb_vorbis.c"
//
// #define DR_AUDIO_IMPLEMENTATION
// #include "dr_audio.h"
//
// dr_wav, dr_flac and stb_vorbis are entirely optional, and dr_audio will automatically detect the ones that are available without
// any additional intervention on your part.
//
//
// dr_audio has a layered API with different levels of flexibility vs simplicity. An example of the high level API follows:
//
// dra_device* pDevice;
// dra_result result = dra_device_create(NULL, dra_device_type_playback, &pDevice);
// if (result != DRA_RESULT_SUCCESS) {
// return -1;
// }
//
// dra_voice* pVoice;
// result = dra_voice_create_from_file(pDevice, "my_song.flac", &pVoice);
// if (result != DRA_RESULT_SUCCESS) {
// return -1;
// }
//
// dra_voice_play(pVoice, DR_FALSE);
//
// ...
//
// dra_voice_delete(pVoice);
// dra_device_delete(pDevice);
//
//
// An example of the low level API:
//
// dra_context context;
// dra_result result = dra_context_init(&context); // Initializes the backend (DirectSound/ALSA)
// if (result != DRA_RESULT_SUCCESS) {
// return -1;
// }
//
// unsigned int deviceID = 0; // Default device
// unsigned int channels = 2; // Stereo
// unsigned int sampleRate = 48000;
// unsigned int latencyInMilliseconds = 0; // 0 will default to DR_AUDIO_DEFAULT_LATENCY.
// dra_device device;
// result = dra_device_init_ex(&context, dra_device_type_playback, deviceID, channels, sampleRate, latencyInMilliseconds, &device);
// if (result != DRA_RESULT_SUCCESS) {
// return -1;
// }
//
// dra_voice* pVoice;
// dra_result result = dra_voice_create(pDevice, dra_format_f32, channels, sampleRate, voiceBufferSizeInBytes, pVoiceSampleData, &pVoice);
// if (result != DRA_RESULT_SUCCESS) {
// return -1;
// }
//
// ...
//
// // Sometime later you may need to update the data inside the voice's internal buffer... It's your job to handle
// // synchronization - have fun! Hint: use playback events at fixed locations to know when a region of the buffer
// // is available for updating.
// float* pVoiceData = (float*)dra_voice_get_buffer_ptr_by_sample(pVoice, sampleOffset);
// if (pVoiceData == NULL) {
// return -1;
// }
//
// memcpy(pVoiceData, pNewVoiceData, sizeof(float) * samplesToCopy);
//
// ...
//
// dra_voice_delete(pVoice);
// dra_device_uninit(pDevice);
// dra_context_uninit(pContext);
//
// In the above example the voice and device are configured to use the same number of channels and sample rate, however they are
// allowed to differ, in which case dr_audio will automatically convert the data. Note that sample rate conversion is currently
// very low quality.
//
// To handle streaming buffers, you can attach a callback that's fired when a voice's playback position reaches a certain point.
// Usually you would set this to the middle and end of the buffer, filling the previous half with new data. Use the
// dra_voice_add_playback_event() API for this.
//
// The playback position of a voice can be retrieved and set with dra_voice_get_playback_position() and dra_voice_set_playback_position()
// respctively. The playback is specified in samples. dra_voice_get_playback_position() will always return a value which is a multiple
// of the channel count. dra_voice_set_playback_position() will round the specified sample index to a multiple of the channel count.
//
//
// dr_audio has support for submixing which basically allows you to control volume (and in the future, effects) for groups of sounds
// which would typically be organized into categories. An abvious example would be in games where you may want to have separate volume
// controls for music, voices, special effects, etc. To do submixing, all you need to do is create a mixer. There is a master mixer
// associated with every device, and all newly created mixers are a child of the master mixer, by default:
//
// dra_mixer* pMusicMixer;
// dra_result result = dra_mixer_create(pDevice, &pMusicMixer);
// if (result != DRA_RESULT_SUCCESS) {
// return -1;
// }
//
// // At this point pMusicMixer is a child of the device's master mixer. To change the hierarchy, just do something like this:
// dra_mixer_attach_submixer(pSomeOtherMixer, pMusicMixer);
//
// // A voice is attached to the master mixer by default, but you can attach it to a different mixer like this:
// dra_mixer_attach_voice(pMusicMixer, pMyMusicVoice);
//
// // Control the volume of the mixer...
// dra_mixer_set_volume(pMusicMixer, 0.5f); // <-- The volume is linear, so this is half volume.
//
//
//
// dr_audio includes an abstraction for audio decoding. Built-in support is included for WAV, FLAC and Vorbis streams:
//
// dra_decoder decoder;
// if (dra_decoder_open_file(&decoder, filePath) != DRA_RESULT_SUCCESS) {
// return -1;
// }
//
// dr_uint64 samplesRead = dra_decoder_read_f32(&decoder, samplesToRead, pSamples);
// update_my_voice_data(pVoice, pSamples, samplesRead);
//
// dra_decoder_close(&decoder);
//
// Decoders can be opened/initialized from files, a block of memory, or application-defined callbacks.
//
//
//
// OPTIONS
// #define these options before including this file.
//
// #define DR_AUDIO_NO_DSOUND
// Disables the DirectSound backend. Note that this is the only backend for the Windows platform.
//
// #define DR_AUDIO_NO_ALSA
// Disables the ALSA backend. Note that this is the only backend for the Linux platform.
//
// #define DR_AUDIO_NO_STDIO
// Disables any functions that use stdio, such as dra_sound_create_from_file().
#ifndef dr_audio2_h
#define dr_audio2_h
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#ifndef DR_SIZED_TYPES_DEFINED
#define DR_SIZED_TYPES_DEFINED
#if defined(_MSC_VER) && _MSC_VER < 1600
typedef signed char dr_int8;
typedef unsigned char dr_uint8;
typedef signed short dr_int16;
typedef unsigned short dr_uint16;
typedef signed int dr_int32;
typedef unsigned int dr_uint32;
typedef signed __int64 dr_int64;
typedef unsigned __int64 dr_uint64;
#else
#include <stdint.h>
typedef int8_t dr_int8;
typedef uint8_t dr_uint8;
typedef int16_t dr_int16;
typedef uint16_t dr_uint16;
typedef int32_t dr_int32;
typedef uint32_t dr_uint32;
typedef int64_t dr_int64;
typedef uint64_t dr_uint64;
#endif
typedef dr_int8 dr_bool8;
typedef dr_int32 dr_bool32;
#define DR_TRUE 1
#define DR_FALSE 0
#endif
#ifndef DR_AUDIO_MAX_CHANNEL_COUNT
#define DR_AUDIO_MAX_CHANNEL_COUNT 16
#endif
#ifndef DR_AUDIO_MAX_EVENT_COUNT
#define DR_AUDIO_MAX_EVENT_COUNT 16
#endif
#define DR_AUDIO_EVENT_ID_STOP 0xFFFFFFFFFFFFFFFFULL
#define DR_AUDIO_EVENT_ID_PLAY 0xFFFFFFFFFFFFFFFEULL
typedef int dra_result;
#define DRA_RESULT_SUCCESS 0
#define DRA_RESULT_UNKNOWN_ERROR -1
#define DRA_RESULT_INVALID_ARGS -2
#define DRA_RESULT_OUT_OF_MEMORY -3
#define DRA_RESULT_FAILED_TO_OPEN_FILE -4
#define DRA_RESULT_NO_BACKEND -1024
#define DRA_RESULT_NO_BACKEND_DEVICE -1025
#define DRA_RESULT_NO_DECODER -1026
#define DRA_FAILED(result) ((result) != 0)
#define DRA_SUCCEEDED(result) ((result) == 0)
#define DRA_MIXER_FLAG_PAUSED (1 << 0)
typedef enum
{
dra_device_type_playback = 0,
dra_device_type_capture
} dra_device_type;
typedef enum
{
dra_format_u8 = 0,
dra_format_s16,
dra_format_s24,
dra_format_s32,
dra_format_f32,
dra_format_default = dra_format_f32
} dra_format;
typedef enum
{
dra_src_algorithm_none,
dra_src_algorithm_linear,
} dra_src_algorithm;
// dra_thread_event_type is used internally for thread management.
typedef enum
{
dra_thread_event_type_none,
dra_thread_event_type_terminate,
dra_thread_event_type_play
} dra_thread_event_type;
typedef struct dra_backend dra_backend;
typedef struct dra_backend_device dra_backend_device;
typedef struct dra_context dra_context;
typedef struct dra_device dra_device;
typedef struct dra_mixer dra_mixer;
typedef struct dra_voice dra_voice;
typedef void* dra_thread;
typedef void* dra_mutex;
typedef void* dra_semaphore;
typedef void (* dra_event_proc) (dr_uint64 eventID, void* pUserData);
typedef void (* dra_samples_processed_proc)(dra_device* pDevice, const size_t sampleCount, const float* pSamples, void* pUserData);
typedef struct
{
dr_uint64 id;
void* pUserData;
dr_uint64 sampleIndex;
dra_event_proc proc;
dra_voice* pVoice;
dr_bool32 hasBeenSignaled;
} dra__event;
typedef struct
{
size_t firstEvent;
size_t eventCount;
size_t eventBufferSize;
dra__event* pEvents;
dra_mutex lock;
} dra__event_queue;
struct dra_context
{
dra_backend* pBackend;
};
struct dra_device
{
// The context that created and owns this device.
dra_context* pContext;
// The backend device. This is used to connect the cross-platform front-end with the backend.
dra_backend_device* pBackendDevice;
// The main mutex for handling thread-safety.
dra_mutex mutex;
// The main thread. For playback devices, this is the thread that waits for fragments to finish processing an then
// mixes and pushes new audio data to the hardware buffer for playback.
dra_thread thread;
// The semaphore used by the main thread to determine whether or not an event is waiting to be processed.
dra_semaphore threadEventSem;
// The event type of the most recent event.
dra_thread_event_type nextThreadEventType;
// TODO: Make these booleans flags.
// Whether or not the device owns the context. This basically just means whether or not the device was created with a null
// context and needs to delete the context itself when the device is deleted.
dr_bool32 ownsContext;
// Whether or not the device is being closed. This is used by the thread to determine if it needs to terminate. When
// dra_device_close() is called, this flag will be set and threadEventSem released. The thread will then see this as it's
// signal to terminate.
dr_bool32 isClosed;
// Whether or not the device is currently playing. When at least one voice is playing, this will be DR_TRUE. When there
// are no voices playing, this will be set to DR_FALSE and the background thread will sit dormant until another voice
// starts playing or the device is closed.
dr_bool32 isPlaying;
// Whether or not the device should stop on the next fragment. This is used for stopping playback of devices that
// have no voice's playing.
dr_bool32 stopOnNextFragment;
// The master mixer. This is the one and only top-level mixer.
dra_mixer* pMasterMixer;
// The number of voices currently being played. This is used to determine whether or not the device should be placed
// into a dormant state when nothing is being played.
size_t playingVoicesCount;
// When a playback event is scheduled it is added to this queue. Events are not posted straight away, but are instead
// placed in a queue for processing later at specific times to ensure the event is posted AFTER the device has actually
// played the sample the event is set for.
dra__event_queue eventQueue;
// The function to call when a segment of samples has been processed. This is only really needed for capture devices,
// but can also be used to keep track of all of the audio data that's passed through a playback device. This is called
// as the read pointer moves passed each segment and again for the leftover partial segment that'll occur when the
// device is stopped.
dra_samples_processed_proc onSamplesProcessed;
void* pUserDataForOnSamplesProcessed;
// The number of channels being used by the device.
unsigned int channels;
// The sample rate in seconds.
unsigned int sampleRate;
};
struct dra_mixer
{
// The device that created and owns this mixer.
dra_device* pDevice;
// Whether or not the mixer is paused.
dr_uint32 flags;
// The parent mixer.
dra_mixer* pParentMixer;
// The first child mixer.
dra_mixer* pFirstChildMixer;
// The last child mixer.
dra_mixer* pLastChildMixer;
// The next sibling mixer.
dra_mixer* pNextSiblingMixer;
// The previous sibling mixer.
dra_mixer* pPrevSiblingMixer;
// The first voice attached to the mixer.
dra_voice* pFirstVoice;
// The last voice attached to the mixer.
dra_voice* pLastVoice;
// The volume of the buffer as a linear scale. A value of 0.5 means the sound is at half volume. There is no hard
// limit on the volume, however going beyond 1 may introduce clipping.
float linearVolume;
// Mixers output the results of the final mix into a buffer referred to as the staging buffer. A parent mixer will
// use the staging buffer when it mixes the results of it's submixers. This is an offset of pData.
float* pStagingBuffer;
// A pointer to the buffer containing the sample data of the buffer currently being mixed, as floating point values.
// This is an offset of pData.
float* pNextSamplesToMix;
// The sample data for pStagingBuffer and pNextSamplesToMix.
float pData[1];
};
struct dra_voice
{
// The device that created and owns this voice.
dra_device* pDevice;
// The mixer the voice is attached to. Should never be null. The mixer doesn't "own" the voice - the voice
// is simply attached to it.
dra_mixer* pMixer;
// The next voice in the linked list of voices attached to the mixer.
dra_voice* pNextVoice;
// The previous voice in the linked list of voices attached to the mixer.
dra_voice* pPrevVoice;
// The format of the audio data contained within this voice.
dra_format format;
// The number of channels.
unsigned int channels;
// The sample rate in seconds.
unsigned int sampleRate;
// The volume of the voice as a linear scale. A value of 0.5 means the sound is at half volume. There is no hard
// limit on the volume, however going beyond 1 may introduce clipping.
float linearVolume;
// Whether or not the voice is currently playing.
dr_bool32 isPlaying;
// Whether or not the voice is currently looping. Whether or not the voice is looping is determined by the last
// call to dra_voice_play().
dr_bool32 isLooping;
// The total number of frames in the voice.
dr_uint64 frameCount;
// The current read position, in frames. An important detail with this is that it's based on the sample rate of the
// device, not the voice.
dr_uint64 currentReadPos;
// A buffer for storing converted frames. This is used by dra_voice__next_frame(). As frames are converted to
// floats, that are placed into this buffer.
float convertedFrame[DR_AUDIO_MAX_CHANNEL_COUNT];
// Data for sample rate conversion. Different SRC algorithms will use different data, which will be stored in their
// own structure.
struct
{
// The sample rate conversion algorithm to use.
dra_src_algorithm algorithm;
union
{
struct
{
dr_uint64 prevFrameIndex;
float prevFrame[DR_AUDIO_MAX_CHANNEL_COUNT];
float nextFrame[DR_AUDIO_MAX_CHANNEL_COUNT];
} linear;
} data;
} src;
// The number of playback notification events. This does not include the stop and play events.
size_t playbackEventCount;
// A pointer to the list of playback events.
dra__event playbackEvents[DR_AUDIO_MAX_EVENT_COUNT];
// The event to call when the voice has stopped playing, either naturally or explicitly with dra_voice_stop().
dra__event stopEvent;
// The event to call when the voice starts playing.
dra__event playEvent;
// Application defined user data.
void* pUserData;
// The size of the buffer in bytes.
size_t sizeInBytes;
// The actual buffer containing the raw audio data in it's native format. At mix time the data will be converted
// to floats.
dr_uint8 pData[1];
};
// dra_context_init()
dra_result dra_context_init(dra_context* pContext);
void dra_context_uninit(dra_context* pContext);
// Helper for allocating and initializing a context. If you want to do your own memory management or just want to put the context
// object on the stack, just use dra_context_init() instead.
dra_result dra_context_create(dra_context** ppContext);
void dra_context_delete(dra_context* pContext);
// dra_device_init_ex()
//
// If deviceID is 0 the default device for the given type is used.
// format can be dra_format_default which is dra_format_s32.
// If channels is set to 0, defaults 2 channels (stereo).
// If sampleRate is set to 0, defaults to 48000.
// If latency is 0, defaults to 50 milliseconds. See notes about latency above.
//
// Concerning the DirectSound backend (From MSDN):
// Note that if your application is playing sounds as well as capturing them, capture buffer creation can fail when
// the format of the capture buffer is not the same as that of the primary buffer. The reason is that some cards have
// only a single clock and cannot support capture and playback at two different frequencies.
//
// This means you will need to keep the channels and sample rate consistent across playback and capture devices when
// using the DirectSound backend.
dra_result dra_device_init_ex(dra_context* pContext, dra_device_type type, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds, dra_device* pDevice);
dra_result dra_device_init(dra_context* pContext, dra_device_type type, dra_device* pDevice);
void dra_device_uninit(dra_device* pDevice);
// Helper for allocating and initializing a device object.
dra_result dra_device_create_ex(dra_context* pContext, dra_device_type type, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds, dra_device** ppDevice);
dra_result dra_device_create(dra_context* pContext, dra_device_type type, dra_device** ppDevice);
void dra_device_delete(dra_device* pDevice);
// Starts a capture device.
//
// Do not call this on a playback device - this is managed by dr_audio. This will fail for playback devices.
dra_result dra_device_start(dra_device* pDevice);
// Stops a capture device.
//
// Do not call this on a playback device - this is managed by dr_audio. This will fail for playback devices.
dra_result dra_device_stop(dra_device* pDevice);
// Sets the function to call when a segment of samples have been processed by the device (either captured
// or played back). Use this to keep track of the audio data that's passed to a playback device or from a
// capture device.
void dra_device_set_samples_processed_callback(dra_device* pDevice, dra_samples_processed_proc proc, void* pUserData);
// dra_mixer_create()
dra_result dra_mixer_create(dra_device* pDevice, dra_mixer** ppMixer);
// dra_mixer_delete()
//
// Deleting a mixer will detach any attached voices and sub-mixers and attach them to the master mixer. It is
// up to the application to manage the allocation of sub-mixers and voices. Typically you'll want to delete
// child mixers and voices before deleting a mixer.
void dra_mixer_delete(dra_mixer* pMixer);
// dra_mixer_attach_submixer()
void dra_mixer_attach_submixer(dra_mixer* pMixer, dra_mixer* pSubmixer);
// dra_mixer_detach_submixer()
void dra_mixer_detach_submixer(dra_mixer* pMixer, dra_mixer* pSubmixer);
// dra_mixer_detach_all_submixers()
void dra_mixer_detach_all_submixers(dra_mixer* pMixer);
// dra_mixer_attach_voice()
void dra_mixer_attach_voice(dra_mixer* pMixer, dra_voice* pVoice);
// dra_mixer_detach_voice()
void dra_mixer_detach_voice(dra_mixer* pMixer, dra_voice* pVoice);
// dra_mixer_detach_all_voices()
void dra_mixer_detach_all_voices(dra_mixer* pMixer);
// dra_voice_set_volume()
void dra_mixer_set_volume(dra_mixer* pMixer, float linearVolume);
// dra_voice_get_volume()
float dra_mixer_get_volume(dra_mixer* pMixer);
// Mixes the next number of frameCount and moves the playback position appropriately.
//
// pMixer [in] The mixer.
// frameCount [in] The number of frames to mix.
//
// Returns the number of frames actually mixed.
//
// The return value is used to determine whether or not there's anything left to mix in the future. When there are
// no samples left to mix, the device can be put into a dormant state to prevent unnecessary processing.
//
// Mixed samples will be placed in pMixer->pStagingBuffer.
size_t dra_mixer_mix_next_frames(dra_mixer* pMixer, size_t frameCount);
// Non-recursively counts the number of voices that are attached to the given mixer.
size_t dra_mixer_count_attached_voices(dra_mixer* pMixer);
// Recursively counts the number of voices that are attached to the given mixer.
size_t dra_mixer_count_attached_voices_recursive(dra_mixer* pMixer);
// Non-recursively gathers all of the voices that are currently attached to the given mixer.
size_t dra_mixer_gather_attached_voices(dra_mixer* pMixer, dra_voice** ppVoicesOut);
// Recursively gathers all of the voices that are currently attached to the given mixer.
size_t dra_mixer_gather_attached_voices_recursive(dra_mixer* pMixer, dra_voice** ppVoicesOut);
// Marks the given mixer as paused.
void dra_mixer_pause(dra_mixer* pMixer);
// Unmarks the given mixer as paused.
void dra_mixer_resume(dra_mixer* pMixer);
// Determines whether or not the given mixer is paused.
dr_bool32 dra_mixer_is_paused(dra_mixer* pMixer);
// dra_voice_create()
dra_result dra_voice_create(dra_device* pDevice, dra_format format, unsigned int channels, unsigned int sampleRate, size_t sizeInBytes, const void* pInitialData, dra_voice** ppVoice);
dra_result dra_voice_create_compatible(dra_device* pDevice, size_t sizeInBytes, const void* pInitialData, dra_voice** ppVoice);
// dra_voice_delete()
void dra_voice_delete(dra_voice* pVoice);
// dra_voice_play()
//
// If the mixer the voice is attached to is not playing, the voice will be marked as playing, but won't actually play anything until
// the mixer is started again.
void dra_voice_play(dra_voice* pVoice, dr_bool32 loop);
// dra_voice_stop()
void dra_voice_stop(dra_voice* pVoice);
// dra_voice_is_playing()
dr_bool32 dra_voice_is_playing(dra_voice* pVoice);
// dra_voice_is_looping()
dr_bool32 dra_voice_is_looping(dra_voice* pVoice);
// dra_voice_set_volume()
void dra_voice_set_volume(dra_voice* pVoice, float linearVolume);
// dra_voice_get_volume()
float dra_voice_get_volume(dra_voice* pVoice);
void dra_voice_set_on_stop(dra_voice* pVoice, dra_event_proc proc, void* pUserData);
void dra_voice_set_on_play(dra_voice* pVoice, dra_event_proc proc, void* pUserData);
dr_bool32 dra_voice_add_playback_event(dra_voice* pVoice, dr_uint64 sampleIndex, dr_uint64 eventID, dra_event_proc proc, void* pUserData);
void dra_voice_remove_playback_event(dra_voice* pVoice, dr_uint64 eventID);
// dra_voice_get_playback_position()
dr_uint64 dra_voice_get_playback_position(dra_voice* pVoice);
// dra_voice_set_playback_position()
void dra_voice_set_playback_position(dra_voice* pVoice, dr_uint64 sampleIndex);
// dra_voice_get_buffer_ptr_by_sample()
void* dra_voice_get_buffer_ptr_by_sample(dra_voice* pVoice, dr_uint64 sample);
// dra_voice_write_silence()
void dra_voice_write_silence(dra_voice* pVoice, dr_uint64 sampleOffset, dr_uint64 sampleCount);
//// Other APIs ////
// Frees memory that was allocated internally by dr_audio.
void dra_free(void* p);
// Retrieves the number of bits per sample based on the given format.
unsigned int dra_get_bits_per_sample_by_format(dra_format format);
// Retrieves the number of bytes per sample based on the given format.
unsigned int dra_get_bytes_per_sample_by_format(dra_format format);
//// Decoder APIs ////
typedef enum
{
dra_seek_origin_start,
dra_seek_origin_current
} dra_seek_origin;
typedef size_t (* dra_decoder_on_read_proc) (void* pUserData, void* pDataOut, size_t bytesToRead);
typedef dr_bool32 (* dra_decoder_on_seek_proc) (void* pUserData, int offset, dra_seek_origin origin);
typedef void (* dra_decoder_on_delete_proc) (void* pBackendDecoder);
typedef dr_uint64 (* dra_decoder_on_read_samples_proc) (void* pBackendDecoder, dr_uint64 samplesToRead, float* pSamplesOut);
typedef dr_bool32 (* dra_decoder_on_seek_samples_proc) (void* pBackendDecoder, dr_uint64 sample);
typedef struct
{
const dr_uint8* data;
size_t dataSize;
size_t currentReadPos;
} dra__memory_stream;
typedef struct
{
unsigned int channels;
unsigned int sampleRate;
dr_uint64 totalSampleCount; // <-- Can be 0.
dra_decoder_on_read_proc onRead;
dra_decoder_on_seek_proc onSeek;
void* pUserData;
void* pBackendDecoder;
dra_decoder_on_delete_proc onDelete;
dra_decoder_on_read_samples_proc onReadSamples;
dra_decoder_on_seek_samples_proc onSeekSamples;
// A hack to enable decoding from memory without mallocing the user data.
dra__memory_stream memoryStream;
} dra_decoder;
// dra_decoder_open()
dra_result dra_decoder_open(dra_decoder* pDecoder, dra_decoder_on_read_proc onRead, dra_decoder_on_seek_proc onSeek, void* pUserData);
float* dra_decoder_open_and_decode_f32(dra_decoder_on_read_proc onRead, dra_decoder_on_seek_proc onSeek, void* pUserData, unsigned int* channels, unsigned int* sampleRate, dr_uint64* totalSampleCount);
dra_result dra_decoder_open_memory(dra_decoder* pDecoder, const void* pData, size_t dataSize);
float* dra_decoder_open_and_decode_memory_f32(const void* pData, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, dr_uint64* totalSampleCount);
#ifndef DR_AUDIO_NO_STDIO
dra_result dra_decoder_open_file(dra_decoder* pDecoder, const char* filePath);
float* dra_decoder_open_and_decode_file_f32(const char* filePath, unsigned int* channels, unsigned int* sampleRate, dr_uint64* totalSampleCount);
#endif
// dra_decoder_close()
void dra_decoder_close(dra_decoder* pDecoder);
// dra_decoder_read_f32()
dr_uint64 dra_decoder_read_f32(dra_decoder* pDecoder, dr_uint64 samplesToRead, float* pSamplesOut);
// dra_decoder_seek_to_sample()
dr_bool32 dra_decoder_seek_to_sample(dra_decoder* pDecoder, dr_uint64 sample);
//// High Level Helper APIs ////
#ifndef DR_AUDIO_NO_STDIO
// Creates a voice from a file.
dra_result dra_voice_create_from_file(dra_device* pDevice, const char* filePath, dra_voice** ppVoice);
#endif
//// High Level World API ////
//
// This section is for the sound world APIs. These are high-level APIs that sit directly on top of the main API.
typedef struct dra_sound_world dra_sound_world;
typedef struct dra_sound dra_sound;
typedef struct dra_sound_desc dra_sound_desc;
typedef void (* dra_sound_on_delete_proc) (dra_sound* pSound);
typedef dr_uint64 (* dra_sound_on_read_proc) (dra_sound* pSound, dr_uint64 samplesToRead, void* pSamplesOut);
typedef dr_bool32 (* dra_sound_on_seek_proc) (dra_sound* pSound, dr_uint64 sample);
struct dra_sound_desc
{
// The format of the sound.
dra_format format;
// The number of channels in the audio data.
unsigned int channels;
// The sample rate of the audio data.
unsigned int sampleRate;
// The size of the audio data in bytes. If this is 0 it is assumed the data will be streamed.
size_t dataSize;
// A pointer to the audio data. If this is null it is assumed the audio data is streamed.
void* pData;
// A pointer to the function to call when the sound object is deleted. This is used to give the application an
// opportunity to do any clean up, such as closing decoders or whatnot.
dra_sound_on_delete_proc onDelete;
// A pointer to the function to call when dr_audio needs to request a chunk of audio data. This is only used when
// streaming data.
dra_sound_on_read_proc onRead;
// A pointer to the function to call when dr_audio needs to seek the audio data. This is only used when streaming
// data.
dra_sound_on_seek_proc onSeek;
// A pointer to some application defined user data that can be associated with the sound.
void* pUserData;
};
struct dra_sound_world
{
// The playback device.
dra_device* pPlaybackDevice;
// Whether or not the world owns the playback device. When this is set to DR_TRUE, it will be deleted when the world is deleted.
dr_bool32 ownsPlaybackDevice;
};
struct dra_sound
{
// The world that owns this sound.
dra_sound_world* pWorld;
// The voice object for emitting audio out of the device.
dra_voice* pVoice;
// The descriptor of the sound that was used to initialize the sound.
dra_sound_desc desc;
// Whether or not the sound is looping.
dr_bool32 isLooping;
// Whether or not the sound should be stopped at the end of the chunk that's currently playing.
dr_bool32 stopOnNextChunk;
// Application defined user data.
void* pUserData;
};
// dra_sound_world_create()
//
// The playback device can be null, in which case a default one will be created.
dra_sound_world* dra_sound_world_create(dra_device* pPlaybackDevice);
// dra_sound_world_delete()
//
// This will delete every sound this world owns.
void dra_sound_world_delete(dra_sound_world* pWorld);
// dra_sound_world_play_inline()
//
// pMixer [in, optional] The mixer to attach the sound to. Can null, in which case it's attached to the master mixer.
void dra_sound_world_play_inline(dra_sound_world* pWorld, dra_sound_desc* pDesc, dra_mixer* pMixer);
// Plays an inlined sound in 3D space.
//
// This is a placeholder function. 3D position is not yet implemented.
void dra_sound_world_play_inline_3f(dra_sound_world* pWorld, dra_sound_desc* pDesc, dra_mixer* pMixer, float xPos, float yPos, float zPos);
// Stops playing every sound.
//
// This will stop all voices that are attached to the world's playback deviecs, including those that are not attached to a dra_sound object.
void dra_sound_world_stop_all_sounds(dra_sound_world* pWorld);
// Sets the position of the listener for 3D effects.
//
// This is placeholder.
void dra_sound_world_set_listener_position(dra_sound_world* pWorld, float xPos, float yPos, float zPos);
// Sets the orientation of the listener for 3D effects.
//
// This is placeholder.
void dra_sound_world_set_listener_orientation(dra_sound_world* pWorld, float xForward, float yForward, float zForward, float xUp, float yUp, float zUp);
// dra_sound_create()
//
// The datails in "desc" can be accessed from the returned object directly.
//
// This uses the pUserData member of the internal voice. Do not overwrite this. Instead, use the pUserData member of
// the returned dra_sound object.
dra_sound* dra_sound_create(dra_sound_world* pWorld, dra_sound_desc* pDesc);
#ifndef DR_AUDIO_NO_STDIO
// dra_sound_create_from_file()
//
// This will hold a handle to the file for the life of the sound.
dra_sound* dra_sound_create_from_file(dra_sound_world* pWorld, const char* filePath);
#endif
// dra_sound_delete()
void dra_sound_delete(dra_sound* pSound);
// dra_sound_play()
void dra_sound_play(dra_sound* pSound, dr_bool32 loop);
// dra_sound_stop()
void dra_sound_stop(dra_sound* pSound);
// Attaches the given sound to the given mixer.
//
// Setting pMixer to null will detach the sound from the mixer it is currently attached to and attach it
// to the master mixer.
void dra_sound_attach_to_mixer(dra_sound* pSound, dra_mixer* pMixer);
// dra_sound_set_on_stop()
void dra_sound_set_on_stop(dra_sound* pSound, dra_event_proc proc, void* pUserData);
// dra_sound_set_on_play()
void dra_sound_set_on_play(dra_sound* pSound, dra_event_proc proc, void* pUserData);
#ifdef __cplusplus
}
#endif
#endif //dr_audio2_h
///////////////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION
//
///////////////////////////////////////////////////////////////////////////////
#ifdef DR_AUDIO_IMPLEMENTATION
#include <stdlib.h>
#include <math.h>
#include <assert.h>
#include <stdio.h> // For good old printf debugging. Delete later.
#ifdef _MSC_VER
#define DR_AUDIO_INLINE static __forceinline
#else
#define DR_AUDIO_INLINE static inline
#endif
#define DR_AUDIO_DEFAULT_CHANNEL_COUNT 2
#define DR_AUDIO_DEFAULT_SAMPLE_RATE 48000
#define DR_AUDIO_DEFAULT_LATENCY 100 // Milliseconds. TODO: Test this with very low values. DirectSound appears to not signal the fragment events when it's too small. With values of about 20 it sounds crackly.
#define DR_AUDIO_DEFAULT_FRAGMENT_COUNT 3 // The hardware buffer is divided up into latency-sized blocks. This controls that number. Must be at least 2.
#define DR_AUDIO_BACKEND_TYPE_NULL 0
#define DR_AUDIO_BACKEND_TYPE_DSOUND 1
#define DR_AUDIO_BACKEND_TYPE_ALSA 2
#ifdef dr_wav_h
#define DR_AUDIO_HAS_WAV
#ifndef DR_WAV_NO_STDIO
#define DR_AUDIO_HAS_WAV_STDIO
#endif
#endif
#ifdef dr_flac_h
#define DR_AUDIO_HAS_FLAC
#ifndef DR_FLAC_NO_STDIO
#define DR_AUDIO_HAS_FLAC_STDIO
#endif
#endif
#ifdef STB_VORBIS_INCLUDE_STB_VORBIS_H
#define DR_AUDIO_HAS_VORBIS
#ifndef STB_VORBIS_NO_STDIO
#define DR_AUDIO_HAS_VORBIS_STDIO
#endif
#endif
#if defined(DR_AUDIO_HAS_WAV) || \
defined(DR_AUDIO_HAS_FLAC) || \
defined(DR_AUDIO_HAS_VORBIS)
#define DR_AUDIO_HAS_EXTERNAL_DECODER
#endif
// Thanks to good old Bit Twiddling Hacks for this one: http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
DR_AUDIO_INLINE unsigned int dra_next_power_of_2(unsigned int x)
{
x--;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x++;
return x;
}
DR_AUDIO_INLINE unsigned int dra_prev_power_of_2(unsigned int x)
{
return dra_next_power_of_2(x) >> 1;
}
DR_AUDIO_INLINE float dra_mixf(float x, float y, float a)
{
return x*(1-a) + y*a;
}
///////////////////////////////////////////////////////////////////////////////
//
// Platform Specific
//
///////////////////////////////////////////////////////////////////////////////
// Every backend struct should begin with these properties.
struct dra_backend
{
#define DR_AUDIO_BASE_BACKEND_ATTRIBS \
unsigned int type; \
DR_AUDIO_BASE_BACKEND_ATTRIBS
};
// Every backend device struct should begin with these properties.
struct dra_backend_device
{
#define DR_AUDIO_BASE_BACKEND_DEVICE_ATTRIBS \
dra_backend* pBackend; \
dra_device_type type; \
unsigned int channels; \
unsigned int sampleRate; \
unsigned int fragmentCount; \
unsigned int samplesPerFragment; \
DR_AUDIO_BASE_BACKEND_DEVICE_ATTRIBS
};
// Compile-time platform detection and #includes.
#ifdef _WIN32
#include <windows.h>
//// Threading (Win32) ////
typedef DWORD (* dra_thread_entry_proc)(LPVOID pData);
dra_thread dra_thread_create(dra_thread_entry_proc entryProc, void* pData)
{
return (dra_thread)CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)entryProc, pData, 0, NULL);
}
void dra_thread_delete(dra_thread thread)
{
CloseHandle((HANDLE)thread);
}
void dra_thread_wait(dra_thread thread)
{
WaitForSingleObject((HANDLE)thread, INFINITE);
}
dra_mutex dra_mutex_create()
{
return (dra_mutex)CreateEventA(NULL, FALSE, TRUE, NULL);
}
void dra_mutex_delete(dra_mutex mutex)
{
CloseHandle((HANDLE)mutex);
}
void dra_mutex_lock(dra_mutex mutex)
{
WaitForSingleObject((HANDLE)mutex, INFINITE);
}
void dra_mutex_unlock(dra_mutex mutex)
{
SetEvent((HANDLE)mutex);
}
dra_semaphore dra_semaphore_create(int initialValue)
{
return (void*)CreateSemaphoreA(NULL, initialValue, LONG_MAX, NULL);
}
void dra_semaphore_delete(dra_semaphore semaphore)
{
CloseHandle((HANDLE)semaphore);
}
dr_bool32 dra_semaphore_wait(dra_semaphore semaphore)
{
return WaitForSingleObject((HANDLE)semaphore, INFINITE) == WAIT_OBJECT_0;
}
dr_bool32 dra_semaphore_release(dra_semaphore semaphore)
{
return ReleaseSemaphore((HANDLE)semaphore, 1, NULL) != 0;
}
//// DirectSound ////
#ifndef DR_AUDIO_NO_DSOUND
#define DR_AUDIO_ENABLE_DSOUND
#include <dsound.h>
#include <mmreg.h> // WAVEFORMATEX
GUID DR_AUDIO_GUID_NULL = {0x00000000, 0x0000, 0x0000, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
static GUID _g_draGUID_IID_DirectSoundNotify = {0xb0210783, 0x89cd, 0x11d0, {0xaf, 0x08, 0x00, 0xa0, 0xc9, 0x25, 0xcd, 0x16}};
static GUID _g_draGUID_IID_IDirectSoundCaptureBuffer8 = {0x00990df4, 0x0dbb, 0x4872, {0x83, 0x3e, 0x6d, 0x30, 0x3e, 0x80, 0xae, 0xb6}};
static GUID _g_draGUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = {0x00000003, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}};
#ifdef __cplusplus
static GUID g_draGUID_IID_DirectSoundNotify = _g_draGUID_IID_DirectSoundNotify;
static GUID g_draGUID_IID_IDirectSoundCaptureBuffer8 = _g_draGUID_IID_IDirectSoundCaptureBuffer8;
//static GUID g_draGUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = _g_draGUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
#else
static GUID* g_draGUID_IID_DirectSoundNotify = &_g_draGUID_IID_DirectSoundNotify;
static GUID* g_draGUID_IID_IDirectSoundCaptureBuffer8 = &_g_draGUID_IID_IDirectSoundCaptureBuffer8;
//static GUID* g_draGUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = &_g_draGUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
#endif
typedef HRESULT (WINAPI * pDirectSoundCreate8Proc)(LPCGUID pcGuidDevice, LPDIRECTSOUND8 *ppDS8, LPUNKNOWN pUnkOuter);
typedef HRESULT (WINAPI * pDirectSoundEnumerateAProc)(LPDSENUMCALLBACKA pDSEnumCallback, LPVOID pContext);
typedef HRESULT (WINAPI * pDirectSoundCaptureCreate8Proc)(LPCGUID pcGuidDevice, LPDIRECTSOUNDCAPTURE8 *ppDSC8, LPUNKNOWN pUnkOuter);
typedef HRESULT (WINAPI * pDirectSoundCaptureEnumerateAProc)(LPDSENUMCALLBACKA pDSEnumCallback, LPVOID pContext);
typedef struct
{
DR_AUDIO_BASE_BACKEND_ATTRIBS
// A handle to the dsound DLL for doing run-time linking.
HMODULE hDSoundDLL;
pDirectSoundCreate8Proc pDirectSoundCreate8;
pDirectSoundEnumerateAProc pDirectSoundEnumerateA;
pDirectSoundCaptureCreate8Proc pDirectSoundCaptureCreate8;
pDirectSoundCaptureEnumerateAProc pDirectSoundCaptureEnumerateA;
} dra_backend_dsound;
typedef struct
{
DR_AUDIO_BASE_BACKEND_DEVICE_ATTRIBS
// The main device object for use with DirectSound.
LPDIRECTSOUND8 pDS;
// The DirectSound "primary buffer". It's basically just representing the connection between us and the hardware device.
LPDIRECTSOUNDBUFFER pDSPrimaryBuffer;
// The DirectSound "secondary buffer". This is where the actual audio data will be written to by dr_audio when it's time
// to play back some audio through the speakers. This represents the hardware buffer.
LPDIRECTSOUNDBUFFER pDSSecondaryBuffer;
// The main capture device object for use with DirectSound. This is only used by capture devices and is created by DirectSoundCaptureCreate8().
LPDIRECTSOUNDCAPTURE8 pDSCapture;
// The capture buffer. This is where captured audio data will be placed. This is only used by capture devices.
LPDIRECTSOUNDCAPTUREBUFFER8 pDSCaptureBuffer;
// The notification object used by DirectSound to notify dr_audio that it's ready for the next fragment of audio data.
LPDIRECTSOUNDNOTIFY pDSNotify;
// Notification events for each fragment.
HANDLE pNotifyEvents[DR_AUDIO_DEFAULT_FRAGMENT_COUNT];
// The event the main playback thread will wait on to determine whether or not the playback loop should terminate.
HANDLE hStopEvent;
// The index of the fragment that is currently being played.
unsigned int currentFragmentIndex;
// The address of the mapped fragment. This is set with IDirectSoundBuffer8::Lock() and passed to IDriectSoundBuffer8::Unlock().
void* pLockPtr;
// The size of the locked buffer. This is set with IDirectSoundBuffer8::Lock() and passed to IDriectSoundBuffer8::Unlock().
DWORD lockSize;
} dra_backend_device_dsound;
typedef struct
{
unsigned int deviceID;
unsigned int counter;
const GUID* pGuid;
} dra_dsound__device_enum_data;
static BOOL CALLBACK dra_dsound__get_device_guid_by_id__callback(LPGUID lpGuid, LPCSTR lpcstrDescription, LPCSTR lpcstrModule, LPVOID lpContext)
{
(void)lpcstrDescription;
(void)lpcstrModule;
dra_dsound__device_enum_data* pData = (dra_dsound__device_enum_data*)lpContext;
assert(pData != NULL);
if (pData->counter == pData->deviceID) {
pData->pGuid = lpGuid;
return DR_FALSE;
}
pData->counter += 1;
return DR_TRUE;
}
const GUID* dra_dsound__get_playback_device_guid_by_id(dra_backend* pBackend, unsigned int deviceID)
{
// From MSDN:
//
// The first device enumerated is always called the Primary Sound Driver, and the lpGUID parameter of the callback is
// NULL. This device represents the preferred output device set by the user in Control Panel.
if (deviceID == 0) {
return NULL;
}
dra_backend_dsound* pBackendDS = (dra_backend_dsound*)pBackend;
if (pBackendDS == NULL) {
return NULL;
}
// The device ID is treated as the device index. The actual ID for use by DirectSound is a GUID. We use DirectSoundEnumerateA()
// iterate over each device. This function is usually only going to be used during initialization time so it won't be a performance
// issue to not cache these.
dra_dsound__device_enum_data data = {0};
data.deviceID = deviceID;
pBackendDS->pDirectSoundEnumerateA(dra_dsound__get_device_guid_by_id__callback, &data);
return data.pGuid;
}
const GUID* dra_dsound__get_capture_device_guid_by_id(dra_backend* pBackend, unsigned int deviceID)
{
// From MSDN:
//
// The first device enumerated is always called the Primary Sound Driver, and the lpGUID parameter of the callback is
// NULL. This device represents the preferred output device set by the user in Control Panel.
if (deviceID == 0) {
return NULL;
}
dra_backend_dsound* pBackendDS = (dra_backend_dsound*)pBackend;
if (pBackendDS == NULL) {
return NULL;
}
// The device ID is treated as the device index. The actual ID for use by DirectSound is a GUID. We use DirectSoundEnumerateA()
// iterate over each device. This function is usually only going to be used during initialization time so it won't be a performance
// issue to not cache these.
dra_dsound__device_enum_data data = {0};
data.deviceID = deviceID;
pBackendDS->pDirectSoundCaptureEnumerateA(dra_dsound__get_device_guid_by_id__callback, &data);
return data.pGuid;
}
dra_backend* dra_backend_create_dsound()
{
dra_backend_dsound* pBackendDS = (dra_backend_dsound*)calloc(1, sizeof(*pBackendDS)); // <-- Note the calloc() - makes it easier to handle the on_error goto.
if (pBackendDS == NULL) {
return NULL;
}
pBackendDS->type = DR_AUDIO_BACKEND_TYPE_DSOUND;
pBackendDS->hDSoundDLL = LoadLibraryW(L"dsound.dll");
if (pBackendDS->hDSoundDLL == NULL) {
goto on_error;
}
pBackendDS->pDirectSoundCreate8 = (pDirectSoundCreate8Proc)GetProcAddress(pBackendDS->hDSoundDLL, "DirectSoundCreate8");
if (pBackendDS->pDirectSoundCreate8 == NULL){
goto on_error;
}
pBackendDS->pDirectSoundEnumerateA = (pDirectSoundEnumerateAProc)GetProcAddress(pBackendDS->hDSoundDLL, "DirectSoundEnumerateA");
if (pBackendDS->pDirectSoundEnumerateA == NULL){
goto on_error;
}
pBackendDS->pDirectSoundCaptureCreate8 = (pDirectSoundCaptureCreate8Proc)GetProcAddress(pBackendDS->hDSoundDLL, "DirectSoundCaptureCreate8");
if (pBackendDS->pDirectSoundCaptureCreate8 == NULL){
goto on_error;
}
pBackendDS->pDirectSoundCaptureEnumerateA = (pDirectSoundCaptureEnumerateAProc)GetProcAddress(pBackendDS->hDSoundDLL, "DirectSoundCaptureEnumerateA");
if (pBackendDS->pDirectSoundCaptureEnumerateA == NULL){
goto on_error;
}
return (dra_backend*)pBackendDS;
on_error:
if (pBackendDS != NULL) {
if (pBackendDS->hDSoundDLL != NULL) {
FreeLibrary(pBackendDS->hDSoundDLL);
}
free(pBackendDS);
}
return NULL;
}
void dra_backend_delete_dsound(dra_backend* pBackend)
{
dra_backend_dsound* pBackendDS = (dra_backend_dsound*)pBackend;
if (pBackendDS == NULL) {
return;
}
if (pBackendDS->hDSoundDLL != NULL) {
FreeLibrary(pBackendDS->hDSoundDLL);
}
free(pBackendDS);
}
void dra_backend_device_close_dsound(dra_backend_device* pDevice)
{
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)pDevice;
if (pDeviceDS == NULL) {
return;
}
if (pDeviceDS->pDSNotify) IDirectSoundNotify_Release(pDeviceDS->pDSNotify);
if (pDevice->type == dra_device_type_playback) {
if (pDeviceDS->pDSSecondaryBuffer) IDirectSoundBuffer_Release(pDeviceDS->pDSSecondaryBuffer);
if (pDeviceDS->pDSPrimaryBuffer) IDirectSoundBuffer_Release(pDeviceDS->pDSPrimaryBuffer);
if (pDeviceDS->pDS) IDirectSound_Release(pDeviceDS->pDS);
} else {
if (pDeviceDS->pDSCaptureBuffer) IDirectSoundCaptureBuffer_Release(pDeviceDS->pDSCaptureBuffer);
if (pDeviceDS->pDSCapture) IDirectSoundCapture_Release(pDeviceDS->pDSCapture);
}
for (int i = 0; i < DR_AUDIO_DEFAULT_FRAGMENT_COUNT; ++i) {
CloseHandle(pDeviceDS->pNotifyEvents[i]);
}
if (pDeviceDS->hStopEvent != NULL) {
CloseHandle(pDeviceDS->hStopEvent);
}
free(pDeviceDS);
}
dra_backend_device* dra_backend_device_open_playback_dsound(dra_backend* pBackend, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds)
{
// These are declared at the top to stop compilations errors on GCC about goto statements skipping over variable initialization.
HRESULT hr;
WAVEFORMATEXTENSIBLE* actualFormat;
unsigned int sampleRateInMilliseconds;
unsigned int proposedFramesPerFragment;
unsigned int framesPerFragment;
size_t fragmentSize;
size_t hardwareBufferSize;
dra_backend_dsound* pBackendDS = (dra_backend_dsound*)pBackend;
if (pBackendDS == NULL) {
return NULL;
}
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)calloc(1, sizeof(*pDeviceDS));
if (pDeviceDS == NULL) {
goto on_error;
}
if (channels == 0) {
channels = DR_AUDIO_DEFAULT_CHANNEL_COUNT;
}
pDeviceDS->pBackend = pBackend;
pDeviceDS->type = dra_device_type_playback;
pDeviceDS->channels = channels;
pDeviceDS->sampleRate = sampleRate;
hr = pBackendDS->pDirectSoundCreate8(dra_dsound__get_playback_device_guid_by_id(pBackend, deviceID), &pDeviceDS->pDS, NULL);
if (FAILED(hr)) {
goto on_error;
}
// The cooperative level must be set before doing anything else.
hr = IDirectSound_SetCooperativeLevel(pDeviceDS->pDS, GetForegroundWindow(), DSSCL_PRIORITY);
if (FAILED(hr)) {
goto on_error;
}
// The primary buffer is basically just the connection to the hardware.
DSBUFFERDESC descDSPrimary;
memset(&descDSPrimary, 0, sizeof(DSBUFFERDESC));
descDSPrimary.dwSize = sizeof(DSBUFFERDESC);
descDSPrimary.dwFlags = DSBCAPS_PRIMARYBUFFER | DSBCAPS_CTRLVOLUME;
hr = IDirectSound_CreateSoundBuffer(pDeviceDS->pDS, &descDSPrimary, &pDeviceDS->pDSPrimaryBuffer, NULL);
if (FAILED(hr)) {
goto on_error;
}
// If the channel count is 0 then we need to use the default. From MSDN:
//
// The method succeeds even if the hardware does not support the requested format; DirectSound sets the buffer to the closest
// supported format. To determine whether this has happened, an application can call the GetFormat method for the primary buffer
// and compare the result with the format that was requested with the SetFormat method.
WAVEFORMATEXTENSIBLE wf;
memset(&wf, 0, sizeof(wf));
wf.Format.cbSize = sizeof(wf);
wf.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
wf.Format.nChannels = (WORD)channels;
wf.Format.nSamplesPerSec = (DWORD)sampleRate;
wf.Format.wBitsPerSample = sizeof(float)*8;
wf.Format.nBlockAlign = (wf.Format.nChannels * wf.Format.wBitsPerSample) / 8;
wf.Format.nAvgBytesPerSec = wf.Format.nBlockAlign * wf.Format.nSamplesPerSec;
wf.Samples.wValidBitsPerSample = wf.Format.wBitsPerSample;
wf.dwChannelMask = 0;
wf.SubFormat = _g_draGUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
if (channels > 2) {
wf.dwChannelMask = ~(((DWORD)-1) << channels);
}
hr = IDirectSoundBuffer_SetFormat(pDeviceDS->pDSPrimaryBuffer, (WAVEFORMATEX*)&wf);
if (FAILED(hr)) {
goto on_error;
}
// Get the ACTUAL properties of the buffer. This is silly API design...
DWORD requiredSize;
hr = IDirectSoundBuffer_GetFormat(pDeviceDS->pDSPrimaryBuffer, NULL, 0, &requiredSize);
if (FAILED(hr)) {
goto on_error;
}
char rawdata[1024];
actualFormat = (WAVEFORMATEXTENSIBLE*)rawdata;
hr = IDirectSoundBuffer_GetFormat(pDeviceDS->pDSPrimaryBuffer, (WAVEFORMATEX*)actualFormat, requiredSize, NULL);
if (FAILED(hr)) {
goto on_error;
}
pDeviceDS->channels = actualFormat->Format.nChannels;
pDeviceDS->sampleRate = actualFormat->Format.nSamplesPerSec;
// DirectSound always has the same number of fragments.
pDeviceDS->fragmentCount = DR_AUDIO_DEFAULT_FRAGMENT_COUNT;
// The secondary buffer is the buffer where the real audio data will be written to and used by the hardware device. It's
// size is based on the latency, sample rate and channels.
//
// The format of the secondary buffer should exactly match the primary buffer as to avoid unnecessary data conversions.
sampleRateInMilliseconds = pDeviceDS->sampleRate / 1000;
if (sampleRateInMilliseconds == 0) {
sampleRateInMilliseconds = 1;
}
// The size of a fragment is sized such that the number of frames contained within it is a multiple of 2. The reason for
// this is to keep it consistent with the ALSA backend.
proposedFramesPerFragment = sampleRateInMilliseconds * latencyInMilliseconds;
framesPerFragment = dra_prev_power_of_2(proposedFramesPerFragment);
if (framesPerFragment == 0) {
framesPerFragment = 2;
}
pDeviceDS->samplesPerFragment = framesPerFragment * pDeviceDS->channels;
fragmentSize = pDeviceDS->samplesPerFragment * sizeof(float);
hardwareBufferSize = fragmentSize * pDeviceDS->fragmentCount;
assert(hardwareBufferSize > 0); // <-- If you've triggered this is means you've got something set to 0. You haven't been setting that latency to 0 have you?! That's not allowed!
// Meaning of dwFlags (from MSDN):
//
// DSBCAPS_CTRLPOSITIONNOTIFY
// The buffer has position notification capability.
//
// DSBCAPS_GLOBALFOCUS
// With this flag set, an application using DirectSound can continue to play its buffers if the user switches focus to
// another application, even if the new application uses DirectSound.
//
// DSBCAPS_GETCURRENTPOSITION2
// In the first version of DirectSound, the play cursor was significantly ahead of the actual playing sound on emulated
// sound cards; it was directly behind the write cursor. Now, if the DSBCAPS_GETCURRENTPOSITION2 flag is specified, the
// application can get a more accurate play cursor.
DSBUFFERDESC descDS;
memset(&descDS, 0, sizeof(DSBUFFERDESC));
descDS.dwSize = sizeof(DSBUFFERDESC);
descDS.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GLOBALFOCUS | DSBCAPS_GETCURRENTPOSITION2;
descDS.dwBufferBytes = (DWORD)hardwareBufferSize;
descDS.lpwfxFormat = (WAVEFORMATEX*)&wf;
hr = IDirectSound_CreateSoundBuffer(pDeviceDS->pDS, &descDS, &pDeviceDS->pDSSecondaryBuffer, NULL);
if (FAILED(hr)) {
goto on_error;
}
// As DirectSound is playing back the hardware buffer it needs to notify dr_audio when it's ready for new data. This is done
// through a notification object which we retrieve from the secondary buffer.
hr = IDirectSoundBuffer8_QueryInterface(pDeviceDS->pDSSecondaryBuffer, g_draGUID_IID_DirectSoundNotify, (void**)&pDeviceDS->pDSNotify);
if (FAILED(hr)) {
goto on_error;
}
DSBPOSITIONNOTIFY notifyPoints[DR_AUDIO_DEFAULT_FRAGMENT_COUNT]; // One notification event for each fragment.
for (int i = 0; i < DR_AUDIO_DEFAULT_FRAGMENT_COUNT; ++i)
{
pDeviceDS->pNotifyEvents[i] = CreateEventA(NULL, FALSE, FALSE, NULL);
if (pDeviceDS->pNotifyEvents[i] == NULL) {
goto on_error;
}
notifyPoints[i].dwOffset = (DWORD)(i * fragmentSize); // <-- This is in bytes.
notifyPoints[i].hEventNotify = pDeviceDS->pNotifyEvents[i];
}
hr = IDirectSoundNotify_SetNotificationPositions(pDeviceDS->pDSNotify, DR_AUDIO_DEFAULT_FRAGMENT_COUNT, notifyPoints);
if (FAILED(hr)) {
goto on_error;
}
// The termination event is used to determine when the playback thread should be terminated. The playback thread
// will wait on this event in addition to the notification events in it's main loop.
pDeviceDS->hStopEvent = CreateEventA(NULL, FALSE, FALSE, NULL);
if (pDeviceDS->hStopEvent == NULL) {
goto on_error;
}
return (dra_backend_device*)pDeviceDS;
on_error:
dra_backend_device_close_dsound((dra_backend_device*)pDeviceDS);
return NULL;
}
dra_backend_device* dra_backend_device_open_capture_dsound(dra_backend* pBackend, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds)
{
(void)latencyInMilliseconds;
HRESULT hr;
unsigned int sampleRateInMilliseconds;
unsigned int proposedFramesPerFragment;
unsigned int framesPerFragment;
size_t fragmentSize;
size_t hardwareBufferSize;
dra_backend_dsound* pBackendDS = (dra_backend_dsound*)pBackend;
if (pBackendDS == NULL) {
return NULL;
}
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)calloc(1, sizeof(*pDeviceDS));
if (pDeviceDS == NULL) {
goto on_error;
}
if (channels == 0) {
channels = DR_AUDIO_DEFAULT_CHANNEL_COUNT;
}
pDeviceDS->pBackend = pBackend;
pDeviceDS->type = dra_device_type_capture;
pDeviceDS->channels = channels;
pDeviceDS->sampleRate = sampleRate;
hr = pBackendDS->pDirectSoundCaptureCreate8(dra_dsound__get_capture_device_guid_by_id(pBackend, deviceID), &pDeviceDS->pDSCapture, NULL);
if (FAILED(hr)) {
goto on_error;
}
pDeviceDS->fragmentCount = DR_AUDIO_DEFAULT_FRAGMENT_COUNT;
// The secondary buffer is the buffer where the real audio data will be written to and used by the hardware device. It's
// size is based on the latency, sample rate and channels.
//
// The format of the secondary buffer should exactly match the primary buffer as to avoid unnecessary data conversions.
sampleRateInMilliseconds = pDeviceDS->sampleRate / 1000;
if (sampleRateInMilliseconds == 0) {
sampleRateInMilliseconds = 1;
}
// The size of a fragment is sized such that the number of frames contained within it is a multiple of 2. The reason for
// this is to keep it consistent with the ALSA backend.
proposedFramesPerFragment = sampleRateInMilliseconds * latencyInMilliseconds;
framesPerFragment = dra_prev_power_of_2(proposedFramesPerFragment);
if (framesPerFragment == 0) {
framesPerFragment = 2;
}
pDeviceDS->samplesPerFragment = framesPerFragment * pDeviceDS->channels;
fragmentSize = pDeviceDS->samplesPerFragment * sizeof(float);
hardwareBufferSize = fragmentSize * pDeviceDS->fragmentCount;
assert(hardwareBufferSize > 0); // <-- If you've triggered this is means you've got something set to 0. You haven't been setting that latency to 0 have you?! That's not allowed!
WAVEFORMATEXTENSIBLE wf;
memset(&wf, 0, sizeof(wf));
wf.Format.cbSize = sizeof(wf);
wf.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
wf.Format.nChannels = (WORD)channels;
wf.Format.nSamplesPerSec = (DWORD)sampleRate;
wf.Format.wBitsPerSample = sizeof(float)*8;
wf.Format.nBlockAlign = (wf.Format.nChannels * wf.Format.wBitsPerSample) / 8;
wf.Format.nAvgBytesPerSec = wf.Format.nBlockAlign * wf.Format.nSamplesPerSec;
wf.Samples.wValidBitsPerSample = wf.Format.wBitsPerSample;
wf.dwChannelMask = 0;
wf.SubFormat = _g_draGUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
if (channels > 2) {
wf.dwChannelMask = ~(((DWORD)-1) << channels);
}
DSCBUFFERDESC descDS;
memset(&descDS, 0, sizeof(descDS));
descDS.dwSize = sizeof(descDS);
descDS.dwFlags = 0;
descDS.dwBufferBytes = (DWORD)hardwareBufferSize;
descDS.lpwfxFormat = (WAVEFORMATEX*)&wf;
LPDIRECTSOUNDCAPTUREBUFFER pDSCB_Temp;
hr = IDirectSoundCapture_CreateCaptureBuffer(pDeviceDS->pDSCapture, &descDS, &pDSCB_Temp, NULL);
if (FAILED(hr)) {
goto on_error;
}
hr = IDirectSoundCapture_QueryInterface(pDSCB_Temp, g_draGUID_IID_IDirectSoundCaptureBuffer8, (LPVOID*)&pDeviceDS->pDSCaptureBuffer);
IDirectSoundCaptureBuffer_Release(pDSCB_Temp);
if (FAILED(hr)) {
goto on_error; // Failed to retrieve the DirectSoundCaptureBuffer8 interface.
}
// As DirectSound is playing back the hardware buffer it needs to notify dr_audio when it's ready for new data. This is done
// through a notification object which we retrieve from the secondary buffer.
hr = IDirectSoundCaptureBuffer8_QueryInterface(pDeviceDS->pDSCaptureBuffer, g_draGUID_IID_DirectSoundNotify, (void**)&pDeviceDS->pDSNotify);
if (FAILED(hr)) {
goto on_error;
}
DSBPOSITIONNOTIFY notifyPoints[DR_AUDIO_DEFAULT_FRAGMENT_COUNT]; // One notification event for each fragment.
for (int i = 0; i < DR_AUDIO_DEFAULT_FRAGMENT_COUNT; ++i)
{
pDeviceDS->pNotifyEvents[i] = CreateEventA(NULL, FALSE, FALSE, NULL);
if (pDeviceDS->pNotifyEvents[i] == NULL) {
goto on_error;
}
notifyPoints[i].dwOffset = (DWORD)(((i+1) * fragmentSize) % hardwareBufferSize); // <-- This is in bytes.
notifyPoints[i].hEventNotify = pDeviceDS->pNotifyEvents[i];
}
hr = IDirectSoundNotify_SetNotificationPositions(pDeviceDS->pDSNotify, DR_AUDIO_DEFAULT_FRAGMENT_COUNT, notifyPoints);
if (FAILED(hr)) {
goto on_error;
}
// The termination event is used to determine when the capture thread should be terminated. This thread
// will wait on this event in addition to the notification events in it's main loop.
pDeviceDS->hStopEvent = CreateEventA(NULL, FALSE, FALSE, NULL);
if (pDeviceDS->hStopEvent == NULL) {
goto on_error;
}
return (dra_backend_device*)pDeviceDS;
on_error:
dra_backend_device_close_dsound((dra_backend_device*)pDeviceDS);
return NULL;
}
dra_backend_device* dra_backend_device_open_dsound(dra_backend* pBackend, dra_device_type type, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds)
{
if (type == dra_device_type_playback) {
return dra_backend_device_open_playback_dsound(pBackend, deviceID, channels, sampleRate, latencyInMilliseconds);
} else {
return dra_backend_device_open_capture_dsound(pBackend, deviceID, channels, sampleRate, latencyInMilliseconds);
}
}
void dra_backend_device_play(dra_backend_device* pDevice)
{
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)pDevice;
if (pDeviceDS == NULL) {
return;
}
if (pDevice->type == dra_device_type_playback) {
IDirectSoundBuffer_Play(pDeviceDS->pDSSecondaryBuffer, 0, 0, DSBPLAY_LOOPING);
} else {
IDirectSoundCaptureBuffer8_Start(pDeviceDS->pDSCaptureBuffer, DSCBSTART_LOOPING);
}
}
void dra_backend_device_stop(dra_backend_device* pDevice)
{
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)pDevice;
if (pDeviceDS == NULL) {
return;
}
if (pDevice->type == dra_device_type_playback) {
// Don't do anything if the buffer is not already playing.
DWORD status;
IDirectSoundBuffer_GetStatus(pDeviceDS->pDSSecondaryBuffer, &status);
if ((status & DSBSTATUS_PLAYING) == 0) {
return; // The buffer is already stopped.
}
// Stop the playback straight away to ensure output to the hardware device is stopped as soon as possible.
IDirectSoundBuffer_Stop(pDeviceDS->pDSSecondaryBuffer);
IDirectSoundBuffer_SetCurrentPosition(pDeviceDS->pDSSecondaryBuffer, 0);
} else {
// Don't do anything if the buffer is not already playing.
DWORD status;
IDirectSoundCaptureBuffer8_GetStatus(pDeviceDS->pDSCaptureBuffer, &status);
if ((status & DSBSTATUS_PLAYING) == 0) {
return; // The buffer is already stopped.
}
// Stop capture straight away to ensure output to the hardware device is stopped as soon as possible.
//IDirectSoundCaptureBuffer8_Stop(pDeviceDS->pDSCaptureBuffer); // <-- There's actually a typo in my version of dsound.h which trigger's a compilation error here. The call below is safe, albeit slightly less intuitive.
IDirectSoundCaptureBuffer_Stop(pDeviceDS->pDSCaptureBuffer);
}
// Now we just need to make dra_backend_device_play() return which in the case of DirectSound we do by
// simply signaling the stop event.
SetEvent(pDeviceDS->hStopEvent);
}
dr_bool32 dra_backend_device_wait(dra_backend_device* pDevice) // <-- Returns DR_TRUE if the function has returned because it needs more data; DR_FALSE if the device has been stopped or an error has occured.
{
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)pDevice;
if (pDeviceDS == NULL) {
return DR_FALSE;
}
unsigned int eventCount = DR_AUDIO_DEFAULT_FRAGMENT_COUNT + 1;
HANDLE eventHandles[DR_AUDIO_DEFAULT_FRAGMENT_COUNT + 1]; // +1 for the stop event.
memcpy(eventHandles, pDeviceDS->pNotifyEvents, sizeof(HANDLE) * DR_AUDIO_DEFAULT_FRAGMENT_COUNT);
eventHandles[DR_AUDIO_DEFAULT_FRAGMENT_COUNT] = pDeviceDS->hStopEvent;
DWORD rc = WaitForMultipleObjects(DR_AUDIO_DEFAULT_FRAGMENT_COUNT + 1, eventHandles, FALSE, INFINITE);
if (rc >= WAIT_OBJECT_0 && rc < eventCount)
{
unsigned int eventIndex = rc - WAIT_OBJECT_0;
HANDLE hEvent = eventHandles[eventIndex];
// Has the device been stopped? If so, need to return DR_FALSE.
if (hEvent == pDeviceDS->hStopEvent) {
return DR_FALSE;
}
// If we get here it means the event that's been signaled represents a fragment.
pDeviceDS->currentFragmentIndex = eventIndex;
return DR_TRUE;
}
return DR_FALSE;
}
void* dra_backend_device_map_next_fragment(dra_backend_device* pDevice, size_t* pSamplesInFragmentOut)
{
assert(pSamplesInFragmentOut != NULL);
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)pDevice;
if (pDeviceDS == NULL) {
return NULL;
}
if (pDeviceDS->pLockPtr != NULL) {
return NULL; // A fragment is already mapped. Can only have a single fragment mapped at a time.
}
if (pDevice->type == dra_device_type_playback) {
// If the device is not currently playing, we just return the first fragment. Otherwise we return the fragment that's sitting just past the
// one that's currently playing.
DWORD dwOffset = 0;
DWORD dwBytes = pDeviceDS->samplesPerFragment * sizeof(float);
DWORD status;
IDirectSoundBuffer_GetStatus(pDeviceDS->pDSSecondaryBuffer, &status);
if ((status & DSBSTATUS_PLAYING) != 0) {
dwOffset = (((pDeviceDS->currentFragmentIndex + 1) % pDeviceDS->fragmentCount) * pDeviceDS->samplesPerFragment) * sizeof(float);
}
HRESULT hr = IDirectSoundBuffer_Lock(pDeviceDS->pDSSecondaryBuffer, dwOffset, dwBytes, &pDeviceDS->pLockPtr, &pDeviceDS->lockSize, NULL, NULL, 0);
if (FAILED(hr)) {
return NULL;
}
} else {
DWORD dwOffset = (pDeviceDS->currentFragmentIndex * pDeviceDS->samplesPerFragment) * sizeof(float);
DWORD dwBytes = pDeviceDS->samplesPerFragment * sizeof(float);
HRESULT hr = IDirectSoundCaptureBuffer8_Lock(pDeviceDS->pDSCaptureBuffer, dwOffset, dwBytes, &pDeviceDS->pLockPtr, &pDeviceDS->lockSize, NULL, NULL, 0);
if (FAILED(hr)) {
return NULL;
}
}
*pSamplesInFragmentOut = pDeviceDS->samplesPerFragment;
return pDeviceDS->pLockPtr;
}
void dra_backend_device_unmap_next_fragment(dra_backend_device* pDevice)
{
dra_backend_device_dsound* pDeviceDS = (dra_backend_device_dsound*)pDevice;
if (pDeviceDS == NULL) {
return;
}
if (pDeviceDS->pLockPtr == NULL) {
return; // Nothing is mapped.
}
if (pDevice->type == dra_device_type_playback) {
IDirectSoundBuffer_Unlock(pDeviceDS->pDSSecondaryBuffer, pDeviceDS->pLockPtr, pDeviceDS->lockSize, NULL, 0);
} else {
IDirectSoundCaptureBuffer8_Unlock(pDeviceDS->pDSCaptureBuffer, pDeviceDS->pLockPtr, pDeviceDS->lockSize, NULL, 0);
}
pDeviceDS->pLockPtr = NULL;
pDeviceDS->lockSize = 0;
}
#endif // DR_AUDIO_NO_SOUND
#endif // _WIN32
#ifdef __linux__
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <pthread.h>
#include <fcntl.h>
#include <semaphore.h>
//// Threading (POSIX) ////
typedef void* (* dra_thread_entry_proc)(void* pData);
dra_thread dra_thread_create(dra_thread_entry_proc entryProc, void* pData)
{
pthread_t thread;
if (pthread_create(&thread, NULL, entryProc, pData) != 0) {
return NULL;
}
return (dra_thread)thread;
}
void dra_thread_delete(dra_thread thread)
{
(void)thread;
}
void dra_thread_wait(dra_thread thread)
{
pthread_join((pthread_t)thread, NULL);
}
dra_mutex dra_mutex_create()
{
// The pthread_mutex_t object is not a void* castable handle type. Just create it on the heap and be done with it.
pthread_mutex_t* mutex = (pthread_mutex_t*)malloc(sizeof(pthread_mutex_t));
if (pthread_mutex_init(mutex, NULL) != 0) {
free(mutex);
mutex = NULL;
}
return mutex;
}
void dra_mutex_delete(dra_mutex mutex)
{
pthread_mutex_destroy((pthread_mutex_t*)mutex);
}
void dra_mutex_lock(dra_mutex mutex)
{
pthread_mutex_lock((pthread_mutex_t*)mutex);
}
void dra_mutex_unlock(dra_mutex mutex)
{
pthread_mutex_unlock((pthread_mutex_t*)mutex);
}
dra_semaphore dra_semaphore_create(int initialValue)
{
sem_t* semaphore = (sem_t*)malloc(sizeof(sem_t));
if (sem_init(semaphore, 0, (unsigned int)initialValue) == -1) {
free(semaphore);
semaphore = NULL;
}
return (dra_semaphore)semaphore;
}
void dra_semaphore_delete(dra_semaphore semaphore)
{
sem_close((sem_t*)semaphore);
}
dr_bool32 dra_semaphore_wait(dra_semaphore semaphore)
{
return sem_wait((sem_t*)semaphore) != -1;
}
dr_bool32 dra_semaphore_release(dra_semaphore semaphore)
{
return sem_post((sem_t*)semaphore) != -1;
}
//// ALSA ////
#ifndef DR_AUDIO_NO_ALSA
#define DR_AUDIO_ENABLE_ALSA
#include <alsa/asoundlib.h>
typedef struct
{
DR_AUDIO_BASE_BACKEND_ATTRIBS
int unused;
} dra_backend_alsa;
typedef struct
{
DR_AUDIO_BASE_BACKEND_DEVICE_ATTRIBS
// The ALSA device handle.
snd_pcm_t* deviceALSA;
// Whether or not the device is currently playing.
dr_bool32 isPlaying;
// Whether or not the intermediary buffer is mapped.
dr_bool32 isBufferMapped;
// The intermediary buffer where audio data is written before being submitted to the device.
float* pIntermediaryBuffer;
} dra_backend_device_alsa;
static dr_bool32 dra_alsa__get_device_name_by_id(dra_backend* pBackend, unsigned int deviceID, char* deviceNameOut)
{
assert(pBackend != NULL);
assert(deviceNameOut != NULL);
deviceNameOut[0] = '\0'; // Safety.
if (deviceID == 0) {
strcpy(deviceNameOut, "default");
return DR_TRUE;
}
unsigned int iDevice = 0;
char** deviceHints;
if (snd_device_name_hint(-1, "pcm", (void***)&deviceHints) < 0) {
//printf("Failed to iterate devices.");
return -1;
}
char** nextDeviceHint = deviceHints;
while (*nextDeviceHint != NULL && iDevice < deviceID) {
nextDeviceHint += 1;
iDevice += 1;
}
dr_bool32 result = DR_FALSE;
if (iDevice == deviceID) {
strcpy(deviceNameOut, snd_device_name_get_hint(*nextDeviceHint, "NAME"));
result = DR_TRUE;
}
snd_device_name_free_hint((void**)deviceHints);
return result;
}
dra_backend* dra_backend_create_alsa()
{
dra_backend_alsa* pBackendALSA = (dra_backend_alsa*)calloc(1, sizeof(*pBackendALSA)); // <-- Note the calloc() - makes it easier to handle the on_error goto.
if (pBackendALSA == NULL) {
return NULL;
}
pBackendALSA->type = DR_AUDIO_BACKEND_TYPE_ALSA;
return (dra_backend*)pBackendALSA;
#if 0
on_error:
if (pBackendALSA != NULL) {
free(pBackendALSA);
}
return NULL;
#endif
}
void dra_backend_delete_alsa(dra_backend* pBackend)
{
dra_backend_alsa* pBackendALSA = (dra_backend_alsa*)pBackend;
if (pBackendALSA == NULL) {
return;
}
free(pBackend);
}
void dra_backend_device_close_alsa(dra_backend_device* pDevice)
{
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)pDevice;
if (pDeviceALSA == NULL) {
return;
}
if (pDeviceALSA->deviceALSA != NULL) {
snd_pcm_close(pDeviceALSA->deviceALSA);
}
free(pDeviceALSA->pIntermediaryBuffer);
free(pDeviceALSA);
}
dra_backend_device* dra_backend_device_open_playback_alsa(dra_backend* pBackend, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds)
{
unsigned int periods;
int dir;
size_t sampleRateInMilliseconds;
unsigned int proposedFramesPerFragment;
unsigned int framesPerFragment;
snd_pcm_sw_params_t* pSWParams;
dra_backend_alsa* pBackendALSA = (dra_backend_alsa*)pBackend;
if (pBackendALSA == NULL) {
return NULL;
}
snd_pcm_hw_params_t* pHWParams = NULL;
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)calloc(1, sizeof(*pDeviceALSA));
if (pDeviceALSA == NULL) {
goto on_error;
}
pDeviceALSA->pBackend = pBackend;
pDeviceALSA->type = dra_device_type_playback;
pDeviceALSA->channels = channels;
pDeviceALSA->sampleRate = sampleRate;
char deviceName[1024];
if (!dra_alsa__get_device_name_by_id(pBackend, deviceID, deviceName)) { // <-- This will return "default" if deviceID is 0.
goto on_error;
}
if (snd_pcm_open(&pDeviceALSA->deviceALSA, deviceName, SND_PCM_STREAM_PLAYBACK, 0) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_malloc(&pHWParams) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_any(pDeviceALSA->deviceALSA, pHWParams) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_access(pDeviceALSA->deviceALSA, pHWParams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_format(pDeviceALSA->deviceALSA, pHWParams, SND_PCM_FORMAT_FLOAT_LE) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_rate_near(pDeviceALSA->deviceALSA, pHWParams, &sampleRate, 0) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_channels_near(pDeviceALSA->deviceALSA, pHWParams, &channels) < 0) {
goto on_error;
}
pDeviceALSA->sampleRate = sampleRate;
pDeviceALSA->channels = channels;
periods = DR_AUDIO_DEFAULT_FRAGMENT_COUNT;
dir = 1;
if (snd_pcm_hw_params_set_periods_near(pDeviceALSA->deviceALSA, pHWParams, &periods, &dir) < 0) {
//printf("Failed to set periods.\n");
goto on_error;
}
pDeviceALSA->fragmentCount = periods;
//printf("Periods: %d | Direction: %d\n", periods, dir);
sampleRateInMilliseconds = pDeviceALSA->sampleRate / 1000;
if (sampleRateInMilliseconds == 0) {
sampleRateInMilliseconds = 1;
}
// According to the ALSA documentation, the value passed to snd_pcm_sw_params_set_avail_min() must be a power
// of 2 on some hardware. The value passed to this function is the size in frames of a fragment. Thus, to be
// as robust as possible the size of the hardware buffer should be sized based on the size of a closest power-
// of-two fragment.
//
// To calculate the size of a fragment, the first step is to determine the initial proposed size. From that
// it is dropped to the previous power of two. The reason for this is that, based on admittedly very basic
// testing, ALSA seems to have good latency characteristics, and less latency is always preferable.
proposedFramesPerFragment = sampleRateInMilliseconds * latencyInMilliseconds;
framesPerFragment = dra_prev_power_of_2(proposedFramesPerFragment);
if (framesPerFragment == 0) {
framesPerFragment = 2;
}
pDeviceALSA->samplesPerFragment = framesPerFragment * pDeviceALSA->channels;
if (snd_pcm_hw_params_set_buffer_size(pDeviceALSA->deviceALSA, pHWParams, framesPerFragment * pDeviceALSA->fragmentCount) < 0) {
//printf("Failed to set buffer size.\n");
goto on_error;
}
if (snd_pcm_hw_params(pDeviceALSA->deviceALSA, pHWParams) < 0) {
goto on_error;
}
snd_pcm_hw_params_free(pHWParams);
// Software params. There needs to be at least fragmentSize bytes in the hardware buffer before playing it, and there needs
// be fragmentSize bytes available after every wait.
pSWParams = NULL;
if (snd_pcm_sw_params_malloc(&pSWParams) < 0) {
goto on_error;
}
if (snd_pcm_sw_params_current(pDeviceALSA->deviceALSA, pSWParams) != 0) {
goto on_error;
}
if (snd_pcm_sw_params_set_start_threshold(pDeviceALSA->deviceALSA, pSWParams, framesPerFragment) != 0) {
goto on_error;
}
if (snd_pcm_sw_params_set_avail_min(pDeviceALSA->deviceALSA, pSWParams, framesPerFragment) != 0) {
goto on_error;
}
if (snd_pcm_sw_params(pDeviceALSA->deviceALSA, pSWParams) != 0) {
goto on_error;
}
snd_pcm_sw_params_free(pSWParams);
// The intermediary buffer that will be used for mapping/unmapping.
pDeviceALSA->isBufferMapped = DR_FALSE;
pDeviceALSA->pIntermediaryBuffer = (float*)malloc(pDeviceALSA->samplesPerFragment * sizeof(float));
if (pDeviceALSA->pIntermediaryBuffer == NULL) {
goto on_error;
}
return (dra_backend_device*)pDeviceALSA;
on_error:
if (pHWParams) {
snd_pcm_hw_params_free(pHWParams);
}
if (pDeviceALSA != NULL) {
if (pDeviceALSA->deviceALSA != NULL) {
snd_pcm_close(pDeviceALSA->deviceALSA);
}
free(pDeviceALSA->pIntermediaryBuffer);
free(pDeviceALSA);
}
return NULL;
}
dra_backend_device* dra_backend_device_open_capture_alsa(dra_backend* pBackend, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds)
{
unsigned int periods;
int dir;
size_t sampleRateInMilliseconds;
unsigned int proposedFramesPerFragment;
unsigned int framesPerFragment;
snd_pcm_sw_params_t* pSWParams;
dra_backend_alsa* pBackendALSA = (dra_backend_alsa*)pBackend;
if (pBackendALSA == NULL) {
return NULL;
}
snd_pcm_hw_params_t* pHWParams = NULL;
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)calloc(1, sizeof(*pDeviceALSA));
if (pDeviceALSA == NULL) {
goto on_error;
}
pDeviceALSA->pBackend = pBackend;
pDeviceALSA->type = dra_device_type_capture;
pDeviceALSA->channels = channels;
pDeviceALSA->sampleRate = sampleRate;
char deviceName[1024];
if (!dra_alsa__get_device_name_by_id(pBackend, deviceID, deviceName)) { // <-- This will return "default" if deviceID is 0.
goto on_error;
}
if (snd_pcm_open(&pDeviceALSA->deviceALSA, deviceName, SND_PCM_STREAM_CAPTURE, 0) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_malloc(&pHWParams) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_any(pDeviceALSA->deviceALSA, pHWParams) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_access(pDeviceALSA->deviceALSA, pHWParams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_format(pDeviceALSA->deviceALSA, pHWParams, SND_PCM_FORMAT_FLOAT_LE) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_rate_near(pDeviceALSA->deviceALSA, pHWParams, &sampleRate, 0) < 0) {
goto on_error;
}
if (snd_pcm_hw_params_set_channels_near(pDeviceALSA->deviceALSA, pHWParams, &channels) < 0) {
goto on_error;
}
pDeviceALSA->sampleRate = sampleRate;
pDeviceALSA->channels = channels;
periods = DR_AUDIO_DEFAULT_FRAGMENT_COUNT;
dir = 1;
if (snd_pcm_hw_params_set_periods_near(pDeviceALSA->deviceALSA, pHWParams, &periods, &dir) < 0) {
//printf("Failed to set periods.\n");
goto on_error;
}
pDeviceALSA->fragmentCount = periods;
//printf("Periods: %d | Direction: %d\n", periods, dir);
sampleRateInMilliseconds = pDeviceALSA->sampleRate / 1000;
if (sampleRateInMilliseconds == 0) {
sampleRateInMilliseconds = 1;
}
// According to the ALSA documentation, the value passed to snd_pcm_sw_params_set_avail_min() must be a power
// of 2 on some hardware. The value passed to this function is the size in frames of a fragment. Thus, to be
// as robust as possible the size of the hardware buffer should be sized based on the size of a closest power-
// of-two fragment.
//
// To calculate the size of a fragment, the first step is to determine the initial proposed size. From that
// it is dropped to the previous power of two. The reason for this is that, based on admittedly very basic
// testing, ALSA seems to have good latency characteristics, and less latency is always preferable.
proposedFramesPerFragment = sampleRateInMilliseconds * latencyInMilliseconds;
framesPerFragment = dra_prev_power_of_2(proposedFramesPerFragment);
if (framesPerFragment == 0) {
framesPerFragment = 2;
}
pDeviceALSA->samplesPerFragment = framesPerFragment * pDeviceALSA->channels;
if (snd_pcm_hw_params_set_buffer_size(pDeviceALSA->deviceALSA, pHWParams, framesPerFragment * pDeviceALSA->fragmentCount) < 0) {
//printf("Failed to set buffer size.\n");
goto on_error;
}
if (snd_pcm_hw_params(pDeviceALSA->deviceALSA, pHWParams) < 0) {
goto on_error;
}
snd_pcm_hw_params_free(pHWParams);
// Software params. There needs to be at least fragmentSize bytes in the hardware buffer before playing it, and there needs
// be fragmentSize bytes available after every wait.
pSWParams = NULL;
if (snd_pcm_sw_params_malloc(&pSWParams) < 0) {
goto on_error;
}
if (snd_pcm_sw_params_current(pDeviceALSA->deviceALSA, pSWParams) != 0) {
goto on_error;
}
if (snd_pcm_sw_params_set_start_threshold(pDeviceALSA->deviceALSA, pSWParams, framesPerFragment) != 0) {
goto on_error;
}
if (snd_pcm_sw_params_set_avail_min(pDeviceALSA->deviceALSA, pSWParams, framesPerFragment) != 0) {
goto on_error;
}
if (snd_pcm_sw_params(pDeviceALSA->deviceALSA, pSWParams) != 0) {
goto on_error;
}
snd_pcm_sw_params_free(pSWParams);
// The intermediary buffer that will be used for mapping/unmapping.
pDeviceALSA->isBufferMapped = DR_FALSE;
pDeviceALSA->pIntermediaryBuffer = (float*)malloc(pDeviceALSA->samplesPerFragment * sizeof(float));
if (pDeviceALSA->pIntermediaryBuffer == NULL) {
goto on_error;
}
return (dra_backend_device*)pDeviceALSA;
on_error:
dra_backend_device_close_alsa((dra_backend_device*)pDeviceALSA);
return NULL;
}
dra_backend_device* dra_backend_device_open_alsa(dra_backend* pBackend, dra_device_type type, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds)
{
if (type == dra_device_type_playback) {
return dra_backend_device_open_playback_alsa(pBackend, deviceID, channels, sampleRate, latencyInMilliseconds);
} else {
return dra_backend_device_open_capture_alsa(pBackend, deviceID, channels, sampleRate, latencyInMilliseconds);
}
}
void dra_backend_device_play(dra_backend_device* pDevice)
{
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)pDevice;
if (pDeviceALSA == NULL) {
return;
}
snd_pcm_prepare(pDeviceALSA->deviceALSA);
pDeviceALSA->isPlaying = DR_TRUE;
}
void dra_backend_device_stop(dra_backend_device* pDevice)
{
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)pDevice;
if (pDeviceALSA == NULL) {
return;
}
snd_pcm_drop(pDeviceALSA->deviceALSA);
pDeviceALSA->isPlaying = DR_FALSE;
}
dr_bool32 dra_backend_device_wait(dra_backend_device* pDevice)
{
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)pDevice;
if (pDeviceALSA == NULL) {
return DR_FALSE;
}
if (!pDeviceALSA->isPlaying) {
return DR_FALSE;
}
if (pDevice->type == dra_device_type_playback) {
int result = snd_pcm_wait(pDeviceALSA->deviceALSA, -1);
if (result > 0) {
return DR_TRUE;
}
if (result == -EPIPE) {
// xrun. Prepare the device again and just return DR_TRUE.
snd_pcm_prepare(pDeviceALSA->deviceALSA);
return DR_TRUE;
}
} else {
snd_pcm_uframes_t frameCount = pDeviceALSA->samplesPerFragment / pDeviceALSA->channels;
snd_pcm_sframes_t framesRead = snd_pcm_readi(pDeviceALSA->deviceALSA, pDeviceALSA->pIntermediaryBuffer, frameCount);
if (framesRead > 0) {
return DR_TRUE;
}
if (framesRead == -EPIPE) {
// xrun. Prepare the device again and just return DR_TRUE.
snd_pcm_prepare(pDeviceALSA->deviceALSA);
return DR_TRUE;
}
}
return DR_FALSE;
}
void* dra_backend_device_map_next_fragment(dra_backend_device* pDevice, size_t* pSamplesInFragmentOut)
{
assert(pSamplesInFragmentOut != NULL);
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)pDevice;
if (pDeviceALSA == NULL) {
return NULL;
}
if (pDeviceALSA->isBufferMapped) {
return NULL; // A fragment is already mapped. Can only have a single fragment mapped at a time.
}
//if (pDeviceALSA->type == dra_device_type_capture) {
// snd_pcm_readi(pDeviceALSA->deviceALSA, pDeviceALSA->pIntermediaryBuffer, pDeviceALSA->samplesPerFragment / pDeviceALSA->channels);
//}
*pSamplesInFragmentOut = pDeviceALSA->samplesPerFragment;
return pDeviceALSA->pIntermediaryBuffer;
}
void dra_backend_device_unmap_next_fragment(dra_backend_device* pDevice)
{
dra_backend_device_alsa* pDeviceALSA = (dra_backend_device_alsa*)pDevice;
if (pDeviceALSA == NULL) {
return;
}
if (pDeviceALSA->isBufferMapped) {
return; // Nothing is mapped.
}
// Unammping is when the data is written to the device.
if (pDeviceALSA->type == dra_device_type_playback) {
snd_pcm_writei(pDeviceALSA->deviceALSA, pDeviceALSA->pIntermediaryBuffer, pDeviceALSA->samplesPerFragment / pDeviceALSA->channels);
}
}
#endif // DR_AUDIO_NO_ALSA
#endif // __linux__
void dra_thread_wait_and_delete(dra_thread thread)
{
dra_thread_wait(thread);
dra_thread_delete(thread);
}
dra_backend* dra_backend_create()
{
dra_backend* pBackend = NULL;
#ifdef DR_AUDIO_ENABLE_DSOUND
pBackend = dra_backend_create_dsound();
if (pBackend != NULL) {
return pBackend;
}
#endif
#ifdef DR_AUDIO_ENABLE_ALSA
pBackend = dra_backend_create_alsa();
if (pBackend != NULL) {
return pBackend;
}
#endif
// If we get here it means we couldn't find a backend. Default to a NULL backend? Returning NULL makes it clearer that an error occured.
return NULL;
}
void dra_backend_delete(dra_backend* pBackend)
{
if (pBackend == NULL) {
return;
}
#ifdef DR_AUDIO_ENABLE_DSOUND
if (pBackend->type == DR_AUDIO_BACKEND_TYPE_DSOUND) {
dra_backend_delete_dsound(pBackend);
return;
}
#endif
#ifdef DR_AUDIO_ENABLE_ALSA
if (pBackend->type == DR_AUDIO_BACKEND_TYPE_ALSA) {
dra_backend_delete_alsa(pBackend);
return;
}
#endif
// Should never get here. If this assert is triggered it means you haven't plugged in the API in the list above.
assert(DR_FALSE);
}
dra_backend_device* dra_backend_device_open(dra_backend* pBackend, dra_device_type type, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds)
{
if (pBackend == NULL) {
return NULL;
}
#ifdef DR_AUDIO_ENABLE_DSOUND
if (pBackend->type == DR_AUDIO_BACKEND_TYPE_DSOUND) {
return dra_backend_device_open_dsound(pBackend, type, deviceID, channels, sampleRate, latencyInMilliseconds);
}
#endif
#ifdef DR_AUDIO_ENABLE_ALSA
if (pBackend->type == DR_AUDIO_BACKEND_TYPE_ALSA) {
return dra_backend_device_open_alsa(pBackend, type, deviceID, channels, sampleRate, latencyInMilliseconds);
}
#endif
// Should never get here. If this assert is triggered it means you haven't plugged in the API in the list above.
assert(DR_FALSE);
return NULL;
}
void dra_backend_device_close(dra_backend_device* pDevice)
{
if (pDevice == NULL) {
return;
}
assert(pDevice->pBackend != NULL);
#ifdef DR_AUDIO_ENABLE_DSOUND
if (pDevice->pBackend->type == DR_AUDIO_BACKEND_TYPE_DSOUND) {
dra_backend_device_close_dsound(pDevice);
return;
}
#endif
#ifdef DR_AUDIO_ENABLE_ALSA
if (pDevice->pBackend->type == DR_AUDIO_BACKEND_TYPE_ALSA) {
dra_backend_device_close_alsa(pDevice);
return;
}
#endif
}
///////////////////////////////////////////////////////////////////////////////
//
// Cross Platform
//
///////////////////////////////////////////////////////////////////////////////
// Reads the next frame.
//
// Frames are retrieved with respect to the device the voice is attached to. What this basically means is
// that any data conversions will be done within this function.
//
// The return value is a pointer to the voice containing the converted samples, always as floating point.
//
// If the voice is in the same format as the device (floating point, same sample rate and channels), then
// this function will be on a fast path and will return almost immediately with a pointer that points to
// the voice's actual data without any data conversion.
//
// If an error occurs, null is returned. Null will be returned if the end of the voice's buffer is reached
// and it's non-looping. This will not return NULL if the voice is looping - it will just loop back to the
// start as one would expect.
//
// This function is not thread safe, but can be called from multiple threads if you do your own
// synchronization. Just keep in mind that the return value may point to the voice's actual internal data.
float* dra_voice__next_frame(dra_voice* pVoice);
// dra_voice__next_frames()
size_t dra_voice__next_frames(dra_voice* pVoice, size_t frameCount, float* pSamplesOut);
dra_result dra_context_init(dra_context* pContext)
{
if (pContext == NULL) return DRA_RESULT_INVALID_ARGS;
memset(pContext, 0, sizeof(*pContext));
// We need a backend first.
pContext->pBackend = dra_backend_create();
if (pContext->pBackend == NULL) {
return DRA_RESULT_NO_BACKEND; // Failed to create a backend.
}
return DRA_RESULT_SUCCESS;
}
void dra_context_uninit(dra_context* pContext)
{
if (pContext == NULL || pContext->pBackend == NULL) return;
dra_backend_delete(pContext->pBackend);
}
dra_result dra_context_create(dra_context** ppContext)
{
if (ppContext == NULL) return DRA_RESULT_INVALID_ARGS;
*ppContext = NULL;
dra_context* pContext = (dra_context*)malloc(sizeof(*pContext));
if (pContext == NULL) {
return DRA_RESULT_OUT_OF_MEMORY;
}
dra_result result = dra_context_init(pContext);
if (result != DRA_RESULT_SUCCESS) {
free(pContext);
return result;
}
*ppContext = pContext;
return DRA_RESULT_SUCCESS;
}
void dra_context_delete(dra_context* pContext)
{
if (pContext == NULL) return;
dra_context_uninit(pContext);
free(pContext);
}
void dra_event_queue__schedule_event(dra__event_queue* pQueue, dra__event* pEvent)
{
if (pQueue == NULL || pEvent == NULL) {
return;
}
dra_mutex_lock(pQueue->lock);
{
if (pQueue->eventCount == pQueue->eventBufferSize)
{
// Ran out of room. Resize.
size_t newEventBufferSize = (pQueue->eventBufferSize == 0) ? 16 : pQueue->eventBufferSize*2;
dra__event* pNewEvents = (dra__event*)malloc(newEventBufferSize * sizeof(*pNewEvents));
if (pNewEvents == NULL) {
return;
}
for (size_t i = 0; i < pQueue->eventCount; ++i) {
pQueue->pEvents[i] = pQueue->pEvents[(pQueue->firstEvent + i) % pQueue->eventBufferSize];
}
pQueue->firstEvent = 0;
pQueue->eventBufferSize = newEventBufferSize;
pQueue->pEvents = pNewEvents;
}
assert(pQueue->eventCount < pQueue->eventBufferSize);
pQueue->pEvents[(pQueue->firstEvent + pQueue->eventCount) % pQueue->eventBufferSize] = *pEvent;
pQueue->eventCount += 1;
}
dra_mutex_unlock(pQueue->lock);
}
void dra_event_queue__cancel_events_of_voice(dra__event_queue* pQueue, dra_voice* pVoice)
{
if (pQueue == NULL || pVoice == NULL) {
return;
}
dra_mutex_lock(pQueue->lock);
{
// We don't actually remove anything from the queue, but instead zero out the event's data.
for (size_t i = 0; i < pQueue->eventCount; ++i) {
dra__event* pEvent = &pQueue->pEvents[(pQueue->firstEvent + i) % pQueue->eventBufferSize];
if (pEvent->pVoice == pVoice) {
pEvent->pVoice = NULL;
pEvent->proc = NULL;
}
}
}
dra_mutex_unlock(pQueue->lock);
}
dr_bool32 dra_event_queue__next_event(dra__event_queue* pQueue, dra__event* pEventOut)
{
if (pQueue == NULL || pEventOut == NULL) {
return DR_FALSE;
}
dr_bool32 result = DR_FALSE;
dra_mutex_lock(pQueue->lock);
{
if (pQueue->eventCount > 0) {
*pEventOut = pQueue->pEvents[pQueue->firstEvent];
pQueue->firstEvent = (pQueue->firstEvent + 1) % pQueue->eventBufferSize;
pQueue->eventCount -= 1;
result = DR_TRUE;
}
}
dra_mutex_unlock(pQueue->lock);
return result;
}
void dra_event_queue__post_events(dra__event_queue* pQueue)
{
if (pQueue == NULL) {
return;
}
dra__event nextEvent;
while (dra_event_queue__next_event(pQueue, &nextEvent)) {
if (nextEvent.proc) {
nextEvent.proc(nextEvent.id, nextEvent.pUserData);
}
}
}
void dra_device__post_event(dra_device* pDevice, dra_thread_event_type type)
{
assert(pDevice != NULL);
pDevice->nextThreadEventType = type;
dra_semaphore_release(pDevice->threadEventSem);
}
void dra_device__lock(dra_device* pDevice)
{
assert(pDevice != NULL);
dra_mutex_lock(pDevice->mutex);
}
void dra_device__unlock(dra_device* pDevice)
{
assert(pDevice != NULL);
dra_mutex_unlock(pDevice->mutex);
}
dr_bool32 dra_device__is_playing_nolock(dra_device* pDevice)
{
assert(pDevice != NULL);
return pDevice->isPlaying;
}
dr_bool32 dra_device__mix_next_fragment(dra_device* pDevice)
{
assert(pDevice != NULL);
size_t samplesInFragment;
void* pSampleData = dra_backend_device_map_next_fragment(pDevice->pBackendDevice, &samplesInFragment);
if (pSampleData == NULL) {
dra_backend_device_stop(pDevice->pBackendDevice);
return DR_FALSE;
}
size_t framesInFragment = samplesInFragment / pDevice->channels;
size_t framesMixed = dra_mixer_mix_next_frames(pDevice->pMasterMixer, framesInFragment);
memcpy(pSampleData, pDevice->pMasterMixer->pStagingBuffer, (size_t)samplesInFragment * sizeof(float));
if (pDevice->onSamplesProcessed) {
pDevice->onSamplesProcessed(pDevice, framesMixed * pDevice->channels, (const float*)pSampleData, pDevice->pUserDataForOnSamplesProcessed);
}
dra_backend_device_unmap_next_fragment(pDevice->pBackendDevice);
if (framesMixed == 0) {
pDevice->stopOnNextFragment = DR_TRUE;
}
//printf("Mixed next fragment into %p\n", pSampleData);
return DR_TRUE;
}
void dra_device__play(dra_device* pDevice)
{
assert(pDevice != NULL);
dra_device__lock(pDevice);
{
// Don't do anything if the device is already playing.
if (!dra_device__is_playing_nolock(pDevice))
{
assert(pDevice->pBackendDevice->type == dra_device_type_capture || pDevice->playingVoicesCount > 0);
dra_device__post_event(pDevice, dra_thread_event_type_play);
pDevice->isPlaying = DR_TRUE;
pDevice->stopOnNextFragment = DR_FALSE;
}
}
dra_device__unlock(pDevice);
}
void dra_device__stop(dra_device* pDevice)
{
assert(pDevice != NULL);
dra_device__lock(pDevice);
{
// Don't do anything if the device is already stopped.
if (dra_device__is_playing_nolock(pDevice))
{
//assert(pDevice->playingVoicesCount == 0);
dra_backend_device_stop(pDevice->pBackendDevice);
pDevice->isPlaying = DR_FALSE;
}
}
dra_device__unlock(pDevice);
}
void dra_device__voice_playback_count_inc(dra_device* pDevice)
{
assert(pDevice != NULL);
dra_device__lock(pDevice);
{
pDevice->playingVoicesCount += 1;
pDevice->stopOnNextFragment = DR_FALSE;
}
dra_device__unlock(pDevice);
}
void dra_device__voice_playback_count_dec(dra_device* pDevice)
{
dra_device__lock(pDevice);
{
pDevice->playingVoicesCount -= 1;
}
dra_device__unlock(pDevice);
}
// The entry point signature is slightly different depending on whether or not we're using Win32 or POSIX threads.
#ifdef _WIN32
DWORD dra_device__thread_proc(LPVOID pData)
#else
void* dra_device__thread_proc(void* pData)
#endif
{
dra_device* pDevice = (dra_device*)pData;
assert(pDevice != NULL);
// The thread is always open for the life of the device. The loop below will only terminate when a terminate message is received.
for (;;)
{
// Wait for an event...
dra_semaphore_wait(pDevice->threadEventSem);
if (pDevice->nextThreadEventType == dra_thread_event_type_terminate) {
//printf("Terminated!\n");
break;
}
if (pDevice->nextThreadEventType == dra_thread_event_type_play)
{
if (pDevice->pBackendDevice->type == dra_device_type_playback) {
// The backend device needs to start playing, but we first need to ensure it has an initial chunk of data available.
dra_device__mix_next_fragment(pDevice);
}
// Start playing the backend device only after the initial fragment has been mixed, and only if it's a playback device.
dra_backend_device_play(pDevice->pBackendDevice);
// There could be "play" events needing to be posted.
dra_event_queue__post_events(&pDevice->eventQueue);
// Wait for the device to request more data...
while (dra_backend_device_wait(pDevice->pBackendDevice)) {
dra_event_queue__post_events(&pDevice->eventQueue);
if (pDevice->stopOnNextFragment) {
dra_device__stop(pDevice); // <-- Don't break from the loop here. Instead have dra_backend_device_wait() return naturally from the stop notification.
} else {
if (pDevice->pBackendDevice->type == dra_device_type_playback) {
dra_device__mix_next_fragment(pDevice);
} else {
size_t sampleCount;
void* pSampleData = dra_backend_device_map_next_fragment(pDevice->pBackendDevice, &sampleCount);
if (pSampleData != NULL) {
if (pDevice->onSamplesProcessed) {
pDevice->onSamplesProcessed(pDevice, sampleCount, (const float*)pSampleData, pDevice->pUserDataForOnSamplesProcessed);
}
dra_backend_device_unmap_next_fragment(pDevice->pBackendDevice);
}
}
}
}
// There could be some events needing to be posted.
dra_event_queue__post_events(&pDevice->eventQueue);
//printf("Stopped!\n");
// Don't fall through.
continue;
}
}
return 0;
}
dra_result dra_device_init_ex(dra_context* pContext, dra_device_type type, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds, dra_device* pDevice)
{
if (pDevice == NULL) return DRA_RESULT_INVALID_ARGS;
dr_bool32 ownsContext = DR_FALSE;
if (pContext == NULL) {
pContext = (dra_context*)malloc(sizeof(*pContext));
if (pContext == NULL) {
return DRA_RESULT_OUT_OF_MEMORY;
}
dra_result result = dra_context_init(pContext);
if (result != DRA_RESULT_SUCCESS) {
return result;
}
ownsContext = DR_TRUE;
}
if (sampleRate == 0) sampleRate = DR_AUDIO_DEFAULT_SAMPLE_RATE;
if (latencyInMilliseconds == 0) latencyInMilliseconds = DR_AUDIO_DEFAULT_LATENCY;
dra_result result = DRA_RESULT_SUCCESS;
memset(pDevice, 0, sizeof(*pDevice));
pDevice->pContext = pContext;
pDevice->ownsContext = ownsContext;
pDevice->pBackendDevice = dra_backend_device_open(pContext->pBackend, type, deviceID, channels, sampleRate, latencyInMilliseconds);
if (pDevice->pBackendDevice == NULL) {
result = DRA_RESULT_NO_BACKEND_DEVICE;
goto on_error;
}
pDevice->channels = pDevice->pBackendDevice->channels;
pDevice->sampleRate = pDevice->pBackendDevice->sampleRate;
pDevice->mutex = dra_mutex_create();
if (pDevice->mutex == NULL) {
result = DRA_RESULT_UNKNOWN_ERROR; // TODO: Change this to the return value of dra_mutex_create().
goto on_error;
}
pDevice->threadEventSem = dra_semaphore_create(0);
if (pDevice->threadEventSem == NULL) {
result = DRA_RESULT_UNKNOWN_ERROR; // TODO: Change this to the return value of dra_semaphore_create().
goto on_error;
}
result = dra_mixer_create(pDevice, &pDevice->pMasterMixer);
if (result != DRA_RESULT_SUCCESS) {
goto on_error;
}
pDevice->eventQueue.lock = dra_mutex_create();
if (pDevice->eventQueue.lock == NULL) {
result = DRA_RESULT_UNKNOWN_ERROR; // TODO: Change this to the return value of dra_mutex_create().
goto on_error;
}
// Create the thread last to ensure the device is in a valid state as soon as the entry procedure is run.
pDevice->thread = dra_thread_create(dra_device__thread_proc, pDevice);
if (pDevice->thread == NULL) {
result = DRA_RESULT_UNKNOWN_ERROR; // TODO: Change this to the return value of dra_thread_create().
goto on_error;
}
return result;
on_error:
if (pDevice != NULL) {
if (pDevice->pMasterMixer != NULL) dra_mixer_delete(pDevice->pMasterMixer);
if (pDevice->pBackendDevice != NULL) dra_backend_device_close(pDevice->pBackendDevice);
if (pDevice->threadEventSem != NULL) dra_semaphore_delete(pDevice->threadEventSem);
if (pDevice->mutex != NULL) dra_mutex_delete(pDevice->mutex);
if (pDevice->ownsContext) {
dra_context_uninit(pDevice->pContext);
free(pDevice->pContext);
}
}
return result;
}
dra_result dra_device_init(dra_context* pContext, dra_device_type type, dra_device* pDevice)
{
return dra_device_init_ex(pContext, type, 0, 0, DR_AUDIO_DEFAULT_SAMPLE_RATE, DR_AUDIO_DEFAULT_LATENCY, pDevice);
}
void dra_device_uninit(dra_device* pDevice)
{
if (pDevice == NULL || pDevice->pContext == NULL) return;
// Mark the device as closed in order to prevent other threads from doing work after closing.
dra_device__lock(pDevice);
{
pDevice->isClosed = DR_TRUE;
}
dra_device__unlock(pDevice);
// Stop playback before doing anything else.
dra_device__stop(pDevice);
// The background thread needs to be terminated at this point.
dra_device__post_event(pDevice, dra_thread_event_type_terminate);
dra_thread_wait_and_delete(pDevice->thread);
// At this point the device is marked as closed which should prevent voice's and mixers from being created and deleted. We now need
// to delete the master mixer which in turn will delete all of the attached voices and submixers.
if (pDevice->pMasterMixer != NULL) {
dra_mixer_delete(pDevice->pMasterMixer);
}
if (pDevice->pBackendDevice != NULL) {
dra_backend_device_close(pDevice->pBackendDevice);
}
if (pDevice->threadEventSem != NULL) {
dra_semaphore_delete(pDevice->threadEventSem);
}
if (pDevice->mutex != NULL) {
dra_mutex_delete(pDevice->mutex);
}
if (pDevice->eventQueue.pEvents) {
free(pDevice->eventQueue.pEvents);
}
if (pDevice->ownsContext) {
dra_context_uninit(pDevice->pContext);
free(pDevice->pContext);
}
}
dra_result dra_device_create_ex(dra_context* pContext, dra_device_type type, unsigned int deviceID, unsigned int channels, unsigned int sampleRate, unsigned int latencyInMilliseconds, dra_device** ppDevice)
{
if (ppDevice == NULL) return DRA_RESULT_INVALID_ARGS;
*ppDevice = NULL;
dra_device* pDevice = (dra_device*)malloc(sizeof(*pDevice));
if (pDevice == NULL) {
return DRA_RESULT_OUT_OF_MEMORY;
}
dra_result result = dra_device_init_ex(pContext, type, deviceID, channels, sampleRate, latencyInMilliseconds, pDevice);
if (result != DRA_RESULT_SUCCESS) {
free(pDevice);
return result;
}
*ppDevice = pDevice;
return DRA_RESULT_SUCCESS;
}
dra_result dra_device_create(dra_context* pContext, dra_device_type type, dra_device** ppDevice)
{
return dra_device_create_ex(pContext, type, 0, 0, DR_AUDIO_DEFAULT_SAMPLE_RATE, DR_AUDIO_DEFAULT_LATENCY, ppDevice);
}
void dra_device_delete(dra_device* pDevice)
{
if (pDevice == NULL) return;
dra_device_uninit(pDevice);
free(pDevice);
}
dra_result dra_device_start(dra_device* pDevice)
{
if (pDevice == NULL || pDevice->pBackendDevice->type == dra_device_type_playback) return DRA_RESULT_INVALID_ARGS;
dra_device__play(pDevice);
return DRA_RESULT_SUCCESS;
}
dra_result dra_device_stop(dra_device* pDevice)
{
if (pDevice == NULL || pDevice->pBackendDevice->type == dra_device_type_playback) return DRA_RESULT_INVALID_ARGS;
dra_device__stop(pDevice);
return DRA_RESULT_SUCCESS;
}
void dra_device_set_samples_processed_callback(dra_device* pDevice, dra_samples_processed_proc proc, void* pUserData)
{
if (pDevice == NULL) return;
pDevice->onSamplesProcessed = proc;
pDevice->pUserDataForOnSamplesProcessed = pUserData;
}
dra_result dra_mixer_create(dra_device* pDevice, dra_mixer** ppMixer)
{
if (ppMixer == NULL) return DRA_RESULT_INVALID_ARGS;
*ppMixer = NULL;
if (pDevice == NULL) return DRA_RESULT_INVALID_ARGS;
// There needs to be two blocks of memory at the end of the mixer - one for the staging buffer and another for the buffer that
// will store the float32 samples of the voice currently being mixed.
size_t extraDataSize = (size_t)pDevice->pBackendDevice->samplesPerFragment * sizeof(float) * 2;
dra_mixer* pMixer = (dra_mixer*)calloc(1, sizeof(*pMixer) + extraDataSize);
if (pMixer == NULL) {
return DRA_RESULT_OUT_OF_MEMORY;
}
pMixer->pDevice = pDevice;
pMixer->linearVolume = 1;
pMixer->pStagingBuffer = pMixer->pData;
pMixer->pNextSamplesToMix = pMixer->pStagingBuffer + pDevice->pBackendDevice->samplesPerFragment;
// Attach the mixer to the master mixer by default. If the master mixer is null it means we're creating the master mixer itself.
if (pDevice->pMasterMixer != NULL) {
dra_mixer_attach_submixer(pDevice->pMasterMixer, pMixer);
}
*ppMixer = pMixer;
return DRA_RESULT_SUCCESS;
}
void dra_mixer_delete(dra_mixer* pMixer)
{
if (pMixer == NULL) return;
dra_mixer_detach_all_submixers(pMixer);
dra_mixer_detach_all_voices(pMixer);
if (pMixer->pParentMixer != NULL) {
dra_mixer_detach_submixer(pMixer->pParentMixer, pMixer);
}
free(pMixer);
}
void dra_mixer_attach_submixer(dra_mixer* pMixer, dra_mixer* pSubmixer)
{
if (pMixer == NULL || pSubmixer == NULL) {
return;
}
if (pSubmixer->pParentMixer != NULL) {
dra_mixer_detach_submixer(pSubmixer->pParentMixer, pSubmixer);
}
pSubmixer->pParentMixer = pMixer;
if (pMixer->pFirstChildMixer == NULL) {
pMixer->pFirstChildMixer = pSubmixer;
pMixer->pLastChildMixer = pSubmixer;
return;
}
assert(pMixer->pLastChildMixer != NULL);
pMixer->pLastChildMixer->pNextSiblingMixer = pSubmixer;
pSubmixer->pPrevSiblingMixer = pMixer->pLastChildMixer;
pSubmixer->pNextSiblingMixer = NULL;
pMixer->pLastChildMixer = pSubmixer;
}
void dra_mixer_detach_submixer(dra_mixer* pMixer, dra_mixer* pSubmixer)
{
if (pMixer == NULL || pSubmixer == NULL) {
return;
}
if (pSubmixer->pParentMixer != pMixer) {
return; // Doesn't have the same parent.
}
// Detach from parent.
if (pSubmixer->pParentMixer->pFirstChildMixer == pSubmixer) {
pSubmixer->pParentMixer->pFirstChildMixer = pSubmixer->pNextSiblingMixer;
}
if (pSubmixer->pParentMixer->pLastChildMixer == pSubmixer) {
pSubmixer->pParentMixer->pLastChildMixer = pSubmixer->pPrevSiblingMixer;
}
pSubmixer->pParentMixer = NULL;
// Detach from siblings.
if (pSubmixer->pPrevSiblingMixer) {
pSubmixer->pPrevSiblingMixer->pNextSiblingMixer = pSubmixer->pNextSiblingMixer;
}
if (pSubmixer->pNextSiblingMixer) {
pSubmixer->pNextSiblingMixer->pPrevSiblingMixer = pSubmixer->pPrevSiblingMixer;
}
pSubmixer->pNextSiblingMixer = NULL;
pSubmixer->pPrevSiblingMixer = NULL;
}
void dra_mixer_detach_all_submixers(dra_mixer* pMixer)
{
if (pMixer == NULL) {
return;
}
while (pMixer->pFirstChildMixer != NULL) {
dra_mixer_detach_submixer(pMixer, pMixer->pFirstChildMixer);
}
}
void dra_mixer_attach_voice(dra_mixer* pMixer, dra_voice* pVoice)
{
if (pMixer == NULL || pVoice == NULL) {
return;
}
if (pVoice->pMixer != NULL) {
dra_mixer_detach_voice(pVoice->pMixer, pVoice);
}
pVoice->pMixer = pMixer;
if (pMixer->pFirstVoice == NULL) {
pMixer->pFirstVoice = pVoice;
pMixer->pLastVoice = pVoice;
return;
}
// Attach the voice to the end of the list.
pVoice->pPrevVoice = pMixer->pLastVoice;
pVoice->pNextVoice = NULL;
pMixer->pLastVoice->pNextVoice = pVoice;
pMixer->pLastVoice = pVoice;
}
void dra_mixer_detach_voice(dra_mixer* pMixer, dra_voice* pVoice)
{
if (pMixer == NULL || pVoice == NULL) {
return;
}
// Detach from mixer.
if (pMixer->pFirstVoice == pVoice) {
pMixer->pFirstVoice = pMixer->pFirstVoice->pNextVoice;
}
if (pMixer->pLastVoice == pVoice) {
pMixer->pLastVoice = pMixer->pLastVoice->pPrevVoice;
}
pVoice->pMixer = NULL;
// Remove from list.
if (pVoice->pNextVoice) {
pVoice->pNextVoice->pPrevVoice = pVoice->pPrevVoice;
}
if (pVoice->pPrevVoice) {
pVoice->pPrevVoice->pNextVoice = pVoice->pNextVoice;
}
pVoice->pNextVoice = NULL;
pVoice->pPrevVoice = NULL;
}
void dra_mixer_detach_all_voices(dra_mixer* pMixer)
{
if (pMixer == NULL) {
return;
}
while (pMixer->pFirstVoice) {
dra_mixer_detach_voice(pMixer, pMixer->pFirstVoice);
}
}
void dra_mixer_set_volume(dra_mixer* pMixer, float linearVolume)
{
if (pMixer == NULL) {
return;
}
pMixer->linearVolume = linearVolume;
}
float dra_mixer_get_volume(dra_mixer* pMixer)
{
if (pMixer == NULL) {
return 0;
}
return pMixer->linearVolume;
}
size_t dra_mixer_mix_next_frames(dra_mixer* pMixer, size_t frameCount)
{
if (pMixer == NULL) {
return 0;
}
if (pMixer->pFirstVoice == NULL && pMixer->pFirstChildMixer == NULL) {
return 0;
}
if (dra_mixer_is_paused(pMixer)) {
return 0;
}
size_t framesMixed = 0;
// Mixing works by simply adding together the sample data of each voice and submixer. We just start at 0 and then
// just accumulate each one.
memset(pMixer->pStagingBuffer, 0, frameCount * pMixer->pDevice->channels * sizeof(float));
// Voices first. Doesn't really matter if we do voices or submixers first.
for (dra_voice* pVoice = pMixer->pFirstVoice; pVoice != NULL; pVoice = pVoice->pNextVoice)
{
if (pVoice->isPlaying) {
size_t framesJustRead = dra_voice__next_frames(pVoice, frameCount, pMixer->pNextSamplesToMix);
for (size_t i = 0; i < framesJustRead * pMixer->pDevice->channels; ++i) {
pMixer->pStagingBuffer[i] += (pMixer->pNextSamplesToMix[i] * pVoice->linearVolume);
}
// Has the end of the voice's buffer been reached?
if (framesJustRead < frameCount)
{
// We'll get here if the end of the voice's buffer has been reached. The voice needs to be forcefully stopped to
// ensure the device is aware of it and is able to put itself into a dormant state if necessary. Also note that
// the playback position is moved back to start. The rationale for this is that it's a little bit more useful than
// just leaving the playback position sitting on the end. Also it allows an application to restart playback with
// a single call to dra_voice_play() without having to explicitly set the playback position.
pVoice->currentReadPos = 0;
dra_voice_stop(pVoice);
}
if (framesMixed < framesJustRead) {
framesMixed = framesJustRead;
}
}
}
// Submixers.
for (dra_mixer* pSubmixer = pMixer->pFirstChildMixer; pSubmixer != NULL; pSubmixer = pSubmixer->pNextSiblingMixer)
{
size_t framesJustMixed = dra_mixer_mix_next_frames(pSubmixer, frameCount);
for (size_t i = 0; i < framesJustMixed * pMixer->pDevice->channels; ++i) {
pMixer->pStagingBuffer[i] += pSubmixer->pStagingBuffer[i];
}
if (framesMixed < framesJustMixed) {
framesMixed = framesJustMixed;
}
}
// At this point the mixer's effects and volume need to be applied to each sample.
size_t samplesMixed = framesMixed * pMixer->pDevice->channels;
for (size_t i = 0; i < samplesMixed; ++i) {
pMixer->pStagingBuffer[i] *= pMixer->linearVolume;
}
// Finally we need to ensure every samples is clamped to -1 to 1. There are two easy ways to do this: clamp or normalize. For now I'm just
// clamping to keep it simple, but it might be valuable to make this configurable.
for (size_t i = 0; i < framesMixed * pMixer->pDevice->channels; ++i)
{
// TODO: Investigate using SSE here (MINPS/MAXPS)
// TODO: Investigate if the backends clamp the samples themselves, thus making this redundant.
if (pMixer->pStagingBuffer[i] < -1) {
pMixer->pStagingBuffer[i] = -1;
} else if (pMixer->pStagingBuffer[i] > 1) {
pMixer->pStagingBuffer[i] = 1;
}
}
return framesMixed;
}
size_t dra_mixer_count_attached_voices(dra_mixer* pMixer)
{
if (pMixer == NULL) {
return 0;
}
size_t count = 0;
for (dra_voice* pVoice = pMixer->pFirstVoice; pVoice != NULL; pVoice = pVoice->pNextVoice) {
count += 1;
}
return count;
}
size_t dra_mixer_count_attached_voices_recursive(dra_mixer* pMixer)
{
if (pMixer == NULL) {
return 0;
}
size_t count = dra_mixer_count_attached_voices(pMixer);
// Children.
for (dra_mixer* pChildMixer = pMixer->pFirstChildMixer; pChildMixer != NULL; pChildMixer = pChildMixer->pNextSiblingMixer) {
count += dra_mixer_count_attached_voices_recursive(pChildMixer);
}
return count;
}
size_t dra_mixer_gather_attached_voices(dra_mixer* pMixer, dra_voice** ppVoicesOut)
{
if (pMixer == NULL) {
return 0;
}
if (ppVoicesOut == NULL) {
return dra_mixer_count_attached_voices(pMixer);
}
size_t count = 0;
for (dra_voice* pVoice = pMixer->pFirstVoice; pVoice != NULL; pVoice = pVoice->pNextVoice) {
ppVoicesOut[count] = pVoice;
count += 1;
}
return count;
}
size_t dra_mixer_gather_attached_voices_recursive(dra_mixer* pMixer, dra_voice** ppVoicesOut)
{
if (pMixer == NULL) {
return 0;
}
if (ppVoicesOut == NULL) {
return dra_mixer_count_attached_voices_recursive(pMixer);
}
size_t count = dra_mixer_gather_attached_voices(pMixer, ppVoicesOut);
// Children.
for (dra_mixer* pChildMixer = pMixer->pFirstChildMixer; pChildMixer != NULL; pChildMixer = pChildMixer->pNextSiblingMixer) {
count += dra_mixer_gather_attached_voices_recursive(pChildMixer, ppVoicesOut + count);
}
return count;
}
void dra_mixer_pause(dra_mixer* pMixer)
{
if (pMixer == NULL) return;
pMixer->flags |= DRA_MIXER_FLAG_PAUSED;
}
void dra_mixer_resume(dra_mixer* pMixer)
{
if (pMixer == NULL) return;
pMixer->flags &= ~DRA_MIXER_FLAG_PAUSED;
// When the mixer was paused it may have resulted in no audio being played which means dr_audio will have stopped the device
// to save CPU usage. We need to make sure we wake up the device.
dra_device__play(pMixer->pDevice);
}
dr_bool32 dra_mixer_is_paused(dra_mixer* pMixer)
{
if (pMixer == NULL) return DR_FALSE;
return (pMixer->flags & DRA_MIXER_FLAG_PAUSED) != 0;
}
dra_result dra_voice_create(dra_device* pDevice, dra_format format, unsigned int channels, unsigned int sampleRate, size_t sizeInBytes, const void* pInitialData, dra_voice** ppVoice)
{
if (ppVoice == NULL) return DRA_RESULT_INVALID_ARGS;
*ppVoice = NULL;
if (pDevice == NULL || sizeInBytes == 0) return DRA_RESULT_INVALID_ARGS;
// The number of bytes must be a multiple of the size of a frame.
size_t bytesPerSample = dra_get_bytes_per_sample_by_format(format);
if ((sizeInBytes % (bytesPerSample * channels)) != 0) {
return DRA_RESULT_INVALID_ARGS;
}
dra_voice* pVoice = (dra_voice*)calloc(1, sizeof(*pVoice) + sizeInBytes);
if (pVoice == NULL) {
return DRA_RESULT_OUT_OF_MEMORY;
}
pVoice->pDevice = pDevice;
pVoice->pMixer = NULL;
pVoice->format = format;
pVoice->channels = channels;
pVoice->sampleRate = sampleRate;
pVoice->linearVolume = 1;
pVoice->isPlaying = DR_FALSE;
pVoice->isLooping = DR_FALSE;
pVoice->frameCount = sizeInBytes / (bytesPerSample * channels);
pVoice->currentReadPos = 0;
pVoice->sizeInBytes = sizeInBytes;
pVoice->pNextVoice = NULL;
pVoice->pPrevVoice = NULL;
if (pInitialData != NULL) {
memcpy(pVoice->pData, pInitialData, sizeInBytes);
} else {
//memset(pVoice->pData, 0, sizeInBytes); // <-- This is already zeroed by the calloc() above, but leaving this comment here for emphasis.
}
// Sample rate conversion.
if (sampleRate == pDevice->sampleRate) {
pVoice->src.algorithm = dra_src_algorithm_none;
} else {
pVoice->src.algorithm = dra_src_algorithm_linear;
}
// Attach the voice to the master mixer by default.
if (pDevice->pMasterMixer != NULL) {
dra_mixer_attach_voice(pDevice->pMasterMixer, pVoice);
}
*ppVoice = pVoice;
return DRA_RESULT_SUCCESS;
}
dra_result dra_voice_create_compatible(dra_device* pDevice, size_t sizeInBytes, const void* pInitialData, dra_voice** ppVoice)
{
return dra_voice_create(pDevice, dra_format_f32, pDevice->channels, pDevice->sampleRate, sizeInBytes, pInitialData, ppVoice);
}
void dra_voice_delete(dra_voice* pVoice)
{
if (pVoice == NULL) return;
// The voice needs to be stopped...
dra_voice_stop(pVoice);
// ... and all pending events need to be cancelled to ensure the application isn't notified of an event of a deleted voice.
dra_event_queue__cancel_events_of_voice(&pVoice->pDevice->eventQueue, pVoice);
if (pVoice->pMixer != NULL) {
dra_mixer_detach_voice(pVoice->pMixer, pVoice);
}
free(pVoice);
}
void dra_voice_play(dra_voice* pVoice, dr_bool32 loop)
{
if (pVoice == NULL) {
return;
}
if (!dra_voice_is_playing(pVoice)) {
dra_device__voice_playback_count_inc(pVoice->pDevice);
} else {
if (dra_voice_is_looping(pVoice) == loop) {
return; // Nothing has changed - don't need to do anything.
}
}
pVoice->isPlaying = DR_TRUE;
pVoice->isLooping = loop;
dra_event_queue__schedule_event(&pVoice->pDevice->eventQueue, &pVoice->playEvent);
// When playing a voice we need to ensure the backend device is playing.
dra_device__play(pVoice->pDevice);
}
void dra_voice_stop(dra_voice* pVoice)
{
if (pVoice == NULL) {
return;
}
if (!dra_voice_is_playing(pVoice)) {
return; // The voice is already stopped.
}
dra_device__voice_playback_count_dec(pVoice->pDevice);
pVoice->isPlaying = DR_FALSE;
pVoice->isLooping = DR_FALSE;
dra_event_queue__schedule_event(&pVoice->pDevice->eventQueue, &pVoice->stopEvent);
}
dr_bool32 dra_voice_is_playing(dra_voice* pVoice)
{
if (pVoice == NULL) {
return DR_FALSE;
}
return pVoice->isPlaying;
}
dr_bool32 dra_voice_is_looping(dra_voice* pVoice)
{
if (pVoice == NULL) {
return DR_FALSE;
}
return pVoice->isLooping;
}
void dra_voice_set_volume(dra_voice* pVoice, float linearVolume)
{
if (pVoice == NULL) {
return;
}
pVoice->linearVolume = linearVolume;
}
float dra_voice_get_volume(dra_voice* pVoice)
{
if (pVoice == NULL) {
return 0;
}
return pVoice->linearVolume;
}
void dra_f32_to_f32(float* pOut, const float* pIn, size_t sampleCount)
{
memcpy(pOut, pIn, sampleCount * sizeof(float));
}
void dra_s32_to_f32(float* pOut, const dr_int32* pIn, size_t sampleCount)
{
// TODO: Try SSE-ifying this.
for (size_t i = 0; i < sampleCount; ++i) {
pOut[i] = pIn[i] / 2147483648.0f;
}
}
void dra_s24_to_f32(float* pOut, const dr_uint8* pIn, size_t sampleCount)
{
// TODO: Try SSE-ifying this.
for (size_t i = 0; i < sampleCount; ++i) {
dr_uint8 s0 = pIn[i*3 + 0];
dr_uint8 s1 = pIn[i*3 + 1];
dr_uint8 s2 = pIn[i*3 + 2];
dr_int32 sample32 = (dr_int32)((s0 << 8) | (s1 << 16) | (s2 << 24));
pOut[i] = sample32 / 2147483648.0f;
}
}
void dra_s16_to_f32(float* pOut, const dr_int16* pIn, size_t sampleCount)
{
// TODO: Try SSE-ifying this.
for (size_t i = 0; i < sampleCount; ++i) {
pOut[i] = pIn[i] / 32768.0f;
}
}
void dra_u8_to_f32(float* pOut, const dr_uint8* pIn, size_t sampleCount)
{
// TODO: Try SSE-ifying this.
for (size_t i = 0; i < sampleCount; ++i) {
pOut[i] = (pIn[i] / 127.5f) - 1;
}
}
// Generic sample format conversion function. To add basic, unoptimized support for a new format, just add it to this function.
// Format-specific optimizations need to be implemented specifically for each format, at a higher level.
void dra_to_f32(float* pOut, const void* pIn, size_t sampleCount, dra_format format)
{
switch (format)
{
case dra_format_f32:
{
dra_f32_to_f32(pOut, (float*)pIn, sampleCount);
} break;
case dra_format_s32:
{
dra_s32_to_f32(pOut, (dr_int32*)pIn, sampleCount);
} break;
case dra_format_s24:
{
dra_s24_to_f32(pOut, (dr_uint8*)pIn, sampleCount);
} break;
case dra_format_s16:
{
dra_s16_to_f32(pOut, (dr_int16*)pIn, sampleCount);
} break;
case dra_format_u8:
{
dra_u8_to_f32(pOut, (dr_uint8*)pIn, sampleCount);
} break;
default: break; // Unknown or unsupported format.
}
}
// Notes on channel shuffling.
//
// Channels are shuffled frame-by-frame by first normalizing everything to floats. Then, a shuffling function is called to
// shuffle the channels in a particular way depending on the destination and source channel assignments.
void dra_shuffle_channels__generic_inc(float* pOut, const float* pIn, unsigned int channelsOut, unsigned int channelsIn)
{
// This is the generic function for taking a frame with a smaller number of channels and expanding it to a frame with
// a greater number of channels. This just copies the first channelsIn samples to the output and silences the remaing
// channels.
assert(channelsOut > channelsIn);
for (unsigned int i = 0; i < channelsIn; ++i) {
pOut[i] = pIn[i];
}
// Silence the left over.
for (unsigned int i = channelsIn; i < channelsOut; ++i) {
pOut[i] = 0;
}
}
void dra_shuffle_channels__generic_dec(float* pOut, const float* pIn, unsigned int channelsOut, unsigned int channelsIn)
{
// This is the opposite of dra_shuffle_channels__generic_inc() - it decreases the number of channels in the input stream
// by simply stripping the excess channels.
assert(channelsOut < channelsIn);
(void)channelsIn;
// Just copy the first channelsOut.
for (unsigned int i = 0; i < channelsOut; ++i) {
pOut[i] = pIn[i];
}
}
void dra_shuffle_channels(float* pOut, const float* pIn, unsigned int channelsOut, unsigned int channelsIn)
{
assert(channelsOut != 0);
assert(channelsIn != 0);
if (channelsOut == channelsIn) {
for (unsigned int i = 0; i < channelsOut; ++i) {
pOut[i] = pIn[i];
}
} else {
switch (channelsIn)
{
case 1:
{
// Mono input. This is a simple case - just copy the value of the mono channel to every output channel.
for (unsigned int i = 0; i < channelsOut; ++i) {
pOut[i] = pIn[0];
}
} break;
case 2:
{
// Stereo input.
if (channelsOut == 1)
{
// For mono output, just average.
pOut[0] = (pIn[0] + pIn[1]) * 0.5f;
}
else
{
// TODO: Do a specialized implementation for all major formats, in particluar 5.1.
dra_shuffle_channels__generic_inc(pOut, pIn, channelsOut, channelsIn);
}
} break;
default:
{
if (channelsOut == 1)
{
// For mono output, just average each sample.
float total = 0;
for (unsigned int i = 0; i < channelsIn; ++i) {
total += pIn[i];
}
pOut[0] = total / channelsIn;
}
else
{
if (channelsOut > channelsIn) {
dra_shuffle_channels__generic_inc(pOut, pIn, channelsOut, channelsIn);
} else {
dra_shuffle_channels__generic_dec(pOut, pIn, channelsOut, channelsIn);
}
}
} break;
}
}
}
float dra_voice__get_sample_rate_factor(dra_voice* pVoice)
{
if (pVoice == NULL) {
return 1;
}
return pVoice->pDevice->sampleRate / (float)pVoice->sampleRate;
}
void dra_voice__unsignal_playback_events(dra_voice* pVoice)
{
// This function will be called when the voice has looped back to the start. In this case the playback notification events need
// to be marked as unsignaled so that they're able to be fired again.
for (size_t i = 0; i < pVoice->playbackEventCount; ++i) {
pVoice->playbackEvents[i].hasBeenSignaled = DR_FALSE;
}
}
float* dra_voice__next_frame(dra_voice* pVoice)
{
if (pVoice == NULL) {
return NULL;
}
if (pVoice->format == dra_format_f32 && pVoice->sampleRate == pVoice->pDevice->sampleRate && pVoice->channels == pVoice->pDevice->channels)
{
// Fast path.
if (!pVoice->isLooping && pVoice->currentReadPos == pVoice->frameCount) {
return NULL; // At the end of a non-looping voice.
}
float* pOut = (float*)pVoice->pData + (pVoice->currentReadPos * pVoice->channels);
pVoice->currentReadPos += 1;
if (pVoice->currentReadPos == pVoice->frameCount && pVoice->isLooping) {
pVoice->currentReadPos = 0;
dra_voice__unsignal_playback_events(pVoice);
}
return pOut;
}
else
{
size_t bytesPerSample = dra_get_bytes_per_sample_by_format(pVoice->format);
if (pVoice->sampleRate == pVoice->pDevice->sampleRate)
{
// Same sample rate. This path isn't ideal, but it's not too bad since there is no need for sample rate conversion.
if (!pVoice->isLooping && pVoice->currentReadPos == pVoice->frameCount) {
return NULL; // At the end of a non-looping voice.
}
float* pOut = pVoice->convertedFrame;
unsigned int channelsIn = pVoice->channels;
unsigned int channelsOut = pVoice->pDevice->channels;
float tempFrame[DR_AUDIO_MAX_CHANNEL_COUNT];
dr_uint64 sampleOffset = pVoice->currentReadPos * channelsIn;
// The conversion is done differently depending on the format of the voice.
if (pVoice->format == dra_format_f32) {
dra_shuffle_channels(pOut, (float*)pVoice->pData + sampleOffset, channelsOut, channelsIn);
} else {
sampleOffset = pVoice->currentReadPos * (channelsIn * bytesPerSample);
dra_to_f32(tempFrame, (dr_uint8*)pVoice->pData + sampleOffset, channelsIn, pVoice->format);
dra_shuffle_channels(pOut, tempFrame, channelsOut, channelsIn);
}
pVoice->currentReadPos += 1;
if (pVoice->currentReadPos == pVoice->frameCount && pVoice->isLooping) {
pVoice->currentReadPos = 0;
dra_voice__unsignal_playback_events(pVoice);
}
return pOut;
}
else
{
// Different sample rate. This is the truly slow path.
unsigned int sampleRateIn = pVoice->sampleRate;
unsigned int sampleRateOut = pVoice->pDevice->sampleRate;
unsigned int channelsIn = pVoice->channels;
unsigned int channelsOut = pVoice->pDevice->channels;
float factor = (float)sampleRateOut / (float)sampleRateIn;
float invfactor = 1 / factor;
if (!pVoice->isLooping && pVoice->currentReadPos >= (pVoice->frameCount * factor)) {
return NULL; // At the end of a non-looping voice.
}
float* pOut = pVoice->convertedFrame;
if (pVoice->src.algorithm == dra_src_algorithm_linear) {
// Linear filtering.
float timeIn = pVoice->currentReadPos * invfactor;
dr_uint64 prevFrameIndexIn = (dr_uint64)(timeIn);
dr_uint64 nextFrameIndexIn = prevFrameIndexIn + 1;
if (nextFrameIndexIn >= pVoice->frameCount) {
nextFrameIndexIn = pVoice->frameCount-1;
}
if (prevFrameIndexIn != pVoice->src.data.linear.prevFrameIndex)
{
dr_uint64 sampleOffset = prevFrameIndexIn * (channelsIn * bytesPerSample);
dra_to_f32(pVoice->src.data.linear.prevFrame, (dr_uint8*)pVoice->pData + sampleOffset, channelsIn, pVoice->format);
sampleOffset = nextFrameIndexIn * (channelsIn * bytesPerSample);
dra_to_f32(pVoice->src.data.linear.nextFrame, (dr_uint8*)pVoice->pData + sampleOffset, channelsIn, pVoice->format);
pVoice->src.data.linear.prevFrameIndex = prevFrameIndexIn;
}
float alpha = timeIn - prevFrameIndexIn;
float frame[DR_AUDIO_MAX_CHANNEL_COUNT];
for (unsigned int i = 0; i < pVoice->channels; ++i) {
frame[i] = dra_mixf(pVoice->src.data.linear.prevFrame[i], pVoice->src.data.linear.nextFrame[i], alpha);
}
dra_shuffle_channels(pOut, frame, channelsOut, channelsIn);
}
pVoice->currentReadPos += 1;
if (pVoice->currentReadPos >= (pVoice->frameCount * factor) && pVoice->isLooping) {
pVoice->currentReadPos = 0;
dra_voice__unsignal_playback_events(pVoice);
}
return pOut;
}
}
}
size_t dra_voice__next_frames(dra_voice* pVoice, size_t frameCount, float* pSamplesOut)
{
// TODO: Check for the fast path and do a bulk copy rather than frame-by-frame. Don't forget playback event handling.
size_t framesRead = 0;
dr_uint64 prevReadPosLocal = pVoice->currentReadPos * pVoice->channels;
float* pNextFrame = NULL;
while ((framesRead < frameCount) && (pNextFrame = dra_voice__next_frame(pVoice)) != NULL) {
memcpy(pSamplesOut, pNextFrame, pVoice->pDevice->channels * sizeof(float));
pSamplesOut += pVoice->pDevice->channels;
framesRead += 1;
}
float sampleRateFactor = dra_voice__get_sample_rate_factor(pVoice);
dr_uint64 totalSampleCount = (dr_uint64)((pVoice->frameCount * pVoice->channels) * sampleRateFactor);
// Now we need to check if we've got past any notification events and post events for them if so.
dr_uint64 currentReadPosLocal = (prevReadPosLocal + (framesRead * pVoice->channels)) % totalSampleCount;
for (size_t i = 0; i < pVoice->playbackEventCount; ++i) {
dra__event* pEvent = &pVoice->playbackEvents[i];
if (!pEvent->hasBeenSignaled && pEvent->sampleIndex*sampleRateFactor <= currentReadPosLocal) {
dra_event_queue__schedule_event(&pVoice->pDevice->eventQueue, pEvent); // <-- TODO: Check that this really needs to be scheduled. Can probably call it directly and avoid a mutex lock/unlock.
pEvent->hasBeenSignaled = DR_TRUE;
}
}
return framesRead;
}
void dra_voice_set_on_stop(dra_voice* pVoice, dra_event_proc proc, void* pUserData)
{
if (pVoice == NULL) {
return;
}
pVoice->stopEvent.id = DR_AUDIO_EVENT_ID_STOP;
pVoice->stopEvent.pUserData = pUserData;
pVoice->stopEvent.sampleIndex = 0;
pVoice->stopEvent.proc = proc;
pVoice->stopEvent.pVoice = pVoice;
}
void dra_voice_set_on_play(dra_voice* pVoice, dra_event_proc proc, void* pUserData)
{
if (pVoice == NULL) {
return;
}
pVoice->playEvent.id = DR_AUDIO_EVENT_ID_PLAY;
pVoice->playEvent.pUserData = pUserData;
pVoice->playEvent.sampleIndex = 0;
pVoice->playEvent.proc = proc;
pVoice->playEvent.pVoice = pVoice;
}
dr_bool32 dra_voice_add_playback_event(dra_voice* pVoice, dr_uint64 sampleIndex, dr_uint64 eventID, dra_event_proc proc, void* pUserData)
{
if (pVoice == NULL) {
return DR_FALSE;
}
if (pVoice->playbackEventCount >= DR_AUDIO_MAX_EVENT_COUNT) {
return DR_FALSE;
}
pVoice->playbackEvents[pVoice->playbackEventCount].id = eventID;
pVoice->playbackEvents[pVoice->playbackEventCount].pUserData = pUserData;
pVoice->playbackEvents[pVoice->playbackEventCount].sampleIndex = sampleIndex;
pVoice->playbackEvents[pVoice->playbackEventCount].proc = proc;
pVoice->playbackEvents[pVoice->playbackEventCount].pVoice = pVoice;
pVoice->playbackEventCount += 1;
return DR_TRUE;
}
void dra_voice_remove_playback_event(dra_voice* pVoice, dr_uint64 eventID)
{
if (pVoice == NULL) {
return;
}
for (size_t i = 0; i < pVoice->playbackEventCount; /* DO NOTHING */) {
if (pVoice->playbackEvents[i].id == eventID) {
memmove(&pVoice->playbackEvents[i], &pVoice->playbackEvents[i + 1], (pVoice->playbackEventCount - (i+1)) * sizeof(dra__event));
pVoice->playbackEventCount -= 1;
} else {
i += 1;
}
}
}
dr_uint64 dra_voice_get_playback_position(dra_voice* pVoice)
{
if (pVoice == NULL) {
return 0;
}
return (dr_uint64)((pVoice->currentReadPos * pVoice->channels) / dra_voice__get_sample_rate_factor(pVoice));
}
void dra_voice_set_playback_position(dra_voice* pVoice, dr_uint64 sampleIndex)
{
if (pVoice == NULL) {
return;
}
// When setting the playback position it's important to consider sample-rate conversion. Sample rate conversion will often depend on
// previous and next frames in order to calculate the next frame. Therefore, depending on the type of SRC we're using, we'll need to
// seek a few frames earlier and then re-fill the delay-line buffer used for a particular SRC algorithm.
dr_uint64 localFramePos = sampleIndex / pVoice->channels;
pVoice->currentReadPos = (dr_uint64)(localFramePos * dra_voice__get_sample_rate_factor(pVoice));
if (pVoice->sampleRate != pVoice->pDevice->sampleRate) {
if (pVoice->src.algorithm == dra_src_algorithm_linear) {
// Linear filtering just requires the previous frame. However, this is handled at mixing time for linear SRC so all we need to
// do is ensure the mixing function is aware that the previous frame need to be re-read. This is done by simply resetting the
// variable the mixer uses to determine whether or not the previous frame needs to be re-read.
pVoice->src.data.linear.prevFrameIndex = 0;
}
}
// TODO: Normalize the hasBeenSignaled properties of events.
}
void* dra_voice_get_buffer_ptr_by_sample(dra_voice* pVoice, dr_uint64 sample)
{
if (pVoice == NULL) {
return NULL;
}
dr_uint64 totalSampleCount = pVoice->frameCount * pVoice->channels;
if (sample > totalSampleCount) {
return NULL;
}
return pVoice->pData + (sample * dra_get_bytes_per_sample_by_format(pVoice->format));
}
void dra_voice_write_silence(dra_voice* pVoice, dr_uint64 sampleOffset, dr_uint64 sampleCount)
{
void* pData = dra_voice_get_buffer_ptr_by_sample(pVoice, sampleOffset);
if (pData == NULL) {
return;
}
dr_uint64 totalSamplesRemaining = (pVoice->frameCount * pVoice->channels) - sampleOffset;
if (sampleCount > totalSamplesRemaining) {
sampleCount = totalSamplesRemaining;
}
memset(pData, 0, (size_t)(sampleCount * dra_get_bytes_per_sample_by_format(pVoice->format)));
}
//// Other APIs ////
void dra_free(void* p)
{
free(p);
}
unsigned int dra_get_bits_per_sample_by_format(dra_format format)
{
unsigned int lookup[] = {
8, // dra_format_u8
16, // dra_format_s16
24, // dra_format_s24
32, // dra_format_s32
32 // dra_format_f32
};
return lookup[format];
}
unsigned int dra_get_bytes_per_sample_by_format(dra_format format)
{
return dra_get_bits_per_sample_by_format(format) / 8;
}
//// STDIO ////
#ifndef DR_AUDIO_NO_STDIO
static FILE* dra__fopen(const char* filePath)
{
FILE* pFile;
#ifdef _MSC_VER
if (fopen_s(&pFile, filePath, "rb") != 0) {
return NULL;
}
#else
pFile = fopen(filePath, "rb");
if (pFile == NULL) {
return NULL;
}
#endif
return (FILE*)pFile;
}
#endif //DR_AUDIO_NO_STDIO
//// Decoder APIs ////
#ifdef DR_AUDIO_HAS_WAV
size_t dra_decoder_on_read__wav(void* pUserData, void* pDataOut, size_t bytesToRead)
{
dra_decoder* pDecoder = (dra_decoder*)pUserData;
assert(pDecoder != NULL);
assert(pDecoder->onRead != NULL);
return pDecoder->onRead(pDecoder->pUserData, pDataOut, bytesToRead);
}
drwav_bool32 dra_decoder_on_seek__wav(void* pUserData, int offset, drwav_seek_origin origin)
{
dra_decoder* pDecoder = (dra_decoder*)pUserData;
assert(pDecoder != NULL);
assert(pDecoder->onSeek != NULL);
return pDecoder->onSeek(pDecoder->pUserData, offset, (origin == drwav_seek_origin_start) ? dra_seek_origin_start : dra_seek_origin_current);
}
void dra_decoder_on_delete__wav(void* pBackendDecoder)
{
drwav* pWav = (drwav*)pBackendDecoder;
assert(pWav != NULL);
drwav_close(pWav);
}
dr_uint64 dra_decoder_on_read_samples__wav(void* pBackendDecoder, dr_uint64 samplesToRead, float* pSamplesOut)
{
drwav* pWav = (drwav*)pBackendDecoder;
assert(pWav != NULL);
return drwav_read_f32(pWav, samplesToRead, pSamplesOut);
}
dr_bool32 dra_decoder_on_seek_samples__wav(void* pBackendDecoder, dr_uint64 sample)
{
drwav* pWav = (drwav*)pBackendDecoder;
assert(pWav != NULL);
return drwav_seek_to_sample(pWav, sample);
}
void dra_decoder_init__wav(dra_decoder* pDecoder, drwav* pWav)
{
assert(pDecoder != NULL);
assert(pWav != NULL);
pDecoder->channels = pWav->channels;
pDecoder->sampleRate = pWav->sampleRate;
pDecoder->totalSampleCount = pWav->totalSampleCount;
pDecoder->pBackendDecoder = pWav;
pDecoder->onDelete = dra_decoder_on_delete__wav;
pDecoder->onReadSamples = dra_decoder_on_read_samples__wav;
pDecoder->onSeekSamples = dra_decoder_on_seek_samples__wav;
}
dr_bool32 dra_decoder_open__wav(dra_decoder* pDecoder)
{
drwav* pWav = drwav_open(dra_decoder_on_read__wav, dra_decoder_on_seek__wav, pDecoder);
if (pWav == NULL) {
return DR_FALSE;
}
dra_decoder_init__wav(pDecoder, pWav);
return DR_TRUE;
}
dr_bool32 dra_decoder_open_memory__wav(dra_decoder* pDecoder, const void* pData, size_t dataSize)
{
drwav* pWav = drwav_open_memory(pData, dataSize);
if (pWav == NULL) {
return DR_FALSE;
}
dra_decoder_init__wav(pDecoder, pWav);
return DR_TRUE;
}
#ifdef DR_AUDIO_HAS_WAV_STDIO
dr_bool32 dra_decoder_open_file__wav(dra_decoder* pDecoder, const char* filePath)
{
drwav* pWav = drwav_open_file(filePath);
if (pWav == NULL) {
return DR_FALSE;
}
dra_decoder_init__wav(pDecoder, pWav);
return DR_TRUE;
}
#endif
#endif //WAV
#ifdef DR_AUDIO_HAS_FLAC
size_t dra_decoder_on_read__flac(void* pUserData, void* pDataOut, size_t bytesToRead)
{
dra_decoder* pDecoder = (dra_decoder*)pUserData;
assert(pDecoder != NULL);
assert(pDecoder->onRead != NULL);
return pDecoder->onRead(pDecoder->pUserData, pDataOut, bytesToRead);
}
drflac_bool32 dra_decoder_on_seek__flac(void* pUserData, int offset, drflac_seek_origin origin)
{
dra_decoder* pDecoder = (dra_decoder*)pUserData;
assert(pDecoder != NULL);
assert(pDecoder->onSeek != NULL);
return pDecoder->onSeek(pDecoder->pUserData, offset, (origin == drflac_seek_origin_start) ? dra_seek_origin_start : dra_seek_origin_current);
}
void dra_decoder_on_delete__flac(void* pBackendDecoder)
{
drflac* pFlac = (drflac*)pBackendDecoder;
assert(pFlac != NULL);
drflac_close(pFlac);
}
dr_uint64 dra_decoder_on_read_samples__flac(void* pBackendDecoder, dr_uint64 samplesToRead, float* pSamplesOut)
{
drflac* pFlac = (drflac*)pBackendDecoder;
assert(pFlac != NULL);
dr_uint64 samplesRead = drflac_read_s32(pFlac, samplesToRead, (dr_int32*)pSamplesOut);
dra_s32_to_f32(pSamplesOut, (dr_int32*)pSamplesOut, (size_t)samplesRead);
return samplesRead;
}
dr_bool32 dra_decoder_on_seek_samples__flac(void* pBackendDecoder, dr_uint64 sample)
{
drflac* pFlac = (drflac*)pBackendDecoder;
assert(pFlac != NULL);
return drflac_seek_to_sample(pFlac, sample);
}
void dra_decoder_init__flac(dra_decoder* pDecoder, drflac* pFlac)
{
assert(pDecoder != NULL);
assert(pFlac != NULL);
pDecoder->channels = pFlac->channels;
pDecoder->sampleRate = pFlac->sampleRate;
pDecoder->totalSampleCount = pFlac->totalSampleCount;
pDecoder->pBackendDecoder = pFlac;
pDecoder->onDelete = dra_decoder_on_delete__flac;
pDecoder->onReadSamples = dra_decoder_on_read_samples__flac;
pDecoder->onSeekSamples = dra_decoder_on_seek_samples__flac;
}
dr_bool32 dra_decoder_open__flac(dra_decoder* pDecoder)
{
drflac* pFlac = drflac_open(dra_decoder_on_read__flac, dra_decoder_on_seek__flac, pDecoder);
if (pFlac == NULL) {
return DR_FALSE;
}
dra_decoder_init__flac(pDecoder, pFlac);
return DR_TRUE;
}
dr_bool32 dra_decoder_open_memory__flac(dra_decoder* pDecoder, const void* pData, size_t dataSize)
{
drflac* pFlac = drflac_open_memory(pData, dataSize);
if (pFlac == NULL) {
return DR_FALSE;
}
dra_decoder_init__flac(pDecoder, pFlac);
return DR_TRUE;
}
#ifdef DR_AUDIO_HAS_FLAC_STDIO
dr_bool32 dra_decoder_open_file__flac(dra_decoder* pDecoder, const char* filePath)
{
drflac* pFlac = drflac_open_file(filePath);
if (pFlac == NULL) {
return DR_FALSE;
}
dra_decoder_init__flac(pDecoder, pFlac);
return DR_TRUE;
}
#endif
#endif //FLAC
#ifdef DR_AUDIO_HAS_VORBIS
void dra_decoder_on_delete__vorbis(void* pBackendDecoder)
{
stb_vorbis* pVorbis = (stb_vorbis*)pBackendDecoder;
assert(pVorbis != NULL);
stb_vorbis_close(pVorbis);
}
dr_uint64 dra_decoder_on_read_samples__vorbis(void* pBackendDecoder, dr_uint64 samplesToRead, float* pSamplesOut)
{
stb_vorbis* pVorbis = (stb_vorbis*)pBackendDecoder;
assert(pVorbis != NULL);
stb_vorbis_info info = stb_vorbis_get_info(pVorbis);
return (dr_uint64)stb_vorbis_get_samples_float_interleaved(pVorbis, info.channels, pSamplesOut, (int)samplesToRead) * info.channels;
}
dr_bool32 dra_decoder_on_seek_samples__vorbis(void* pBackendDecoder, dr_uint64 sample)
{
stb_vorbis* pVorbis = (stb_vorbis*)pBackendDecoder;
assert(pVorbis != NULL);
return stb_vorbis_seek(pVorbis, (unsigned int)sample) != 0;
}
void dra_decoder_init__vorbis(dra_decoder* pDecoder, stb_vorbis* pVorbis)
{
assert(pDecoder != NULL);
assert(pVorbis != NULL);
stb_vorbis_info info = stb_vorbis_get_info(pVorbis);
pDecoder->channels = info.channels;
pDecoder->sampleRate = info.sample_rate;
pDecoder->totalSampleCount = stb_vorbis_stream_length_in_samples(pVorbis) * info.channels;
pDecoder->pBackendDecoder = pVorbis;
pDecoder->onDelete = dra_decoder_on_delete__vorbis;
pDecoder->onReadSamples = dra_decoder_on_read_samples__vorbis;
pDecoder->onSeekSamples = dra_decoder_on_seek_samples__vorbis;
}
dr_bool32 dra_decoder_open__vorbis(dra_decoder* pDecoder)
{
// TODO: Add support for the push API.
// Not currently supporting callback based decoding.
(void)pDecoder;
return DR_FALSE;
}
dr_bool32 dra_decoder_open_memory__vorbis(dra_decoder* pDecoder, const void* pData, size_t dataSize)
{
stb_vorbis* pVorbis = stb_vorbis_open_memory((const unsigned char*)pData, (int)dataSize, NULL, NULL);
if (pVorbis == NULL) {
return DR_FALSE;
}
dra_decoder_init__vorbis(pDecoder, pVorbis);
return DR_TRUE;
}
#ifdef DR_AUDIO_HAS_VORBIS_STDIO
dr_bool32 dra_decoder_open_file__vorbis(dra_decoder* pDecoder, const char* filePath)
{
stb_vorbis* pVorbis = stb_vorbis_open_filename(filePath, NULL, NULL);
if (pVorbis == NULL) {
return DR_FALSE;
}
dra_decoder_init__vorbis(pDecoder, pVorbis);
return DR_TRUE;
}
#endif
#endif //Vorbis
dra_result dra_decoder_open(dra_decoder* pDecoder, dra_decoder_on_read_proc onRead, dra_decoder_on_seek_proc onSeek, void* pUserData)
{
if (pDecoder == NULL) return DRA_RESULT_INVALID_ARGS;
memset(pDecoder, 0, sizeof(*pDecoder));
if (onRead == NULL || onSeek == NULL) return DRA_RESULT_INVALID_ARGS;
pDecoder->onRead = onRead;
pDecoder->onSeek = onSeek;
pDecoder->pUserData = pUserData;
#ifdef DR_AUDIO_HAS_WAV_STDIO
if (dra_decoder_open__wav(pDecoder)) {
return DRA_RESULT_SUCCESS;
}
onSeek(pUserData, 0, dra_seek_origin_start);
#endif
#ifdef DR_AUDIO_HAS_FLAC_STDIO
if (dra_decoder_open__flac(pDecoder)) {
return DRA_RESULT_SUCCESS;
}
onSeek(pUserData, 0, dra_seek_origin_start);
#endif
#ifdef DR_AUDIO_HAS_VORBIS_STDIO
if (dra_decoder_open__vorbis(pDecoder)) {
return DRA_RESULT_SUCCESS;
}
onSeek(pUserData, 0, dra_seek_origin_start);
#endif
// If we get here it means we were unable to open a decoder.
return DRA_RESULT_NO_DECODER;
}
size_t dra_decoder__on_read_memory(void* pUserData, void* pDataOut, size_t bytesToRead)
{
dra__memory_stream* memoryStream = (dra__memory_stream*)pUserData;
assert(memoryStream != NULL);
assert(memoryStream->dataSize >= memoryStream->currentReadPos);
size_t bytesRemaining = memoryStream->dataSize - memoryStream->currentReadPos;
if (bytesToRead > bytesRemaining) {
bytesToRead = bytesRemaining;
}
if (bytesToRead > 0) {
memcpy(pDataOut, memoryStream->data + memoryStream->currentReadPos, bytesToRead);
memoryStream->currentReadPos += bytesToRead;
}
return bytesToRead;
}
dr_bool32 dra_decoder__on_seek_memory(void* pUserData, int offset, dra_seek_origin origin)
{
dra__memory_stream* memoryStream = (dra__memory_stream*)pUserData;
assert(memoryStream != NULL);
assert(offset > 0 || (offset == 0 && origin == dra_seek_origin_start));
if (origin == dra_seek_origin_current) {
if (memoryStream->currentReadPos + offset <= memoryStream->dataSize) {
memoryStream->currentReadPos += offset;
} else {
memoryStream->currentReadPos = memoryStream->dataSize; // Trying to seek too far forward.
}
} else {
if ((dr_uint32)offset <= memoryStream->dataSize) {
memoryStream->currentReadPos = offset;
} else {
memoryStream->currentReadPos = memoryStream->dataSize; // Trying to seek too far forward.
}
}
return DR_TRUE;
}
dra_result dra_decoder_open_memory(dra_decoder* pDecoder, const void* pData, size_t dataSize)
{
if (pDecoder == NULL) return DRA_RESULT_INVALID_ARGS;
memset(pDecoder, 0, sizeof(*pDecoder));
if (pData == NULL || dataSize == 0) return DRA_RESULT_INVALID_ARGS;
// Prefer the backend's native APIs.
#if defined(DR_AUDIO_HAS_WAV)
if (dra_decoder_open_memory__wav(pDecoder, pData, dataSize)) {
return DRA_RESULT_SUCCESS;
}
#endif
#if defined(DR_AUDIO_HAS_FLAC)
if (dra_decoder_open_memory__flac(pDecoder, pData, dataSize)) {
return DRA_RESULT_SUCCESS;
}
#endif
#if defined(DR_AUDIO_HAS_VORBIS)
if (dra_decoder_open_memory__vorbis(pDecoder, pData, dataSize)) {
return DRA_RESULT_SUCCESS;
}
#endif
// If we get here it means the backend does not have a native memory loader so we'll need to it ourselves.
dra__memory_stream memoryStream;
memoryStream.data = (const unsigned char*)pData;
memoryStream.dataSize = dataSize;
memoryStream.currentReadPos = 0;
dra_result result = dra_decoder_open(pDecoder, dra_decoder__on_read_memory, dra_decoder__on_seek_memory, &memoryStream);
if (result != DRA_RESULT_SUCCESS) {
return result;
}
pDecoder->memoryStream = memoryStream;
pDecoder->pUserData = &pDecoder->memoryStream;
return DRA_RESULT_SUCCESS;
}
#ifndef DR_AUDIO_NO_STDIO
size_t dra_decoder__on_read_stdio(void* pUserData, void* pDataOut, size_t bytesToRead)
{
return fread(pDataOut, 1, bytesToRead, (FILE*)pUserData);
}
dr_bool32 dra_decoder__on_seek_stdio(void* pUserData, int offset, dra_seek_origin origin)
{
return fseek((FILE*)pUserData, offset, (origin == dra_seek_origin_current) ? SEEK_CUR : SEEK_SET) == 0;
}
dra_result dra_decoder_open_file(dra_decoder* pDecoder, const char* filePath)
{
if (pDecoder == NULL) return DRA_RESULT_INVALID_ARGS;
memset(pDecoder, 0, sizeof(*pDecoder));
if (filePath == NULL) return DR_FALSE;
// When opening a decoder from a file it's preferrable to use the backend's native file IO APIs if it has them.
#if defined(DR_AUDIO_HAS_WAV) && defined(DR_AUDIO_HAS_WAV_STDIO)
if (dra_decoder_open_file__wav(pDecoder, filePath)) {
return DRA_RESULT_SUCCESS;
}
#endif
#if defined(DR_AUDIO_HAS_FLAC) && defined(DR_AUDIO_HAS_FLAC_STDIO)
if (dra_decoder_open_file__flac(pDecoder, filePath)) {
return DRA_RESULT_SUCCESS;
}
#endif
#if defined(DR_AUDIO_HAS_VORBIS) && defined(DR_AUDIO_HAS_VORBIS_STDIO)
if (dra_decoder_open_file__vorbis(pDecoder, filePath)) {
return DRA_RESULT_SUCCESS;
}
#endif
// If we get here it means we were unable to open the decoder using any of the backends' native file IO
// APIs. In this case we fall back to a generic method.
FILE* pFile = dra__fopen(filePath);
if (pFile == NULL) {
return DRA_RESULT_FAILED_TO_OPEN_FILE;
}
dra_result result = dra_decoder_open(pDecoder, dra_decoder__on_read_stdio, dra_decoder__on_seek_stdio, pFile);
if (result != DRA_RESULT_SUCCESS) {
fclose(pFile);
return result;
}
return DRA_RESULT_SUCCESS;
}
#endif
void dra_decoder_close(dra_decoder* pDecoder)
{
if (pDecoder == NULL) {
return;
}
if (pDecoder->onDelete) {
pDecoder->onDelete(pDecoder->pBackendDecoder);
}
#ifndef DR_AUDIO_NO_STDIO
if (pDecoder->onRead == dra_decoder__on_read_stdio) {
fclose((FILE*)pDecoder->pUserData);
}
#endif
}
dr_uint64 dra_decoder_read_f32(dra_decoder* pDecoder, dr_uint64 samplesToRead, float* pSamplesOut)
{
if (pDecoder == NULL || pSamplesOut == NULL) {
return 0;
}
return pDecoder->onReadSamples(pDecoder->pBackendDecoder, samplesToRead, pSamplesOut);
}
dr_bool32 dra_decoder_seek_to_sample(dra_decoder* pDecoder, dr_uint64 sample)
{
if (pDecoder == NULL) {
return DR_FALSE;
}
return pDecoder->onSeekSamples(pDecoder->pBackendDecoder, sample);
}
float* dra_decoder__full_decode_and_close(dra_decoder* pDecoder, unsigned int* channelsOut, unsigned int* sampleRateOut, dr_uint64* totalSampleCountOut)
{
assert(pDecoder != NULL);
float* pSampleData = NULL;
dr_uint64 totalSampleCount = pDecoder->totalSampleCount;
if (totalSampleCount == 0)
{
float buffer[4096];
size_t sampleDataBufferSize = sizeof(buffer);
pSampleData = (float*)malloc(sampleDataBufferSize);
if (pSampleData == NULL) {
goto on_error;
}
dr_uint64 samplesRead;
while ((samplesRead = (dr_uint64)dra_decoder_read_f32(pDecoder, sizeof(buffer)/sizeof(buffer[0]), buffer)) > 0)
{
if (((totalSampleCount + samplesRead) * sizeof(float)) > sampleDataBufferSize) {
sampleDataBufferSize *= 2;
float* pNewSampleData = (float*)realloc(pSampleData, sampleDataBufferSize);
if (pNewSampleData == NULL) {
free(pSampleData);
goto on_error;
}
pSampleData = pNewSampleData;
}
memcpy(pSampleData + totalSampleCount, buffer, (size_t)(samplesRead*sizeof(float)));
totalSampleCount += samplesRead;
}
// At this point everything should be decoded, but we just want to fill the unused part buffer with silence - need to
// protect those ears from random noise!
memset(pSampleData + totalSampleCount, 0, (size_t)(sampleDataBufferSize - totalSampleCount*sizeof(float)));
}
else
{
dr_uint64 dataSize = totalSampleCount * sizeof(float);
if (dataSize > SIZE_MAX) {
goto on_error; // The decoded data is too big.
}
pSampleData = (float*)malloc((size_t)dataSize); // <-- Safe cast as per the check above.
if (pSampleData == NULL) {
goto on_error;
}
dr_uint64 samplesDecoded = dra_decoder_read_f32(pDecoder, pDecoder->totalSampleCount, pSampleData);
if (samplesDecoded != pDecoder->totalSampleCount) {
free(pSampleData);
goto on_error; // Something went wrong when decoding the FLAC stream.
}
}
if (channelsOut) *channelsOut = pDecoder->channels;
if (sampleRateOut) *sampleRateOut = pDecoder->sampleRate;
if (totalSampleCountOut) *totalSampleCountOut = totalSampleCount;
dra_decoder_close(pDecoder);
return pSampleData;
on_error:
dra_decoder_close(pDecoder);
return NULL;
}
float* dra_decoder_open_and_decode_f32(dra_decoder_on_read_proc onRead, dra_decoder_on_seek_proc onSeek, void* pUserData, unsigned int* channels, unsigned int* sampleRate, dr_uint64* totalSampleCount)
{
// Safety.
if (channels) *channels = 0;
if (sampleRate) *sampleRate = 0;
if (totalSampleCount) *totalSampleCount = 0;
dra_decoder decoder;
dra_result result = dra_decoder_open(&decoder, onRead, onSeek, pUserData);
if (result != DRA_RESULT_SUCCESS) {
return NULL;
}
return dra_decoder__full_decode_and_close(&decoder, channels, sampleRate, totalSampleCount);
}
float* dra_decoder_open_and_decode_memory_f32(const void* pData, size_t dataSize, unsigned int* channels, unsigned int* sampleRate, dr_uint64* totalSampleCount)
{
// Safety.
if (channels) *channels = 0;
if (sampleRate) *sampleRate = 0;
if (totalSampleCount) *totalSampleCount = 0;
dra_decoder decoder;
dra_result result = dra_decoder_open_memory(&decoder, pData, dataSize);
if (result != DRA_RESULT_SUCCESS) {
return NULL;
}
return dra_decoder__full_decode_and_close(&decoder, channels, sampleRate, totalSampleCount);
}
#ifndef DR_AUDIO_NO_STDIO
float* dra_decoder_open_and_decode_file_f32(const char* filePath, unsigned int* channels, unsigned int* sampleRate, dr_uint64* totalSampleCount)
{
// Safety.
if (channels) *channels = 0;
if (sampleRate) *sampleRate = 0;
if (totalSampleCount) *totalSampleCount = 0;
dra_decoder decoder;
dra_result result = dra_decoder_open_file(&decoder, filePath);
if (result != DRA_RESULT_SUCCESS) {
return NULL;
}
return dra_decoder__full_decode_and_close(&decoder, channels, sampleRate, totalSampleCount);
}
#endif
//// High Level Helper APIs ////
#ifndef DR_AUDIO_NO_STDIO
dra_result dra_voice_create_from_file(dra_device* pDevice, const char* filePath, dra_voice** ppVoice)
{
if (pDevice == NULL || filePath == NULL) return DRA_RESULT_INVALID_ARGS;
unsigned int channels;
unsigned int sampleRate;
dr_uint64 totalSampleCount;
float* pSampleData = dra_decoder_open_and_decode_file_f32(filePath, &channels, &sampleRate, &totalSampleCount);
if (pSampleData == NULL) {
return DRA_RESULT_UNKNOWN_ERROR;
}
dra_result result = dra_voice_create(pDevice, dra_format_f32, channels, sampleRate, (size_t)totalSampleCount * sizeof(float), pSampleData, ppVoice);
free(pSampleData);
return result;
}
#endif
//// High Level World APIs ////
dra_sound_world* dra_sound_world_create(dra_device* pPlaybackDevice)
{
dra_sound_world* pWorld = (dra_sound_world*)calloc(1, sizeof(*pWorld));
if (pWorld == NULL) {
goto on_error;
}
pWorld->pPlaybackDevice = pPlaybackDevice;
if (pWorld->pPlaybackDevice == NULL) {
dra_result result = dra_device_create(NULL, dra_device_type_playback, &pWorld->pPlaybackDevice);
if (result != DRA_RESULT_SUCCESS) {
return NULL;
}
pWorld->ownsPlaybackDevice = DR_TRUE;
}
return pWorld;
on_error:
dra_sound_world_delete(pWorld);
return NULL;
}
void dra_sound_world_delete(dra_sound_world* pWorld)
{
if (pWorld == NULL) {
return;
}
if (pWorld->ownsPlaybackDevice) {
dra_device_delete(pWorld->pPlaybackDevice);
}
free(pWorld);
}
void dra_sound_world__on_inline_sound_stop(dr_uint64 eventID, void* pUserData)
{
(void)eventID;
dra_sound* pSound = (dra_sound*)pUserData;
assert(pSound != NULL);
dra_sound_delete(pSound);
}
void dra_sound_world_play_inline(dra_sound_world* pWorld, dra_sound_desc* pDesc, dra_mixer* pMixer)
{
if (pWorld == NULL || pDesc == NULL) {
return;
}
// An inline sound is just like any other, except it never loops and it's deleted automatically when it stops playing. Therefore what
// we need to do is attach an event handler to the voice's stop callback which is where the sound will be deleted.
dra_sound* pSound = dra_sound_create(pWorld, pDesc);
if (pSound == NULL) {
return;
}
if (pMixer != NULL) {
dra_sound_attach_to_mixer(pSound, pMixer);
}
dra_voice_set_on_stop(pSound->pVoice, dra_sound_world__on_inline_sound_stop, pSound);
dra_sound_play(pSound, DR_FALSE);
}
void dra_sound_world_play_inline_3f(dra_sound_world* pWorld, dra_sound_desc* pDesc, dra_mixer* pMixer, float xPos, float yPos, float zPos)
{
if (pWorld == NULL || pDesc == NULL) {
return;
}
// TODO: Implement 3D positioning once the effects framework is in.
(void)xPos;
(void)yPos;
(void)zPos;
dra_sound_world_play_inline(pWorld, pDesc, pMixer);
}
void dra_sound_world_stop_all_sounds(dra_sound_world* pWorld)
{
if (pWorld == NULL) {
return;
}
// When sounds are stopped the on_stop event handler will be fired. It is possible for the implementation of this event handler to
// delete the sound, so we'll first need to gather the sounds into a separate list.
size_t voiceCount = dra_mixer_count_attached_voices_recursive(pWorld->pPlaybackDevice->pMasterMixer);
if (voiceCount == 0) {
return;
}
dra_voice** ppVoices = (dra_voice**)malloc(voiceCount * sizeof(*ppVoices));
if (ppVoices == NULL) {
return;
}
voiceCount = dra_mixer_gather_attached_voices_recursive(pWorld->pPlaybackDevice->pMasterMixer, ppVoices);
assert(voiceCount != 0);
for (size_t iVoice = 0; iVoice < voiceCount; ++iVoice) {
dra_voice_stop(ppVoices[iVoice]);
}
free(ppVoices);
}
void dra_sound_world_set_listener_position(dra_sound_world* pWorld, float xPos, float yPos, float zPos)
{
if (pWorld == NULL) {
return;
}
// TODO: Implement me.
(void)xPos;
(void)yPos;
(void)zPos;
}
void dra_sound_world_set_listener_orientation(dra_sound_world* pWorld, float xForward, float yForward, float zForward, float xUp, float yUp, float zUp)
{
if (pWorld == NULL) {
return;
}
// TODO: Implement me.
(void)xForward;
(void)yForward;
(void)zForward;
(void)xUp;
(void)yUp;
(void)zUp;
}
dr_bool32 dra_sound__is_streaming(dra_sound* pSound)
{
assert(pSound != NULL);
return pSound->desc.dataSize == 0 || pSound->desc.pData == NULL;
}
dr_bool32 dra_sound__read_next_chunk(dra_sound* pSound, dr_uint64 outputSampleOffset)
{
assert(pSound != NULL);
if (pSound->desc.onRead == NULL) {
return DR_FALSE;
}
dr_uint64 chunkSizeInSamples = (pSound->pVoice->frameCount * pSound->pVoice->channels) / 2;
assert(chunkSizeInSamples > 0);
dr_uint64 samplesRead = pSound->desc.onRead(pSound, chunkSizeInSamples, dra_voice_get_buffer_ptr_by_sample(pSound->pVoice, outputSampleOffset));
if (samplesRead == 0 && !pSound->isLooping) {
dra_voice_write_silence(pSound->pVoice, outputSampleOffset, chunkSizeInSamples);
return DR_FALSE; // Ran out of samples in a non-looping buffer.
}
if (samplesRead == chunkSizeInSamples) {
return DR_TRUE;
}
assert(samplesRead > 0);
assert(samplesRead < chunkSizeInSamples);
// Ran out of samples. If the sound is not looping it simply means the end of the data has been reached. The remaining samples need
// to be zeroed out to create silence.
if (!pSound->isLooping) {
dra_voice_write_silence(pSound->pVoice, outputSampleOffset + samplesRead, chunkSizeInSamples - samplesRead);
return DR_TRUE;
}
// At this point the sound will not be looping. We want to continuously loop back to the start and keep reading samples until the
// chunk is filled.
while (samplesRead < chunkSizeInSamples) {
if (!pSound->desc.onSeek(pSound, 0)) {
return DR_FALSE;
}
dr_uint64 samplesRemaining = chunkSizeInSamples - samplesRead;
dr_uint64 samplesJustRead = pSound->desc.onRead(pSound, samplesRemaining, dra_voice_get_buffer_ptr_by_sample(pSound->pVoice, outputSampleOffset + samplesRead));
if (samplesJustRead == 0) {
return DR_FALSE;
}
samplesRead += samplesJustRead;
}
return DR_TRUE;
}
void dra_sound__on_read_next_chunk(dr_uint64 eventID, void* pUserData)
{
dra_sound* pSound = (dra_sound*)pUserData;
assert(pSound != NULL);
if (pSound->stopOnNextChunk) {
pSound->stopOnNextChunk = DR_FALSE;
dra_sound_stop(pSound);
return;
}
// The event ID is the index of the sample to write to.
dr_uint64 sampleOffset = eventID;
if (!dra_sound__read_next_chunk(pSound, sampleOffset)) {
pSound->stopOnNextChunk = DR_TRUE;
}
}
dra_sound* dra_sound_create(dra_sound_world* pWorld, dra_sound_desc* pDesc)
{
if (pWorld == NULL) {
return NULL;
}
dr_bool32 isStreaming = DR_FALSE;
dra_result result = DRA_RESULT_SUCCESS;
dra_sound* pSound = (dra_sound*)calloc(1, sizeof(*pSound));
if (pSound == NULL) {
result = DRA_RESULT_OUT_OF_MEMORY;
goto on_error;
}
pSound->pWorld = pWorld;
pSound->desc = *pDesc;
isStreaming = dra_sound__is_streaming(pSound);
if (!isStreaming) {
result = dra_voice_create(pWorld->pPlaybackDevice, pDesc->format, pDesc->channels, pDesc->sampleRate, pDesc->dataSize, pDesc->pData, &pSound->pVoice);
} else {
size_t streamingBufferSize = (pDesc->sampleRate * pDesc->channels) * 2; // 2 seconds total, 1 second chunks. Keep total an even number and a multiple of the channel count.
result = dra_voice_create(pWorld->pPlaybackDevice, pDesc->format, pDesc->channels, pDesc->sampleRate, streamingBufferSize * dra_get_bytes_per_sample_by_format(pDesc->format), NULL, &pSound->pVoice);
// Streaming buffers require 2 playback events. As one is being played, the other is filled. The event ID is set to the sample
// index of the next chunk that needs updating and is used in determining where to place new data.
dra_voice_add_playback_event(pSound->pVoice, 0, streamingBufferSize/2, dra_sound__on_read_next_chunk, pSound);
dra_voice_add_playback_event(pSound->pVoice, streamingBufferSize/2, 0, dra_sound__on_read_next_chunk, pSound);
}
if (result != DRA_RESULT_SUCCESS) {
goto on_error;
}
pSound->pVoice->pUserData = pSound;
// Streaming buffers need to have an initial chunk of data loaded before returning. This ensures the internal buffer contains valid audio data in
// preparation for being played for the first time.
if (isStreaming) {
if (!dra_sound__read_next_chunk(pSound, 0)) {
goto on_error;
}
}
return pSound;
on_error:
dra_sound_delete(pSound);
return NULL;
}
void dra_sound__on_delete_decoder(dra_sound* pSound)
{
dra_decoder* pDecoder = (dra_decoder*)pSound->desc.pUserData;
assert(pDecoder != NULL);
dra_decoder_close(pDecoder);
free(pDecoder);
}
dr_uint64 dra_sound__on_read_decoder(dra_sound* pSound, dr_uint64 samplesToRead, void* pSamplesOut)
{
dra_decoder* pDecoder = (dra_decoder*)pSound->desc.pUserData;
assert(pDecoder != NULL);
return dra_decoder_read_f32(pDecoder, samplesToRead, (float*)pSamplesOut);
}
dr_bool32 dra_sound__on_seek_decoder(dra_sound* pSound, dr_uint64 sample)
{
dra_decoder* pDecoder = (dra_decoder*)pSound->desc.pUserData;
assert(pDecoder != NULL);
return dra_decoder_seek_to_sample(pDecoder, sample);
}
#ifndef DR_AUDIO_NO_STDIO
dra_sound* dra_sound_create_from_file(dra_sound_world* pWorld, const char* filePath)
{
if (pWorld == NULL || filePath == NULL) {
return NULL;
}
dra_decoder* pDecoder = (dra_decoder*)malloc(sizeof(*pDecoder));
if (pDecoder == NULL) {
return NULL;
}
dra_result result = dra_decoder_open_file(pDecoder, filePath);
if (result != DRA_RESULT_SUCCESS) {
free(pDecoder);
return NULL;
}
dra_sound_desc desc;
desc.format = dra_format_f32;
desc.channels = pDecoder->channels;
desc.sampleRate = pDecoder->sampleRate;
desc.dataSize = 0;
desc.pData = NULL;
desc.onDelete = dra_sound__on_delete_decoder;
desc.onRead = dra_sound__on_read_decoder;
desc.onSeek = dra_sound__on_seek_decoder;
desc.pUserData = pDecoder;
dra_sound* pSound = dra_sound_create(pWorld, &desc);
// After creating the sound, the audio data of a non-streaming voice can be deleted.
if (desc.pData != NULL) {
free(desc.pData);
}
return pSound;
}
#endif
void dra_sound_delete(dra_sound* pSound)
{
if (pSound == NULL) {
return;
}
if (pSound->pVoice != NULL) {
dra_voice_delete(pSound->pVoice);
}
if (pSound->desc.onDelete) {
pSound->desc.onDelete(pSound);
}
free(pSound);
}
void dra_sound_play(dra_sound* pSound, dr_bool32 loop)
{
if (pSound == NULL) {
return;
}
// The voice is always set to loop for streaming sounds.
if (dra_sound__is_streaming(pSound)) {
dra_voice_play(pSound->pVoice, DR_TRUE);
} else {
dra_voice_play(pSound->pVoice, loop);
}
pSound->isLooping = loop;
}
void dra_sound_stop(dra_sound* pSound)
{
if (pSound == NULL) {
return;
}
dra_voice_stop(pSound->pVoice);
}
void dra_sound_attach_to_mixer(dra_sound* pSound, dra_mixer* pMixer)
{
if (pSound == NULL) {
return;
}
if (pMixer == NULL) {
pMixer = pSound->pWorld->pPlaybackDevice->pMasterMixer;
}
dra_mixer_attach_voice(pMixer, pSound->pVoice);
}
void dra_sound_set_on_stop(dra_sound* pSound, dra_event_proc proc, void* pUserData)
{
dra_voice_set_on_stop(pSound->pVoice, proc, pUserData);
}
void dra_sound_set_on_play(dra_sound* pSound, dra_event_proc proc, void* pUserData)
{
dra_voice_set_on_play(pSound->pVoice, proc, pUserData);
}
#endif //DR_AUDIO_IMPLEMENTATION
// TODO
//
// - Forward declare every backend function and document them.
// - Add support for the push API in stb_vorbis.
// DEVELOPMENT NOTES AND BRAINSTORMING
//
// This is just random brainstorming and is likely very out of date and often just outright incorrect.
//
//
// Latency
//
// When a device is created it'll create a "hardware buffer" which is basically the buffer that the hardware
// device will read from when it needs to play audio. The hardware buffer is divided into two halves. As the
// buffer plays, it moves the playback pointer forward through the buffer and loops. When it hits the half
// way point it notifies the application that it needs more data to continue playing. Once one half starts
// playing the data within it is committed and cannot be changed. The size of each half determines the latency
// of the device.
//
// It sounds tempting to set this to something small like 1ms, but making it too small will
// increase the chance that the CPU won't be able to keep filling it with fresh data. In addition it will
// incrase overall CPU usage because operating system's scheduler will need to wake up the thread more often.
// Increasing the latency will increase memory usage and make playback of new sound sources take longer to
// begin playing. For example, if the latency was set to something like 1 second, a sound effect in a game
// may take up to a whole second to start playing. A balance needs to be made when setting the latency, and
// it can be configured when the device is created.
//
// (mention the fragments system to help avoiding the CPU running out of time to fill new audio data)
//
//
// Mixing
//
// Mixing is done via dra_mixer objects. Buffers can be attached to a mixer, but not more than one at a time.
// By default buffers are attached to a master mixer. Effects like volume can be applied on a per-buffer and
// per-mixer basis.
//
// Mixers can be chained together in a hierarchial manner. Child mixers will be mixed first with the result
// then passed on to higher level mixing. The master mixer is always the top level mixer.
//
// An obvious use case for mixers in games is to have separate volume controls for different categories of
// sounds, such as music, voices and sounds effects. Another example may be in music production where you may
// want to have separate mixers for vocal tracks, percussion tracks, etc.
//
// A mixer can be thought of as a complex buffer - it can be played/stopped/paused and have effects such as
// volume apllied to it. All of this affects all attached buffers and sub-mixers. You can, for example, pause
// every buffer attached to the mixer by simply pausing the mixer. This is an efficient and easy way to pause
// a group of audio buffers at the same time, such as when the user hits the pause button in a game.
//
// Every device includes a master mixer which is the one that buffers are automatically attached to. This one
// is intentionally hidden from the public facing API in order to keep it simpler. For basic audio playback
// using the master mixer will work just fine, however for more complex sound interactions you'll want to use
// your own mixers. Mixers, like buffers, are attached to the master mixer by default
//
//
// Thread Safety
//
// Everything in dr_audio should be thread-safe.
//
// Backends are implemented as if they are always run from a single thread. It's up to the cross-platform
// section to ensure thread-safety. This is an important property because if each backend is responsible for
// their own thread safety it increases the likelyhood of subtle backend-specific bugs.
//
// Every device has their own thread for asynchronous playback. This thread is created when the device is
// created, and deleted when the device is deleted.
//
// (Note edge cases when thread-safety may be an issue)
/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
*/