mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-10-02 11:22:22 +00:00
Merge pull request #114 from vo90/agent/asio-close-before-reconfigure
fix(audio): close Windows ASIO before reconfiguration
This commit is contained in:
@@ -131,6 +131,49 @@ DeviceOptions DeviceSetup::probeDual(const juce::String& inputTypeName,
|
||||
|
||||
if (isDuplex)
|
||||
{
|
||||
// ASIO drivers commonly allow only one live device object. If
|
||||
// this exact duplex endpoint is already open, constructing a
|
||||
// second object can succeed but report zero channel names (the
|
||||
// settings UI then incorrectly falls back to Inputs 1-2). Reuse
|
||||
// the live device's immutable capability lists instead. The
|
||||
// endpoint checks keep a newly selected device on the normal
|
||||
// temporary-probe path.
|
||||
//
|
||||
// Reading the live device here is safe only because probes and
|
||||
// device mutations are serialised on the JUCE message thread
|
||||
// (runDeviceLifecycleOp, PR #113) — a caller probing off-thread
|
||||
// would race applyDuplex's close/reopen.
|
||||
auto* liveDevice = inMgr.getCurrentAudioDevice();
|
||||
auto* liveType = inMgr.getCurrentDeviceTypeObject();
|
||||
const auto liveSetup = inMgr.getAudioDeviceSetup();
|
||||
const bool requestedEndpointIsLive =
|
||||
liveDevice != nullptr
|
||||
&& liveDevice->isOpen()
|
||||
&& liveType != nullptr
|
||||
&& liveType->getTypeName() == options.inputType
|
||||
&& liveSetup.inputDeviceName == probeInputName
|
||||
&& liveSetup.outputDeviceName == probeOutputName;
|
||||
|
||||
if (requestedEndpointIsLive)
|
||||
{
|
||||
options.inputChannels = liveDevice->getInputChannelNames();
|
||||
options.outputChannels = liveDevice->getOutputChannelNames();
|
||||
for (auto rate : liveDevice->getAvailableSampleRates())
|
||||
options.sampleRates.addIfNotAlreadyThere(rate);
|
||||
for (auto size : liveDevice->getAvailableBufferSizes())
|
||||
options.bufferSizes.addIfNotAlreadyThere(size);
|
||||
|
||||
fprintf(stderr, "[AudioEngine] Probed live device options: "
|
||||
"inType='%s' outType='%s' in='%s' out='%s' "
|
||||
"inputs=%d outputs=%d rates=%d buffers=%d compatible=%d\n",
|
||||
options.inputType.toRawUTF8(), options.outputType.toRawUTF8(),
|
||||
options.input.toRawUTF8(), options.output.toRawUTF8(),
|
||||
options.inputChannels.size(), options.outputChannels.size(),
|
||||
options.sampleRates.size(), options.bufferSizes.size(),
|
||||
(int) options.compatible);
|
||||
return options;
|
||||
}
|
||||
|
||||
std::unique_ptr<juce::AudioIODevice> dev(
|
||||
inputType->createDevice(probeOutputName, probeInputName));
|
||||
if (dev)
|
||||
@@ -252,6 +295,28 @@ juce::String DeviceSetup::applyDuplex(const juce::String& inputName,
|
||||
setup.useDefaultInputChannels = inputName.isEmpty();
|
||||
setup.useDefaultOutputChannels = outputName.isEmpty();
|
||||
|
||||
// Every unsuccessful reconfiguration must leave the manager and the
|
||||
// externally readable engine format in one truthful state: closed/zero.
|
||||
// In particular, never keep a stale ASIO device or the previous 256-sample
|
||||
// state alive after a failed request for 512.
|
||||
auto failClosed = [&](const juce::String& error) -> juce::String {
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure failed: %s; closing device\n",
|
||||
error.toRawUTF8());
|
||||
try { inMgr.closeAudioDevice(); }
|
||||
catch (...) {
|
||||
fprintf(stderr, "[AudioEngine] Duplex failure cleanup: closeAudioDevice threw\n");
|
||||
}
|
||||
state.currentSampleRate.store(0.0, std::memory_order_relaxed);
|
||||
state.inputBlockSize.store(0, std::memory_order_relaxed);
|
||||
state.outputBlockSize.store(0, std::memory_order_relaxed);
|
||||
state.duplexMode.store(false, std::memory_order_relaxed);
|
||||
try { monitorChain.releaseMonitorChain(); }
|
||||
catch (...) {
|
||||
fprintf(stderr, "[AudioEngine] Duplex failure cleanup: monitor release threw\n");
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
// Channel masks must match too — high-numbered selectedInputChannel needs
|
||||
// the expanded mask that an older session may not have opened.
|
||||
if (auto* currentDevice = inMgr.getCurrentAudioDevice())
|
||||
@@ -277,6 +342,7 @@ juce::String DeviceSetup::applyDuplex(const juce::String& inputName,
|
||||
&& current.useDefaultOutputChannels == setup.useDefaultOutputChannels
|
||||
&& current.inputChannels == expectedInputs
|
||||
&& current.outputChannels == expectedOutputs
|
||||
&& currentDevice->isOpen()
|
||||
&& state.duplexMode.load(std::memory_order_relaxed))
|
||||
{
|
||||
fprintf(stderr, "[AudioEngine] Duplex device already configured with same settings, skipping\n");
|
||||
@@ -293,29 +359,45 @@ juce::String DeviceSetup::applyDuplex(const juce::String& inputName,
|
||||
}
|
||||
}
|
||||
|
||||
// ALSA deadlocks on reconfigure unless we fully close first. WASAPI
|
||||
// reconfigures in place and is much slower if closed.
|
||||
#if JUCE_LINUX
|
||||
// ALSA and Windows ASIO both need a full close before reconfiguration.
|
||||
// The Helix driver was observed accepting a first request without changing
|
||||
// its buffer, then wedging JUCE's message thread on the next in-place
|
||||
// request. Close BEFORE the temporary channel probe too: constructing an
|
||||
// ASIO device initialises the driver and briefly starts dummy buffers, so a
|
||||
// probe must never overlap the live primary instance. WASAPI remains
|
||||
// in-place because closing it is materially slower and this failure mode is
|
||||
// specific to ASIO.
|
||||
juce::String currentTypeName;
|
||||
if (auto* currentType = inMgr.getCurrentDeviceTypeObject())
|
||||
currentTypeName = currentType->getTypeName();
|
||||
if (inMgr.getCurrentAudioDevice() != nullptr)
|
||||
|
||||
bool closeBeforeReconfigure = false;
|
||||
#if JUCE_LINUX
|
||||
closeBeforeReconfigure = true;
|
||||
#elif JUCE_WINDOWS
|
||||
closeBeforeReconfigure = (currentTypeName == "ASIO");
|
||||
#endif
|
||||
|
||||
if (closeBeforeReconfigure && inMgr.getCurrentAudioDevice() != nullptr)
|
||||
{
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure phase=close begin type='%s'\n",
|
||||
currentTypeName.toRawUTF8());
|
||||
try {
|
||||
inMgr.closeAudioDevice();
|
||||
fprintf(stderr, "[AudioEngine] Closed device for reconfiguration\n");
|
||||
if (currentTypeName.isNotEmpty())
|
||||
inMgr.setCurrentAudioDeviceType(currentTypeName, true);
|
||||
// AudioDeviceManager::closeAudioDevice() preserves its current
|
||||
// device type/setup. setAudioDeviceSetup() below sees a null
|
||||
// device and creates a fresh instance of that same type.
|
||||
} catch (...) {
|
||||
fprintf(stderr, "[AudioEngine] closeAudioDevice crashed, continuing\n");
|
||||
return failClosed("closeAudioDevice threw before reconfiguration");
|
||||
}
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure phase=close complete\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
int inputChannelCount = 0;
|
||||
int outputChannelCount = 0;
|
||||
if (auto* type = inMgr.getCurrentDeviceTypeObject())
|
||||
{
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure phase=probe begin\n");
|
||||
try
|
||||
{
|
||||
if (auto probe = std::unique_ptr<juce::AudioIODevice>(type->createDevice(outputName, inputName)))
|
||||
@@ -332,6 +414,8 @@ juce::String DeviceSetup::applyDuplex(const juce::String& inputName,
|
||||
{
|
||||
fprintf(stderr, "[AudioEngine] Channel probe failed (unknown)\n");
|
||||
}
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure phase=probe complete inputs=%d outputs=%d\n",
|
||||
inputChannelCount, outputChannelCount);
|
||||
}
|
||||
if (inputChannelCount <= 0) inputChannelCount = 2;
|
||||
if (outputChannelCount <= 0) outputChannelCount = 2;
|
||||
@@ -340,27 +424,109 @@ juce::String DeviceSetup::applyDuplex(const juce::String& inputName,
|
||||
setup.outputChannels.setRange(0, juce::jmin(outputChannelCount, 2), true);
|
||||
|
||||
juce::String result;
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure phase=open begin sr=%.0f bs=%d\n",
|
||||
setup.sampleRate, setup.bufferSize);
|
||||
try {
|
||||
result = inMgr.setAudioDeviceSetup(setup, true);
|
||||
} catch (...) {
|
||||
return "setAudioDeviceSetup threw";
|
||||
return failClosed("setAudioDeviceSetup threw");
|
||||
}
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure phase=open complete error='%s'\n",
|
||||
result.toRawUTF8());
|
||||
if (result.isNotEmpty())
|
||||
{
|
||||
fprintf(stderr, "[AudioEngine] Device setup error: %s\n", result.toRawUTF8());
|
||||
try {
|
||||
result = inMgr.initialiseWithDefaultDevices(2, 2);
|
||||
} catch (...) {
|
||||
return "fallback initialiseWithDefaultDevices threw";
|
||||
}
|
||||
if (result.isNotEmpty())
|
||||
return "device setup failed: " + result;
|
||||
// A default-device fallback used to convert this failure into success,
|
||||
// leaving only two channels active while the UI saved the requested
|
||||
// ASIO device. Preserve the original error and stay closed instead.
|
||||
return failClosed("device setup failed: " + result);
|
||||
}
|
||||
|
||||
if (auto* configuredDevice = inMgr.getCurrentAudioDevice())
|
||||
{
|
||||
if (!configuredDevice->isOpen())
|
||||
return failClosed("device is not open after setup");
|
||||
|
||||
const double sr = configuredDevice->getCurrentSampleRate();
|
||||
const int bs = configuredDevice->getCurrentBufferSizeSamples();
|
||||
|
||||
// For explicitly named endpoints, "all inputs / first two outputs" is
|
||||
// the requested contract. Rebuild those masks from the opened device's
|
||||
// advertised channels so a failed pre-open probe cannot silently
|
||||
// collapse an 8-input ASIO interface to the old two-channel fallback.
|
||||
juce::BigInteger expectedInputs;
|
||||
if (setup.useDefaultInputChannels)
|
||||
expectedInputs = setup.inputChannels;
|
||||
else
|
||||
expectedInputs.setRange(
|
||||
0, configuredDevice->getInputChannelNames().size(), true);
|
||||
juce::BigInteger expectedOutputs;
|
||||
if (setup.useDefaultOutputChannels)
|
||||
expectedOutputs = setup.outputChannels;
|
||||
else
|
||||
expectedOutputs.setRange(
|
||||
0, juce::jmin(configuredDevice->getOutputChannelNames().size(), 2), true);
|
||||
|
||||
const auto actualInputs = configuredDevice->getActiveInputChannels();
|
||||
const auto actualOutputs = configuredDevice->getActiveOutputChannels();
|
||||
const bool inputChannelsMatch =
|
||||
setup.useDefaultInputChannels || actualInputs == expectedInputs;
|
||||
const bool outputChannelsMatch =
|
||||
setup.useDefaultOutputChannels || actualOutputs == expectedOutputs;
|
||||
|
||||
fprintf(stderr,
|
||||
"[AudioEngine] Duplex reconfigure phase=verify requested(sr=%.0f bs=%d in=%s out=%s) "
|
||||
"actual(sr=%.0f bs=%d in=%s out=%s)\n",
|
||||
setup.sampleRate, setup.bufferSize,
|
||||
expectedInputs.toString(2).toRawUTF8(),
|
||||
expectedOutputs.toString(2).toRawUTF8(),
|
||||
sr, bs,
|
||||
actualInputs.toString(2).toRawUTF8(),
|
||||
actualOutputs.toString(2).toRawUTF8());
|
||||
|
||||
// Buffer-size strictness is ASIO-only. The Helix regression this
|
||||
// gate exists for (success reported at the old buffer size, next
|
||||
// request wedging the message thread) is an ASIO driver behaviour;
|
||||
// ALSA rounds requests to period-size constraints and CoreAudio can
|
||||
// clamp, and both previously worked by storing the driver-adjusted
|
||||
// actuals. Failing those closed would turn a working driver-rounded
|
||||
// 480-for-512 open into "no audio". Rate and channel-mask
|
||||
// verification stay strict on every backend.
|
||||
juce::String configuredTypeName;
|
||||
if (auto* configuredType = inMgr.getCurrentDeviceTypeObject())
|
||||
configuredTypeName = configuredType->getTypeName();
|
||||
const bool strictBufferSize = (configuredTypeName == "ASIO");
|
||||
if (!strictBufferSize && bs != setup.bufferSize)
|
||||
fprintf(stderr, "[AudioEngine] Duplex reconfigure phase=verify "
|
||||
"accepting driver-adjusted buffer size %d (requested %d, type='%s')\n",
|
||||
bs, setup.bufferSize, configuredTypeName.toRawUTF8());
|
||||
|
||||
switch (validateOpenedDeviceFormat(
|
||||
setup.sampleRate,
|
||||
strictBufferSize ? setup.bufferSize : bs, sr, bs,
|
||||
inputChannelsMatch, outputChannelsMatch))
|
||||
{
|
||||
case DeviceFormatMismatch::sampleRate:
|
||||
return failClosed(
|
||||
"device opened at sample rate " + juce::String(sr)
|
||||
+ " (requested " + juce::String(setup.sampleRate) + ")");
|
||||
case DeviceFormatMismatch::bufferSize:
|
||||
return failClosed(
|
||||
"device opened at buffer size " + juce::String(bs)
|
||||
+ " (requested " + juce::String(setup.bufferSize) + ")");
|
||||
case DeviceFormatMismatch::inputChannels:
|
||||
return failClosed(
|
||||
"device opened with input channel mask "
|
||||
+ actualInputs.toString(2) + " (requested "
|
||||
+ expectedInputs.toString(2) + ")");
|
||||
case DeviceFormatMismatch::outputChannels:
|
||||
return failClosed(
|
||||
"device opened with output channel mask "
|
||||
+ actualOutputs.toString(2) + " (requested "
|
||||
+ expectedOutputs.toString(2) + ")");
|
||||
case DeviceFormatMismatch::none:
|
||||
break;
|
||||
}
|
||||
|
||||
state.currentSampleRate.store(sr, std::memory_order_relaxed);
|
||||
state.inputBlockSize.store(bs, std::memory_order_relaxed);
|
||||
state.outputBlockSize.store(bs, std::memory_order_relaxed);
|
||||
@@ -373,11 +539,7 @@ juce::String DeviceSetup::applyDuplex(const juce::String& inputName,
|
||||
monitorChain.prepareMonitorChain(sr, bs);
|
||||
return {};
|
||||
}
|
||||
state.currentSampleRate.store(0.0, std::memory_order_relaxed);
|
||||
state.inputBlockSize.store(0, std::memory_order_relaxed);
|
||||
state.outputBlockSize.store(0, std::memory_order_relaxed);
|
||||
monitorChain.releaseMonitorChain();
|
||||
return "no current device after setup";
|
||||
return failClosed("no current device after setup");
|
||||
}
|
||||
|
||||
DeviceConfigResult DeviceSetup::applySplit(const DeviceConfig& config,
|
||||
|
||||
@@ -30,4 +30,38 @@ inline bool nominalRateCandidate(double r, double r2, double& candidate) noexcep
|
||||
return ratesMatch(r, candidate) && ratesMatch(r2, candidate);
|
||||
}
|
||||
|
||||
// Post-open verification result shared by device setup. JUCE drivers may
|
||||
// choose a "best" format when the exact request is unavailable; that is useful
|
||||
// for generic callers, but this app must not report success for a different
|
||||
// buffer/rate/channel mask than the user selected. Channel equality is
|
||||
// computed by the JUCE-facing caller and passed as booleans so this decision
|
||||
// table stays JUCE-free and unit-testable.
|
||||
enum class DeviceFormatMismatch
|
||||
{
|
||||
none,
|
||||
sampleRate,
|
||||
bufferSize,
|
||||
inputChannels,
|
||||
outputChannels,
|
||||
};
|
||||
|
||||
inline DeviceFormatMismatch validateOpenedDeviceFormat(
|
||||
double requestedSampleRate,
|
||||
int requestedBufferSize,
|
||||
double actualSampleRate,
|
||||
int actualBufferSize,
|
||||
bool inputChannelsMatch,
|
||||
bool outputChannelsMatch) noexcept
|
||||
{
|
||||
if (!ratesMatch(requestedSampleRate, actualSampleRate))
|
||||
return DeviceFormatMismatch::sampleRate;
|
||||
if (requestedBufferSize != actualBufferSize)
|
||||
return DeviceFormatMismatch::bufferSize;
|
||||
if (!inputChannelsMatch)
|
||||
return DeviceFormatMismatch::inputChannels;
|
||||
if (!outputChannelsMatch)
|
||||
return DeviceFormatMismatch::outputChannels;
|
||||
return DeviceFormatMismatch::none;
|
||||
}
|
||||
|
||||
} // namespace slopsmith
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
// Source-level lifecycle contract for the hardware-dependent half of the ASIO
|
||||
// repair. The pure format decision table is covered by rate_match_test.cpp;
|
||||
// these assertions pin the ordering that cannot be exercised without loading
|
||||
// a real Windows ASIO driver in CI.
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'src', 'audio', 'engine', 'DeviceSetup.cpp'),
|
||||
'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
const start = source.indexOf('juce::String DeviceSetup::applyDuplex');
|
||||
const end = source.indexOf('DeviceConfigResult DeviceSetup::applySplit', start);
|
||||
// Fail with a clear message before any slicing if the function markers move —
|
||||
// a bad slice would otherwise make every assertion below fail confusingly.
|
||||
assert.ok(start >= 0 && end > start, 'could not locate applyDuplex in DeviceSetup.cpp');
|
||||
const applyDuplex = source.slice(start, end);
|
||||
const probeStart = source.indexOf('DeviceOptions DeviceSetup::probeDual');
|
||||
const probeEnd = source.indexOf('juce::String DeviceSetup::applyDuplex', probeStart);
|
||||
const probeDual = source.slice(probeStart, probeEnd);
|
||||
|
||||
test('duplex probe reuses an exact live endpoint before constructing a competing device', () => {
|
||||
assert.ok(probeStart >= 0 && probeEnd > probeStart, 'could not locate probeDual');
|
||||
|
||||
const inspectLive = probeDual.indexOf('inMgr.getCurrentAudioDevice()');
|
||||
const exactEndpoint = probeDual.indexOf('const bool requestedEndpointIsLive');
|
||||
const requireOpen = probeDual.indexOf('liveDevice->isOpen()', exactEndpoint);
|
||||
const reuseChannels = probeDual.indexOf(
|
||||
'options.inputChannels = liveDevice->getInputChannelNames()');
|
||||
const temporaryProbe = probeDual.indexOf(
|
||||
'inputType->createDevice(probeOutputName, probeInputName)');
|
||||
|
||||
assert.ok(inspectLive >= 0 && exactEndpoint > inspectLive,
|
||||
'the probe must inspect and identity-check the live endpoint');
|
||||
assert.ok(requireOpen > exactEndpoint && reuseChannels > requireOpen
|
||||
&& temporaryProbe > reuseChannels,
|
||||
'matching live capabilities must be returned before a temporary device is created');
|
||||
});
|
||||
|
||||
test('duplex setup closes Windows ASIO before constructing its channel probe', () => {
|
||||
assert.match(
|
||||
applyDuplex,
|
||||
/#elif JUCE_WINDOWS\s+closeBeforeReconfigure = \(currentTypeName == "ASIO"\);/);
|
||||
|
||||
const guardedClose = applyDuplex.indexOf(
|
||||
'if (closeBeforeReconfigure && inMgr.getCurrentAudioDevice() != nullptr)');
|
||||
const close = applyDuplex.indexOf('inMgr.closeAudioDevice();', guardedClose);
|
||||
const probe = applyDuplex.indexOf('type->createDevice(outputName, inputName)');
|
||||
assert.ok(guardedClose >= 0 && close > guardedClose && probe > close,
|
||||
'the live ASIO device must be closed before a temporary probe is created');
|
||||
});
|
||||
|
||||
test('duplex setup never converts requested-device failure into default-device success', () => {
|
||||
assert.doesNotMatch(applyDuplex, /initialiseWithDefaultDevices/);
|
||||
assert.match(applyDuplex, /return failClosed\("device setup failed: " \+ result\);/);
|
||||
});
|
||||
|
||||
test('duplex success is gated on actual rate, buffer, and active channel masks', () => {
|
||||
assert.match(applyDuplex, /getCurrentSampleRate\(\)/);
|
||||
assert.match(applyDuplex, /getCurrentBufferSizeSamples\(\)/);
|
||||
assert.match(applyDuplex, /getActiveInputChannels\(\)/);
|
||||
assert.match(applyDuplex, /getActiveOutputChannels\(\)/);
|
||||
assert.match(applyDuplex, /validateOpenedDeviceFormat\(/);
|
||||
});
|
||||
|
||||
test('strict buffer-size verification is gated to ASIO; other backends accept driver-adjusted', () => {
|
||||
// ALSA rounds to period constraints and CoreAudio can clamp; both must
|
||||
// keep their pre-#114 store-the-actuals behaviour instead of failing
|
||||
// closed on a working driver-rounded open.
|
||||
assert.match(applyDuplex,
|
||||
/const bool strictBufferSize = \(configuredTypeName == "ASIO"\);/);
|
||||
assert.match(applyDuplex,
|
||||
/strictBufferSize \? setup\.bufferSize : bs/);
|
||||
assert.match(applyDuplex, /accepting driver-adjusted buffer size/);
|
||||
});
|
||||
|
||||
test('duplex failures clear observable format state and release monitor resources', () => {
|
||||
const cleanupStart = applyDuplex.indexOf('auto failClosed =');
|
||||
const cleanupEnd = applyDuplex.indexOf('// Channel masks must match too', cleanupStart);
|
||||
const cleanup = applyDuplex.slice(cleanupStart, cleanupEnd);
|
||||
|
||||
assert.match(cleanup, /inMgr\.closeAudioDevice\(\)/);
|
||||
assert.match(cleanup, /currentSampleRate\.store\(0\.0/);
|
||||
assert.match(cleanup, /inputBlockSize\.store\(0/);
|
||||
assert.match(cleanup, /outputBlockSize\.store\(0/);
|
||||
assert.match(cleanup, /monitorChain\.releaseMonitorChain\(\)/);
|
||||
});
|
||||
@@ -5,42 +5,76 @@
|
||||
|
||||
#include "../../src/audio/engine/RateMatch.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
using slopsmith::ratesMatch;
|
||||
using slopsmith::nominalRateCandidate;
|
||||
using slopsmith::DeviceFormatMismatch;
|
||||
using slopsmith::validateOpenedDeviceFormat;
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failed = 0;
|
||||
|
||||
void check(bool condition, const char* expression, const char* file, int line)
|
||||
{
|
||||
if (condition)
|
||||
return;
|
||||
|
||||
++g_failed;
|
||||
std::fprintf(stderr, " FAIL: %s (%s:%d)\n", expression, file, line);
|
||||
}
|
||||
|
||||
#define CHECK(condition) check((condition), #condition, __FILE__, __LINE__)
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// Tolerance is <= 0.5 (not <): a backend reporting 47999.5 against a
|
||||
// 48000 nominal sits exactly on the boundary and MUST pass — the probe
|
||||
// accepted it, so preflight and post-open verify must too.
|
||||
assert(ratesMatch(47999.5, 48000.0));
|
||||
assert(ratesMatch(48000.0, 47999.5));
|
||||
assert(ratesMatch(48000.0, 48000.0));
|
||||
assert(!ratesMatch(47999.4, 48000.0)); // 0.6 apart → reject
|
||||
assert(!ratesMatch(44100.0, 48000.0));
|
||||
CHECK(ratesMatch(47999.5, 48000.0));
|
||||
CHECK(ratesMatch(48000.0, 47999.5));
|
||||
CHECK(ratesMatch(48000.0, 48000.0));
|
||||
CHECK(!ratesMatch(47999.4, 48000.0)); // 0.6 apart → reject
|
||||
CHECK(!ratesMatch(44100.0, 48000.0));
|
||||
|
||||
double c = 0.0;
|
||||
|
||||
// Exact pair → exact nominal.
|
||||
assert(nominalRateCandidate(48000.0, 48000.0, c) && c == 48000.0);
|
||||
CHECK(nominalRateCandidate(48000.0, 48000.0, c) && c == 48000.0);
|
||||
|
||||
// Fractional drift on both sides rounds to the clean nominal.
|
||||
assert(nominalRateCandidate(47999.5, 48000.0, c) && c == 48000.0);
|
||||
assert(nominalRateCandidate(48000.4, 48000.1, c) && c == 48000.0);
|
||||
CHECK(nominalRateCandidate(47999.5, 48000.0, c) && c == 48000.0);
|
||||
CHECK(nominalRateCandidate(48000.4, 48000.1, c) && c == 48000.0);
|
||||
|
||||
// Fail-closed midpoint case from the original comment: 48000.4/48000.6
|
||||
// passes the pair check (diff 0.2) but rounds to 48001 (midpoint 48000.5
|
||||
// rounds up), which is 0.6 from 48000.4 — outside tolerance of one side,
|
||||
// so no candidate is surfaced.
|
||||
const bool ok = nominalRateCandidate(48000.4, 48000.6, c);
|
||||
assert(!ok && "midpoint-rounding must stay fail-closed");
|
||||
CHECK(!ok && "midpoint-rounding must stay fail-closed");
|
||||
|
||||
// Non-matching pair → no candidate at all.
|
||||
assert(!nominalRateCandidate(44100.0, 48000.0, c));
|
||||
CHECK(!nominalRateCandidate(44100.0, 48000.0, c));
|
||||
|
||||
std::puts("rate_match: all cases passed");
|
||||
return 0;
|
||||
// A device setup is successful only when the driver accepted every
|
||||
// requested format field. This pins the Helix regression where JUCE
|
||||
// returned success for a 512 request while the driver remained at 256.
|
||||
CHECK(validateOpenedDeviceFormat(48000.0, 512, 48000.0, 512, true, true)
|
||||
== DeviceFormatMismatch::none);
|
||||
CHECK(validateOpenedDeviceFormat(48000.0, 512, 44100.0, 512, true, true)
|
||||
== DeviceFormatMismatch::sampleRate);
|
||||
CHECK(validateOpenedDeviceFormat(48000.0, 512, 48000.0, 256, true, true)
|
||||
== DeviceFormatMismatch::bufferSize);
|
||||
CHECK(validateOpenedDeviceFormat(48000.0, 512, 48000.0, 512, false, true)
|
||||
== DeviceFormatMismatch::inputChannels);
|
||||
CHECK(validateOpenedDeviceFormat(48000.0, 512, 48000.0, 512, true, false)
|
||||
== DeviceFormatMismatch::outputChannels);
|
||||
|
||||
if (g_failed == 0)
|
||||
std::puts("rate_match: all cases passed");
|
||||
|
||||
return g_failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user