diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 6ebc71534..5f88cd587 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -43,6 +43,10 @@ int32_t GetUserId() { return g_config->user_id; } +const std::string& GetAudioInputDevice() { + return g_config->audio_input_device; +} + PresentMode GetPresentMode() { return g_config->present_mode; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 5c43c5609..924733fbe 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -43,6 +43,7 @@ struct ConfigOptions { uint32_t screen_height = 720; std::string user_name = "Kyty"; int32_t user_id = DEFAULT_USER_ID; + std::string audio_input_device; PresentMode present_mode = PresentMode::Mailbox; int32_t gpu_index = -1; bool fullscreen_enabled = false; @@ -77,6 +78,7 @@ uint32_t GetScreenWidth(); uint32_t GetScreenHeight(); const std::string& GetUserName(); int32_t GetUserId(); +const std::string& GetAudioInputDevice(); PresentMode GetPresentMode(); int32_t GetGpuIndex(); bool FullscreenEnabled(); diff --git a/src/launcher/CMakeLists.txt b/src/launcher/CMakeLists.txt index 4d4f64bba..01d5ec1fc 100644 --- a/src/launcher/CMakeLists.txt +++ b/src/launcher/CMakeLists.txt @@ -20,7 +20,7 @@ add_executable(launcher WIN32 ) -target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers kyty_git_version) +target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL2-static kyty_git_version) if(WIN32) target_link_libraries(launcher setupapi) endif() diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index a52388976..bfd2109fe 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -75,7 +75,25 @@ - + + + Audio + + + QFormLayout::AllNonFixedFieldsGrow + + + + Microphone: + + + + + + + + + Graphics && display @@ -152,7 +170,7 @@ - + Debugging && logs diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 5a2ebde05..36111bcb0 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -87,6 +87,7 @@ public: Resolution screen_resolution = Resolution::R1280X720; QString user_name = "Kyty"; int user_id = Config::DEFAULT_USER_ID; + QString audio_input_device; PresentMode present_mode = PresentMode::Mailbox; int gpu_index = -1; bool fullscreen_enabled = false; @@ -115,6 +116,7 @@ public: screen_resolution = other.screen_resolution; user_name = other.user_name; user_id = other.user_id; + audio_input_device = other.audio_input_device; present_mode = other.present_mode; gpu_index = other.gpu_index; fullscreen_enabled = other.fullscreen_enabled; @@ -160,6 +162,7 @@ public: KYTY_CFG_SET(screen_resolution); KYTY_CFG_SET(user_name); KYTY_CFG_SET(user_id); + KYTY_CFG_SET(audio_input_device); KYTY_CFG_SET(present_mode); KYTY_CFG_SET(gpu_index); KYTY_CFG_SET(fullscreen_enabled); @@ -196,6 +199,7 @@ public: user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id) ? saved_user_id : Config::DEFAULT_USER_ID; + audio_input_device = s->value("audio_input_device", audio_input_device).toString(); KYTY_CFG_GET(present_mode); gpu_index = s->value("gpu_index", -1).toInt(); if (EnumToText(present_mode).isEmpty()) { diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index 7e69aff2b..a0250aa34 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -3,6 +3,7 @@ #include "common/emulatorConfig.h" #include "configuration.h" #include "mandatoryLineEdit.h" +#include "SDL.h" #include #include @@ -167,6 +168,31 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->lineEdit_user_name->setMaxLength(static_cast(Config::MAX_USER_NAME_LENGTH)); m_ui->lineEdit_user_name->setText(info.user_name); m_ui->spinBox_user_id->setValue(info.user_id); + auto* microphone = m_ui->comboBox_audio_input_device; + microphone->clear(); + microphone->addItem(tr("None"), QString {}); + microphone->setToolTip(tr("Microphone used by games. None supplies silence.")); + SDL_SetMainReady(); + if (SDL_InitSubSystem(SDL_INIT_AUDIO) == 0) { + const int device_count = SDL_GetNumAudioDevices(SDL_TRUE); + for (int i = 0; i < device_count; i++) { + if (const auto* device = SDL_GetAudioDeviceName(i, SDL_TRUE); device != nullptr) { + const auto name = QString::fromUtf8(device); + if (microphone->findData(name) < 0) { + microphone->addItem(name, name); + } + } + } + SDL_QuitSubSystem(SDL_INIT_AUDIO); + } else { + microphone->setToolTip(tr("Microphones could not be listed: %1") + .arg(QString::fromUtf8(SDL_GetError()))); + } + if (microphone->findData(info.audio_input_device) < 0) { + microphone->addItem(tr("%1 (unavailable)").arg(info.audio_input_device), + info.audio_input_device); + } + microphone->setCurrentIndex(microphone->findData(info.audio_input_device)); ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution); ListInit(m_ui->comboBox_present_mode, info.present_mode); m_ui->comboBox_gpu->clear(); @@ -340,6 +366,7 @@ void ConfigurationEditDialog::resizeEvent(QResizeEvent* event) { static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) { info.user_name = ui.lineEdit_user_name->text().trimmed(); info.user_id = ui.spinBox_user_id->value(); + info.audio_input_device = ui.comboBox_audio_input_device->currentData().toString(); info.screen_resolution = TextToEnum(ui.comboBox_screen_resolution->currentText()); info.present_mode = diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 5a910ef1b..7a4cee5d5 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -228,6 +228,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { args << "--screen-height" << r.at(1); args << "--user-name" << info.user_name; args << "--user-id" << QString::number(info.user_id); + if (!info.audio_input_device.isEmpty()) { + args << "--mic" << info.audio_input_device; + } args << "--present-mode" << EnumToText(info.present_mode); if (info.gpu_index >= 0) { args << "--gpu" << QString::number(info.gpu_index); diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index a96d4603c..66bf04a57 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -3,6 +3,7 @@ #include "SDL.h" #include "common/assert.h" #include "common/common.h" +#include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/magicEnum.h" #include "common/stringUtils.h" @@ -38,6 +39,9 @@ constexpr int AUDIO_OUT_PORT_TYPE_AUX = 127; constexpr uint32_t AUDIO_OUT_PARAM_FORMAT_MASK = 0x000000ffu; +constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1; +constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384; + static bool audio_out_port_type_is_valid(int type) { return (type >= AUDIO_OUT_PORT_TYPE_MAIN && type <= AUDIO_OUT_PORT_TYPE_PADSPK) || type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_AUDIO3D || @@ -91,7 +95,7 @@ public: Id AudioInOpen(uint32_t samples_num, uint32_t freq, Format format, bool asynchronous); int AudioInClose(Id handle); - bool AudioInValid(Id handle); + int AudioInGetSilentState(Id handle); int AudioInInput(Id handle, void* dest); static constexpr int OUT_PORTS_MAX = 32; @@ -113,13 +117,14 @@ private: }; struct PortIn { - bool used = false; - bool busy = false; - bool asynchronous = false; - uint32_t samples_num = 0; - uint32_t freq = 0; - uint32_t bytes_per_frame = 0; - uint64_t last_input_time = 0; + bool used = false; + bool busy = false; + bool asynchronous = false; + uint32_t samples_num = 0; + uint32_t freq = 0; + uint32_t bytes_per_frame = 0; + uint64_t last_input_time = 0; + SDL_AudioDeviceID audio_device = 0; }; PortIn* GetAudioInPort(Id handle); // Caller holds m_mutex. @@ -135,6 +140,8 @@ private: static SDL_AudioFormat SdlFormat(Format format); static bool OpenSdlDevice(PortOut* port); static void CloseSdlDevice(PortOut* port); + static void OpenSdlDevice(PortIn* port, Format format); + static void CloseSdlDevice(PortIn* port); static const void* PrepareOutputBuffer(const PortOut& port, const void* data, std::vector* buffer); static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking); @@ -202,6 +209,9 @@ Audio::~Audio() { for (auto& port: m_out_ports) { CloseSdlDevice(&port); } + for (auto& port: m_in_ports) { + CloseSdlDevice(&port); + } } bool Audio::FormatIsFloat(Format format) { @@ -570,6 +580,38 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking return first_port.samples_num; } +void Audio::OpenSdlDevice(PortIn* port, Format format) { + const auto& name = Config::GetAudioInputDevice(); + if (name.empty()) { + return; + } + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + LOGF("AudioIn: SDL init failed: %s\n", SDL_GetError()); + return; + } + SDL_AudioSpec desired {}; + desired.freq = static_cast(port->freq); + desired.format = SdlFormat(format); + desired.channels = static_cast(port->bytes_per_frame / BytesPerSample(format)); + desired.samples = static_cast(port->samples_num); + // SDL converts capture data to the guest format when allowed_changes is zero. + port->audio_device = SDL_OpenAudioDevice(name.c_str(), 1, &desired, nullptr, 0); + if (port->audio_device == 0) { + LOGF("AudioIn: cannot open '%s': %s; using silence\n", name.c_str(), SDL_GetError()); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + return; + } + LOGF("AudioIn: opened '%s' (%u Hz, %u ch)\n", name.c_str(), port->freq, desired.channels); +} + +void Audio::CloseSdlDevice(PortIn* port) { + if (port->audio_device != 0) { + SDL_CloseAudioDevice(port->audio_device); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + port->audio_device = 0; + } +} + Audio::Id Audio::AudioInOpen(uint32_t samples_num, uint32_t freq, Format format, bool asynchronous) { Common::LockGuard lock(m_mutex); @@ -586,6 +628,7 @@ Audio::Id Audio::AudioInOpen(uint32_t samples_num, uint32_t freq, Format format, if (format == Format::Signed16bitStereo || format == Format::FloatStereo) { port.bytes_per_frame *= 2; } + OpenSdlDevice(&port, format); return Id::Create(id); } @@ -611,20 +654,29 @@ int Audio::AudioInClose(Id handle) { if (port->busy) { return AUDIO_IN_ERROR_BUSY; } + CloseSdlDevice(port); *port = {}; return OK; } -bool Audio::AudioInValid(Id handle) { +int Audio::AudioInGetSilentState(Id handle) { Common::LockGuard lock(m_mutex); - return GetAudioInPort(handle) != nullptr; + auto* port = GetAudioInPort(handle); + if (port == nullptr) { + return AUDIO_IN_ERROR_INVALID_HANDLE; + } + if (port->audio_device == 0 || + SDL_GetAudioDeviceStatus(port->audio_device) == SDL_AUDIO_STOPPED) { + return AUDIO_IN_SILENT_STATE_DEVICE_NONE; + } + return 0; } int Audio::AudioInInput(Id handle, void* dest) { PortIn snapshot; { Common::LockGuard lock(m_mutex); - auto* port = GetAudioInPort(handle); + auto* port = GetAudioInPort(handle); if (port == nullptr) { return AUDIO_IN_ERROR_INVALID_HANDLE; } @@ -634,6 +686,9 @@ int Audio::AudioInInput(Id handle, void* dest) { port->busy = true; snapshot = *port; } + if (snapshot.audio_device != 0 && dest != nullptr) { + SDL_PauseAudioDevice(snapshot.audio_device, 0); + } const uint64_t block_time = (1000000ULL * snapshot.samples_num) / snapshot.freq; uint64_t wait_time = 0; @@ -641,7 +696,7 @@ int Audio::AudioInInput(Id handle, void* dest) { if (dest != nullptr) { wait_time = block_time; } - } else if (snapshot.last_input_time != 0) { + } else if (snapshot.last_input_time != 0 && (snapshot.audio_device == 0 || dest == nullptr)) { const auto now = LibKernel::KernelGetProcessTime(); const auto next = snapshot.last_input_time + block_time; if (next > now) { @@ -649,17 +704,44 @@ int Audio::AudioInInput(Id handle, void* dest) { } } Common::Thread::SleepMicro(wait_time); + uint32_t frames = snapshot.samples_num; + bool failed = false; if (dest != nullptr) { - // No input device backend: return silence matching AUDIO_IN_SILENT_STATE_DEVICE_NONE. - std::memset(dest, 0, snapshot.samples_num * snapshot.bytes_per_frame); + if (snapshot.audio_device != 0) { + while (!snapshot.asynchronous && + SDL_GetQueuedAudioSize(snapshot.audio_device) < + frames * snapshot.bytes_per_frame && + SDL_GetAudioDeviceStatus(snapshot.audio_device) == SDL_AUDIO_PLAYING) { + Common::Thread::SleepMicro(1000); + } + failed = SDL_GetAudioDeviceStatus(snapshot.audio_device) != SDL_AUDIO_PLAYING; + if (!failed) { + if (snapshot.asynchronous) { + frames = AUDIO_IN_GRAIN_MAX_ASYNC; + } + frames = SDL_DequeueAudio(snapshot.audio_device, dest, + frames * snapshot.bytes_per_frame) / + snapshot.bytes_per_frame; + } + } + if (snapshot.audio_device == 0 || failed) { + std::memset(dest, 0, frames * snapshot.bytes_per_frame); + } + } else if (snapshot.audio_device != 0) { + SDL_PauseAudioDevice(snapshot.audio_device, 1); + SDL_ClearQueuedAudio(snapshot.audio_device); } { Common::LockGuard lock(m_mutex); - auto& port = m_in_ports[handle.GetId()]; + auto& port = m_in_ports[handle.GetId()]; + if (failed) { + LOGF("AudioIn: capture stopped; using silence\n"); + CloseSdlDevice(&port); + } port.last_input_time = dest != nullptr ? LibKernel::KernelGetProcessTime() : 0; port.busy = false; } - return dest != nullptr ? static_cast(snapshot.samples_num) : 0; + return dest != nullptr ? static_cast(frames) : 0; } namespace AudioOut { @@ -851,8 +933,6 @@ namespace AudioIn { LIB_NAME("AudioIn", "AudioIn"); -constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1; - static int OpenPort(int user_id, int type, int index, uint32_t len, uint32_t freq, uint32_t param, bool asynchronous) { LOGF("\t user_id = %d\n" @@ -935,12 +1015,11 @@ int KYTY_SYSV_ABI AudioInGetSilentState(int handle) { EXIT_IF(g_audio == nullptr); - if (handle <= 0 || !g_audio->AudioInValid(Audio::Id(handle))) { + if (handle <= 0) { return AUDIO_IN_ERROR_INVALID_HANDLE; } - // Audio input has no device backend yet, so every valid port receives silence. - return AUDIO_IN_SILENT_STATE_DEVICE_NONE; + return g_audio->AudioInGetSilentState(Audio::Id(handle)); } } // namespace AudioIn diff --git a/src/main.cpp b/src/main.cpp index a5dd5be52..1c35cd237 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,6 +49,7 @@ static void PrintUsage() { " --user-name Local user name (1-16 bytes). Default: Kyty.\n"); ::printf(" --user-id Local user ID. Default: %d.\n", Config::DEFAULT_USER_ID); + ::printf(" --mic Capture from this microphone; omit for silence.\n"); ::printf( " --present-mode Fifo, Mailbox, or Immediate. Default: Mailbox.\n"); ::printf( @@ -241,6 +242,8 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he ::printf("invalid user ID: %s\n", value.c_str()); return false; } + } else if (arg == "--mic") { + options.config.audio_input_device = value; } else if (arg == "--present-mode") { if (!ParseEnum(value, options.config.present_mode)) { ::printf("invalid present mode: %s\n", value.c_str());