Files
PS5_RetroArch/tests/microphone_ps5_test.cpp
T
Jorge Feitosa 6f03aa224a Microphone input, optional melonDS DS core and its touchpad stylus (0134, 0135)
From mihawk-99/PS5_RetroArch#53 and #52 (Jorge Feitosa), renumbered
(0131 to 0133 were taken), taken together: #52 fixes the read loop #53
turns on.

- src/microphone_ps5.cpp (0134): a native AudioIn microphone driver,
  the default one, for the first controller's user. It captures only
  while a core has its microphone on, sends silence under the DualSense
  mute, and keeps PCM only in a bounded queue in memory. RetroArch is
  configured with the microphone; the title links a host stub for
  libSceAudioIn's imports.
- 0135: a microphone read that gets no frames returns silence instead of
  looping forever (a paused core, an empty nonblocking device, a failed
  capture).
- tools/build-melondsds.sh: melonDS DS at pinned revisions (git objects
  by SHA), software renderer and a native-memory JIT, staged only with
  PS5_WITH_MELONDSDS=1; DeSmuME stays the DS default. The DualSense
  touchpad drives its stylus as it drives DeSmuME's.

Changed from the pull requests: the README and the 0134 entry describe
what happens, not an opt-in the code never had. RetroArch's microphone is
on by default, as upstream; melonDS DS listens only while its microphone
button is held (its default), and Settings > Audio > Microphone turns it
off for every core. tests/test_microphone_read.py applies the port's
edits to vendor/retroarch itself rather than reading build/ra-conf, which
a tree configured before 0135 would fail. The SwanStation and melonDS DS
title changes are merged into one licence-file rule.

Evidence: tests/test_microphone_ps5.py (the driver against a simulated
AudioIn: lifecycle, mute, input failure); tests/test_microphone_read.py
(it times out with upstream's loop); tests/test_touchpad_stylus.py
(melonDS DS's relative stylus); tools/build-melondsds.sh, core ABI PASS
(25 exports, kernel_web, sha256 b03d4c93...). A title built with both
optional cores: 61 notice parts, check-notices covers every shipped
executable, check-manifest PASS, 78 melonDS DS options in the catalog.
Gates (default build): format, unit (204), build (staged cores reused),
integration, evidence PASS. Console: with this series' deploy.
2026-10-17 21:25:33 -04:00

113 lines
4.1 KiB
C++

/* Exercise the actual native driver with a paced AudioIn substitute. */
#include <audio/microphone_driver.h>
#include <algorithm>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <thread>
extern "C" microphone_driver_t microphone_ps5;
static std::atomic<int> calls{0}, opens{0}, closes{0}, muted{0}, fail_read{0};
static bool reject_48k = false;
extern "C" int sceAudioInInit()
{
return 0;
}
extern "C" int ps5_input_user_id(unsigned port)
{
return port == 0 ? 123 : -1;
}
extern "C" int sceAudioInOpen(int user, int, int, unsigned grain, unsigned rate, unsigned format)
{
assert(user == 123 && grain == 256 && format == 0);
++opens;
return reject_48k && rate == 48000 ? -2 : 7;
}
extern "C" int sceAudioInInput(int handle, void *dest)
{
assert(handle == 7);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
int sequence = ++calls;
if (fail_read.load())
return -42;
std::fill_n(static_cast<int16_t *>(dest), 256, int16_t(sequence));
return 256;
}
extern "C" int sceAudioInGetSilentState(int)
{
return muted.load();
}
extern "C" int sceAudioInClose(int handle)
{
assert(handle == 7);
++closes;
return 0;
}
static void wait_calls(int target)
{
for (int i = 0; i < 1000 && calls.load() < target; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
assert(calls.load() >= target);
}
int main()
{
auto &api = microphone_ps5;
void *driver = api.init();
assert(driver && opens == 0);
unsigned rate = 0;
void *mic = api.open_mic(driver, "default", 47605, 64, &rate);
assert(mic && rate == 48000 && !api.mic_alive(driver, mic));
assert(!api.open_mic(driver, "default", 48000, 64, nullptr));
std::this_thread::sleep_for(std::chrono::milliseconds(8));
assert(calls == 0); // Opening the handle does not capture before core opt-in.
int16_t samples[256];
assert(api.read(driver, mic, samples, sizeof(samples)) == -1);
assert(api.start_mic(driver, mic));
assert(api.start_mic(driver, mic)); // An already-active start must be harmless.
wait_calls(24);
int before = calls.load();
assert(api.read(driver, mic, samples, sizeof(samples)) == sizeof(samples));
assert(samples[0] >= before - 18 &&
samples[0] > 1); // Drops old audio instead of growing without bound.
assert(std::all_of(samples, samples + 256, [](int16_t x) { return x > 0; }));
int16_t across_wrap[5000];
assert(api.read(driver, mic, across_wrap, sizeof(across_wrap)) == sizeof(across_wrap));
assert(across_wrap[0] > 0 && std::is_sorted(across_wrap, across_wrap + 5000));
muted = 4;
wait_calls(calls.load() + 4);
assert(api.read(driver, mic, samples, sizeof(samples)) == sizeof(samples));
assert(std::all_of(samples, samples + 256, [](int16_t x) { return x == 0; }));
assert(api.stop_mic(driver, mic));
int stopped = calls.load();
std::this_thread::sleep_for(std::chrono::milliseconds(8));
assert(calls == stopped && !api.mic_alive(driver, mic));
assert(api.read(driver, mic, nullptr, 0) == 0);
assert(api.read(driver, mic, samples, 1) == -1);
api.close_mic(driver, mic);
assert(closes == 1);
// Unsupported 48k falls back to the independently device-tested 16k route.
reject_48k = true;
mic = api.open_mic(driver, "default", 47605, 64, &rate);
assert(mic && rate == 16000);
api.set_nonblock_state(driver, true);
assert(api.start_mic(driver, mic));
int got = api.read(driver, mic, samples, sizeof(samples));
assert(got >= 0 && got <= int(sizeof(samples)) && got % 2 == 0);
muted = 0;
fail_read = 1;
wait_calls(calls.load() + 1);
for (int i = 0; i < 100 && api.mic_alive(driver, mic); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
assert(!api.mic_alive(driver, mic));
assert(api.read(driver, mic, samples, sizeof(samples)) == -1);
api.close_mic(driver, mic);
api.free(driver);
assert(closes == 2);
std::puts(
"microphone backend: opt-in, bounded queue, mute, stop, fallback and input failure PASS");
}