diff --git a/examples/audio/audio_stream_recording.c b/examples/audio/audio_stream_recording.c new file mode 100644 index 000000000..636b053d7 --- /dev/null +++ b/examples/audio/audio_stream_recording.c @@ -0,0 +1,148 @@ +/******************************************************************************************* +* +* raylib [audio] example - stream recording +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example created by Jairo Correa (@jn-jairo) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Jairo Correa (@jn-jairo) +* +********************************************************************************************/ + +#include "raylib.h" + +#include // Required for: NULL +#include // Required for: memcpy() + +//---------------------------------------------------------------------------------- +// Global Variables Definition +//---------------------------------------------------------------------------------- +static unsigned int maxFrameCount = 0; +static Wave wave = { 0 }; + +//------------------------------------------------------------------------------------ +// Module Functions Declaration +//------------------------------------------------------------------------------------ +static void RecordingCallback(const void *framesIn, unsigned int frameCount); + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [audio] example - stream recording"); + + InitAudioDevice(); + InitAudioRecordingDevice(); + // InitAudioRecordingDeviceEx(24000, 16, 1); + + // Configure it so that RecordingCallback is called whenever new samples are read from the microphone + SetAudioRecordingCallback(RecordingCallback); + + // Configure the wave to match the recording + wave.sampleRate = GetAudioRecordingSampleRate(); + wave.sampleSize = GetAudioRecordingSampleSize(); + wave.channels = GetAudioRecordingChannels(); + + // Allocate buffer for the audio recording + maxFrameCount = wave.sampleRate*5; // 5 seconds + wave.data = RL_CALLOC(maxFrameCount*wave.channels, wave.sampleSize/8); + + Sound sound = { 0 }; + + float timeRecorded = 0.0f; + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + + // Space key pressed or the recording buffer is full + if (IsKeyPressed(KEY_SPACE) || (IsAudioRecording() && (wave.frameCount == maxFrameCount))) + { + if (IsAudioRecording()) + { + StopAudioRecording(); + + // Reload and play the sound + UnloadSound(sound); + sound = LoadSoundFromWave(wave); + PlaySound(sound); + } + else + { + // Stop the sound and reset the recording frameCount + StopSound(sound); + wave.frameCount = 0; + + StartAudioRecording(); + } + } + + // Get timeRecorded scaled to bar dimensions + timeRecorded = ((float)wave.frameCount/(float)maxFrameCount)*(screenWidth - 40); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + // Draw time bar + DrawRectangle(20, screenHeight - 20 - 12, screenWidth - 40, 12, LIGHTGRAY); + DrawRectangle(20, screenHeight - 20 - 12, (int)timeRecorded, 12, MAROON); + DrawRectangleLines(20, screenHeight - 20 - 12, screenWidth - 40, 12, GRAY); + + // Draw help instructions + DrawText("Press SPACE to START/STOP recording the sound!", 130, 180, 20, LIGHTGRAY); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseAudioRecordingDevice(); // Close audio recording device (recording is automatically stopped) + CloseAudioDevice(); // Close audio device (music streaming is automatically stopped) + + UnloadSound(sound); + UnloadWave(wave); + + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} + +//------------------------------------------------------------------------------------ +// Module Functions Definition +//------------------------------------------------------------------------------------ +static void RecordingCallback(const void *framesIn, unsigned int frameCount) +{ + // Check how many frames left in the buffer + unsigned int framesLeft = maxFrameCount - wave.frameCount; + + // Do not overflow the buffer + if (framesLeft == 0) return; + unsigned int framesToCopy = (frameCount < framesLeft)? frameCount : framesLeft; + + // Append the new data + memcpy((unsigned char *)wave.data + wave.frameCount*wave.channels*wave.sampleSize/8, framesIn, framesToCopy*wave.channels*wave.sampleSize/8); + wave.frameCount += framesToCopy; +} diff --git a/examples/audio/audio_stream_recording.png b/examples/audio/audio_stream_recording.png new file mode 100644 index 000000000..2fffd76e7 Binary files /dev/null and b/examples/audio/audio_stream_recording.png differ diff --git a/src/raudio.c b/src/raudio.c index 094ea7533..c98894029 100644 --- a/src/raudio.c +++ b/src/raudio.c @@ -390,6 +390,13 @@ typedef struct AudioData { size_t pcmBufferSize; // Pre-allocated buffer size void *pcmBuffer; // Pre-allocated buffer to read audio data from file/memory } System; + struct { + ma_context context; // miniaudio context data + ma_device device; // miniaudio device + ma_mutex lock; // miniaudio mutex lock + bool isReady; // Check if audio device is ready + AudioRecordingCallback callback; // Recording callback function + } RecordingSystem; struct { AudioBuffer *first; // Pointer to first AudioBuffer in the list AudioBuffer *last; // Pointer to last AudioBuffer in the list @@ -421,6 +428,7 @@ static ma_uint32 ReadAudioBufferFramesInInternalFormat(AudioBuffer *audioBuffer, static ma_uint32 ReadAudioBufferFramesInMixingFormat(AudioBuffer *audioBuffer, float *framesOut, ma_uint32 frameCount); static void OnSendAudioDataToDevice(ma_device *pDevice, void *pFramesOut, const void *pFramesInput, ma_uint32 frameCount); +static void OnReceiveAudioDataFromDevice(ma_device *pDevice, void *pFramesOut, const void *pFramesInput, ma_uint32 frameCount); static void MixAudioFrames(float *framesOut, const float *framesIn, ma_uint32 frameCount, AudioBuffer *buffer); static bool IsAudioBufferPlayingInLockedState(AudioBuffer *buffer); @@ -2369,6 +2377,181 @@ void DetachAudioMixedProcessor(AudioCallback process) ma_mutex_unlock(&AUDIO.System.lock); } +//---------------------------------------------------------------------------------- +// Module Functions Definition - Audio Recording Device initialization and closing +//---------------------------------------------------------------------------------- + +// Initialize audio recording device with custom sampleRate, sampleSize and channels +void InitAudioRecordingDeviceEx(unsigned int sampleRate, unsigned int sampleSize, unsigned int channels) +{ + // Init audio context + ma_context_config ctxConfig = ma_context_config_init(); + ma_log_callback_init(OnLog, NULL); + + ma_result result = ma_context_init(NULL, 0, &ctxConfig, &AUDIO.RecordingSystem.context); + if (result != MA_SUCCESS) + { + TRACELOG(LOG_WARNING, "AUDIO: Failed to initialize recording context"); + return; + } + + ma_format format = ((sampleSize == 8)? ma_format_u8 : ((sampleSize == 16)? ma_format_s16 : ma_format_f32)); + + // Init audio device + // NOTE: Using the default device + ma_device_config config = ma_device_config_init(ma_device_type_capture); + config.capture.pDeviceID = NULL; // NULL for the default capture AUDIO.RecordingSystem.device + config.capture.format = format; + config.capture.channels = channels; + config.sampleRate = sampleRate; + config.periodSizeInFrames = AUDIO_DEVICE_PERIOD_SIZE_IN_FRAMES; + config.dataCallback = OnReceiveAudioDataFromDevice; + config.pUserData = NULL; + config.noFixedSizedCallback = true; // raylib does not require fixed sized callback guarantees. This bypasses an internal intermediary buffer + + result = ma_device_init(&AUDIO.RecordingSystem.context, &config, &AUDIO.RecordingSystem.device); + if (result != MA_SUCCESS) + { + TRACELOG(LOG_WARNING, "AUDIO: Failed to initialize recording device"); + ma_context_uninit(&AUDIO.RecordingSystem.context); + return; + } + + // Recording happens on a separate thread which means synchronization is needed + // A mutex is used 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 (ma_mutex_init(&AUDIO.RecordingSystem.lock) != MA_SUCCESS) + { + TRACELOG(LOG_WARNING, "AUDIO: Failed to create mutex for recording"); + ma_device_uninit(&AUDIO.RecordingSystem.device); + ma_context_uninit(&AUDIO.RecordingSystem.context); + return; + } + + TRACELOG(LOG_INFO, "AUDIO: Recording device initialized successfully"); + TRACELOG(LOG_INFO, " > Backend: miniaudio | %s", ma_get_backend_name(AUDIO.RecordingSystem.context.backend)); + TRACELOG(LOG_INFO, " > Format: %s -> %s", ma_get_format_name(AUDIO.RecordingSystem.device.capture.format), ma_get_format_name(AUDIO.RecordingSystem.device.capture.internalFormat)); + TRACELOG(LOG_INFO, " > Channels: %d -> %d", AUDIO.RecordingSystem.device.capture.channels, AUDIO.RecordingSystem.device.capture.internalChannels); + TRACELOG(LOG_INFO, " > Sample rate: %d -> %d", AUDIO.RecordingSystem.device.sampleRate, AUDIO.RecordingSystem.device.capture.internalSampleRate); + TRACELOG(LOG_INFO, " > Periods size: %d", AUDIO.RecordingSystem.device.capture.internalPeriodSizeInFrames*AUDIO.RecordingSystem.device.capture.internalPeriods); + + AUDIO.RecordingSystem.isReady = true; +} + +// Initialize audio recording device +void InitAudioRecordingDevice(void) +{ + unsigned int sampleSize = ((AUDIO_DEVICE_FORMAT == ma_format_u8)? 8 : ((AUDIO_DEVICE_FORMAT == ma_format_s16)? 16 : 32)); + InitAudioRecordingDeviceEx(AUDIO_DEVICE_SAMPLE_RATE, sampleSize, AUDIO_DEVICE_CHANNELS); +} + +// Close the audio recording device for all contexts +void CloseAudioRecordingDevice(void) +{ + if (AUDIO.RecordingSystem.isReady) + { + ma_mutex_uninit(&AUDIO.RecordingSystem.lock); + ma_device_uninit(&AUDIO.RecordingSystem.device); + ma_context_uninit(&AUDIO.RecordingSystem.context); + + AUDIO.RecordingSystem.isReady = false; + + TRACELOG(LOG_INFO, "AUDIO: Recording device closed successfully"); + } + else TRACELOG(LOG_WARNING, "AUDIO: Recording device could not be closed, not currently initialized"); +} + +// Check if recording device has been initialized successfully +bool IsAudioRecordingDeviceReady(void) +{ + return AUDIO.RecordingSystem.isReady; +} + +// Get recording sample rate +unsigned int GetAudioRecordingSampleRate(void) +{ + return AUDIO.RecordingSystem.device.sampleRate; +} + +// Get recording sample size in bits +unsigned int GetAudioRecordingSampleSize(void) +{ + return 8 * ma_get_bytes_per_sample(AUDIO.RecordingSystem.device.capture.format); +} + +// Get recording channels +unsigned int GetAudioRecordingChannels(void) +{ + return AUDIO.RecordingSystem.device.capture.channels; +} + +// Set master volume (recording) +void SetMasterRecordingVolume(float volume) +{ + ma_device_set_master_volume(&AUDIO.RecordingSystem.device, volume); +} + +// Get master volume (recording) +float GetMasterRecordingVolume(void) +{ + float volume = 0.0f; + ma_device_get_master_volume(&AUDIO.RecordingSystem.device, &volume); + return volume; +} + +// Audio recording callback to receive new data +void SetAudioRecordingCallback(AudioRecordingCallback callback) +{ + ma_mutex_lock(&AUDIO.RecordingSystem.lock); + AUDIO.RecordingSystem.callback = callback; + ma_mutex_unlock(&AUDIO.RecordingSystem.lock); +} + +// Start audio recording +void StartAudioRecording(void) +{ + if (!AUDIO.RecordingSystem.isReady) + { + TRACELOG(LOG_WARNING, "AUDIO: Recording device could not be started, not currently initialized"); + return; + } + + ma_result result = ma_device_start(&AUDIO.RecordingSystem.device); + if (result != MA_SUCCESS) + { + TRACELOG(LOG_WARNING, "AUDIO: Failed to start recording device"); + return; + } +} + +// Stop audio recording +void StopAudioRecording(void) +{ + if (!AUDIO.RecordingSystem.isReady) + { + TRACELOG(LOG_WARNING, "AUDIO: Recording device could not be stopped, not currently initialized"); + return; + } + + ma_result result = ma_device_stop(&AUDIO.RecordingSystem.device); + if (result != MA_SUCCESS) + { + TRACELOG(LOG_WARNING, "AUDIO: Failed to stop recording device"); + return; + } +} + +// Check if device is recording +bool IsAudioRecording(void) +{ + if (!AUDIO.RecordingSystem.isReady) + { + return false; + } + + return ma_device_is_started(&AUDIO.RecordingSystem.device); +} + //---------------------------------------------------------------------------------- // Module Internal Functions Definition //---------------------------------------------------------------------------------- @@ -2653,6 +2836,22 @@ static void OnSendAudioDataToDevice(ma_device *pDevice, void *pFramesOut, const ma_mutex_unlock(&AUDIO.System.lock); } +// Sending audio data to device callback function +// This function will be called when miniaudio sends more data +static void OnReceiveAudioDataFromDevice(ma_device *pDevice, void *pFramesOut, const void *pFramesInput, ma_uint32 frameCount) +{ + AudioRecordingCallback callback = NULL; + + ma_mutex_lock(&AUDIO.RecordingSystem.lock); + callback = AUDIO.RecordingSystem.callback; + ma_mutex_unlock(&AUDIO.RecordingSystem.lock); + + if (callback) + { + callback(pFramesInput, frameCount); + } +} + // Main mixing function, pretty simple in this project, only an accumulation // NOTE: framesOut is both an input and an output, it is initially filled with zeros outside of this function static void MixAudioFrames(float *framesOut, const float *framesIn, ma_uint32 frameCount, AudioBuffer *buffer) diff --git a/src/raylib.h b/src/raylib.h index e49d6b2f7..8efcf4375 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -1663,9 +1663,10 @@ RLAPI RayCollision GetRayCollisionTriangle(Ray ray, Vector3 p1, Vector3 p2, Vect RLAPI RayCollision GetRayCollisionQuad(Ray ray, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4); // Get collision info between ray and quad //------------------------------------------------------------------------------------ -// Audio Loading and Playing Functions (Module: audio) +// Audio Loading, Playing and Recording Functions (Module: audio) //------------------------------------------------------------------------------------ typedef void (*AudioCallback)(void *bufferData, unsigned int frames); +typedef void (*AudioRecordingCallback)(const void *bufferData, unsigned int frames); // Audio device management functions RLAPI void InitAudioDevice(void); // Initialize audio device and context @@ -1674,6 +1675,21 @@ RLAPI bool IsAudioDeviceReady(void); // Check i RLAPI void SetMasterVolume(float volume); // Set master volume (listener) RLAPI float GetMasterVolume(void); // Get master volume (listener) +// Audio recording device management functions +RLAPI void InitAudioRecordingDevice(void); // Initialize audio recording device +RLAPI void InitAudioRecordingDeviceEx(unsigned int sampleRate, unsigned int sampleSize, unsigned int channels); // Initialize audio recording device with custom sampleRate, sampleSize and channels +RLAPI void CloseAudioRecordingDevice(void); // Close the audio recording device for all contexts +RLAPI bool IsAudioRecordingDeviceReady(void); // Check if recording device has been initialized successfully +RLAPI unsigned int GetAudioRecordingSampleRate(void); // Get recording sample rate +RLAPI unsigned int GetAudioRecordingSampleSize(void); // Get recording sample size in bits +RLAPI unsigned int GetAudioRecordingChannels(void); // Get recording channels +RLAPI void SetMasterRecordingVolume(float volume); // Set master volume (recording) +RLAPI float GetMasterRecordingVolume(void); // Get master volume (recording) +RLAPI void SetAudioRecordingCallback(AudioRecordingCallback callback); // Audio recording callback to receive new data +RLAPI void StartAudioRecording(void); // Start audio recording +RLAPI void StopAudioRecording(void); // Stop audio recording +RLAPI bool IsAudioRecording(void); // Check if device is recording + // Wave/Sound loading/unloading functions RLAPI Wave LoadWave(const char *fileName); // Load wave data from file RLAPI Wave LoadWaveFromMemory(const char *fileType, const unsigned char *fileData, int dataSize); // Load wave from memory buffer, fileType refers to extension: i.e. '.wav'