Add launcher microphone selection and SDL AudioIn capture

This commit is contained in:
nmzik 2026-09-15 11:20:19 +04:00
parent 98b5d88d67
commit d1372abf46
9 changed files with 164 additions and 24 deletions

View File

@ -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;
}

View File

@ -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();

View File

@ -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()

View File

@ -75,7 +75,25 @@
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="2">
<item row="2" column="0">
<widget class="QGroupBox" name="audio_group">
<property name="title"><string>Audio</string></property>
<layout class="QFormLayout" name="audioLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_audio_input_device">
<property name="text"><string>Microphone:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_audio_input_device"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="3">
<widget class="QGroupBox" name="graphics_group">
<property name="title">
<string>Graphics &amp;&amp; display</string>
@ -152,7 +170,7 @@
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QGroupBox" name="debug_group">
<property name="title">
<string>Debugging &amp;&amp; logs</string>

View File

@ -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()) {

View File

@ -3,6 +3,7 @@
#include "common/emulatorConfig.h"
#include "configuration.h"
#include "mandatoryLineEdit.h"
#include "SDL.h"
#include <QAbstractItemView>
#include <QCheckBox>
@ -167,6 +168,31 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(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<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
info.present_mode =

View File

@ -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);

View File

@ -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<uint8_t>* 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<int>(port->freq);
desired.format = SdlFormat(format);
desired.channels = static_cast<Uint8>(port->bytes_per_frame / BytesPerSample(format));
desired.samples = static_cast<Uint16>(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<int>(snapshot.samples_num) : 0;
return dest != nullptr ? static_cast<int>(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

View File

@ -49,6 +49,7 @@ static void PrintUsage() {
" --user-name <name> Local user name (1-16 bytes). Default: Kyty.\n");
::printf(" --user-id <num> Local user ID. Default: %d.\n",
Config::DEFAULT_USER_ID);
::printf(" --mic <name> Capture from this microphone; omit for silence.\n");
::printf(
" --present-mode <value> 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());