Skip to content

Commit

Permalink
initial mic support
Browse files Browse the repository at this point in the history
  • Loading branch information
Jamiras committed Nov 12, 2023
1 parent d9fd664 commit 6d8cd45
Show file tree
Hide file tree
Showing 10 changed files with 481 additions and 5 deletions.
30 changes: 26 additions & 4 deletions src/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Application::Application(): _fsm(*this)
_components.videoContext = &_videoContext;
_components.video = &_video;
_components.audio = &_audio;
_components.microphone = &_microphone;
_components.input = &_input;
_components.allocator = &_allocator;
}
Expand Down Expand Up @@ -259,6 +260,11 @@ bool Application::init(const char* title, int width, int height)
goto error;
}

if (!_microphone.init(&_logger))
{
goto error;
}

inited = kAudioInited;

if (!_input.init(&_logger))
Expand Down Expand Up @@ -334,7 +340,8 @@ bool Application::init(const char* title, int width, int height)
case kInputInited: _input.destroy();
case kAudioInited: _audio.destroy();
case kFifoInited: _fifo.destroy();
case kAudioDeviceInited: SDL_CloseAudioDevice(_audioDev);
case kAudioDeviceInited: _microphone.destroy();
SDL_CloseAudioDevice(_audioDev);
case kWindowInited: SDL_DestroyWindow(_window);
case kKeyBindsInited: _keybinds.destroy();
case kSdlInited: SDL_Quit();
Expand Down Expand Up @@ -692,6 +699,7 @@ void Application::destroy()
_keybinds.destroy();
_input.destroy();
_config.destroy();
_microphone.destroy();
_audio.destroy();
_fifo.destroy();

Expand Down Expand Up @@ -1409,6 +1417,19 @@ bool Application::isGameActive()
}
}

bool Application::isPaused() const
{
switch (_fsm.currentState())
{
case Fsm::State::GamePaused:
case Fsm::State::GamePausedNoOvl:
return true;

default:
return false;
}
}

void Application::onRotationChanged(Video::Rotation oldRotation, Video::Rotation newRotation)
{
const Uint32 fullscreen = SDL_GetWindowFlags(_window) & SDL_WINDOW_FULLSCREEN_DESKTOP;
Expand Down Expand Up @@ -2165,14 +2186,15 @@ void Application::resizeWindow(int width, int height)
void Application::toggleFullscreen()
{
Uint32 fullscreen = SDL_GetWindowFlags(_window) & SDL_WINDOW_FULLSCREEN_DESKTOP;
SDL_SetWindowFullscreen(_window, fullscreen ^ SDL_WINDOW_FULLSCREEN_DESKTOP);

if (fullscreen)
{
SetMenu(g_mainWindow, _menu);
SDL_ShowCursor(SDL_ENABLE);
}
else

SDL_SetWindowFullscreen(_window, fullscreen ^ SDL_WINDOW_FULLSCREEN_DESKTOP);

if (!fullscreen)
{
SetMenu(g_mainWindow, NULL);
SDL_ShowCursor(SDL_DISABLE);
Expand Down
3 changes: 3 additions & 0 deletions src/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ along with RALibretro. If not, see <http://www.gnu.org/licenses/>.
#include "components/Config.h"
#include "components/Input.h"
#include "components/Logger.h"
#include "components/Microphone.h"
#include "components/VideoContext.h"
#include "components/Video.h"

Expand Down Expand Up @@ -61,6 +62,7 @@ class Application
bool hardcore();
bool unloadGame();
void pauseGame(bool pause);
bool isPaused() const;

void printf(const char* fmt, ...);
Logger& logger() { return _logger; }
Expand Down Expand Up @@ -143,6 +145,7 @@ class Application
VideoContext _videoContext;
Video _video;
Audio _audio;
Microphone _microphone;
Input _input;
Memory _memory;
States _states;
Expand Down
1 change: 1 addition & 0 deletions src/RALibretro.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ $(ProjectDir)RAInterface\CopyOverlay.bat $(SolutionDir)\bin64</Command>
<ClCompile Include="components\Dialog.cpp" />
<ClCompile Include="components\Input.cpp" />
<ClCompile Include="components\Logger.cpp" />
<ClCompile Include="components\Microphone.cpp" />
<ClCompile Include="components\Video.cpp" />
<ClCompile Include="components\VideoContext.cpp" />
<ClCompile Include="dynlib\dynlib.c" />
Expand Down
3 changes: 3 additions & 0 deletions src/RALibretro.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@
<ClCompile Include="HashCHD.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="components\Microphone.cpp">
<Filter>Source Files\components</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="About.h">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Audio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ void Audio::mix(const int16_t* samples, size_t frames)
if (error != RESAMPLER_ERR_SUCCESS)
{
memset(output, 0, output_size);
_logger->error(TAG "speex_resampler_process_interleaved_int: %s", speex_resampler_strerror(error));
_logger->error(TAG "speex_resampler_process_int: %s", speex_resampler_strerror(error));
}
}
}
Expand Down
271 changes: 271 additions & 0 deletions src/components/Microphone.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/*
Copyright (C) 2023 Brian Weiss
This file is part of RALibretro.
RALibretro is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
RALibretro is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with RALibretro. If not, see <http://www.gnu.org/licenses/>.
*/

#include "Microphone.h"

#include "Application.h"

#include <SDL_audio.h>
#include "speex/speex_resampler.h"

#define TAG "[MIC] "

struct Microphone::SDLData
{
SDL_AudioDeviceID deviceId;
SDL_AudioSpec deviceSpec;
unsigned coreRate;
bool enabled;
Fifo fifo;
libretro::LoggerComponent* logger;
SpeexResamplerState* resampler;
};

bool Microphone::init(libretro::LoggerComponent* logger)
{
_logger = logger;
return true;
}

void Microphone::destroy()
{
if (_data)
closeMic((retro_microphone_t*)_data);
}

void Microphone::recordCallback(void* data, Uint8* stream, int len)
{
SDLData* sdlData = (SDLData*)data;

if (!sdlData->enabled) /* don't fill buffer when mic is not active, nothing is going to read the buffer */
return;

extern Application app;
if (app.isPaused()) /* don't fill buffer while paused, nothing is running to empty buffer */
return;

int frames = len / sizeof(int16_t);
int16_t* samples = (int16_t*)stream;

size_t output_size;
int16_t* output;

if (sdlData->deviceSpec.freq == sdlData->coreRate)
{
/* no resampling needed */
output = (int16_t*)samples;
output_size = frames * sizeof(int16_t);
}
else
{
/* allocate output buffer */
spx_uint32_t out_len = (spx_uint32_t)ceil(frames * sdlData->coreRate / sdlData->deviceSpec.freq);
output_size = out_len * sizeof(int16_t);
output = (int16_t*)alloca(output_size);

if (output == NULL)
{
sdlData->logger->error(TAG "Error allocating audio resampling output buffer");
return;
}

/* do the resampling */
int error;
if (!sdlData->resampler)
{
sdlData->resampler = speex_resampler_init(1, sdlData->deviceSpec.freq, sdlData->coreRate, SPEEX_RESAMPLER_QUALITY_DEFAULT, &error);

if (!sdlData->resampler)
{
sdlData->logger->error(TAG "speex_resampler_init: %s", speex_resampler_strerror(error));
SDL_PauseAudioDevice(sdlData->deviceId, true);
return;
}
}

spx_uint32_t in_frames = frames;
spx_uint32_t out_frames = out_len;
sdlData->logger->debug(TAG "Resampling %u samples to %u", in_frames, out_frames);
error = speex_resampler_process_int(sdlData->resampler, 0, (const int16_t*)samples, &in_frames, output, &out_frames);
if (error == RESAMPLER_ERR_SUCCESS)
{
/* update with the actual number of frames created */
output_size = out_frames * sizeof(int16_t);
}
else
{
memset(output, 0, output_size);
sdlData->logger->error(TAG "speex_resampler_process_int: %s", speex_resampler_strerror(error));
}
}

/* flush to FIFO queue */
size_t avail = sdlData->fifo.free();
if (avail < output_size)
{
sdlData->logger->debug(TAG "Waiting for FIFO (need %zu bytes but only %zu available), sleeping", output_size, avail);

const int MAX_WAIT = 250;
int tries = MAX_WAIT;
do
{
SDL_Delay(1);
if (!sdlData->enabled)
return;

avail = sdlData->fifo.free();
if (avail >= output_size)
break;

/* prevent infinite loop if fifo full */
if (--tries == 0)
{
sdlData->logger->debug(TAG "FIFO still full after %dms, flushing", MAX_WAIT);
sdlData->fifo.reset();
avail = sdlData->fifo.free();
break;
}
} while (true);

if (avail < output_size)
output_size = avail;
}

sdlData->fifo.write(output, output_size);
}

retro_microphone_t* Microphone::openMic(const retro_microphone_params_t* params)
{
if (_data != NULL) /* only one microphone allowed at a time*/
return NULL;

SDLData* data = new SDLData();
if (data == NULL)
return NULL;

data->logger = _logger;
data->resampler = nullptr;
data->coreRate = params->rate;
data->enabled = false;

SDL_AudioSpec desired_spec {};
desired_spec.freq = params->rate;
desired_spec.format = AUDIO_S16SYS;
desired_spec.channels = 1; /* Microphones only usually provide input in mono */
desired_spec.samples = 2048;
desired_spec.userdata = data;
desired_spec.callback = recordCallback;

data->deviceId = SDL_OpenAudioDevice(NULL, true, &desired_spec, &data->deviceSpec,
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);

if (data->deviceId == 0)
{
_logger->error(TAG "Failed to open SDL audio input device: %s", SDL_GetError());
delete data;
return NULL;
}

_logger->info(TAG "Opened microphone with ID %u (frequency %u [requested %u])", data->deviceId, data->deviceSpec.freq, desired_spec.freq);
_logger->info(TAG "Microphone audio format: %u-bit %s %s %s endian\n",
SDL_AUDIO_BITSIZE(data->deviceSpec.format),
SDL_AUDIO_ISSIGNED(data->deviceSpec.format) ? "signed" : "unsigned",
SDL_AUDIO_ISFLOAT(data->deviceSpec.format) ? "floating-point" : "integer",
SDL_AUDIO_ISBIGENDIAN(data->deviceSpec.format) ? "big" : "little");

data->fifo.init(data->deviceSpec.size * 4);
_data = data;
return (retro_microphone_t*)data;
}

void Microphone::closeMic(retro_microphone_t* microphone)
{
if ((SDLData*)microphone == _data)
{
SDL_CloseAudioDevice(_data->deviceId);
_data->fifo.destroy();

if (_data->resampler != NULL)
speex_resampler_destroy(_data->resampler);

delete _data;
_data = NULL;
}
}

bool Microphone::getMicParams(const retro_microphone_t* microphone, retro_microphone_params_t* params)
{
if (!_data)
return false;

params->rate = _data->deviceSpec.freq;
return true;
}

bool Microphone::getMicState(const retro_microphone_t* microphone)
{
return _data && _data->enabled;
}

bool Microphone::setMicState(retro_microphone_t* microphone, bool state)
{
if (!_data)
return false;

SDL_PauseAudioDevice(_data->deviceId, !state);
if (state)
{
if (SDL_GetAudioDeviceStatus(_data->deviceId) != SDL_AUDIO_PLAYING)
{
_logger->warn(TAG "Failed to start microphone %u: %s", _data->deviceId, SDL_GetError());
return false;
}
}
else
{
switch (SDL_GetAudioDeviceStatus(_data->deviceId))
{
case SDL_AUDIO_PLAYING:
_logger->warn(TAG "Failed to pause microphone %u: %s", _data->deviceId, SDL_GetError());
return false;
case SDL_AUDIO_STOPPED:
_logger->warn(TAG "Microphone %u has been stopped and may not start again: %s", _data->deviceId, SDL_GetError());
return false;
default:
break;
}

_data->fifo.reset();
}

_data->enabled = state;
return true;
}

int Microphone::readMic(retro_microphone_t* microphone, int16_t* frames, size_t num_frames)
{
if (!_data || !_data->enabled)
return 0;

size_t num_read = std::min(num_frames, _data->fifo.occupied() / sizeof(int16_t));
_data->fifo.read(frames, num_read * sizeof(int16_t));
return (int)num_read;
}


Loading

0 comments on commit 6d8cd45

Please sign in to comment.