Skip to content

Commit

Permalink
Added support for ASIOOutputReady().
Browse files Browse the repository at this point in the history
Fixes #4.
  • Loading branch information
dechamps committed Feb 2, 2019
1 parent dec22cf commit 4a5cb74
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 26 deletions.
87 changes: 66 additions & 21 deletions src/asio401/ASIO401/asio401.cpp
Expand Up @@ -406,8 +406,15 @@ namespace asio401 {
void ASIO401::PreparedState::GetLatencies(long* inputLatency, long* outputLatency)
{
*inputLatency = long(buffers.bufferSizeInFrames);
*outputLatency = long(buffers.bufferSizeInFrames) * 2; // Because we don't support ASIOOutputReady() - see ASIO SDK docs, dechamps_ASIOUtil/BUFFERS.md
if (buffers.inputChannelCount == 0 && !asio401.config.forceRead) *outputLatency += asio401.qa401.hardwareQueueSizeInFrames;
*outputLatency = long(buffers.bufferSizeInFrames);
if (!asio401.hostSupportsOutputReady) {
Log() << buffers.bufferSizeInFrames << " samples added to output latency due to the ASIO Host Application not supporting OutputReady";
*outputLatency += long(buffers.bufferSizeInFrames);
}
if (buffers.inputChannelCount == 0 && !asio401.config.forceRead) {
Log() << asio401.qa401.hardwareQueueSizeInFrames << " samples added to output latency due to write-only mode";
*outputLatency += asio401.qa401.hardwareQueueSizeInFrames;
}
Log() << "Returning input latency of " << *inputLatency << " samples and output latency of " << *outputLatency << " samples";
}

Expand All @@ -425,6 +432,7 @@ namespace asio401 {
ASIO401::PreparedState::RunningState::RunningState(PreparedState& preparedState) :
preparedState(preparedState),
sampleRate(preparedState.asio401.sampleRate),
hostSupportsOutputReady(preparedState.asio401.hostSupportsOutputReady),
host_supports_timeinfo([&] {
Log() << "Checking if the host supports time info";
const bool result = preparedState.callbacks.asioMessage &&
Expand Down Expand Up @@ -504,18 +512,28 @@ namespace asio401 {
// On the first iteration, we can't start playing a real signal just yet because the QA401 write queue is empty at this point (see above), which means it could underrun (glitch) while the write is taking place.
// So instead we just send a buffer of silence, which will "hide" any glitches; that will guarantee that on the next iteration the QA401 write queue will be in a stable state and we can queue a real signal behind the silence.
if (!firstIteration) {
if (hostSupportsOutputReady) {
std::unique_lock outputReadyLock(outputReadyMutex);
if (!outputReady) {
if (IsLoggingEnabled()) Log() << "Waiting for the ASIO Host Application to signal OutputReady";
outputReadyCondition.wait(outputReadyLock, [&]{ return outputReady; });
outputReady = false;
}
}

PreProcessASIOOutputBuffers(preparedState.bufferInfos, driverBufferIndex, preparedState.buffers.bufferSizeInFrames);
preparedState.asio401.qa401.FinishWrite();
if (IsLoggingEnabled()) Log() << "Sending data from buffer index " << driverBufferIndex << " to QA401";
CopyToQA401Buffer(preparedState.bufferInfos, preparedState.buffers.bufferSizeInFrames, driverBufferIndex, writeBuffer.data());
}
preparedState.asio401.qa401.StartWrite(writeBuffer.data(), writeBufferSizeInBytes);
}

if (hostSupportsOutputReady) driverBufferIndex = (driverBufferIndex + 1) % 2;

if (!firstIteration && mustRead) preparedState.asio401.qa401.FinishRead();
currentSamplePosition.timestamp = ::dechamps_ASIOUtil::Int64ToASIO<ASIOTimeStamp>(((long long int) win32HighResolutionTimer.GetTimeMilliseconds()) * 1000000);
if (!firstIteration) {
currentSamplePosition.samples = ::dechamps_ASIOUtil::Int64ToASIO<ASIOSamples>(::dechamps_ASIOUtil::ASIOToInt64(currentSamplePosition.samples) + preparedState.buffers.bufferSizeInFrames);
if (preparedState.buffers.inputChannelCount > 0) {
if (IsLoggingEnabled()) Log() << "Received data from QA401 for buffer index " << driverBufferIndex;
CopyFromQA401Buffer(preparedState.bufferInfos, preparedState.buffers.bufferSizeInFrames, driverBufferIndex, readBuffer.data());
Expand All @@ -526,27 +544,34 @@ namespace asio401 {
preparedState.asio401.qa401.StartRead(readBuffer.data(), readBufferSizeInBytes);
if (!firstIteration && preparedState.buffers.inputChannelCount > 0) PostProcessASIOInputBuffers(preparedState.bufferInfos, driverBufferIndex, preparedState.buffers.bufferSizeInFrames);
}

if (IsLoggingEnabled()) Log() << "Updating position: " << ::dechamps_ASIOUtil::ASIOToInt64(currentSamplePosition.samples) << " samples, timestamp " << ::dechamps_ASIOUtil::ASIOToInt64(currentSamplePosition.timestamp);
samplePosition = currentSamplePosition;

if (!host_supports_timeinfo) {
if (IsLoggingEnabled()) Log() << "Firing ASIO bufferSwitch() callback with buffer index: " << driverBufferIndex;
preparedState.callbacks.bufferSwitch(long(driverBufferIndex), ASIOTrue);
if (IsLoggingEnabled()) Log() << "bufferSwitch() complete";
}
else {
ASIOTime time = { 0 };
time.timeInfo.flags = kSystemTimeValid | kSamplePositionValid | kSampleRateValid;
time.timeInfo.samplePosition = currentSamplePosition.samples;
time.timeInfo.systemTime = currentSamplePosition.timestamp;
time.timeInfo.sampleRate = sampleRate;
if (IsLoggingEnabled()) Log() << "Firing ASIO bufferSwitchTimeInfo() callback with buffer index: " << driverBufferIndex << ", time info: (" << ::dechamps_ASIOUtil::DescribeASIOTime(time) << ")";
const auto timeResult = preparedState.callbacks.bufferSwitchTimeInfo(&time, long(driverBufferIndex), ASIOTrue);
if (IsLoggingEnabled()) Log() << "bufferSwitchTimeInfo() complete, returned time info: " << (timeResult == nullptr ? "none" : ::dechamps_ASIOUtil::DescribeASIOTime(*timeResult));
// If the host supports OutputReady then we only need to write one buffer in advance, not two.
// Therefore, we can wait for the initial silent buffer to make it through and buffer 1 to start playing before we ask the application to start generating buffer 0.
if (!(firstIteration && hostSupportsOutputReady)) {
if (IsLoggingEnabled()) Log() << "Updating position: " << ::dechamps_ASIOUtil::ASIOToInt64(currentSamplePosition.samples) << " samples, timestamp " << ::dechamps_ASIOUtil::ASIOToInt64(currentSamplePosition.timestamp);
samplePosition = currentSamplePosition;

if (!host_supports_timeinfo) {
if (IsLoggingEnabled()) Log() << "Firing ASIO bufferSwitch() callback with buffer index: " << driverBufferIndex;
preparedState.callbacks.bufferSwitch(long(driverBufferIndex), ASIOTrue);
if (IsLoggingEnabled()) Log() << "bufferSwitch() complete";
}
else {
ASIOTime time = { 0 };
time.timeInfo.flags = kSystemTimeValid | kSamplePositionValid | kSampleRateValid;
time.timeInfo.samplePosition = currentSamplePosition.samples;
time.timeInfo.systemTime = currentSamplePosition.timestamp;
time.timeInfo.sampleRate = sampleRate;
if (IsLoggingEnabled()) Log() << "Firing ASIO bufferSwitchTimeInfo() callback with buffer index: " << driverBufferIndex << ", time info: (" << ::dechamps_ASIOUtil::DescribeASIOTime(time) << ")";
const auto timeResult = preparedState.callbacks.bufferSwitchTimeInfo(&time, long(driverBufferIndex), ASIOTrue);
if (IsLoggingEnabled()) Log() << "bufferSwitchTimeInfo() complete, returned time info: " << (timeResult == nullptr ? "none" : ::dechamps_ASIOUtil::DescribeASIOTime(*timeResult));
}

currentSamplePosition.samples = ::dechamps_ASIOUtil::Int64ToASIO<ASIOSamples>(::dechamps_ASIOUtil::ASIOToInt64(currentSamplePosition.samples) + preparedState.buffers.bufferSizeInFrames);
}

driverBufferIndex = (driverBufferIndex + 1) % 2;
if (!hostSupportsOutputReady) driverBufferIndex = (driverBufferIndex + 1) % 2;

preparedState.asio401.qa401.Ping();
firstIteration = false;
}
Expand Down Expand Up @@ -604,6 +629,26 @@ namespace asio401 {
if (IsLoggingEnabled()) Log() << "Returning: sample position " << ::dechamps_ASIOUtil::ASIOToInt64(*sPos) << ", timestamp " << ::dechamps_ASIOUtil::ASIOToInt64(*tStamp);
}

void ASIO401::OutputReady() {
if (!hostSupportsOutputReady) {
Log() << "Host supports OutputReady";
hostSupportsOutputReady = true;
}
if (preparedState.has_value()) preparedState->OutputReady();
}

void ASIO401::PreparedState::OutputReady() {
if (runningState != nullptr) runningState->OutputReady();
}

void ASIO401::PreparedState::RunningState::OutputReady() {
{
std::scoped_lock outputReadyLock(outputReadyMutex);
outputReady = true;
}
outputReadyCondition.notify_all();
}

void ASIO401::PreparedState::RequestReset() {
if (!callbacks.asioMessage || Message(callbacks.asioMessage, kAsioSelectorSupported, kAsioResetRequest, nullptr, nullptr) != 1)
throw ASIOException(ASE_InvalidMode, "reset requests are not supported");
Expand Down
10 changes: 10 additions & 0 deletions src/asio401/ASIO401/asio401.h
Expand Up @@ -11,6 +11,7 @@
#include <atomic>
#include <optional>
#include <stdexcept>
#include <mutex>
#include <thread>
#include <vector>

Expand Down Expand Up @@ -43,6 +44,7 @@ namespace asio401 {
void Start();
void Stop();
void GetSamplePosition(ASIOSamples* sPos, ASIOTimeStamp* tStamp);
void OutputReady();

void ControlPanel();

Expand All @@ -61,6 +63,7 @@ namespace asio401 {
void Stop();

void GetSamplePosition(ASIOSamples* sPos, ASIOTimeStamp* tStamp);
void OutputReady();

void RequestReset();

Expand Down Expand Up @@ -95,6 +98,7 @@ namespace asio401 {
~RunningState();

void GetSamplePosition(ASIOSamples* sPos, ASIOTimeStamp* tStamp) const;
void OutputReady();

private:
struct SamplePosition {
Expand All @@ -117,10 +121,15 @@ namespace asio401 {

PreparedState& preparedState;
const ASIOSampleRate sampleRate;
const bool hostSupportsOutputReady;
const bool host_supports_timeinfo;
std::atomic<bool> stopRequested = false;
std::atomic<SamplePosition> samplePosition;

std::mutex outputReadyMutex;
std::condition_variable outputReadyCondition;
bool outputReady = true;

Registration registration{ preparedState.runningState, *this };
std::thread thread;
};
Expand All @@ -144,6 +153,7 @@ namespace asio401 {

ASIOSampleRate sampleRate = 48000;
bool sampleRateWasAccessed = false;
bool hostSupportsOutputReady = false;

std::optional<PreparedState> preparedState;
};
Expand Down
6 changes: 1 addition & 5 deletions src/asio401/ASIO401/casio401.cpp
Expand Up @@ -133,11 +133,7 @@ namespace asio401 {
}

ASIOError outputReady() throw() final {
// We do not use Enter() and throw here, because this can be called in a real-time code path, and the cost
// of throwing exceptions is high and highly variable. When Enter() + throw was used here, this method could
// take as much as ~10 ms to run in the long tail.
if (IsLoggingEnabled()) Log() << "outputReady() called, returning ASE_NotPresent";
return ASE_NotPresent;
return EnterWithMethod("outputReady()", &ASIO401::OutputReady);
}

private:
Expand Down

0 comments on commit 4a5cb74

Please sign in to comment.