diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 27e7d723b..caddd0a3a 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -30,7 +30,7 @@ add_library(audio_core STATIC time_stretch.h $<$:sdl2_sink.cpp sdl2_sink.h> - $<$:cubeb_sink.cpp cubeb_sink.h> + $<$:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h> $<$:hle/ffmpeg_decoder.cpp hle/ffmpeg_decoder.h hle/ffmpeg_dl.cpp hle/ffmpeg_dl.h> $<$:hle/wmf_decoder.cpp hle/wmf_decoder.h hle/wmf_decoder_utils.cpp hle/wmf_decoder_utils.h> ) @@ -61,6 +61,6 @@ endif() if(ENABLE_CUBEB) target_link_libraries(audio_core PRIVATE cubeb) - add_definitions(-DHAVE_CUBEB=1) + target_compile_definitions(audio_core PUBLIC HAVE_CUBEB) endif() diff --git a/src/audio_core/cubeb_input.cpp b/src/audio_core/cubeb_input.cpp new file mode 100644 index 000000000..9d3bf2fe8 --- /dev/null +++ b/src/audio_core/cubeb_input.cpp @@ -0,0 +1,165 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "audio_core/cubeb_input.h" +#include "common/logging/log.h" + +namespace AudioCore { + +using SampleQueue = Common::SPSCQueue; + +struct CubebInput::Impl { + cubeb* ctx = nullptr; + cubeb_stream* stream = nullptr; + + std::unique_ptr sample_queue{}; + u8 sample_size_in_bytes = 0; + + static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* output_buffer, long num_frames); + static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); +}; + +CubebInput::CubebInput() : impl(std::make_unique()) { + if (cubeb_init(&impl->ctx, "Citra Input", nullptr) != CUBEB_OK) { + LOG_ERROR(Audio, "cubeb_init failed! Mic will not work properly"); + return; + } + impl->sample_queue = std::make_unique(); +} + +CubebInput::~CubebInput() { + if (!impl->ctx) + return; + + if (cubeb_stream_stop(impl->stream) != CUBEB_OK) { + LOG_ERROR(Audio, "Error stopping cubeb input stream."); + } + + cubeb_destroy(impl->ctx); +} + +void CubebInput::StartSampling(const Frontend::Mic::Parameters& params) { + // Cubeb apparently only supports signed 16 bit PCM (and float32 which the 3ds doesn't support) + // TODO resample the input stream + if (params.sign == Frontend::Mic::Signedness::Unsigned) { + LOG_ERROR(Audio, + "Application requested unsupported unsigned pcm format. Falling back to signed"); + } + + impl->sample_size_in_bytes = params.sample_size / 8; + + parameters = params; + is_sampling = true; + + cubeb_devid input_device = nullptr; + cubeb_stream_params input_params; + input_params.channels = 1; + input_params.layout = CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = CUBEB_STREAM_PREF_NONE; + input_params.format = CUBEB_SAMPLE_S16LE; + input_params.rate = params.sample_rate; + + u32 latency_frames; + if (cubeb_get_min_latency(impl->ctx, &input_params, &latency_frames) != CUBEB_OK) { + LOG_ERROR(Audio, "Could not get minimum latency"); + } + + if (cubeb_stream_init(impl->ctx, &impl->stream, "Citra Microphone", input_device, &input_params, + nullptr, nullptr, latency_frames, Impl::DataCallback, Impl::StateCallback, + impl.get()) != CUBEB_OK) { + LOG_CRITICAL(Audio, "Error creating cubeb input stream"); + is_sampling = false; + return; + } + + if (cubeb_stream_start(impl->stream) != CUBEB_OK) { + LOG_CRITICAL(Audio, "Error starting cubeb input stream"); + is_sampling = false; + return; + } +} + +void CubebInput::StopSampling() { + if (impl->stream) { + cubeb_stream_stop(impl->stream); + } + is_sampling = false; +} + +void CubebInput::AdjustSampleRate(u32 sample_rate) { + // TODO This should restart the stream with the new sample rate + LOG_ERROR(Audio, "AdjustSampleRate unimplemented!"); +} + +Frontend::Mic::Samples CubebInput::Read() { + Frontend::Mic::Samples samples{}; + Frontend::Mic::Samples queue; + while (impl->sample_queue->Pop(queue)) { + samples.insert(samples.end(), queue.begin(), queue.end()); + } + return samples; +} + +long CubebInput::Impl::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* output_buffer, long num_frames) { + Impl* impl = static_cast(user_data); + if (!impl) { + return 0; + } + + constexpr auto resample_s16_s8 = [](s16 sample) { + return static_cast(static_cast(sample) >> 8); + }; + + std::vector samples{}; + samples.reserve(num_frames * impl->sample_size_in_bytes); + if (impl->sample_size_in_bytes == 1) { + // If the sample format is 8bit, then resample back to 8bit before passing back to core + for (std::size_t i; i < num_frames; i++) { + s16 data; + std::memcpy(&data, static_cast(input_buffer) + i * 2, 2); + samples.push_back(resample_s16_s8(data)); + } + } else { + // Otherwise copy all of the samples to the buffer (which will be treated as s16 by core) + const u8* data = reinterpret_cast(input_buffer); + samples.insert(samples.begin(), data, data + num_frames * impl->sample_size_in_bytes); + } + impl->sample_queue->Push(samples); + + // returning less than num_frames here signals cubeb to stop sampling + return num_frames; +} + +void CubebInput::Impl::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {} + +std::vector ListCubebInputDevices() { + std::vector device_list; + cubeb* ctx; + + if (cubeb_init(&ctx, "Citra Input Device Enumerator", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio, "cubeb_init failed"); + return {}; + } + + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_INPUT, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio input device enumeration not supported"); + } else { + for (std::size_t i = 0; i < collection.count; i++) { + const cubeb_device_info& device = collection.device[i]; + if (device.state == CUBEB_DEVICE_STATE_ENABLED && device.friendly_name) { + device_list.emplace_back(device.friendly_name); + } + } + cubeb_device_collection_destroy(ctx, &collection); + } + + cubeb_destroy(ctx); + return device_list; +} +} // namespace AudioCore diff --git a/src/audio_core/cubeb_input.h b/src/audio_core/cubeb_input.h new file mode 100644 index 000000000..a38737849 --- /dev/null +++ b/src/audio_core/cubeb_input.h @@ -0,0 +1,33 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "core/frontend/mic.h" + +namespace AudioCore { + +class CubebInput final : public Frontend::Mic::Interface { +public: + CubebInput(); + ~CubebInput() override; + + void StartSampling(const Frontend::Mic::Parameters& params) override; + + void StopSampling() override; + + void AdjustSampleRate(u32 sample_rate) override; + + Frontend::Mic::Samples Read() override; + +private: + struct Impl; + std::unique_ptr impl; +}; + +std::vector ListCubebInputDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/cubeb_sink.cpp b/src/audio_core/cubeb_sink.cpp index 49bf3aaa9..a0b0eb944 100644 --- a/src/audio_core/cubeb_sink.cpp +++ b/src/audio_core/cubeb_sink.cpp @@ -27,7 +27,7 @@ struct CubebSink::Impl { }; CubebSink::CubebSink(std::string_view target_device_name) : impl(std::make_unique()) { - if (cubeb_init(&impl->ctx, "Citra", nullptr) != CUBEB_OK) { + if (cubeb_init(&impl->ctx, "Citra Output", nullptr) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); return; } @@ -179,7 +179,7 @@ std::vector ListCubebSinkDevices() { } else { for (std::size_t i = 0; i < collection.count; i++) { const cubeb_device_info& device = collection.device[i]; - if (device.friendly_name) { + if (device.state == CUBEB_DEVICE_STATE_ENABLED && device.friendly_name) { device_list.emplace_back(device.friendly_name); } } diff --git a/src/citra/config.cpp b/src/citra/config.cpp index e6eea37cc..70f05b7b5 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -166,6 +166,10 @@ void Config::ReadValues() { sdl2_config->GetBoolean("Audio", "enable_audio_stretching", true); Settings::values.audio_device_id = sdl2_config->GetString("Audio", "output_device", "auto"); Settings::values.volume = sdl2_config->GetReal("Audio", "volume", 1); + Settings::values.mic_input_device = + sdl2_config->GetString("Audio", "mic_input_device", "Default"); + Settings::values.mic_input_type = + static_cast(sdl2_config->GetInteger("Audio", "mic_input_type", 0)); // Data Storage Settings::values.use_virtual_sd = diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 201acafe4..091a85ce2 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -198,6 +198,10 @@ void Config::ReadValues() { Settings::values.audio_device_id = ReadSetting("output_device", "auto").toString().toStdString(); Settings::values.volume = ReadSetting("volume", 1).toFloat(); + Settings::values.mic_input_type = + static_cast(ReadSetting("mic_input_type", 0).toInt()); + Settings::values.mic_input_device = + ReadSetting("mic_input_device", "Default").toString().toStdString(); qt_config->endGroup(); using namespace Service::CAM; @@ -480,6 +484,9 @@ void Config::SaveValues() { WriteSetting("enable_audio_stretching", Settings::values.enable_audio_stretching, true); WriteSetting("output_device", QString::fromStdString(Settings::values.audio_device_id), "auto"); WriteSetting("volume", Settings::values.volume, 1.0f); + WriteSetting("mic_input_device", QString::fromStdString(Settings::values.mic_input_device), + "Default"); + WriteSetting("mic_input_type", static_cast(Settings::values.mic_input_type), 0); qt_config->endGroup(); using namespace Service::CAM; diff --git a/src/citra_qt/configuration/configure_audio.cpp b/src/citra_qt/configuration/configure_audio.cpp index 04211a161..6c2fd09fd 100644 --- a/src/citra_qt/configuration/configure_audio.cpp +++ b/src/citra_qt/configuration/configure_audio.cpp @@ -3,6 +3,10 @@ // Refer to the license.txt file included. #include +#include +#ifdef HAVE_CUBEB +#include "audio_core/cubeb_input.h" +#endif #include "audio_core/sink.h" #include "audio_core/sink_details.h" #include "citra_qt/configuration/configure_audio.h" @@ -28,10 +32,19 @@ ConfigureAudio::ConfigureAudio(QWidget* parent) connect(ui->volume_slider, &QSlider::valueChanged, this, &ConfigureAudio::setVolumeIndicatorText); + ui->input_device_combo_box->clear(); + ui->input_device_combo_box->addItem(tr("Default")); +#ifdef HAVE_CUBEB + for (const auto& device : AudioCore::ListCubebInputDevices()) { + ui->input_device_combo_box->addItem(QString::fromStdString(device)); + } +#endif + connect(ui->input_type_combo_box, qOverload(&QComboBox::currentIndexChanged), this, + &ConfigureAudio::updateAudioInputDevices); + this->setConfiguration(); - connect(ui->output_sink_combo_box, - static_cast(&QComboBox::currentIndexChanged), this, - &ConfigureAudio::updateAudioDevices); + connect(ui->output_sink_combo_box, qOverload(&QComboBox::currentIndexChanged), this, + &ConfigureAudio::updateAudioOutputDevices); } ConfigureAudio::~ConfigureAudio() {} @@ -40,7 +53,7 @@ void ConfigureAudio::setConfiguration() { setOutputSinkFromSinkID(); // The device list cannot be pre-populated (nor listed) until the output sink is known. - updateAudioDevices(ui->output_sink_combo_box->currentIndex()); + updateAudioOutputDevices(ui->output_sink_combo_box->currentIndex()); setAudioDeviceFromDeviceID(); @@ -59,6 +72,12 @@ void ConfigureAudio::setConfiguration() { selection = 0; } ui->emulation_combo_box->setCurrentIndex(selection); + + int index = static_cast(Settings::values.mic_input_type); + ui->input_type_combo_box->setCurrentIndex(index); + ui->input_device_combo_box->setCurrentText( + QString::fromStdString(Settings::values.mic_input_device)); + updateAudioInputDevices(index); } void ConfigureAudio::setOutputSinkFromSinkID() { @@ -105,9 +124,12 @@ void ConfigureAudio::applyConfiguration() { static_cast(ui->volume_slider->sliderPosition()) / ui->volume_slider->maximum(); Settings::values.enable_dsp_lle = ui->emulation_combo_box->currentIndex() != 0; Settings::values.enable_dsp_lle_multithread = ui->emulation_combo_box->currentIndex() == 2; + Settings::values.mic_input_type = + static_cast(ui->input_type_combo_box->currentIndex()); + Settings::values.mic_input_device = ui->input_device_combo_box->currentText().toStdString(); } -void ConfigureAudio::updateAudioDevices(int sink_index) { +void ConfigureAudio::updateAudioOutputDevices(int sink_index) { ui->audio_device_combo_box->clear(); ui->audio_device_combo_box->addItem(AudioCore::auto_device_name); @@ -117,6 +139,8 @@ void ConfigureAudio::updateAudioDevices(int sink_index) { } } +void ConfigureAudio::updateAudioInputDevices(int index) {} + void ConfigureAudio::retranslateUi() { ui->retranslateUi(this); } diff --git a/src/citra_qt/configuration/configure_audio.h b/src/citra_qt/configuration/configure_audio.h index b8e347a42..9952739cf 100644 --- a/src/citra_qt/configuration/configure_audio.h +++ b/src/citra_qt/configuration/configure_audio.h @@ -23,7 +23,8 @@ public: void setConfiguration(); private: - void updateAudioDevices(int sink_index); + void updateAudioOutputDevices(int sink_index); + void updateAudioInputDevices(int index); void setOutputSinkFromSinkID(); void setAudioDeviceFromDeviceID(); diff --git a/src/citra_qt/configuration/configure_audio.ui b/src/citra_qt/configuration/configure_audio.ui index 9a3768b2a..dd2f49173 100644 --- a/src/citra_qt/configuration/configure_audio.ui +++ b/src/citra_qt/configuration/configure_audio.ui @@ -6,8 +6,8 @@ 0 0 - 188 - 246 + 329 + 332 @@ -39,7 +39,7 @@ - Output Engine: + Output Engine @@ -63,7 +63,7 @@ - Audio Device: + Audio Device @@ -137,6 +137,59 @@ + + + + Microphone + + + + + + + + Input Type + + + + + + + + None + + + + + Real Device + + + + + Static Noise + + + + + + + + + + + + Input Device + + + + + + + + + + + diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6904aaaf6..b70ef57be 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -97,6 +97,8 @@ add_library(core STATIC frontend/framebuffer_layout.cpp frontend/framebuffer_layout.h frontend/input.h + frontend/mic.h + frontend/mic.cpp gdbstub/gdbstub.cpp gdbstub/gdbstub.h hle/applets/applet.cpp diff --git a/src/core/frontend/mic.cpp b/src/core/frontend/mic.cpp new file mode 100644 index 000000000..543a23767 --- /dev/null +++ b/src/core/frontend/mic.cpp @@ -0,0 +1,60 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "core/frontend/mic.h" + +namespace Frontend::Mic { + +constexpr std::array NOISE_SAMPLE_8_BIT = {0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF5, 0xFF, 0xFF, 0xFF, 0xFF, 0x8E, 0xFF}; + +constexpr std::array NOISE_SAMPLE_16_BIT = { + 0x64, 0x61, 0x74, 0x61, 0x56, 0xD7, 0x00, 0x00, 0x48, 0xF7, 0x86, 0x05, 0x77, 0x1A, 0xF4, 0x1F, + 0x28, 0x0F, 0x6B, 0xEB, 0x1C, 0xC0, 0xCB, 0x9D, 0x46, 0x90, 0xDF, 0x98, 0xEA, 0xAE, 0xB5, 0xC4}; + +Interface::~Interface() = default; + +void NullMic::StartSampling(const Parameters& params) { + parameters = params; + is_sampling = true; +} + +void NullMic::StopSampling() { + is_sampling = false; +} + +void NullMic::AdjustSampleRate(u32 sample_rate) { + parameters.sample_rate = sample_rate; +} + +Samples NullMic::Read() { + return {}; +} + +StaticMic::StaticMic() + : CACHE_8_BIT{NOISE_SAMPLE_8_BIT.begin(), NOISE_SAMPLE_8_BIT.end()}, + CACHE_16_BIT{NOISE_SAMPLE_16_BIT.begin(), NOISE_SAMPLE_16_BIT.end()} {} + +StaticMic::~StaticMic() = default; + +void StaticMic::StartSampling(const Parameters& params) { + sample_rate = params.sample_rate; + sample_size = params.sample_size; + + parameters = params; + is_sampling = true; +} + +void StaticMic::StopSampling() { + is_sampling = false; +} + +void StaticMic::AdjustSampleRate(u32 sample_rate) {} + +Samples StaticMic::Read() { + return (sample_size == 8) ? CACHE_8_BIT : CACHE_16_BIT; +} + +} // namespace Frontend::Mic diff --git a/src/core/frontend/mic.h b/src/core/frontend/mic.h new file mode 100644 index 000000000..1605e77c0 --- /dev/null +++ b/src/core/frontend/mic.h @@ -0,0 +1,116 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/swap.h" +#include "common/threadsafe_queue.h" + +namespace Frontend::Mic { + +enum class Signedness : u8 { + Signed, + Unsigned, +}; + +using Samples = std::vector; + +struct Parameters { + Signedness sign; + u8 sample_size; + bool buffer_loop; + u32 sample_rate; + u32 buffer_offset; + u32 buffer_size; +}; + +class Interface { +public: + Interface() = default; + + virtual ~Interface(); + + /// Starts the microphone. Called by Core + virtual void StartSampling(const Parameters& params) = 0; + + /// Stops the microphone. Called by Core + virtual void StopSampling() = 0; + + /** + * Called from the actual event timing at a constant period under a given sample rate. + * When sampling is enabled this function is expected to return a buffer of 16 samples in ideal + * conditions, but can be lax if the data is coming in from another source like a real mic. + */ + virtual Samples Read() = 0; + + /** + * Adjusts the Parameters. Implementations should update the parameters field in addition to + * changing the mic to sample according to the new parameters. Called by Core + */ + virtual void AdjustSampleRate(u32 sample_rate) = 0; + + /// Value from 0 - 100 to adjust the mic gain setting. Called by Core + virtual void SetGain(u8 mic_gain) { + gain = mic_gain; + } + + u8 GetGain() const { + return gain; + } + + void SetPower(bool power) { + powered = power; + } + + bool GetPower() const { + return powered; + } + + bool IsSampling() const { + return is_sampling; + } + + const Parameters& GetParameters() const { + return parameters; + } + +protected: + Parameters parameters; + u8 gain = 0; + bool is_sampling = false; + bool powered = false; +}; + +class NullMic final : public Interface { +public: + void StartSampling(const Parameters& params) override; + + void StopSampling() override; + + void AdjustSampleRate(u32 sample_rate) override; + + Samples Read() override; +}; + +class StaticMic final : public Interface { +public: + StaticMic(); + ~StaticMic() override; + + void StartSampling(const Parameters& params) override; + void StopSampling() override; + void AdjustSampleRate(u32 sample_rate) override; + + Samples Read() override; + +private: + u16 sample_rate = 0; + u8 sample_size = 0; + std::vector CACHE_8_BIT; + std::vector CACHE_16_BIT; +}; + +} // namespace Frontend::Mic diff --git a/src/core/hle/service/mic_u.cpp b/src/core/hle/service/mic_u.cpp index 6c97282aa..779b520b8 100644 --- a/src/core/hle/service/mic_u.cpp +++ b/src/core/hle/service/mic_u.cpp @@ -2,8 +2,12 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#ifdef HAVE_CUBEB +#include "audio_core/cubeb_input.h" +#endif #include "common/logging/log.h" #include "core/core.h" +#include "core/frontend/mic.h" #include "core/hle/ipc.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/event.h" @@ -11,27 +15,96 @@ #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/shared_memory.h" #include "core/hle/service/mic_u.h" +#include "core/settings.h" namespace Service::MIC { +/// Microphone audio encodings. enum class Encoding : u8 { - PCM8 = 0, - PCM16 = 1, - PCM8Signed = 2, - PCM16Signed = 3, + PCM8 = 0, ///< Unsigned 8-bit PCM. + PCM16 = 1, ///< Unsigned 16-bit PCM. + PCM8Signed = 2, ///< Signed 8-bit PCM. + PCM16Signed = 3, ///< Signed 16-bit PCM. }; +/// Microphone audio sampling rates. The actual accurate sampling rate can be calculated using +/// (16756991 / 512) / (SampleRate + 1) where SampleRate is one of the above values. enum class SampleRate : u8 { - SampleRate32730 = 0, - SampleRate16360 = 1, - SampleRate10910 = 2, - SampleRate8180 = 3 + Rate32730 = 0, ///< 32728.498 Hz + Rate16360 = 1, ///< 16364.479 Hz + Rate10910 = 2, ///< 10909.499 Hz + Rate8180 = 3 ///< 8182.1245 Hz +}; + +constexpr u32 GetSampleRateInHz(SampleRate sample_rate) { + switch (sample_rate) { + case SampleRate::Rate8180: + return 8182; + case SampleRate::Rate10910: + return 10909; + case SampleRate::Rate16360: + return 16364; + case SampleRate::Rate32730: + return 32728; + default: + UNREACHABLE(); + } +} + +// The 3ds hardware was tested to write to the sharedmem every 15 samples regardless of sample_rate. +// So we can just divide the sample rate by 16 and that'll give the correct timing for the event +constexpr u64 GetBufferUpdateRate(SampleRate sample_rate) { + return GetSampleRateInHz(sample_rate) / 16; +} + +// Variables holding the current mic buffer writing state +struct State { + u8* sharedmem_buffer = nullptr; + u32 sharedmem_size = 0; + std::size_t size = 0; + u32 offset = 0; + u32 initial_offset = 0; + bool looped_buffer = false; + u8 sample_size = 0; + SampleRate sample_rate = SampleRate::Rate16360; + + void WriteSamples(const std::vector& samples) { + u32 bytes_total_written = 0; + const std::size_t remaining_space = size - offset; + std::size_t bytes_to_write = std::min(samples.size(), remaining_space); + + // Write as many samples as we can to the buffer. + // TODO if the sample size is 16bit, this could theoretically cut a sample in the case where + // the application configures an odd size + std::memcpy(sharedmem_buffer + offset, samples.data(), bytes_to_write); + offset += static_cast(bytes_to_write); + bytes_total_written += static_cast(bytes_to_write); + + // If theres any samples left to write after we looped, go ahead and write them now + if (looped_buffer && samples.size() > bytes_total_written) { + offset = initial_offset; + bytes_to_write = std::min(samples.size() - bytes_total_written, size); + std::memcpy(sharedmem_buffer + offset, samples.data() + bytes_total_written, + bytes_to_write); + offset += static_cast(bytes_to_write); + } + + // The last 4 bytes of the shared memory contains the latest offset + // so update that as well https://www.3dbrew.org/wiki/MIC_Shared_Memory + u32_le off = offset; + std::memcpy(sharedmem_buffer + (sharedmem_size - sizeof(u32)), reinterpret_cast(&off), + sizeof(u32)); + } }; struct MIC_U::Impl { - explicit Impl(Core::System& system) { + explicit Impl(Core::System& system) : timing(system.CoreTiming()) { buffer_full_event = system.Kernel().CreateEvent(Kernel::ResetType::OneShot, "MIC_U::buffer_full_event"); + buffer_write_event = + timing.RegisterEvent("MIC_U::UpdateBuffer", [this](u64 userdata, s64 cycles_late) { + UpdateSharedMemBuffer(userdata, cycles_late); + }); } void MapSharedMem(Kernel::HLERequestContext& ctx) { @@ -41,12 +114,14 @@ struct MIC_U::Impl { if (shared_memory) { shared_memory->SetName("MIC_U:shared_memory"); + state.sharedmem_buffer = shared_memory->GetPointer(); + state.sharedmem_size = size; } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_MIC, "called, size=0x{:X}", size); + LOG_TRACE(Service_MIC, "called, size=0x{:X}", size); } void UnmapSharedMem(Kernel::HLERequestContext& ctx) { @@ -54,55 +129,101 @@ struct MIC_U::Impl { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); shared_memory = nullptr; rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_MIC, "called"); + LOG_TRACE(Service_MIC, "called"); + } + + void UpdateSharedMemBuffer(u64 userdata, s64 cycles_late) { + if (change_mic_impl_requested.exchange(false)) { + CreateMic(); + } + // If the event was scheduled before the application requested the mic to stop sampling + if (!mic->IsSampling()) { + return; + } + + Frontend::Mic::Samples samples = mic->Read(); + if (!samples.empty()) { + // write the samples to sharedmem page + state.WriteSamples(samples); + } + + // schedule next run + timing.ScheduleEvent(GetBufferUpdateRate(state.sample_rate) - cycles_late, + buffer_write_event); } void StartSampling(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x03, 5, 0}; - encoding = rp.PopEnum(); - sample_rate = rp.PopEnum(); - audio_buffer_offset = rp.PopRaw(); - audio_buffer_size = rp.Pop(); - audio_buffer_loop = rp.Pop(); + Encoding encoding = rp.PopEnum(); + SampleRate sample_rate = rp.PopEnum(); + u32 audio_buffer_offset = rp.PopRaw(); + u32 audio_buffer_size = rp.Pop(); + bool audio_buffer_loop = rp.Pop(); + + if (mic->IsSampling()) { + LOG_CRITICAL(Service_MIC, + "Application started sampling again before stopping sampling"); + mic->StopSampling(); + } + + auto sign = encoding == Encoding::PCM8Signed || encoding == Encoding::PCM16Signed + ? Frontend::Mic::Signedness::Signed + : Frontend::Mic::Signedness::Unsigned; + u8 sample_size = encoding == Encoding::PCM8Signed || encoding == Encoding::PCM8 ? 8 : 16; + state.offset = state.initial_offset = audio_buffer_offset; + state.sample_rate = sample_rate; + state.sample_size = sample_size; + state.looped_buffer = audio_buffer_loop; + state.size = audio_buffer_size; + + mic->StartSampling({sign, sample_size, audio_buffer_loop, GetSampleRateInHz(sample_rate), + audio_buffer_offset, audio_buffer_size}); + + timing.ScheduleEvent(GetBufferUpdateRate(state.sample_rate), buffer_write_event); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - is_sampling = true; - LOG_WARNING(Service_MIC, - "(STUBBED) called, encoding={}, sample_rate={}, " - "audio_buffer_offset={}, audio_buffer_size={}, audio_buffer_loop={}", - static_cast(encoding), static_cast(sample_rate), audio_buffer_offset, - audio_buffer_size, audio_buffer_loop); + LOG_TRACE(Service_MIC, + "called, encoding={}, sample_rate={}, " + "audio_buffer_offset={}, audio_buffer_size={}, audio_buffer_loop={}", + static_cast(encoding), static_cast(sample_rate), audio_buffer_offset, + audio_buffer_size, audio_buffer_loop); } void AdjustSampling(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x04, 1, 0}; - sample_rate = rp.PopEnum(); + SampleRate sample_rate = rp.PopEnum(); + mic->AdjustSampleRate(GetSampleRateInHz(sample_rate)); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_MIC, "(STUBBED) called, sample_rate={}", static_cast(sample_rate)); + LOG_TRACE(Service_MIC, "sample_rate={}", static_cast(sample_rate)); } void StopSampling(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x05, 0, 0}; + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - is_sampling = false; - LOG_WARNING(Service_MIC, "(STUBBED) called"); + mic->StopSampling(); + timing.RemoveEvent(buffer_write_event); + LOG_TRACE(Service_MIC, "called"); } void IsSampling(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x06, 0, 0}; + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(RESULT_SUCCESS); + bool is_sampling = mic->IsSampling(); rb.Push(is_sampling); - LOG_WARNING(Service_MIC, "(STUBBED) called"); + LOG_TRACE(Service_MIC, "IsSampling: {}", is_sampling); } void GetBufferFullEvent(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x07, 0, 0}; + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(RESULT_SUCCESS); rb.PushCopyObjects(buffer_full_event); @@ -111,11 +232,12 @@ struct MIC_U::Impl { void SetGain(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x08, 1, 0}; - mic_gain = rp.Pop(); + u8 gain = rp.Pop(); + mic->SetGain(gain); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_MIC, "(STUBBED) called, mic_gain={}", mic_gain); + LOG_TRACE(Service_MIC, "gain={}", gain); } void GetGain(Kernel::HLERequestContext& ctx) { @@ -123,25 +245,29 @@ struct MIC_U::Impl { IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(RESULT_SUCCESS); - rb.Push(mic_gain); - LOG_WARNING(Service_MIC, "(STUBBED) called"); + u8 gain = mic->GetGain(); + rb.Push(gain); + LOG_TRACE(Service_MIC, "gain={}", gain); } void SetPower(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x0A, 1, 0}; - mic_power = rp.Pop(); + bool power = rp.Pop(); + mic->SetPower(power); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(RESULT_SUCCESS); - LOG_WARNING(Service_MIC, "(STUBBED) called, mic_power={}", mic_power); + LOG_TRACE(Service_MIC, "mic_power={}", power); } void GetPower(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x0B, 0, 0}; + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(RESULT_SUCCESS); + bool mic_power = mic->GetPower(); rb.Push(mic_power); - LOG_WARNING(Service_MIC, "(STUBBED) called"); + LOG_TRACE(Service_MIC, "called"); } void SetIirFilterMic(Kernel::HLERequestContext& ctx) { @@ -167,6 +293,7 @@ struct MIC_U::Impl { void GetClamp(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x0E, 0, 0}; + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(RESULT_SUCCESS); rb.Push(clamp); @@ -184,7 +311,6 @@ struct MIC_U::Impl { void SetClientVersion(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx, 0x10, 1, 0}; - const u32 version = rp.Pop(); LOG_WARNING(Service_MIC, "(STUBBED) called, version: 0x{:08X}", version); @@ -192,19 +318,51 @@ struct MIC_U::Impl { rb.Push(RESULT_SUCCESS); } - u32 client_version = 0; + void CreateMic() { + std::unique_ptr new_mic; + switch (Settings::values.mic_input_type) { + case Settings::MicInputType::None: + new_mic = std::make_unique(); + break; + case Settings::MicInputType::Real: +#if HAVE_CUBEB + new_mic = std::make_unique(); +#else + new_mic = std::make_unique(); +#endif + break; + case Settings::MicInputType::Static: + new_mic = std::make_unique(); + break; + default: + LOG_CRITICAL(Audio, "Mic type not found. Defaulting to null mic"); + new_mic = std::make_unique(); + } + // If theres already a mic, copy over any data to the new mic impl + if (mic) { + new_mic->SetGain(mic->GetGain()); + new_mic->SetPower(mic->GetPower()); + auto params = mic->GetParameters(); + if (mic->IsSampling()) { + mic->StopSampling(); + new_mic->StartSampling(params); + } + } + + mic = std::move(new_mic); + change_mic_impl_requested.store(false); + } + + std::atomic change_mic_impl_requested = false; Kernel::SharedPtr buffer_full_event; + Core::TimingEventType* buffer_write_event = nullptr; Kernel::SharedPtr shared_memory; - u8 mic_gain = 0; - bool mic_power = false; - bool is_sampling = false; - bool allow_shell_closed; + u32 client_version = 0; + bool allow_shell_closed = false; bool clamp = false; - Encoding encoding = Encoding::PCM8; - SampleRate sample_rate = SampleRate::SampleRate32730; - s32 audio_buffer_offset = 0; - u32 audio_buffer_size = 0; - bool audio_buffer_loop = false; + std::unique_ptr mic; + Core::Timing& timing; + State state{}; }; void MIC_U::MapSharedMem(Kernel::HLERequestContext& ctx) { @@ -292,10 +450,24 @@ MIC_U::MIC_U(Core::System& system) {0x00100040, &MIC_U::SetClientVersion, "SetClientVersion"}, }; + impl->CreateMic(); RegisterHandlers(functions); } -MIC_U::~MIC_U() = default; +MIC_U::~MIC_U() { + impl->mic->StopSampling(); +} + +void MIC_U::ReloadMic() { + impl->change_mic_impl_requested.store(true); +} + +void ReloadMic(Core::System& system) { + auto micu = system.ServiceManager().GetService("mic:u"); + if (!micu) + return; + micu->ReloadMic(); +} void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); diff --git a/src/core/hle/service/mic_u.h b/src/core/hle/service/mic_u.h index bc4933229..2e40ed404 100644 --- a/src/core/hle/service/mic_u.h +++ b/src/core/hle/service/mic_u.h @@ -19,6 +19,8 @@ public: explicit MIC_U(Core::System& system); ~MIC_U(); + void ReloadMic(); + private: /** * MIC::MapSharedMem service function @@ -190,6 +192,8 @@ private: std::unique_ptr impl; }; +void ReloadMic(Core::System& system); + void InstallInterfaces(Core::System& system); } // namespace Service::MIC diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 4b80e3436..d34aad32a 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -9,6 +9,7 @@ #include "core/hle/service/hid/hid.h" #include "core/hle/service/ir/ir_rst.h" #include "core/hle/service/ir/ir_user.h" +#include "core/hle/service/mic_u.h" #include "core/settings.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" @@ -56,6 +57,8 @@ void Apply() { if (cam) { cam->ReloadCameraDevices(); } + + Service::MIC::ReloadMic(system); } } @@ -86,6 +89,8 @@ void LogSettings() { LogSetting("Audio_OutputEngine", Settings::values.sink_id); LogSetting("Audio_EnableAudioStretching", Settings::values.enable_audio_stretching); LogSetting("Audio_OutputDevice", Settings::values.audio_device_id); + LogSetting("Audio_InputDeviceType", static_cast(Settings::values.mic_input_type)); + LogSetting("Audio_InputDevice", Settings::values.mic_input_device); using namespace Service::CAM; LogSetting("Camera_OuterRightName", Settings::values.camera_name[OuterRightCamera]); LogSetting("Camera_OuterRightConfig", Settings::values.camera_config[OuterRightCamera]); diff --git a/src/core/settings.h b/src/core/settings.h index cea502693..ef5e48410 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -26,6 +26,12 @@ enum class LayoutOption { SideScreen, }; +enum class MicInputType { + None, + Real, + Static, +}; + namespace NativeButton { enum Values { A, @@ -167,6 +173,8 @@ struct Values { bool enable_audio_stretching; std::string audio_device_id; float volume; + MicInputType mic_input_type; + std::string mic_input_device; // Camera std::array camera_name;