mirror of
https://github.com/raysan5/raylib.git
synced 2025-12-25 10:22:33 -05:00
That way, a user needs only to touch a single file to configure what features raylib is built with. Include guards are left out intentionally, because config.h should only be included in source files, not headers. Later on, config.h can also define the raylib version (#461).
2273 lines
80 KiB
C
2273 lines
80 KiB
C
/**********************************************************************************************
|
|
*
|
|
* raylib.audio - Basic funtionality to work with audio
|
|
*
|
|
* FEATURES:
|
|
* - Manage audio device (init/close)
|
|
* - Load and unload audio files
|
|
* - Format wave data (sample rate, size, channels)
|
|
* - Play/Stop/Pause/Resume loaded audio
|
|
* - Manage mixing channels
|
|
* - Manage raw audio context
|
|
*
|
|
* CONFIGURATION:
|
|
*
|
|
* #define AUDIO_STANDALONE
|
|
* Define to use the module as standalone library (independently of raylib).
|
|
* Required types and functions are defined in the same module.
|
|
*
|
|
* #define USE_OPENAL_BACKEND
|
|
* Use OpenAL Soft audio backend usage
|
|
*
|
|
* #define SUPPORT_FILEFORMAT_WAV
|
|
* #define SUPPORT_FILEFORMAT_OGG
|
|
* #define SUPPORT_FILEFORMAT_XM
|
|
* #define SUPPORT_FILEFORMAT_MOD
|
|
* #define SUPPORT_FILEFORMAT_FLAC
|
|
* Selected desired fileformats to be supported for loading. Some of those formats are
|
|
* supported by default, to remove support, just comment unrequired #define in this module
|
|
*
|
|
* LIMITATIONS (only OpenAL Soft):
|
|
* Only up to two channels supported: MONO and STEREO (for additional channels, use AL_EXT_MCFORMATS)
|
|
* Only the following sample sizes supported: 8bit PCM, 16bit PCM, 32-bit float PCM (using AL_EXT_FLOAT32)
|
|
*
|
|
* DEPENDENCIES:
|
|
* mini_al - Audio device/context management (https://github.com/dr-soft/mini_al)
|
|
* stb_vorbis - OGG audio files loading (http://www.nothings.org/stb_vorbis/)
|
|
* jar_xm - XM module file loading
|
|
* jar_mod - MOD audio file loading
|
|
* dr_flac - FLAC audio file loading
|
|
*
|
|
* *OpenAL Soft - Audio device management, still used on HTML5 and OSX platforms
|
|
*
|
|
* CONTRIBUTORS:
|
|
* David Reid (github: @mackron) (Nov. 2017):
|
|
* - Complete port to mini_al library
|
|
*
|
|
* Joshua Reisenauer (github: @kd7tck) (2015)
|
|
* - XM audio module support (jar_xm)
|
|
* - MOD audio module support (jar_mod)
|
|
* - Mixing channels support
|
|
* - Raw audio context support
|
|
*
|
|
*
|
|
* LICENSE: zlib/libpng
|
|
*
|
|
* Copyright (c) 2014-2018 Ramon Santamaria (@raysan5)
|
|
*
|
|
* This software is provided "as-is", without any express or implied warranty. In no event
|
|
* will the authors be held liable for any damages arising from the use of this software.
|
|
*
|
|
* Permission is granted to anyone to use this software for any purpose, including commercial
|
|
* applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
|
*
|
|
* 1. The origin of this software must not be misrepresented; you must not claim that you
|
|
* wrote the original software. If you use this software in a product, an acknowledgment
|
|
* in the product documentation would be appreciated but is not required.
|
|
*
|
|
* 2. Altered source versions must be plainly marked as such, and must not be misrepresented
|
|
* as being the original software.
|
|
*
|
|
* 3. This notice may not be removed or altered from any source distribution.
|
|
*
|
|
**********************************************************************************************/
|
|
|
|
#include "config.h"
|
|
|
|
#if !defined(USE_OPENAL_BACKEND)
|
|
#define USE_MINI_AL 1 // Set to 1 to use mini_al; 0 to use OpenAL.
|
|
#endif
|
|
|
|
#if defined(AUDIO_STANDALONE)
|
|
#include "audio.h"
|
|
#include <stdarg.h> // Required for: va_list, va_start(), vfprintf(), va_end()
|
|
#else
|
|
#include "raylib.h"
|
|
#include "utils.h" // Required for: fopen() Android mapping
|
|
#endif
|
|
|
|
#include "external/mini_al.h" // Implemented in mini_al.c. Cannot implement this here because it conflicts with Win32 APIs such as CloseWindow(), etc.
|
|
|
|
#if !defined(USE_MINI_AL) || (USE_MINI_AL == 0)
|
|
#if defined(__APPLE__)
|
|
#include "OpenAL/al.h" // OpenAL basic header
|
|
#include "OpenAL/alc.h" // OpenAL context header (like OpenGL, OpenAL requires a context to work)
|
|
#else
|
|
#include "AL/al.h" // OpenAL basic header
|
|
#include "AL/alc.h" // OpenAL context header (like OpenGL, OpenAL requires a context to work)
|
|
//#include "AL/alext.h" // OpenAL extensions header, required for AL_EXT_FLOAT32 and AL_EXT_MCFORMATS
|
|
#endif
|
|
|
|
// OpenAL extension: AL_EXT_FLOAT32 - Support for 32bit float samples
|
|
// OpenAL extension: AL_EXT_MCFORMATS - Support for multi-channel formats (Quad, 5.1, 6.1, 7.1)
|
|
#endif
|
|
|
|
#include <stdlib.h> // Required for: malloc(), free()
|
|
#include <string.h> // Required for: strcmp(), strncmp()
|
|
#include <stdio.h> // Required for: FILE, fopen(), fclose(), fread()
|
|
|
|
#if defined(SUPPORT_FILEFORMAT_OGG)
|
|
//#define STB_VORBIS_HEADER_ONLY
|
|
#include "external/stb_vorbis.h" // OGG loading functions
|
|
#endif
|
|
|
|
#if defined(SUPPORT_FILEFORMAT_XM)
|
|
#define JAR_XM_IMPLEMENTATION
|
|
#include "external/jar_xm.h" // XM loading functions
|
|
#endif
|
|
|
|
#if defined(SUPPORT_FILEFORMAT_MOD)
|
|
#define JAR_MOD_IMPLEMENTATION
|
|
#include "external/jar_mod.h" // MOD loading functions
|
|
#endif
|
|
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
#define DR_FLAC_IMPLEMENTATION
|
|
#define DR_FLAC_NO_WIN32_IO
|
|
#include "external/dr_flac.h" // FLAC loading functions
|
|
#endif
|
|
|
|
#ifdef _MSC_VER
|
|
#undef bool
|
|
#endif
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Defines and Macros
|
|
//----------------------------------------------------------------------------------
|
|
#define MAX_STREAM_BUFFERS 2 // Number of buffers for each audio stream
|
|
|
|
// NOTE: Music buffer size is defined by number of samples, independent of sample size and channels number
|
|
// After some math, considering a sampleRate of 48000, a buffer refill rate of 1/60 seconds
|
|
// and double-buffering system, I concluded that a 4096 samples buffer should be enough
|
|
// In case of music-stalls, just increase this number
|
|
#define AUDIO_BUFFER_SIZE 4096 // PCM data samples (i.e. 16bit, Mono: 8Kb)
|
|
|
|
// Support uncompressed PCM data in 32-bit float IEEE format
|
|
// NOTE: This definition is included in "AL/alext.h", but some OpenAL implementations
|
|
// could not provide the extensions header (Android), so its defined here
|
|
#if !defined(AL_EXT_float32)
|
|
#define AL_EXT_float32 1
|
|
#define AL_FORMAT_MONO_FLOAT32 0x10010
|
|
#define AL_FORMAT_STEREO_FLOAT32 0x10011
|
|
#endif
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Types and Structures Definition
|
|
//----------------------------------------------------------------------------------
|
|
|
|
typedef enum {
|
|
MUSIC_AUDIO_OGG = 0,
|
|
MUSIC_AUDIO_FLAC,
|
|
MUSIC_MODULE_XM,
|
|
MUSIC_MODULE_MOD
|
|
} MusicContextType;
|
|
|
|
// Music type (file streaming from memory)
|
|
typedef struct MusicData {
|
|
MusicContextType ctxType; // Type of music context (OGG, XM, MOD)
|
|
#if defined(SUPPORT_FILEFORMAT_OGG)
|
|
stb_vorbis *ctxOgg; // OGG audio context
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
drflac *ctxFlac; // FLAC audio context
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_XM)
|
|
jar_xm_context_t *ctxXm; // XM chiptune context
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_MOD)
|
|
jar_mod_context_t ctxMod; // MOD chiptune context
|
|
#endif
|
|
|
|
AudioStream stream; // Audio stream (double buffering)
|
|
|
|
int loopCount; // Loops count (times music repeats), -1 means infinite loop
|
|
unsigned int totalSamples; // Total number of samples
|
|
unsigned int samplesLeft; // Number of samples left to end
|
|
} MusicData;
|
|
|
|
#if defined(AUDIO_STANDALONE)
|
|
typedef enum {
|
|
LOG_INFO = 0,
|
|
LOG_ERROR,
|
|
LOG_WARNING,
|
|
LOG_DEBUG,
|
|
LOG_OTHER
|
|
} TraceLogType;
|
|
#endif
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Global Variables Definition
|
|
//----------------------------------------------------------------------------------
|
|
// ...
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Module specific Functions Declaration
|
|
//----------------------------------------------------------------------------------
|
|
#if defined(SUPPORT_FILEFORMAT_WAV)
|
|
static Wave LoadWAV(const char *fileName); // Load WAV file
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_OGG)
|
|
static Wave LoadOGG(const char *fileName); // Load OGG file
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
static Wave LoadFLAC(const char *fileName); // Load FLAC file
|
|
#endif
|
|
|
|
#if defined(AUDIO_STANDALONE)
|
|
bool IsFileExtension(const char *fileName, const char *ext); // Check file extension
|
|
void TraceLog(int msgType, const char *text, ...); // Show trace log messages (LOG_INFO, LOG_WARNING, LOG_ERROR, LOG_DEBUG)
|
|
#endif
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// mini_al AudioBuffer Functionality
|
|
//----------------------------------------------------------------------------------
|
|
#if USE_MINI_AL
|
|
|
|
#define DEVICE_FORMAT mal_format_f32
|
|
#define DEVICE_CHANNELS 2
|
|
#define DEVICE_SAMPLE_RATE 44100
|
|
|
|
typedef enum { AUDIO_BUFFER_USAGE_STATIC = 0, AUDIO_BUFFER_USAGE_STREAM } AudioBufferUsage;
|
|
|
|
// Audio buffer structure
|
|
// NOTE: Slightly different logic is used when feeding data to the playback device depending on whether or not data is streamed
|
|
typedef struct AudioBuffer AudioBuffer;
|
|
struct AudioBuffer {
|
|
mal_dsp dsp; // Required for format conversion
|
|
float volume;
|
|
float pitch;
|
|
bool playing;
|
|
bool paused;
|
|
bool looping; // Always true for AudioStreams
|
|
int usage; // AudioBufferUsage type
|
|
bool isSubBufferProcessed[2];
|
|
unsigned int frameCursorPos;
|
|
unsigned int bufferSizeInFrames;
|
|
AudioBuffer *next;
|
|
AudioBuffer *prev;
|
|
unsigned char buffer[1];
|
|
};
|
|
|
|
// mini_al global variables
|
|
static mal_context context;
|
|
static mal_device device;
|
|
static mal_mutex audioLock;
|
|
static bool isAudioInitialized = MAL_FALSE;
|
|
static float masterVolume = 1.0f;
|
|
|
|
// Audio buffers are tracked in a linked list
|
|
static AudioBuffer *firstAudioBuffer = NULL;
|
|
static AudioBuffer *lastAudioBuffer = NULL;
|
|
|
|
// mini_al functions declaration
|
|
static void OnLog(mal_context *pContext, mal_device *pDevice, const char *message);
|
|
static mal_uint32 OnSendAudioDataToDevice(mal_device *pDevice, mal_uint32 frameCount, void *pFramesOut);
|
|
static mal_uint32 OnAudioBufferDSPRead(mal_dsp *pDSP, mal_uint32 frameCount, void *pFramesOut, void *pUserData);
|
|
static void MixAudioFrames(float *framesOut, const float *framesIn, mal_uint32 frameCount, float localVolume);
|
|
|
|
// AudioBuffer management functions declaration
|
|
// NOTE: Those functions are not exposed by raylib... for the moment
|
|
AudioBuffer *CreateAudioBuffer(mal_format format, mal_uint32 channels, mal_uint32 sampleRate, mal_uint32 bufferSizeInFrames, AudioBufferUsage usage);
|
|
void DeleteAudioBuffer(AudioBuffer *audioBuffer);
|
|
bool IsAudioBufferPlaying(AudioBuffer *audioBuffer);
|
|
void PlayAudioBuffer(AudioBuffer *audioBuffer);
|
|
void StopAudioBuffer(AudioBuffer *audioBuffer);
|
|
void PauseAudioBuffer(AudioBuffer *audioBuffer);
|
|
void ResumeAudioBuffer(AudioBuffer *audioBuffer);
|
|
void SetAudioBufferVolume(AudioBuffer *audioBuffer, float volume);
|
|
void SetAudioBufferPitch(AudioBuffer *audioBuffer, float pitch);
|
|
void TrackAudioBuffer(AudioBuffer *audioBuffer);
|
|
void UntrackAudioBuffer(AudioBuffer *audioBuffer);
|
|
|
|
|
|
// Log callback function
|
|
static void OnLog(mal_context *pContext, mal_device *pDevice, const char *message)
|
|
{
|
|
(void)pContext;
|
|
(void)pDevice;
|
|
|
|
TraceLog(LOG_ERROR, message); // All log messages from mini_al are errors
|
|
}
|
|
|
|
// Sending audio data to device callback function
|
|
static mal_uint32 OnSendAudioDataToDevice(mal_device *pDevice, mal_uint32 frameCount, void *pFramesOut)
|
|
{
|
|
// This is where all of the mixing takes place.
|
|
(void)pDevice;
|
|
|
|
// Mixing is basically just an accumulation. We need to initialize the output buffer to 0.
|
|
memset(pFramesOut, 0, frameCount*pDevice->channels*mal_get_sample_size_in_bytes(pDevice->format));
|
|
|
|
// Using a mutex here for thread-safety which makes things not real-time. This is unlikely to be necessary for this project, but may
|
|
// want to consider how you might want to avoid this.
|
|
mal_mutex_lock(&audioLock);
|
|
{
|
|
for (AudioBuffer *audioBuffer = firstAudioBuffer; audioBuffer != NULL; audioBuffer = audioBuffer->next)
|
|
{
|
|
// Ignore stopped or paused sounds.
|
|
if (!audioBuffer->playing || audioBuffer->paused) continue;
|
|
|
|
mal_uint32 framesRead = 0;
|
|
for (;;)
|
|
{
|
|
if (framesRead > frameCount)
|
|
{
|
|
TraceLog(LOG_DEBUG, "Mixed too many frames from audio buffer");
|
|
break;
|
|
}
|
|
|
|
if (framesRead == frameCount) break;
|
|
|
|
// Just read as much data as we can from the stream.
|
|
mal_uint32 framesToRead = (frameCount - framesRead);
|
|
while (framesToRead > 0)
|
|
{
|
|
float tempBuffer[1024]; // 512 frames for stereo.
|
|
|
|
mal_uint32 framesToReadRightNow = framesToRead;
|
|
if (framesToReadRightNow > sizeof(tempBuffer)/sizeof(tempBuffer[0])/DEVICE_CHANNELS)
|
|
{
|
|
framesToReadRightNow = sizeof(tempBuffer)/sizeof(tempBuffer[0])/DEVICE_CHANNELS;
|
|
}
|
|
|
|
// If we're not looping, we need to make sure we flush the internal buffers of the DSP pipeline to ensure we get the
|
|
// last few samples.
|
|
bool flushDSP = !audioBuffer->looping;
|
|
|
|
mal_uint32 framesJustRead = mal_dsp_read_frames_ex(&audioBuffer->dsp, framesToReadRightNow, tempBuffer, flushDSP);
|
|
if (framesJustRead > 0)
|
|
{
|
|
float *framesOut = (float *)pFramesOut + (framesRead*device.channels);
|
|
float *framesIn = tempBuffer;
|
|
MixAudioFrames(framesOut, framesIn, framesJustRead, audioBuffer->volume);
|
|
|
|
framesToRead -= framesJustRead;
|
|
framesRead += framesJustRead;
|
|
}
|
|
|
|
// If we weren't able to read all the frames we requested, break.
|
|
if (framesJustRead < framesToReadRightNow)
|
|
{
|
|
if (!audioBuffer->looping)
|
|
{
|
|
StopAudioBuffer(audioBuffer);
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// Should never get here, but just for safety,
|
|
// move the cursor position back to the start and continue the loop.
|
|
audioBuffer->frameCursorPos = 0;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If for some reason we weren't able to read every frame we'll need to break from the loop.
|
|
// Not doing this could theoretically put us into an infinite loop.
|
|
if (framesToRead > 0) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
mal_mutex_unlock(&audioLock);
|
|
|
|
return frameCount; // We always output the same number of frames that were originally requested.
|
|
}
|
|
|
|
// DSP read from audio buffer callback function
|
|
static mal_uint32 OnAudioBufferDSPRead(mal_dsp *pDSP, mal_uint32 frameCount, void *pFramesOut, void *pUserData)
|
|
{
|
|
AudioBuffer *audioBuffer = (AudioBuffer *)pUserData;
|
|
|
|
mal_uint32 subBufferSizeInFrames = audioBuffer->bufferSizeInFrames/2;
|
|
mal_uint32 currentSubBufferIndex = audioBuffer->frameCursorPos/subBufferSizeInFrames;
|
|
|
|
if (currentSubBufferIndex > 1)
|
|
{
|
|
TraceLog(LOG_DEBUG, "Frame cursor position moved too far forward in audio stream");
|
|
return 0;
|
|
}
|
|
|
|
// Another thread can update the processed state of buffers so we just take a copy here to try and avoid potential synchronization problems.
|
|
bool isSubBufferProcessed[2];
|
|
isSubBufferProcessed[0] = audioBuffer->isSubBufferProcessed[0];
|
|
isSubBufferProcessed[1] = audioBuffer->isSubBufferProcessed[1];
|
|
|
|
mal_uint32 frameSizeInBytes = mal_get_sample_size_in_bytes(audioBuffer->dsp.config.formatIn)*audioBuffer->dsp.config.channelsIn;
|
|
|
|
// Fill out every frame until we find a buffer that's marked as processed. Then fill the remainder with 0.
|
|
mal_uint32 framesRead = 0;
|
|
for (;;)
|
|
{
|
|
// We break from this loop differently depending on the buffer's usage. For static buffers, we simply fill as much data as we can. For
|
|
// streaming buffers we only fill the halves of the buffer that are processed. Unprocessed halves must keep their audio data in-tact.
|
|
if (audioBuffer->usage == AUDIO_BUFFER_USAGE_STATIC)
|
|
{
|
|
if (framesRead >= frameCount) break;
|
|
}
|
|
else
|
|
{
|
|
if (isSubBufferProcessed[currentSubBufferIndex]) break;
|
|
}
|
|
|
|
mal_uint32 totalFramesRemaining = (frameCount - framesRead);
|
|
if (totalFramesRemaining == 0) break;
|
|
|
|
mal_uint32 framesRemainingInOutputBuffer;
|
|
if (audioBuffer->usage == AUDIO_BUFFER_USAGE_STATIC)
|
|
{
|
|
framesRemainingInOutputBuffer = audioBuffer->bufferSizeInFrames - audioBuffer->frameCursorPos;
|
|
}
|
|
else
|
|
{
|
|
mal_uint32 firstFrameIndexOfThisSubBuffer = subBufferSizeInFrames * currentSubBufferIndex;
|
|
framesRemainingInOutputBuffer = subBufferSizeInFrames - (audioBuffer->frameCursorPos - firstFrameIndexOfThisSubBuffer);
|
|
}
|
|
|
|
mal_uint32 framesToRead = totalFramesRemaining;
|
|
if (framesToRead > framesRemainingInOutputBuffer) framesToRead = framesRemainingInOutputBuffer;
|
|
|
|
memcpy((unsigned char *)pFramesOut + (framesRead*frameSizeInBytes), audioBuffer->buffer + (audioBuffer->frameCursorPos*frameSizeInBytes), framesToRead*frameSizeInBytes);
|
|
audioBuffer->frameCursorPos = (audioBuffer->frameCursorPos + framesToRead) % audioBuffer->bufferSizeInFrames;
|
|
framesRead += framesToRead;
|
|
|
|
// If we've read to the end of the buffer, mark it as processed.
|
|
if (framesToRead == framesRemainingInOutputBuffer)
|
|
{
|
|
audioBuffer->isSubBufferProcessed[currentSubBufferIndex] = true;
|
|
isSubBufferProcessed[currentSubBufferIndex] = true;
|
|
|
|
currentSubBufferIndex = (currentSubBufferIndex + 1)%2;
|
|
|
|
// We need to break from this loop if we're not looping.
|
|
if (!audioBuffer->looping)
|
|
{
|
|
StopAudioBuffer(audioBuffer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Zero-fill excess.
|
|
mal_uint32 totalFramesRemaining = (frameCount - framesRead);
|
|
if (totalFramesRemaining > 0)
|
|
{
|
|
memset((unsigned char*)pFramesOut + (framesRead*frameSizeInBytes), 0, totalFramesRemaining*frameSizeInBytes);
|
|
|
|
// For static buffers we can fill the remaining frames with silence for safety, but we don't want
|
|
// to report those frames as "read". The reason for this is that the caller uses the return value
|
|
// to know whether or not a non-looping sound has finished playback.
|
|
if (audioBuffer->usage != AUDIO_BUFFER_USAGE_STATIC) framesRead += totalFramesRemaining;
|
|
}
|
|
|
|
return framesRead;
|
|
}
|
|
|
|
// This is the main mixing function. Mixing is pretty simple in this project - it's just an accumulation.
|
|
// NOTE: framesOut is both an input and an output. It will be initially filled with zeros outside of this function.
|
|
static void MixAudioFrames(float *framesOut, const float *framesIn, mal_uint32 frameCount, float localVolume)
|
|
{
|
|
for (mal_uint32 iFrame = 0; iFrame < frameCount; ++iFrame)
|
|
{
|
|
for (mal_uint32 iChannel = 0; iChannel < device.channels; ++iChannel)
|
|
{
|
|
float *frameOut = framesOut + (iFrame*device.channels);
|
|
const float *frameIn = framesIn + (iFrame*device.channels);
|
|
|
|
frameOut[iChannel] += frameIn[iChannel]*masterVolume*localVolume;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Module Functions Definition - Audio Device initialization and Closing
|
|
//----------------------------------------------------------------------------------
|
|
// Initialize audio device
|
|
void InitAudioDevice(void)
|
|
{
|
|
#if USE_MINI_AL
|
|
// Context.
|
|
mal_context_config contextConfig = mal_context_config_init(OnLog);
|
|
mal_result result = mal_context_init(NULL, 0, &contextConfig, &context);
|
|
if (result != MAL_SUCCESS)
|
|
{
|
|
TraceLog(LOG_ERROR, "Failed to initialize audio context");
|
|
return;
|
|
}
|
|
|
|
// Device. Using the default device. Format is floating point because it simplifies mixing.
|
|
mal_device_config deviceConfig = mal_device_config_init(DEVICE_FORMAT, DEVICE_CHANNELS, DEVICE_SAMPLE_RATE, NULL, OnSendAudioDataToDevice);
|
|
|
|
result = mal_device_init(&context, mal_device_type_playback, NULL, &deviceConfig, NULL, &device);
|
|
if (result != MAL_SUCCESS)
|
|
{
|
|
TraceLog(LOG_ERROR, "Failed to initialize audio playback device");
|
|
mal_context_uninit(&context);
|
|
return;
|
|
}
|
|
|
|
// Keep the device running the whole time. May want to consider doing something a bit smarter and only have the device running
|
|
// while there's at least one sound being played.
|
|
result = mal_device_start(&device);
|
|
if (result != MAL_SUCCESS)
|
|
{
|
|
TraceLog(LOG_ERROR, "Failed to start audio playback device");
|
|
mal_device_uninit(&device);
|
|
mal_context_uninit(&context);
|
|
return;
|
|
}
|
|
|
|
// Mixing happens on a seperate thread which means we need to synchronize. I'm using a mutex here to make things simple, but may
|
|
// want to look at something a bit smarter later on to keep everything real-time, if that's necessary.
|
|
if (mal_mutex_init(&context, &audioLock) != MAL_SUCCESS)
|
|
{
|
|
TraceLog(LOG_ERROR, "Failed to create mutex for audio mixing");
|
|
mal_device_uninit(&device);
|
|
mal_context_uninit(&context);
|
|
return;
|
|
}
|
|
|
|
TraceLog(LOG_INFO, "Audio device initialized successfully: %s", device.name);
|
|
TraceLog(LOG_INFO, "Audio backend: mini_al / %s", mal_get_backend_name(context.backend));
|
|
TraceLog(LOG_INFO, "Audio format: %s -> %s", mal_get_format_name(device.format), mal_get_format_name(device.internalFormat));
|
|
TraceLog(LOG_INFO, "Audio channels: %d -> %d", device.channels, device.internalChannels);
|
|
TraceLog(LOG_INFO, "Audio sample rate: %d -> %d", device.sampleRate, device.internalSampleRate);
|
|
TraceLog(LOG_INFO, "Audio buffer size: %d", device.bufferSizeInFrames);
|
|
|
|
isAudioInitialized = MAL_TRUE;
|
|
#else
|
|
// Open and initialize a device with default settings
|
|
ALCdevice *device = alcOpenDevice(NULL);
|
|
|
|
if (!device) TraceLog(LOG_ERROR, "Audio device could not be opened");
|
|
else
|
|
{
|
|
ALCcontext *context = alcCreateContext(device, NULL);
|
|
|
|
if ((context == NULL) || (alcMakeContextCurrent(context) == ALC_FALSE))
|
|
{
|
|
if (context != NULL) alcDestroyContext(context);
|
|
|
|
alcCloseDevice(device);
|
|
|
|
TraceLog(LOG_ERROR, "Could not initialize audio context");
|
|
}
|
|
else
|
|
{
|
|
TraceLog(LOG_INFO, "Audio device and context initialized successfully: %s", alcGetString(device, ALC_DEVICE_SPECIFIER));
|
|
|
|
// Listener definition (just for 2D)
|
|
alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f);
|
|
alListener3f(AL_VELOCITY, 0.0f, 0.0f, 0.0f);
|
|
alListener3f(AL_ORIENTATION, 0.0f, 0.0f, -1.0f);
|
|
|
|
alListenerf(AL_GAIN, 1.0f);
|
|
|
|
if (alIsExtensionPresent("AL_EXT_float32")) TraceLog(LOG_INFO, "[EXTENSION] AL_EXT_float32 supported");
|
|
else TraceLog(LOG_INFO, "[EXTENSION] AL_EXT_float32 not supported");
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Close the audio device for all contexts
|
|
void CloseAudioDevice(void)
|
|
{
|
|
#if USE_MINI_AL
|
|
if (!isAudioInitialized)
|
|
{
|
|
TraceLog(LOG_WARNING, "Could not close audio device because it is not currently initialized");
|
|
return;
|
|
}
|
|
|
|
mal_mutex_uninit(&audioLock);
|
|
mal_device_uninit(&device);
|
|
mal_context_uninit(&context);
|
|
#else
|
|
ALCdevice *device;
|
|
ALCcontext *context = alcGetCurrentContext();
|
|
|
|
if (context == NULL) TraceLog(LOG_WARNING, "Could not get current audio context for closing");
|
|
|
|
device = alcGetContextsDevice(context);
|
|
|
|
alcMakeContextCurrent(NULL);
|
|
alcDestroyContext(context);
|
|
alcCloseDevice(device);
|
|
#endif
|
|
|
|
TraceLog(LOG_INFO, "Audio device closed successfully");
|
|
}
|
|
|
|
// Check if device has been initialized successfully
|
|
bool IsAudioDeviceReady(void)
|
|
{
|
|
#if USE_MINI_AL
|
|
return isAudioInitialized;
|
|
#else
|
|
ALCcontext *context = alcGetCurrentContext();
|
|
|
|
if (context == NULL) return false;
|
|
else
|
|
{
|
|
ALCdevice *device = alcGetContextsDevice(context);
|
|
|
|
if (device == NULL) return false;
|
|
else return true;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Set master volume (listener)
|
|
void SetMasterVolume(float volume)
|
|
{
|
|
if (volume < 0.0f) volume = 0.0f;
|
|
else if (volume > 1.0f) volume = 1.0f;
|
|
|
|
#if USE_MINI_AL
|
|
masterVolume = volume;
|
|
#else
|
|
alListenerf(AL_GAIN, volume);
|
|
#endif
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Module Functions Definition - Audio Buffer management
|
|
//----------------------------------------------------------------------------------
|
|
#if USE_MINI_AL
|
|
// Create a new audio buffer. Initially filled with silence
|
|
AudioBuffer *CreateAudioBuffer(mal_format format, mal_uint32 channels, mal_uint32 sampleRate, mal_uint32 bufferSizeInFrames, AudioBufferUsage usage)
|
|
{
|
|
AudioBuffer *audioBuffer = (AudioBuffer *)calloc(sizeof(*audioBuffer) + (bufferSizeInFrames*channels*mal_get_sample_size_in_bytes(format)), 1);
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "CreateAudioBuffer() : Failed to allocate memory for audio buffer");
|
|
return NULL;
|
|
}
|
|
|
|
// We run audio data through a format converter.
|
|
mal_dsp_config dspConfig;
|
|
memset(&dspConfig, 0, sizeof(dspConfig));
|
|
dspConfig.formatIn = format;
|
|
dspConfig.formatOut = DEVICE_FORMAT;
|
|
dspConfig.channelsIn = channels;
|
|
dspConfig.channelsOut = DEVICE_CHANNELS;
|
|
dspConfig.sampleRateIn = sampleRate;
|
|
dspConfig.sampleRateOut = DEVICE_SAMPLE_RATE;
|
|
mal_result resultMAL = mal_dsp_init(&dspConfig, OnAudioBufferDSPRead, audioBuffer, &audioBuffer->dsp);
|
|
if (resultMAL != MAL_SUCCESS)
|
|
{
|
|
TraceLog(LOG_ERROR, "LoadSoundFromWave() : Failed to create data conversion pipeline");
|
|
free(audioBuffer);
|
|
return NULL;
|
|
}
|
|
|
|
audioBuffer->volume = 1;
|
|
audioBuffer->pitch = 1;
|
|
audioBuffer->playing = 0;
|
|
audioBuffer->paused = 0;
|
|
audioBuffer->looping = 0;
|
|
audioBuffer->usage = usage;
|
|
audioBuffer->bufferSizeInFrames = bufferSizeInFrames;
|
|
audioBuffer->frameCursorPos = 0;
|
|
|
|
// Buffers should be marked as processed by default so that a call to UpdateAudioStream() immediately after initialization works correctly.
|
|
audioBuffer->isSubBufferProcessed[0] = true;
|
|
audioBuffer->isSubBufferProcessed[1] = true;
|
|
|
|
TrackAudioBuffer(audioBuffer);
|
|
|
|
return audioBuffer;
|
|
}
|
|
|
|
// Delete an audio buffer
|
|
void DeleteAudioBuffer(AudioBuffer *audioBuffer)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
UntrackAudioBuffer(audioBuffer);
|
|
free(audioBuffer);
|
|
}
|
|
|
|
// Check if an audio buffer is playing
|
|
bool IsAudioBufferPlaying(AudioBuffer *audioBuffer)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return false;
|
|
}
|
|
|
|
return audioBuffer->playing && !audioBuffer->paused;
|
|
}
|
|
|
|
// Play an audio buffer
|
|
// NOTE: Buffer is restarted to the start.
|
|
// Use PauseAudioBuffer() and ResumeAudioBuffer() if the playback position should be maintained.
|
|
void PlayAudioBuffer(AudioBuffer *audioBuffer)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
audioBuffer->playing = true;
|
|
audioBuffer->paused = false;
|
|
audioBuffer->frameCursorPos = 0;
|
|
}
|
|
|
|
// Stop an audio buffer
|
|
void StopAudioBuffer(AudioBuffer *audioBuffer)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
// Don't do anything if the audio buffer is already stopped.
|
|
if (!IsAudioBufferPlaying(audioBuffer)) return;
|
|
|
|
audioBuffer->playing = false;
|
|
audioBuffer->paused = false;
|
|
audioBuffer->frameCursorPos = 0;
|
|
audioBuffer->isSubBufferProcessed[0] = true;
|
|
audioBuffer->isSubBufferProcessed[1] = true;
|
|
}
|
|
|
|
// Pause an audio buffer
|
|
void PauseAudioBuffer(AudioBuffer *audioBuffer)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
audioBuffer->paused = true;
|
|
}
|
|
|
|
// Resume an audio buffer
|
|
void ResumeAudioBuffer(AudioBuffer *audioBuffer)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
audioBuffer->paused = false;
|
|
}
|
|
|
|
// Set volume for an audio buffer
|
|
void SetAudioBufferVolume(AudioBuffer *audioBuffer, float volume)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
audioBuffer->volume = volume;
|
|
}
|
|
|
|
// Set pitch for an audio buffer
|
|
void SetAudioBufferPitch(AudioBuffer *audioBuffer, float pitch)
|
|
{
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayAudioBuffer() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
audioBuffer->pitch = pitch;
|
|
|
|
// Pitching is just an adjustment of the sample rate. Note that this changes the duration of the sound - higher pitches
|
|
// will make the sound faster; lower pitches make it slower.
|
|
mal_uint32 newOutputSampleRate = (mal_uint32)((((float)audioBuffer->dsp.config.sampleRateOut / (float)audioBuffer->dsp.config.sampleRateIn) / pitch) * audioBuffer->dsp.config.sampleRateIn);
|
|
mal_dsp_set_output_sample_rate(&audioBuffer->dsp, newOutputSampleRate);
|
|
}
|
|
|
|
// Track audio buffer to linked list next position
|
|
void TrackAudioBuffer(AudioBuffer *audioBuffer)
|
|
{
|
|
mal_mutex_lock(&audioLock);
|
|
|
|
{
|
|
if (firstAudioBuffer == NULL) firstAudioBuffer = audioBuffer;
|
|
else
|
|
{
|
|
lastAudioBuffer->next = audioBuffer;
|
|
audioBuffer->prev = lastAudioBuffer;
|
|
}
|
|
|
|
lastAudioBuffer = audioBuffer;
|
|
}
|
|
|
|
mal_mutex_unlock(&audioLock);
|
|
}
|
|
|
|
// Untrack audio buffer from linked list
|
|
void UntrackAudioBuffer(AudioBuffer *audioBuffer)
|
|
{
|
|
mal_mutex_lock(&audioLock);
|
|
|
|
{
|
|
if (audioBuffer->prev == NULL) firstAudioBuffer = audioBuffer->next;
|
|
else audioBuffer->prev->next = audioBuffer->next;
|
|
|
|
if (audioBuffer->next == NULL) lastAudioBuffer = audioBuffer->prev;
|
|
else audioBuffer->next->prev = audioBuffer->prev;
|
|
|
|
audioBuffer->prev = NULL;
|
|
audioBuffer->next = NULL;
|
|
}
|
|
|
|
mal_mutex_unlock(&audioLock);
|
|
}
|
|
#endif
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Module Functions Definition - Sounds loading and playing (.WAV)
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Load wave data from file
|
|
Wave LoadWave(const char *fileName)
|
|
{
|
|
Wave wave = { 0 };
|
|
|
|
if (IsFileExtension(fileName, ".wav")) wave = LoadWAV(fileName);
|
|
#if defined(SUPPORT_FILEFORMAT_OGG)
|
|
else if (IsFileExtension(fileName, ".ogg")) wave = LoadOGG(fileName);
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
else if (IsFileExtension(fileName, ".flac")) wave = LoadFLAC(fileName);
|
|
#endif
|
|
else TraceLog(LOG_WARNING, "[%s] Audio fileformat not supported, it can't be loaded", fileName);
|
|
|
|
return wave;
|
|
}
|
|
|
|
// Load wave data from raw array data
|
|
Wave LoadWaveEx(void *data, int sampleCount, int sampleRate, int sampleSize, int channels)
|
|
{
|
|
Wave wave;
|
|
|
|
wave.data = data;
|
|
wave.sampleCount = sampleCount;
|
|
wave.sampleRate = sampleRate;
|
|
wave.sampleSize = sampleSize;
|
|
wave.channels = channels;
|
|
|
|
// NOTE: Copy wave data to work with, user is responsible of input data to free
|
|
Wave cwave = WaveCopy(wave);
|
|
|
|
WaveFormat(&cwave, sampleRate, sampleSize, channels);
|
|
|
|
return cwave;
|
|
}
|
|
|
|
// Load sound from file
|
|
// NOTE: The entire file is loaded to memory to be played (no-streaming)
|
|
Sound LoadSound(const char *fileName)
|
|
{
|
|
Wave wave = LoadWave(fileName);
|
|
|
|
Sound sound = LoadSoundFromWave(wave);
|
|
|
|
UnloadWave(wave); // Sound is loaded, we can unload wave
|
|
|
|
return sound;
|
|
}
|
|
|
|
// Load sound from wave data
|
|
// NOTE: Wave data must be unallocated manually
|
|
Sound LoadSoundFromWave(Wave wave)
|
|
{
|
|
Sound sound = { 0 };
|
|
|
|
if (wave.data != NULL)
|
|
{
|
|
#if USE_MINI_AL
|
|
// When using mini_al we need to do our own mixing. To simplify this we need convert the format of each sound to be consistent with
|
|
// the format used to open the playback device. We can do this two ways:
|
|
//
|
|
// 1) Convert the whole sound in one go at load time (here).
|
|
// 2) Convert the audio data in chunks at mixing time.
|
|
//
|
|
// I have decided on the first option because it offloads work required for the format conversion to the to the loading stage. The
|
|
// downside to this is that it uses more memory if the original sound is u8 or s16.
|
|
mal_format formatIn = ((wave.sampleSize == 8) ? mal_format_u8 : ((wave.sampleSize == 16) ? mal_format_s16 : mal_format_f32));
|
|
mal_uint32 frameCountIn = wave.sampleCount; // Is wave->sampleCount actually the frame count? That terminology needs to change, if so.
|
|
|
|
mal_uint32 frameCount = mal_convert_frames(NULL, DEVICE_FORMAT, DEVICE_CHANNELS, DEVICE_SAMPLE_RATE, NULL, formatIn, wave.channels, wave.sampleRate, frameCountIn);
|
|
if (frameCount == 0) TraceLog(LOG_WARNING, "LoadSoundFromWave() : Failed to get frame count for format conversion");
|
|
|
|
AudioBuffer* audioBuffer = CreateAudioBuffer(DEVICE_FORMAT, DEVICE_CHANNELS, DEVICE_SAMPLE_RATE, frameCount, AUDIO_BUFFER_USAGE_STATIC);
|
|
if (audioBuffer == NULL) TraceLog(LOG_WARNING, "LoadSoundFromWave() : Failed to create audio buffer");
|
|
|
|
frameCount = mal_convert_frames(audioBuffer->buffer, audioBuffer->dsp.config.formatIn, audioBuffer->dsp.config.channelsIn, audioBuffer->dsp.config.sampleRateIn, wave.data, formatIn, wave.channels, wave.sampleRate, frameCountIn);
|
|
if (frameCount == 0) TraceLog(LOG_WARNING, "LoadSoundFromWave() : Format conversion failed");
|
|
|
|
sound.audioBuffer = audioBuffer;
|
|
#else
|
|
ALenum format = 0;
|
|
|
|
// The OpenAL format is worked out by looking at the number of channels and the sample size (bits per sample)
|
|
if (wave.channels == 1)
|
|
{
|
|
switch (wave.sampleSize)
|
|
{
|
|
case 8: format = AL_FORMAT_MONO8; break;
|
|
case 16: format = AL_FORMAT_MONO16; break;
|
|
case 32: format = AL_FORMAT_MONO_FLOAT32; break; // Requires OpenAL extension: AL_EXT_FLOAT32
|
|
default: TraceLog(LOG_WARNING, "Wave sample size not supported: %i", wave.sampleSize); break;
|
|
}
|
|
}
|
|
else if (wave.channels == 2)
|
|
{
|
|
switch (wave.sampleSize)
|
|
{
|
|
case 8: format = AL_FORMAT_STEREO8; break;
|
|
case 16: format = AL_FORMAT_STEREO16; break;
|
|
case 32: format = AL_FORMAT_STEREO_FLOAT32; break; // Requires OpenAL extension: AL_EXT_FLOAT32
|
|
default: TraceLog(LOG_WARNING, "Wave sample size not supported: %i", wave.sampleSize); break;
|
|
}
|
|
}
|
|
else TraceLog(LOG_WARNING, "Wave number of channels not supported: %i", wave.channels);
|
|
|
|
// Create an audio source
|
|
ALuint source;
|
|
alGenSources(1, &source); // Generate pointer to audio source
|
|
|
|
alSourcef(source, AL_PITCH, 1.0f);
|
|
alSourcef(source, AL_GAIN, 1.0f);
|
|
alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f);
|
|
alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
|
|
alSourcei(source, AL_LOOPING, AL_FALSE);
|
|
|
|
// Convert loaded data to OpenAL buffer
|
|
//----------------------------------------
|
|
ALuint buffer;
|
|
alGenBuffers(1, &buffer); // Generate pointer to buffer
|
|
|
|
unsigned int dataSize = wave.sampleCount*wave.channels*wave.sampleSize/8; // Size in bytes
|
|
|
|
// Upload sound data to buffer
|
|
alBufferData(buffer, format, wave.data, dataSize, wave.sampleRate);
|
|
|
|
// Attach sound buffer to source
|
|
alSourcei(source, AL_BUFFER, buffer);
|
|
|
|
TraceLog(LOG_INFO, "[SND ID %i][BUFR ID %i] Sound data loaded successfully (%i Hz, %i bit, %s)", source, buffer, wave.sampleRate, wave.sampleSize, (wave.channels == 1) ? "Mono" : "Stereo");
|
|
|
|
sound.source = source;
|
|
sound.buffer = buffer;
|
|
sound.format = format;
|
|
#endif
|
|
}
|
|
|
|
return sound;
|
|
}
|
|
|
|
// Unload wave data
|
|
void UnloadWave(Wave wave)
|
|
{
|
|
if (wave.data != NULL) free(wave.data);
|
|
|
|
TraceLog(LOG_INFO, "Unloaded wave data from RAM");
|
|
}
|
|
|
|
// Unload sound
|
|
void UnloadSound(Sound sound)
|
|
{
|
|
#if USE_MINI_AL
|
|
DeleteAudioBuffer((AudioBuffer *)sound.audioBuffer);
|
|
#else
|
|
alSourceStop(sound.source);
|
|
|
|
alDeleteSources(1, &sound.source);
|
|
alDeleteBuffers(1, &sound.buffer);
|
|
#endif
|
|
|
|
TraceLog(LOG_INFO, "[SND ID %i][BUFR ID %i] Unloaded sound data from RAM", sound.source, sound.buffer);
|
|
}
|
|
|
|
// Update sound buffer with new data
|
|
// NOTE: data must match sound.format
|
|
void UpdateSound(Sound sound, const void *data, int samplesCount)
|
|
{
|
|
#if USE_MINI_AL
|
|
AudioBuffer *audioBuffer = (AudioBuffer *)sound.audioBuffer;
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "UpdateSound() : Invalid sound - no audio buffer");
|
|
return;
|
|
}
|
|
|
|
StopAudioBuffer(audioBuffer);
|
|
|
|
// TODO: May want to lock/unlock this since this data buffer is read at mixing time.
|
|
memcpy(audioBuffer->buffer, data, samplesCount*audioBuffer->dsp.config.channelsIn*mal_get_sample_size_in_bytes(audioBuffer->dsp.config.formatIn));
|
|
#else
|
|
ALint sampleRate, sampleSize, channels;
|
|
alGetBufferi(sound.buffer, AL_FREQUENCY, &sampleRate);
|
|
alGetBufferi(sound.buffer, AL_BITS, &sampleSize); // It could also be retrieved from sound.format
|
|
alGetBufferi(sound.buffer, AL_CHANNELS, &channels); // It could also be retrieved from sound.format
|
|
|
|
TraceLog(LOG_DEBUG, "UpdateSound() : AL_FREQUENCY: %i", sampleRate);
|
|
TraceLog(LOG_DEBUG, "UpdateSound() : AL_BITS: %i", sampleSize);
|
|
TraceLog(LOG_DEBUG, "UpdateSound() : AL_CHANNELS: %i", channels);
|
|
|
|
unsigned int dataSize = samplesCount*channels*sampleSize/8; // Size of data in bytes
|
|
|
|
alSourceStop(sound.source); // Stop sound
|
|
alSourcei(sound.source, AL_BUFFER, 0); // Unbind buffer from sound to update
|
|
//alDeleteBuffers(1, &sound.buffer); // Delete current buffer data
|
|
//alGenBuffers(1, &sound.buffer); // Generate new buffer
|
|
|
|
// Upload new data to sound buffer
|
|
alBufferData(sound.buffer, sound.format, data, dataSize, sampleRate);
|
|
|
|
// Attach sound buffer to source again
|
|
alSourcei(sound.source, AL_BUFFER, sound.buffer);
|
|
#endif
|
|
}
|
|
|
|
// Play a sound
|
|
void PlaySound(Sound sound)
|
|
{
|
|
#if USE_MINI_AL
|
|
PlayAudioBuffer((AudioBuffer *)sound.audioBuffer);
|
|
#else
|
|
alSourcePlay(sound.source); // Play the sound
|
|
#endif
|
|
|
|
//TraceLog(LOG_INFO, "Playing sound");
|
|
|
|
// Find the current position of the sound being played
|
|
// NOTE: Only work when the entire file is in a single buffer
|
|
//int byteOffset;
|
|
//alGetSourcei(sound.source, AL_BYTE_OFFSET, &byteOffset);
|
|
//
|
|
//int sampleRate;
|
|
//alGetBufferi(sound.buffer, AL_FREQUENCY, &sampleRate); // AL_CHANNELS, AL_BITS (bps)
|
|
|
|
//float seconds = (float)byteOffset/sampleRate; // Number of seconds since the beginning of the sound
|
|
//or
|
|
//float result;
|
|
//alGetSourcef(sound.source, AL_SEC_OFFSET, &result); // AL_SAMPLE_OFFSET
|
|
}
|
|
|
|
// Pause a sound
|
|
void PauseSound(Sound sound)
|
|
{
|
|
#if USE_MINI_AL
|
|
PauseAudioBuffer((AudioBuffer *)sound.audioBuffer);
|
|
#else
|
|
alSourcePause(sound.source);
|
|
#endif
|
|
}
|
|
|
|
// Resume a paused sound
|
|
void ResumeSound(Sound sound)
|
|
{
|
|
#if USE_MINI_AL
|
|
ResumeAudioBuffer((AudioBuffer *)sound.audioBuffer);
|
|
#else
|
|
ALenum state;
|
|
|
|
alGetSourcei(sound.source, AL_SOURCE_STATE, &state);
|
|
|
|
if (state == AL_PAUSED) alSourcePlay(sound.source);
|
|
#endif
|
|
}
|
|
|
|
// Stop reproducing a sound
|
|
void StopSound(Sound sound)
|
|
{
|
|
#if USE_MINI_AL
|
|
StopAudioBuffer((AudioBuffer *)sound.audioBuffer);
|
|
#else
|
|
alSourceStop(sound.source);
|
|
#endif
|
|
}
|
|
|
|
// Check if a sound is playing
|
|
bool IsSoundPlaying(Sound sound)
|
|
{
|
|
#if USE_MINI_AL
|
|
return IsAudioBufferPlaying((AudioBuffer *)sound.audioBuffer);
|
|
#else
|
|
bool playing = false;
|
|
ALint state;
|
|
|
|
alGetSourcei(sound.source, AL_SOURCE_STATE, &state);
|
|
if (state == AL_PLAYING) playing = true;
|
|
|
|
return playing;
|
|
#endif
|
|
}
|
|
|
|
// Set volume for a sound
|
|
void SetSoundVolume(Sound sound, float volume)
|
|
{
|
|
#if USE_MINI_AL
|
|
SetAudioBufferVolume((AudioBuffer *)sound.audioBuffer, volume);
|
|
#else
|
|
alSourcef(sound.source, AL_GAIN, volume);
|
|
#endif
|
|
}
|
|
|
|
// Set pitch for a sound
|
|
void SetSoundPitch(Sound sound, float pitch)
|
|
{
|
|
#if USE_MINI_AL
|
|
SetAudioBufferPitch((AudioBuffer *)sound.audioBuffer, pitch);
|
|
#else
|
|
alSourcef(sound.source, AL_PITCH, pitch);
|
|
#endif
|
|
}
|
|
|
|
// Convert wave data to desired format
|
|
void WaveFormat(Wave *wave, int sampleRate, int sampleSize, int channels)
|
|
{
|
|
#if USE_MINI_AL
|
|
mal_format formatIn = ((wave->sampleSize == 8) ? mal_format_u8 : ((wave->sampleSize == 16) ? mal_format_s16 : mal_format_f32));
|
|
mal_format formatOut = (( sampleSize == 8) ? mal_format_u8 : (( sampleSize == 16) ? mal_format_s16 : mal_format_f32));
|
|
|
|
mal_uint32 frameCountIn = wave->sampleCount; // Is wave->sampleCount actually the frame count? That terminology needs to change, if so.
|
|
|
|
mal_uint32 frameCount = mal_convert_frames(NULL, formatOut, channels, sampleRate, NULL, formatIn, wave->channels, wave->sampleRate, frameCountIn);
|
|
if (frameCount == 0)
|
|
{
|
|
TraceLog(LOG_ERROR, "WaveFormat() : Failed to get frame count for format conversion.");
|
|
return;
|
|
}
|
|
|
|
void *data = malloc(frameCount*channels*(sampleSize/8));
|
|
|
|
frameCount = mal_convert_frames(data, formatOut, channels, sampleRate, wave->data, formatIn, wave->channels, wave->sampleRate, frameCountIn);
|
|
if (frameCount == 0)
|
|
{
|
|
TraceLog(LOG_ERROR, "WaveFormat() : Format conversion failed.");
|
|
return;
|
|
}
|
|
|
|
wave->sampleCount = frameCount;
|
|
wave->sampleSize = sampleSize;
|
|
wave->sampleRate = sampleRate;
|
|
wave->channels = channels;
|
|
free(wave->data);
|
|
wave->data = data;
|
|
|
|
#else
|
|
// Format sample rate
|
|
// NOTE: Only supported 22050 <--> 44100
|
|
if (wave->sampleRate != sampleRate)
|
|
{
|
|
// TODO: Resample wave data (upsampling or downsampling)
|
|
// NOTE 1: To downsample, you have to drop samples or average them.
|
|
// NOTE 2: To upsample, you have to interpolate new samples.
|
|
|
|
wave->sampleRate = sampleRate;
|
|
}
|
|
|
|
// Format sample size
|
|
// NOTE: Only supported 8 bit <--> 16 bit <--> 32 bit
|
|
if (wave->sampleSize != sampleSize)
|
|
{
|
|
void *data = malloc(wave->sampleCount*wave->channels*sampleSize/8);
|
|
|
|
for (int i = 0; i < wave->sampleCount; i++)
|
|
{
|
|
for (int j = 0; j < wave->channels; j++)
|
|
{
|
|
if (sampleSize == 8)
|
|
{
|
|
if (wave->sampleSize == 16) ((unsigned char *)data)[wave->channels*i + j] = (unsigned char)(((float)(((short *)wave->data)[wave->channels*i + j])/32767.0f)*256);
|
|
else if (wave->sampleSize == 32) ((unsigned char *)data)[wave->channels*i + j] = (unsigned char)(((float *)wave->data)[wave->channels*i + j]*127.0f + 127);
|
|
}
|
|
else if (sampleSize == 16)
|
|
{
|
|
if (wave->sampleSize == 8) ((short *)data)[wave->channels*i + j] = (short)(((float)(((unsigned char *)wave->data)[wave->channels*i + j] - 127)/256.0f)*32767);
|
|
else if (wave->sampleSize == 32) ((short *)data)[wave->channels*i + j] = (short)((((float *)wave->data)[wave->channels*i + j])*32767);
|
|
}
|
|
else if (sampleSize == 32)
|
|
{
|
|
if (wave->sampleSize == 8) ((float *)data)[wave->channels*i + j] = (float)(((unsigned char *)wave->data)[wave->channels*i + j] - 127)/256.0f;
|
|
else if (wave->sampleSize == 16) ((float *)data)[wave->channels*i + j] = (float)(((short *)wave->data)[wave->channels*i + j])/32767.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
wave->sampleSize = sampleSize;
|
|
free(wave->data);
|
|
wave->data = data;
|
|
}
|
|
|
|
// Format channels (interlaced mode)
|
|
// NOTE: Only supported mono <--> stereo
|
|
if (wave->channels != channels)
|
|
{
|
|
void *data = malloc(wave->sampleCount*wave->sampleSize/8*channels);
|
|
|
|
if ((wave->channels == 1) && (channels == 2)) // mono ---> stereo (duplicate mono information)
|
|
{
|
|
for (int i = 0; i < wave->sampleCount; i++)
|
|
{
|
|
for (int j = 0; j < channels; j++)
|
|
{
|
|
if (wave->sampleSize == 8) ((unsigned char *)data)[channels*i + j] = ((unsigned char *)wave->data)[i];
|
|
else if (wave->sampleSize == 16) ((short *)data)[channels*i + j] = ((short *)wave->data)[i];
|
|
else if (wave->sampleSize == 32) ((float *)data)[channels*i + j] = ((float *)wave->data)[i];
|
|
}
|
|
}
|
|
}
|
|
else if ((wave->channels == 2) && (channels == 1)) // stereo ---> mono (mix stereo channels)
|
|
{
|
|
for (int i = 0, j = 0; i < wave->sampleCount; i++, j += 2)
|
|
{
|
|
if (wave->sampleSize == 8) ((unsigned char *)data)[i] = (((unsigned char *)wave->data)[j] + ((unsigned char *)wave->data)[j + 1])/2;
|
|
else if (wave->sampleSize == 16) ((short *)data)[i] = (((short *)wave->data)[j] + ((short *)wave->data)[j + 1])/2;
|
|
else if (wave->sampleSize == 32) ((float *)data)[i] = (((float *)wave->data)[j] + ((float *)wave->data)[j + 1])/2.0f;
|
|
}
|
|
}
|
|
|
|
// TODO: Add/remove additional interlaced channels
|
|
|
|
wave->channels = channels;
|
|
free(wave->data);
|
|
wave->data = data;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Copy a wave to a new wave
|
|
Wave WaveCopy(Wave wave)
|
|
{
|
|
Wave newWave = { 0 };
|
|
|
|
newWave.data = malloc(wave.sampleCount*wave.sampleSize/8*wave.channels);
|
|
|
|
if (newWave.data != NULL)
|
|
{
|
|
// NOTE: Size must be provided in bytes
|
|
memcpy(newWave.data, wave.data, wave.sampleCount*wave.channels*wave.sampleSize/8);
|
|
|
|
newWave.sampleCount = wave.sampleCount;
|
|
newWave.sampleRate = wave.sampleRate;
|
|
newWave.sampleSize = wave.sampleSize;
|
|
newWave.channels = wave.channels;
|
|
}
|
|
|
|
return newWave;
|
|
}
|
|
|
|
// Crop a wave to defined samples range
|
|
// NOTE: Security check in case of out-of-range
|
|
void WaveCrop(Wave *wave, int initSample, int finalSample)
|
|
{
|
|
if ((initSample >= 0) && (initSample < finalSample) &&
|
|
(finalSample > 0) && (finalSample < wave->sampleCount))
|
|
{
|
|
int sampleCount = finalSample - initSample;
|
|
|
|
void *data = malloc(sampleCount*wave->sampleSize/8*wave->channels);
|
|
|
|
memcpy(data, (unsigned char*)wave->data + (initSample*wave->channels*wave->sampleSize/8), sampleCount*wave->channels*wave->sampleSize/8);
|
|
|
|
free(wave->data);
|
|
wave->data = data;
|
|
}
|
|
else TraceLog(LOG_WARNING, "Wave crop range out of bounds");
|
|
}
|
|
|
|
// Get samples data from wave as a floats array
|
|
// NOTE: Returned sample values are normalized to range [-1..1]
|
|
float *GetWaveData(Wave wave)
|
|
{
|
|
float *samples = (float *)malloc(wave.sampleCount*wave.channels*sizeof(float));
|
|
|
|
for (int i = 0; i < wave.sampleCount; i++)
|
|
{
|
|
for (int j = 0; j < wave.channels; j++)
|
|
{
|
|
if (wave.sampleSize == 8) samples[wave.channels*i + j] = (float)(((unsigned char *)wave.data)[wave.channels*i + j] - 127)/256.0f;
|
|
else if (wave.sampleSize == 16) samples[wave.channels*i + j] = (float)((short *)wave.data)[wave.channels*i + j]/32767.0f;
|
|
else if (wave.sampleSize == 32) samples[wave.channels*i + j] = ((float *)wave.data)[wave.channels*i + j];
|
|
}
|
|
}
|
|
|
|
return samples;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Module Functions Definition - Music loading and stream playing (.OGG)
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Load music stream from file
|
|
Music LoadMusicStream(const char *fileName)
|
|
{
|
|
Music music = (MusicData *)malloc(sizeof(MusicData));
|
|
|
|
if (IsFileExtension(fileName, ".ogg"))
|
|
{
|
|
// Open ogg audio stream
|
|
music->ctxOgg = stb_vorbis_open_filename(fileName, NULL, NULL);
|
|
|
|
if (music->ctxOgg == NULL) TraceLog(LOG_WARNING, "[%s] OGG audio file could not be opened", fileName);
|
|
else
|
|
{
|
|
stb_vorbis_info info = stb_vorbis_get_info(music->ctxOgg); // Get Ogg file info
|
|
|
|
// OGG bit rate defaults to 16 bit, it's enough for compressed format
|
|
music->stream = InitAudioStream(info.sample_rate, 16, info.channels);
|
|
music->totalSamples = (unsigned int)stb_vorbis_stream_length_in_samples(music->ctxOgg); // Independent by channel
|
|
music->samplesLeft = music->totalSamples;
|
|
music->ctxType = MUSIC_AUDIO_OGG;
|
|
music->loopCount = -1; // Infinite loop by default
|
|
|
|
TraceLog(LOG_DEBUG, "[%s] FLAC total samples: %i", fileName, music->totalSamples);
|
|
TraceLog(LOG_DEBUG, "[%s] OGG sample rate: %i", fileName, info.sample_rate);
|
|
TraceLog(LOG_DEBUG, "[%s] OGG channels: %i", fileName, info.channels);
|
|
TraceLog(LOG_DEBUG, "[%s] OGG memory required: %i", fileName, info.temp_memory_required);
|
|
}
|
|
}
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
else if (IsFileExtension(fileName, ".flac"))
|
|
{
|
|
music->ctxFlac = drflac_open_file(fileName);
|
|
|
|
if (music->ctxFlac == NULL) TraceLog(LOG_WARNING, "[%s] FLAC audio file could not be opened", fileName);
|
|
else
|
|
{
|
|
music->stream = InitAudioStream(music->ctxFlac->sampleRate, music->ctxFlac->bitsPerSample, music->ctxFlac->channels);
|
|
music->totalSamples = (unsigned int)music->ctxFlac->totalSampleCount/music->ctxFlac->channels;
|
|
music->samplesLeft = music->totalSamples;
|
|
music->ctxType = MUSIC_AUDIO_FLAC;
|
|
music->loopCount = -1; // Infinite loop by default
|
|
|
|
TraceLog(LOG_DEBUG, "[%s] FLAC total samples: %i", fileName, music->totalSamples);
|
|
TraceLog(LOG_DEBUG, "[%s] FLAC sample rate: %i", fileName, music->ctxFlac->sampleRate);
|
|
TraceLog(LOG_DEBUG, "[%s] FLAC bits per sample: %i", fileName, music->ctxFlac->bitsPerSample);
|
|
TraceLog(LOG_DEBUG, "[%s] FLAC channels: %i", fileName, music->ctxFlac->channels);
|
|
}
|
|
}
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_XM)
|
|
else if (IsFileExtension(fileName, ".xm"))
|
|
{
|
|
int result = jar_xm_create_context_from_file(&music->ctxXm, 48000, fileName);
|
|
|
|
if (!result) // XM context created successfully
|
|
{
|
|
jar_xm_set_max_loop_count(music->ctxXm, 0); // Set infinite number of loops
|
|
|
|
// NOTE: Only stereo is supported for XM
|
|
music->stream = InitAudioStream(48000, 16, 2);
|
|
music->totalSamples = (unsigned int)jar_xm_get_remaining_samples(music->ctxXm);
|
|
music->samplesLeft = music->totalSamples;
|
|
music->ctxType = MUSIC_MODULE_XM;
|
|
music->loopCount = -1; // Infinite loop by default
|
|
|
|
TraceLog(LOG_DEBUG, "[%s] XM number of samples: %i", fileName, music->totalSamples);
|
|
TraceLog(LOG_DEBUG, "[%s] XM track length: %11.6f sec", fileName, (float)music->totalSamples/48000.0f);
|
|
}
|
|
else TraceLog(LOG_WARNING, "[%s] XM file could not be opened", fileName);
|
|
}
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_MOD)
|
|
else if (IsFileExtension(fileName, ".mod"))
|
|
{
|
|
jar_mod_init(&music->ctxMod);
|
|
|
|
if (jar_mod_load_file(&music->ctxMod, fileName))
|
|
{
|
|
music->stream = InitAudioStream(48000, 16, 2);
|
|
music->totalSamples = (unsigned int)jar_mod_max_samples(&music->ctxMod);
|
|
music->samplesLeft = music->totalSamples;
|
|
music->ctxType = MUSIC_MODULE_MOD;
|
|
music->loopCount = -1; // Infinite loop by default
|
|
|
|
TraceLog(LOG_DEBUG, "[%s] MOD number of samples: %i", fileName, music->samplesLeft);
|
|
TraceLog(LOG_DEBUG, "[%s] MOD track length: %11.6f sec", fileName, (float)music->totalSamples/48000.0f);
|
|
}
|
|
else TraceLog(LOG_WARNING, "[%s] MOD file could not be opened", fileName);
|
|
}
|
|
#endif
|
|
else TraceLog(LOG_WARNING, "[%s] Audio fileformat not supported, it can't be loaded", fileName);
|
|
|
|
return music;
|
|
}
|
|
|
|
// Unload music stream
|
|
void UnloadMusicStream(Music music)
|
|
{
|
|
CloseAudioStream(music->stream);
|
|
|
|
if (music->ctxType == MUSIC_AUDIO_OGG) stb_vorbis_close(music->ctxOgg);
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
else if (music->ctxType == MUSIC_AUDIO_FLAC) drflac_free(music->ctxFlac);
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_XM)
|
|
else if (music->ctxType == MUSIC_MODULE_XM) jar_xm_free_context(music->ctxXm);
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_MOD)
|
|
else if (music->ctxType == MUSIC_MODULE_MOD) jar_mod_unload(&music->ctxMod);
|
|
#endif
|
|
|
|
free(music);
|
|
}
|
|
|
|
// Start music playing (open stream)
|
|
void PlayMusicStream(Music music)
|
|
{
|
|
#if USE_MINI_AL
|
|
AudioBuffer *audioBuffer = (AudioBuffer *)music->stream.audioBuffer;
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "PlayMusicStream() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
// For music streams, we need to make sure we maintain the frame cursor position. This is hack for this section of code in UpdateMusicStream()
|
|
// // NOTE: In case window is minimized, music stream is stopped,
|
|
// // just make sure to play again on window restore
|
|
// if (IsMusicPlaying(music)) PlayMusicStream(music);
|
|
mal_uint32 frameCursorPos = audioBuffer->frameCursorPos;
|
|
|
|
PlayAudioStream(music->stream); // <-- This resets the cursor position.
|
|
|
|
audioBuffer->frameCursorPos = frameCursorPos;
|
|
#else
|
|
alSourcePlay(music->stream.source);
|
|
#endif
|
|
}
|
|
|
|
// Pause music playing
|
|
void PauseMusicStream(Music music)
|
|
{
|
|
#if USE_MINI_AL
|
|
PauseAudioStream(music->stream);
|
|
#else
|
|
alSourcePause(music->stream.source);
|
|
#endif
|
|
}
|
|
|
|
// Resume music playing
|
|
void ResumeMusicStream(Music music)
|
|
{
|
|
#if USE_MINI_AL
|
|
ResumeAudioStream(music->stream);
|
|
#else
|
|
ALenum state;
|
|
alGetSourcei(music->stream.source, AL_SOURCE_STATE, &state);
|
|
|
|
if (state == AL_PAUSED)
|
|
{
|
|
TraceLog(LOG_INFO, "[AUD ID %i] Resume music stream playing", music->stream.source);
|
|
alSourcePlay(music->stream.source);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Stop music playing (close stream)
|
|
// TODO: To clear a buffer, make sure they have been already processed!
|
|
void StopMusicStream(Music music)
|
|
{
|
|
#if USE_MINI_AL
|
|
StopAudioStream(music->stream);
|
|
#else
|
|
alSourceStop(music->stream.source);
|
|
|
|
/*
|
|
// Clear stream buffers
|
|
// WARNING: Queued buffers must have been processed before unqueueing and reloaded with data!!!
|
|
void *pcm = calloc(AUDIO_BUFFER_SIZE*music->stream.sampleSize/8*music->stream.channels, 1);
|
|
|
|
for (int i = 0; i < MAX_STREAM_BUFFERS; i++)
|
|
{
|
|
//UpdateAudioStream(music->stream, pcm, AUDIO_BUFFER_SIZE); // Update one buffer at a time
|
|
alBufferData(music->stream.buffers[i], music->stream.format, pcm, AUDIO_BUFFER_SIZE*music->stream.sampleSize/8*music->stream.channels, music->stream.sampleRate);
|
|
}
|
|
|
|
free(pcm);
|
|
*/
|
|
#endif
|
|
|
|
// Restart music context
|
|
switch (music->ctxType)
|
|
{
|
|
case MUSIC_AUDIO_OGG: stb_vorbis_seek_start(music->ctxOgg); break;
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
case MUSIC_MODULE_FLAC: /* TODO: Restart FLAC context */ break;
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_XM)
|
|
case MUSIC_MODULE_XM: /* TODO: Restart XM context */ break;
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_MOD)
|
|
case MUSIC_MODULE_MOD: jar_mod_seek_start(&music->ctxMod); break;
|
|
#endif
|
|
default: break;
|
|
}
|
|
|
|
music->samplesLeft = music->totalSamples;
|
|
}
|
|
|
|
// Update (re-fill) music buffers if data already processed
|
|
// TODO: Make sure buffers are ready for update... check music state
|
|
void UpdateMusicStream(Music music)
|
|
{
|
|
#if USE_MINI_AL
|
|
bool streamEnding = false;
|
|
|
|
unsigned int subBufferSizeInFrames = ((AudioBuffer *)music->stream.audioBuffer)->bufferSizeInFrames/2;
|
|
|
|
// NOTE: Using dynamic allocation because it could require more than 16KB
|
|
void *pcm = calloc(subBufferSizeInFrames*music->stream.sampleSize/8*music->stream.channels, 1);
|
|
|
|
int samplesCount = 0; // Total size of data steamed in L+R samples for xm floats, individual L or R for ogg shorts
|
|
|
|
while (IsAudioBufferProcessed(music->stream))
|
|
{
|
|
if (music->samplesLeft >= subBufferSizeInFrames) samplesCount = subBufferSizeInFrames;
|
|
else samplesCount = music->samplesLeft;
|
|
|
|
// TODO: Really don't like ctxType thingy...
|
|
switch (music->ctxType)
|
|
{
|
|
case MUSIC_AUDIO_OGG:
|
|
{
|
|
// NOTE: Returns the number of samples to process (be careful! we ask for number of shorts!)
|
|
stb_vorbis_get_samples_short_interleaved(music->ctxOgg, music->stream.channels, (short *)pcm, samplesCount*music->stream.channels);
|
|
|
|
} break;
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
case MUSIC_AUDIO_FLAC:
|
|
{
|
|
// NOTE: Returns the number of samples to process
|
|
unsigned int numSamplesFlac = (unsigned int)drflac_read_s16(music->ctxFlac, samplesCount*music->stream.channels, (short *)pcm);
|
|
|
|
} break;
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_XM)
|
|
case MUSIC_MODULE_XM: jar_xm_generate_samples_16bit(music->ctxXm, pcm, samplesCount); break;
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_MOD)
|
|
case MUSIC_MODULE_MOD: jar_mod_fillbuffer(&music->ctxMod, pcm, samplesCount, 0); break;
|
|
#endif
|
|
default: break;
|
|
}
|
|
|
|
UpdateAudioStream(music->stream, pcm, samplesCount);
|
|
music->samplesLeft -= samplesCount;
|
|
|
|
if (music->samplesLeft <= 0)
|
|
{
|
|
streamEnding = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Free allocated pcm data
|
|
free(pcm);
|
|
|
|
// Reset audio stream for looping
|
|
if (streamEnding)
|
|
{
|
|
StopMusicStream(music); // Stop music (and reset)
|
|
|
|
// Decrease loopCount to stop when required
|
|
if (music->loopCount > 0)
|
|
{
|
|
music->loopCount--; // Decrease loop count
|
|
PlayMusicStream(music); // Play again
|
|
}
|
|
else
|
|
{
|
|
if (music->loopCount == -1) PlayMusicStream(music);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// NOTE: In case window is minimized, music stream is stopped,
|
|
// just make sure to play again on window restore
|
|
if (IsMusicPlaying(music)) PlayMusicStream(music);
|
|
}
|
|
#else
|
|
ALenum state;
|
|
ALint processed = 0;
|
|
|
|
alGetSourcei(music->stream.source, AL_SOURCE_STATE, &state); // Get music stream state
|
|
alGetSourcei(music->stream.source, AL_BUFFERS_PROCESSED, &processed); // Get processed buffers
|
|
|
|
if (processed > 0)
|
|
{
|
|
bool streamEnding = false;
|
|
|
|
// NOTE: Using dynamic allocation because it could require more than 16KB
|
|
void *pcm = calloc(AUDIO_BUFFER_SIZE*music->stream.sampleSize/8*music->stream.channels, 1);
|
|
|
|
int numBuffersToProcess = processed;
|
|
int samplesCount = 0; // Total size of data steamed in L+R samples for xm floats,
|
|
// individual L or R for ogg shorts
|
|
|
|
for (int i = 0; i < numBuffersToProcess; i++)
|
|
{
|
|
if (music->samplesLeft >= AUDIO_BUFFER_SIZE) samplesCount = AUDIO_BUFFER_SIZE;
|
|
else samplesCount = music->samplesLeft;
|
|
|
|
// TODO: Really don't like ctxType thingy...
|
|
switch (music->ctxType)
|
|
{
|
|
case MUSIC_AUDIO_OGG:
|
|
{
|
|
// NOTE: Returns the number of samples to process (be careful! we ask for number of shorts!)
|
|
int numSamplesOgg = stb_vorbis_get_samples_short_interleaved(music->ctxOgg, music->stream.channels, (short *)pcm, samplesCount*music->stream.channels);
|
|
|
|
} break;
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
case MUSIC_AUDIO_FLAC:
|
|
{
|
|
// NOTE: Returns the number of samples to process
|
|
unsigned int numSamplesFlac = (unsigned int)drflac_read_s16(music->ctxFlac, samplesCount*music->stream.channels, (short *)pcm);
|
|
|
|
} break;
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_XM)
|
|
case MUSIC_MODULE_XM: jar_xm_generate_samples_16bit(music->ctxXm, pcm, samplesCount); break;
|
|
#endif
|
|
#if defined(SUPPORT_FILEFORMAT_MOD)
|
|
case MUSIC_MODULE_MOD: jar_mod_fillbuffer(&music->ctxMod, pcm, samplesCount, 0); break;
|
|
#endif
|
|
default: break;
|
|
}
|
|
|
|
UpdateAudioStream(music->stream, pcm, samplesCount);
|
|
music->samplesLeft -= samplesCount;
|
|
|
|
if (music->samplesLeft <= 0)
|
|
{
|
|
streamEnding = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Free allocated pcm data
|
|
free(pcm);
|
|
|
|
// Reset audio stream for looping
|
|
if (streamEnding)
|
|
{
|
|
StopMusicStream(music); // Stop music (and reset)
|
|
|
|
// Decrease loopCount to stop when required
|
|
if (music->loopCount > 0)
|
|
{
|
|
music->loopCount--; // Decrease loop count
|
|
PlayMusicStream(music); // Play again
|
|
}
|
|
else
|
|
{
|
|
if (music->loopCount == -1)
|
|
{
|
|
PlayMusicStream(music);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// NOTE: In case window is minimized, music stream is stopped,
|
|
// just make sure to play again on window restore
|
|
if (state != AL_PLAYING) PlayMusicStream(music);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Check if any music is playing
|
|
bool IsMusicPlaying(Music music)
|
|
{
|
|
#if USE_MINI_AL
|
|
return IsAudioStreamPlaying(music->stream);
|
|
#else
|
|
bool playing = false;
|
|
ALint state;
|
|
|
|
alGetSourcei(music->stream.source, AL_SOURCE_STATE, &state);
|
|
|
|
if (state == AL_PLAYING) playing = true;
|
|
|
|
return playing;
|
|
#endif
|
|
}
|
|
|
|
// Set volume for music
|
|
void SetMusicVolume(Music music, float volume)
|
|
{
|
|
#if USE_MINI_AL
|
|
SetAudioStreamVolume(music->stream, volume);
|
|
#else
|
|
alSourcef(music->stream.source, AL_GAIN, volume);
|
|
#endif
|
|
}
|
|
|
|
// Set pitch for music
|
|
void SetMusicPitch(Music music, float pitch)
|
|
{
|
|
#if USE_MINI_AL
|
|
SetAudioStreamPitch(music->stream, pitch);
|
|
#else
|
|
alSourcef(music->stream.source, AL_PITCH, pitch);
|
|
#endif
|
|
}
|
|
|
|
// Set music loop count (loop repeats)
|
|
// NOTE: If set to -1, means infinite loop
|
|
void SetMusicLoopCount(Music music, int count)
|
|
{
|
|
music->loopCount = count;
|
|
}
|
|
|
|
// Get music time length (in seconds)
|
|
float GetMusicTimeLength(Music music)
|
|
{
|
|
float totalSeconds = (float)music->totalSamples/music->stream.sampleRate;
|
|
|
|
return totalSeconds;
|
|
}
|
|
|
|
// Get current music time played (in seconds)
|
|
float GetMusicTimePlayed(Music music)
|
|
{
|
|
float secondsPlayed = 0.0f;
|
|
|
|
unsigned int samplesPlayed = music->totalSamples - music->samplesLeft;
|
|
secondsPlayed = (float)samplesPlayed/music->stream.sampleRate;
|
|
|
|
return secondsPlayed;
|
|
}
|
|
|
|
|
|
// Init audio stream (to stream audio pcm data)
|
|
AudioStream InitAudioStream(unsigned int sampleRate, unsigned int sampleSize, unsigned int channels)
|
|
{
|
|
AudioStream stream = { 0 };
|
|
|
|
stream.sampleRate = sampleRate;
|
|
stream.sampleSize = sampleSize;
|
|
|
|
// Only mono and stereo channels are supported, more channels require AL_EXT_MCFORMATS extension
|
|
if ((channels > 0) && (channels < 3)) stream.channels = channels;
|
|
else
|
|
{
|
|
TraceLog(LOG_WARNING, "Init audio stream: Number of channels not supported: %i", channels);
|
|
stream.channels = 1; // Fallback to mono channel
|
|
}
|
|
|
|
|
|
#if USE_MINI_AL
|
|
mal_format formatIn = ((stream.sampleSize == 8) ? mal_format_u8 : ((stream.sampleSize == 16) ? mal_format_s16 : mal_format_f32));
|
|
|
|
// The size of a streaming buffer must be at least double the size of a period.
|
|
unsigned int periodSize = device.bufferSizeInFrames / device.periods;
|
|
unsigned int subBufferSize = AUDIO_BUFFER_SIZE;
|
|
if (subBufferSize < periodSize) subBufferSize = periodSize;
|
|
|
|
AudioBuffer *audioBuffer = CreateAudioBuffer(formatIn, stream.channels, stream.sampleRate, subBufferSize*2, AUDIO_BUFFER_USAGE_STREAM);
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "InitAudioStream() : Failed to create audio buffer");
|
|
return stream;
|
|
}
|
|
|
|
audioBuffer->looping = true; // Always loop for streaming buffers.
|
|
stream.audioBuffer = audioBuffer;
|
|
#else
|
|
// Setup OpenAL format
|
|
if (stream.channels == 1)
|
|
{
|
|
switch (sampleSize)
|
|
{
|
|
case 8: stream.format = AL_FORMAT_MONO8; break;
|
|
case 16: stream.format = AL_FORMAT_MONO16; break;
|
|
case 32: stream.format = AL_FORMAT_MONO_FLOAT32; break; // Requires OpenAL extension: AL_EXT_FLOAT32
|
|
default: TraceLog(LOG_WARNING, "Init audio stream: Sample size not supported: %i", sampleSize); break;
|
|
}
|
|
}
|
|
else if (stream.channels == 2)
|
|
{
|
|
switch (sampleSize)
|
|
{
|
|
case 8: stream.format = AL_FORMAT_STEREO8; break;
|
|
case 16: stream.format = AL_FORMAT_STEREO16; break;
|
|
case 32: stream.format = AL_FORMAT_STEREO_FLOAT32; break; // Requires OpenAL extension: AL_EXT_FLOAT32
|
|
default: TraceLog(LOG_WARNING, "Init audio stream: Sample size not supported: %i", sampleSize); break;
|
|
}
|
|
}
|
|
|
|
// Create an audio source
|
|
alGenSources(1, &stream.source);
|
|
alSourcef(stream.source, AL_PITCH, 1.0f);
|
|
alSourcef(stream.source, AL_GAIN, 1.0f);
|
|
alSource3f(stream.source, AL_POSITION, 0.0f, 0.0f, 0.0f);
|
|
alSource3f(stream.source, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
|
|
|
|
// Create Buffers (double buffering)
|
|
alGenBuffers(MAX_STREAM_BUFFERS, stream.buffers);
|
|
|
|
// Initialize buffer with zeros by default
|
|
// NOTE: Using dynamic allocation because it requires more than 16KB
|
|
void *pcm = calloc(AUDIO_BUFFER_SIZE*stream.sampleSize/8*stream.channels, 1);
|
|
|
|
for (int i = 0; i < MAX_STREAM_BUFFERS; i++)
|
|
{
|
|
alBufferData(stream.buffers[i], stream.format, pcm, AUDIO_BUFFER_SIZE*stream.sampleSize/8*stream.channels, stream.sampleRate);
|
|
}
|
|
|
|
free(pcm);
|
|
|
|
alSourceQueueBuffers(stream.source, MAX_STREAM_BUFFERS, stream.buffers);
|
|
#endif
|
|
|
|
TraceLog(LOG_INFO, "[AUD ID %i] Audio stream loaded successfully (%i Hz, %i bit, %s)", stream.source, stream.sampleRate, stream.sampleSize, (stream.channels == 1) ? "Mono" : "Stereo");
|
|
|
|
return stream;
|
|
}
|
|
|
|
// Close audio stream and free memory
|
|
void CloseAudioStream(AudioStream stream)
|
|
{
|
|
#if USE_MINI_AL
|
|
DeleteAudioBuffer((AudioBuffer *)stream.audioBuffer);
|
|
#else
|
|
// Stop playing channel
|
|
alSourceStop(stream.source);
|
|
|
|
// Flush out all queued buffers
|
|
int queued = 0;
|
|
alGetSourcei(stream.source, AL_BUFFERS_QUEUED, &queued);
|
|
|
|
ALuint buffer = 0;
|
|
|
|
while (queued > 0)
|
|
{
|
|
alSourceUnqueueBuffers(stream.source, 1, &buffer);
|
|
queued--;
|
|
}
|
|
|
|
// Delete source and buffers
|
|
alDeleteSources(1, &stream.source);
|
|
alDeleteBuffers(MAX_STREAM_BUFFERS, stream.buffers);
|
|
#endif
|
|
|
|
TraceLog(LOG_INFO, "[AUD ID %i] Unloaded audio stream data", stream.source);
|
|
}
|
|
|
|
// Update audio stream buffers with data
|
|
// NOTE 1: Only updates one buffer of the stream source: unqueue -> update -> queue
|
|
// NOTE 2: To unqueue a buffer it needs to be processed: IsAudioBufferProcessed()
|
|
void UpdateAudioStream(AudioStream stream, const void *data, int samplesCount)
|
|
{
|
|
#if USE_MINI_AL
|
|
AudioBuffer *audioBuffer = (AudioBuffer *)stream.audioBuffer;
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "UpdateAudioStream() : No audio buffer");
|
|
return;
|
|
}
|
|
|
|
if (audioBuffer->isSubBufferProcessed[0] || audioBuffer->isSubBufferProcessed[1])
|
|
{
|
|
mal_uint32 subBufferToUpdate;
|
|
if (audioBuffer->isSubBufferProcessed[0] && audioBuffer->isSubBufferProcessed[1])
|
|
{
|
|
// Both buffers are available for updating. Update the first one and make sure the cursor is moved back to the front.
|
|
subBufferToUpdate = 0;
|
|
audioBuffer->frameCursorPos = 0;
|
|
}
|
|
else
|
|
{
|
|
// Just update whichever sub-buffer is processed.
|
|
subBufferToUpdate = (audioBuffer->isSubBufferProcessed[0]) ? 0 : 1;
|
|
}
|
|
|
|
mal_uint32 subBufferSizeInFrames = audioBuffer->bufferSizeInFrames/2;
|
|
unsigned char *subBuffer = audioBuffer->buffer + ((subBufferSizeInFrames*stream.channels*(stream.sampleSize/8))*subBufferToUpdate);
|
|
|
|
// Does this API expect a whole buffer to be updated in one go? Assuming so, but if not will need to change this logic.
|
|
if (subBufferSizeInFrames >= (mal_uint32)samplesCount)
|
|
{
|
|
mal_uint32 framesToWrite = subBufferSizeInFrames;
|
|
if (framesToWrite > (mal_uint32)samplesCount) framesToWrite = (mal_uint32)samplesCount;
|
|
|
|
mal_uint32 bytesToWrite = framesToWrite*stream.channels*(stream.sampleSize/8);
|
|
memcpy(subBuffer, data, bytesToWrite);
|
|
|
|
// Any leftover frames should be filled with zeros.
|
|
mal_uint32 leftoverFrameCount = subBufferSizeInFrames - framesToWrite;
|
|
if (leftoverFrameCount > 0)
|
|
{
|
|
memset(subBuffer + bytesToWrite, 0, leftoverFrameCount*stream.channels*(stream.sampleSize/8));
|
|
}
|
|
|
|
audioBuffer->isSubBufferProcessed[subBufferToUpdate] = false;
|
|
}
|
|
else
|
|
{
|
|
TraceLog(LOG_ERROR, "UpdateAudioStream() : Attempting to write too many frames to buffer");
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TraceLog(LOG_ERROR, "Audio buffer not available for updating");
|
|
return;
|
|
}
|
|
#else
|
|
ALuint buffer = 0;
|
|
alSourceUnqueueBuffers(stream.source, 1, &buffer);
|
|
|
|
// Check if any buffer was available for unqueue
|
|
if (alGetError() != AL_INVALID_VALUE)
|
|
{
|
|
alBufferData(buffer, stream.format, data, samplesCount*stream.sampleSize/8*stream.channels, stream.sampleRate);
|
|
alSourceQueueBuffers(stream.source, 1, &buffer);
|
|
}
|
|
else TraceLog(LOG_WARNING, "[AUD ID %i] Audio buffer not available for unqueuing", stream.source);
|
|
#endif
|
|
}
|
|
|
|
// Check if any audio stream buffers requires refill
|
|
bool IsAudioBufferProcessed(AudioStream stream)
|
|
{
|
|
#if USE_MINI_AL
|
|
AudioBuffer *audioBuffer = (AudioBuffer *)stream.audioBuffer;
|
|
if (audioBuffer == NULL)
|
|
{
|
|
TraceLog(LOG_ERROR, "IsAudioBufferProcessed() : No audio buffer");
|
|
return false;
|
|
}
|
|
|
|
return audioBuffer->isSubBufferProcessed[0] || audioBuffer->isSubBufferProcessed[1];
|
|
#else
|
|
ALint processed = 0;
|
|
|
|
// Determine if music stream is ready to be written
|
|
alGetSourcei(stream.source, AL_BUFFERS_PROCESSED, &processed);
|
|
|
|
return (processed > 0);
|
|
#endif
|
|
}
|
|
|
|
// Play audio stream
|
|
void PlayAudioStream(AudioStream stream)
|
|
{
|
|
#if USE_MINI_AL
|
|
PlayAudioBuffer((AudioBuffer *)stream.audioBuffer);
|
|
#else
|
|
alSourcePlay(stream.source);
|
|
#endif
|
|
}
|
|
|
|
// Play audio stream
|
|
void PauseAudioStream(AudioStream stream)
|
|
{
|
|
#if USE_MINI_AL
|
|
PauseAudioBuffer((AudioBuffer *)stream.audioBuffer);
|
|
#else
|
|
alSourcePause(stream.source);
|
|
#endif
|
|
}
|
|
|
|
// Resume audio stream playing
|
|
void ResumeAudioStream(AudioStream stream)
|
|
{
|
|
#if USE_MINI_AL
|
|
ResumeAudioBuffer((AudioBuffer *)stream.audioBuffer);
|
|
#else
|
|
ALenum state;
|
|
alGetSourcei(stream.source, AL_SOURCE_STATE, &state);
|
|
|
|
if (state == AL_PAUSED) alSourcePlay(stream.source);
|
|
#endif
|
|
}
|
|
|
|
// Check if audio stream is playing.
|
|
bool IsAudioStreamPlaying(AudioStream stream)
|
|
{
|
|
#if USE_MINI_AL
|
|
return IsAudioBufferPlaying((AudioBuffer *)stream.audioBuffer);
|
|
#else
|
|
bool playing = false;
|
|
ALint state;
|
|
|
|
alGetSourcei(stream.source, AL_SOURCE_STATE, &state);
|
|
|
|
if (state == AL_PLAYING) playing = true;
|
|
|
|
return playing;
|
|
#endif
|
|
}
|
|
|
|
// Stop audio stream
|
|
void StopAudioStream(AudioStream stream)
|
|
{
|
|
#if USE_MINI_AL
|
|
StopAudioBuffer((AudioBuffer *)stream.audioBuffer);
|
|
#else
|
|
alSourceStop(stream.source);
|
|
#endif
|
|
}
|
|
|
|
void SetAudioStreamVolume(AudioStream stream, float volume)
|
|
{
|
|
#if USE_MINI_AL
|
|
SetAudioBufferVolume((AudioBuffer *)stream.audioBuffer, volume);
|
|
#else
|
|
alSourcef(stream.source, AL_GAIN, volume);
|
|
#endif
|
|
}
|
|
|
|
void SetAudioStreamPitch(AudioStream stream, float pitch)
|
|
{
|
|
#if USE_MINI_AL
|
|
SetAudioBufferPitch((AudioBuffer *)stream.audioBuffer, pitch);
|
|
#else
|
|
alSourcef(stream.source, AL_PITCH, pitch);
|
|
#endif
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Module specific Functions Definition
|
|
//----------------------------------------------------------------------------------
|
|
|
|
#if defined(SUPPORT_FILEFORMAT_WAV)
|
|
// Load WAV file into Wave structure
|
|
static Wave LoadWAV(const char *fileName)
|
|
{
|
|
// Basic WAV headers structs
|
|
typedef struct {
|
|
char chunkID[4];
|
|
int chunkSize;
|
|
char format[4];
|
|
} WAVRiffHeader;
|
|
|
|
typedef struct {
|
|
char subChunkID[4];
|
|
int subChunkSize;
|
|
short audioFormat;
|
|
short numChannels;
|
|
int sampleRate;
|
|
int byteRate;
|
|
short blockAlign;
|
|
short bitsPerSample;
|
|
} WAVFormat;
|
|
|
|
typedef struct {
|
|
char subChunkID[4];
|
|
int subChunkSize;
|
|
} WAVData;
|
|
|
|
WAVRiffHeader wavRiffHeader;
|
|
WAVFormat wavFormat;
|
|
WAVData wavData;
|
|
|
|
Wave wave = { 0 };
|
|
FILE *wavFile;
|
|
|
|
wavFile = fopen(fileName, "rb");
|
|
|
|
if (wavFile == NULL)
|
|
{
|
|
TraceLog(LOG_WARNING, "[%s] WAV file could not be opened", fileName);
|
|
wave.data = NULL;
|
|
}
|
|
else
|
|
{
|
|
// Read in the first chunk into the struct
|
|
fread(&wavRiffHeader, sizeof(WAVRiffHeader), 1, wavFile);
|
|
|
|
// Check for RIFF and WAVE tags
|
|
if (strncmp(wavRiffHeader.chunkID, "RIFF", 4) ||
|
|
strncmp(wavRiffHeader.format, "WAVE", 4))
|
|
{
|
|
TraceLog(LOG_WARNING, "[%s] Invalid RIFF or WAVE Header", fileName);
|
|
}
|
|
else
|
|
{
|
|
// Read in the 2nd chunk for the wave info
|
|
fread(&wavFormat, sizeof(WAVFormat), 1, wavFile);
|
|
|
|
// Check for fmt tag
|
|
if ((wavFormat.subChunkID[0] != 'f') || (wavFormat.subChunkID[1] != 'm') ||
|
|
(wavFormat.subChunkID[2] != 't') || (wavFormat.subChunkID[3] != ' '))
|
|
{
|
|
TraceLog(LOG_WARNING, "[%s] Invalid Wave format", fileName);
|
|
}
|
|
else
|
|
{
|
|
// Check for extra parameters;
|
|
if (wavFormat.subChunkSize > 16) fseek(wavFile, sizeof(short), SEEK_CUR);
|
|
|
|
// Read in the the last byte of data before the sound file
|
|
fread(&wavData, sizeof(WAVData), 1, wavFile);
|
|
|
|
// Check for data tag
|
|
if ((wavData.subChunkID[0] != 'd') || (wavData.subChunkID[1] != 'a') ||
|
|
(wavData.subChunkID[2] != 't') || (wavData.subChunkID[3] != 'a'))
|
|
{
|
|
TraceLog(LOG_WARNING, "[%s] Invalid data header", fileName);
|
|
}
|
|
else
|
|
{
|
|
// Allocate memory for data
|
|
wave.data = malloc(wavData.subChunkSize);
|
|
|
|
// Read in the sound data into the soundData variable
|
|
fread(wave.data, wavData.subChunkSize, 1, wavFile);
|
|
|
|
// Store wave parameters
|
|
wave.sampleRate = wavFormat.sampleRate;
|
|
wave.sampleSize = wavFormat.bitsPerSample;
|
|
wave.channels = wavFormat.numChannels;
|
|
|
|
// NOTE: Only support 8 bit, 16 bit and 32 bit sample sizes
|
|
if ((wave.sampleSize != 8) && (wave.sampleSize != 16) && (wave.sampleSize != 32))
|
|
{
|
|
TraceLog(LOG_WARNING, "[%s] WAV sample size (%ibit) not supported, converted to 16bit", fileName, wave.sampleSize);
|
|
WaveFormat(&wave, wave.sampleRate, 16, wave.channels);
|
|
}
|
|
|
|
// NOTE: Only support up to 2 channels (mono, stereo)
|
|
if (wave.channels > 2)
|
|
{
|
|
WaveFormat(&wave, wave.sampleRate, wave.sampleSize, 2);
|
|
TraceLog(LOG_WARNING, "[%s] WAV channels number (%i) not supported, converted to 2 channels", fileName, wave.channels);
|
|
}
|
|
|
|
// NOTE: subChunkSize comes in bytes, we need to translate it to number of samples
|
|
wave.sampleCount = (wavData.subChunkSize/(wave.sampleSize/8))/wave.channels;
|
|
|
|
TraceLog(LOG_INFO, "[%s] WAV file loaded successfully (%i Hz, %i bit, %s)", fileName, wave.sampleRate, wave.sampleSize, (wave.channels == 1) ? "Mono" : "Stereo");
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose(wavFile);
|
|
}
|
|
|
|
return wave;
|
|
}
|
|
#endif
|
|
|
|
#if defined(SUPPORT_FILEFORMAT_OGG)
|
|
// Load OGG file into Wave structure
|
|
// NOTE: Using stb_vorbis library
|
|
static Wave LoadOGG(const char *fileName)
|
|
{
|
|
Wave wave = { 0 };
|
|
|
|
stb_vorbis *oggFile = stb_vorbis_open_filename(fileName, NULL, NULL);
|
|
|
|
if (oggFile == NULL) TraceLog(LOG_WARNING, "[%s] OGG file could not be opened", fileName);
|
|
else
|
|
{
|
|
stb_vorbis_info info = stb_vorbis_get_info(oggFile);
|
|
|
|
wave.sampleRate = info.sample_rate;
|
|
wave.sampleSize = 16; // 16 bit per sample (short)
|
|
wave.channels = info.channels;
|
|
wave.sampleCount = (int)stb_vorbis_stream_length_in_samples(oggFile); // Independent by channel
|
|
|
|
float totalSeconds = stb_vorbis_stream_length_in_seconds(oggFile);
|
|
if (totalSeconds > 10) TraceLog(LOG_WARNING, "[%s] Ogg audio length is larger than 10 seconds (%f), that's a big file in memory, consider music streaming", fileName, totalSeconds);
|
|
|
|
wave.data = (short *)malloc(wave.sampleCount*wave.channels*sizeof(short));
|
|
|
|
// NOTE: Returns the number of samples to process (be careful! we ask for number of shorts!)
|
|
int numSamplesOgg = stb_vorbis_get_samples_short_interleaved(oggFile, info.channels, (short *)wave.data, wave.sampleCount*wave.channels);
|
|
|
|
TraceLog(LOG_DEBUG, "[%s] Samples obtained: %i", fileName, numSamplesOgg);
|
|
|
|
TraceLog(LOG_INFO, "[%s] OGG file loaded successfully (%i Hz, %i bit, %s)", fileName, wave.sampleRate, wave.sampleSize, (wave.channels == 1) ? "Mono" : "Stereo");
|
|
|
|
stb_vorbis_close(oggFile);
|
|
}
|
|
|
|
return wave;
|
|
}
|
|
#endif
|
|
|
|
#if defined(SUPPORT_FILEFORMAT_FLAC)
|
|
// Load FLAC file into Wave structure
|
|
// NOTE: Using dr_flac library
|
|
static Wave LoadFLAC(const char *fileName)
|
|
{
|
|
Wave wave;
|
|
|
|
// Decode an entire FLAC file in one go
|
|
uint64_t totalSampleCount;
|
|
wave.data = drflac_open_and_decode_file_s16(fileName, &wave.channels, &wave.sampleRate, &totalSampleCount);
|
|
|
|
wave.sampleCount = (int)totalSampleCount/wave.channels;
|
|
wave.sampleSize = 16;
|
|
|
|
// NOTE: Only support up to 2 channels (mono, stereo)
|
|
if (wave.channels > 2) TraceLog(LOG_WARNING, "[%s] FLAC channels number (%i) not supported", fileName, wave.channels);
|
|
|
|
if (wave.data == NULL) TraceLog(LOG_WARNING, "[%s] FLAC data could not be loaded", fileName);
|
|
else TraceLog(LOG_INFO, "[%s] FLAC file loaded successfully (%i Hz, %i bit, %s)", fileName, wave.sampleRate, wave.sampleSize, (wave.channels == 1) ? "Mono" : "Stereo");
|
|
|
|
return wave;
|
|
}
|
|
#endif
|
|
|
|
// Some required functions for audio standalone module version
|
|
#if defined(AUDIO_STANDALONE)
|
|
// Check file extension
|
|
bool IsFileExtension(const char *fileName, const char *ext)
|
|
{
|
|
bool result = false;
|
|
const char *fileExt;
|
|
|
|
if ((fileExt = strrchr(fileName, '.')) != NULL)
|
|
{
|
|
if (strcmp(fileExt, ext) == 0) result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Show trace log messages (LOG_INFO, LOG_WARNING, LOG_ERROR, LOG_DEBUG)
|
|
void TraceLog(int msgType, const char *text, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, text);
|
|
|
|
switch (msgType)
|
|
{
|
|
case LOG_INFO: fprintf(stdout, "INFO: "); break;
|
|
case LOG_ERROR: fprintf(stdout, "ERROR: "); break;
|
|
case LOG_WARNING: fprintf(stdout, "WARNING: "); break;
|
|
case LOG_DEBUG: fprintf(stdout, "DEBUG: "); break;
|
|
default: break;
|
|
}
|
|
|
|
vfprintf(stdout, text, args);
|
|
fprintf(stdout, "\n");
|
|
|
|
va_end(args);
|
|
|
|
if (msgType == LOG_ERROR) exit(1);
|
|
}
|
|
#endif
|