|
|
|
|
@ -58,6 +58,9 @@ extern "C" void __wrap_ma_context_uninit(ma_context* context) {
|
|
|
|
|
extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) { |
|
|
|
|
if (failDevice) { failDevice = false; return MA_ERROR; } |
|
|
|
|
ma_device_config requested = *config; |
|
|
|
|
// Второй виртуальный вход: отдельный null device с тем же аппаратным драйвером.
|
|
|
|
|
if (context->backend == ma_backend_null && config->deviceType == ma_device_type_capture && |
|
|
|
|
config->capture.pDeviceID && config->capture.pDeviceID->nullbackend == 1) requested.capture.pDeviceID = nullptr; |
|
|
|
|
if (latencyProbe) { |
|
|
|
|
requested.periodSizeInMilliseconds = 0; |
|
|
|
|
requested.periodSizeInFrames = config->deviceType == ma_device_type_capture ? probeCaptureFrames : probePlaybackFrames; |
|
|
|
|
@ -109,6 +112,7 @@ static void shared_capture(SoundManager* manager) {
|
|
|
|
|
AudioCapture stream, radio; |
|
|
|
|
std::atomic<unsigned> streamFrames{0}, radioFrames{0}; |
|
|
|
|
std::atomic<int64_t> streamTime{0}; |
|
|
|
|
std::atomic<bool> compareTime{false}; |
|
|
|
|
int streamReady = 0, radioReady = 0, streamStopped = 0, radioStopped = 0, radioChannels = 0; |
|
|
|
|
auto startStream = [&] { |
|
|
|
|
return stream.open(selected, AudioCapture::Role::Stream, 2, 1, [&](int channels, int) { |
|
|
|
|
@ -123,25 +127,25 @@ static void shared_capture(SoundManager* manager) {
|
|
|
|
|
radioChannels = channels; ++radioReady; return true; |
|
|
|
|
}, [&] { ++radioStopped; }, [&](const AudioCapture::Frame& frame) { |
|
|
|
|
REQUIRE(frame.channels == radioChannels && frame.pcm); |
|
|
|
|
if (streamTime.load()) REQUIRE(frame.timeUs == streamTime.load()); |
|
|
|
|
if (compareTime) REQUIRE(frame.timeUs == streamTime.load()); |
|
|
|
|
radioFrames += frame.frames; |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
int before = captureStarts; |
|
|
|
|
REQUIRE(startStream()); REQUIRE(startRadio()); pump(80); |
|
|
|
|
REQUIRE(startStream()); REQUIRE(startRadio()); compareTime = true; pump(80); |
|
|
|
|
REQUIRE(captureStarts == before + 1 && radioChannels == 2 && streamReady == 1 && radioReady == 1); |
|
|
|
|
REQUIRE(streamFrames > 0 && radioFrames > 0); |
|
|
|
|
radio.close(); auto captured = streamFrames.load(); pump(60); |
|
|
|
|
REQUIRE(streamFrames > captured && captureStarts == before + 1 && streamStopped == 0); |
|
|
|
|
REQUIRE(startRadio()); REQUIRE(captureStarts == before + 1); |
|
|
|
|
// Detach stream не закрывает устройство и не меняет формат оставшейся рации.
|
|
|
|
|
stream.close(); streamTime = 0; captured = radioFrames.load(); pump(60); |
|
|
|
|
compareTime = false; stream.close(); streamTime = 0; captured = radioFrames.load(); pump(60); |
|
|
|
|
REQUIRE(radio.running() && radioFrames > captured && radioChannels == 2 && captureStarts == before + 1); |
|
|
|
|
radio.close(); REQUIRE(streamStopped == 1 && radioStopped == 2); |
|
|
|
|
// Обратный порядок: новый stream заменяет mono-профиль рации на свой stereo.
|
|
|
|
|
before = captureStarts; |
|
|
|
|
REQUIRE(startRadio()); REQUIRE(radioChannels == 1); |
|
|
|
|
REQUIRE(startStream()); pump(60); |
|
|
|
|
REQUIRE(startStream()); compareTime = true; pump(60); |
|
|
|
|
REQUIRE(captureStarts == before + 2 && radioChannels == 2 && stream.running() && radio.running()); |
|
|
|
|
// Потеря устройства: обе подписки восстанавливаются одним открытием.
|
|
|
|
|
for (auto* device : devices) if (device->type == ma_device_type_capture) REQUIRE(ma_device_stop(device) == MA_SUCCESS); |
|
|
|
|
@ -149,6 +153,23 @@ static void shared_capture(SoundManager* manager) {
|
|
|
|
|
pump(300); radio.recover(); |
|
|
|
|
REQUIRE(stream.running() && radio.running() && captureStarts == before + 3); |
|
|
|
|
radio.close(); stream.close(); |
|
|
|
|
compareTime = false; |
|
|
|
|
// Разные выбранные устройства сохраняют независимые форматы и жизненные циклы.
|
|
|
|
|
ma_device_id secondId{}; secondId.nullbackend = 1; |
|
|
|
|
QString second = QString::fromLatin1(ma_get_backend_name(manager->context()->backend)) + '/' + |
|
|
|
|
QByteArray(reinterpret_cast<const char*>(&secondId), sizeof(secondId)).toBase64(); |
|
|
|
|
before = captureStarts; |
|
|
|
|
REQUIRE(startStream()); |
|
|
|
|
REQUIRE(radio.open(second, AudioCapture::Role::Radio, 1, 2, [](int channels, int) { REQUIRE(channels == 1); return true; }, |
|
|
|
|
[] {}, [](const AudioCapture::Frame& frame) { REQUIRE(frame.channels == 1); })); |
|
|
|
|
REQUIRE(captureStarts == before + 2); |
|
|
|
|
captured = streamFrames.load(); radio.close(); pump(60); |
|
|
|
|
REQUIRE(streamFrames > captured && captureStarts == before + 2); |
|
|
|
|
// Неверный явный выбор не заменяется default и не отбирает захват трансляции.
|
|
|
|
|
REQUIRE(!radio.open(QStringLiteral("invalid-input"), AudioCapture::Role::Radio, 1, 2, [](int, int) { return true; }, |
|
|
|
|
[] {}, [](const AudioCapture::Frame&) {})); |
|
|
|
|
REQUIRE(stream.running() && captureStarts == before + 2); |
|
|
|
|
radio.close(); stream.close(); |
|
|
|
|
qInfo("shared capture: default/explicit alias, stream priority, attach/detach without restart, recovery PASS"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|