1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-24 12:30:17 +00:00

Adds an additional buffer. To reduce latency. No, really.

Specifically: there's no way to guarantee no overbuffering due to the startup race, other than having QAudioOutput obtain data by pull rather than push. But if it's pulling then that implies an extra buffer. And since the sizes it may pull are not explicit, there's guesswork involved there.

So: no extra buffer => uncontrollable risk of over-buffering. Extra buffer => a controllable risk of over-buffering.
This commit is contained in:
Thomas Harte 2020-06-09 00:01:22 -04:00
parent bcb23d9a15
commit d9f02aecdf
4 changed files with 120 additions and 19 deletions

View File

@ -248,6 +248,7 @@ HEADERS += \
../../Storage/Tape/Formats/*.hpp \
../../Storage/Tape/Parsers/*.hpp \
\
audiobuffer.h \
mainwindow.h \
scantargetwidget.h \
timer.h

103
OSBindings/Qt/audiobuffer.h Normal file
View File

@ -0,0 +1,103 @@
#ifndef AUDIOSOURCE_H
#define AUDIOSOURCE_H
#include <vector>
#include <cstdint>
#include <QIODevice>
/*!
* \brief Provides an intermediate receipticle for audio data.
*
* Provides a QIODevice that will attempt to buffer the minimum amount
* of data before handing it off to a polling QAudioOutput.
*
* Adding an extra buffer increases worst-case latency but resolves a
* startup race condition in which it is difficult to tell how much data a
* QAudioOutput that is populated by pushing data currently has buffered;
* it also works around what empirically seemed to be a minimum 16384-byte
* latency on push audio generation.
*/
struct AudioBuffer: public QIODevice {
AudioBuffer() {
open(QIODevice::ReadOnly | QIODevice::Unbuffered);
}
void setDepth(size_t depth) {
buffer.resize(depth);
}
// AudioBuffer-specific behaviour: always provide the latest data,
// even if that means skipping some.
qint64 readData(char *data, const qint64 maxlen) override {
if(!maxlen) {
return 0;
}
std::lock_guard lock(mutex);
if(readPointer == writePointer) return 0;
const size_t dataAvailable = std::min(writePointer - readPointer, size_t(maxlen));
size_t dataToCopy = dataAvailable;
// Push the read pointer such that only the most recent chunk is returned;
// nevertheless don't allow it to be pushed to a point where less than half
// a buffer is left, if avoidable. QAudioOutput doesn't make any guarantees
// about how much data it will read at a time so there's some second guessing here.
//
// TODO: can I be smarter than this?
// const size_t newReadPointer = std::min(writePointer - dataToCopy, writePointer - (buffer.size() >> 1));
// readPointer = std::max(readPointer, newReadPointer);
while(dataToCopy) {
const size_t nextLength = std::min(buffer.size() - (readPointer % buffer.size()), dataToCopy);
memcpy(data, &buffer[readPointer % buffer.size()], nextLength);
dataToCopy -= nextLength;
data += nextLength;
readPointer += nextLength;
}
return dataAvailable;
}
qint64 bytesAvailable() const override {
std::lock_guard lock(mutex);
return writePointer - readPointer;
}
// Required to make QIODevice concrete; not used.
qint64 writeData(const char *, qint64) override {
return 0;
}
// Posts a new set of source data. This buffer permits only the amount of data
// specified by @c setDepth to be enqueued into the future. Additional writes
// after the buffer is full will overwrite the newest data.
void write(const std::vector<int16_t> &source) {
std::lock_guard lock(mutex);
const size_t sourceSize = source.size() * sizeof(int16_t);
size_t endPoint = std::min(writePointer + sourceSize, readPointer + buffer.size());
writePointer = endPoint - sourceSize;
size_t bytesToCopy = sourceSize;
auto data = reinterpret_cast<const uint8_t *>(source.data());
while(bytesToCopy) {
size_t nextLength = std::min(buffer.size() - (writePointer % buffer.size()), bytesToCopy);
memcpy(&buffer[writePointer % buffer.size()], data, nextLength);
bytesToCopy -= nextLength;
data += nextLength;
writePointer += nextLength;
}
}
private:
mutable std::mutex mutex;
std::vector<uint8_t> buffer;
mutable size_t readPointer = 0;
size_t writePointer = 0;
};
#endif // AUDIOSOURCE_H

View File

@ -12,7 +12,7 @@
* it seems like Qt doesn't offer a way to constrain the aspect ratio of a view by constraining
the size of the window (i.e. you can use a custom layout to constrain a view, but that won't
affect the window, so isn't useful for this project). Therefore the emulation window
affect the window, so isn't useful for this project). Therefore the emulation window resizes freely.
*/
MainWindow::MainWindow(QWidget *parent)
@ -149,6 +149,7 @@ void MainWindow::launchMachine() {
// Install audio output if required.
const auto audio_producer = machine->audio_producer();
if(audio_producer) {
static constexpr size_t samplesPerBuffer = 256;
const auto speaker = audio_producer->get_speaker();
if(speaker) {
const QAudioDeviceInfo &defaultDeviceInfo = QAudioDeviceInfo::defaultOutputDevice();
@ -158,7 +159,6 @@ void MainWindow::launchMachine() {
// are available, and — at least for now — assume a good buffer size.
audioIsStereo = (idealFormat.channelCount() > 1) && speaker->get_is_stereo();
audioIs8bit = idealFormat.sampleSize() < 16;
const int samplesPerBuffer = 256;
speaker->set_output_rate(idealFormat.sampleRate(), samplesPerBuffer, audioIsStereo);
// Adjust format appropriately, and create an audio output.
@ -166,17 +166,12 @@ void MainWindow::launchMachine() {
idealFormat.setSampleSize(audioIs8bit ? 8 : 16);
audioOutput = std::make_unique<QAudioOutput>(idealFormat, this);
// Start the output. setBufferSize is supposed to set the audio buffer size, but
// appears not to work properly on macOS; experimenting with this on that platform
// revealed that setting the buffer size to 1024, then reading it back to find out
// what I actually got results in 2048. If I use a QIODevice-based audio output,
// i.e. one that pulls data as required, it then pulls in units of... 16384 bytes.
// That's almost 1/6th of a second — epic latency.
//
// So I have no idea. TODO: find an alternative audio library, I guess.
// Start the output. The additional `audioBuffer` is meant to minimise latency,
// believe it or not, given Qt's semantics.
speaker->set_delegate(this);
audioOutput->setBufferSize(samplesPerBuffer * sizeof(int16_t));
audioIODevice = audioOutput->start();
audioOutput->start(&audioBuffer);
audioBuffer.setDepth(audioOutput->bufferSize());
}
}
@ -217,13 +212,14 @@ void MainWindow::launchMachine() {
}
void MainWindow::speaker_did_complete_samples(Outputs::Speaker::Speaker *, const std::vector<int16_t> &buffer) {
const char *data = reinterpret_cast<const char *>(buffer.data());
size_t sizeLeft = buffer.size() * sizeof(int16_t);
while(sizeLeft) {
const auto bytesWritten = audioIODevice->write(data, qint64(sizeLeft));
sizeLeft -= bytesWritten;
data += bytesWritten;
}
audioBuffer.write(buffer);
// const char *data = reinterpret_cast<const char *>(buffer.data());
// size_t sizeLeft = buffer.size() * sizeof(int16_t);
// while(sizeLeft) {
// const auto bytesWritten = audioIODevice->write(data, qint64(sizeLeft));
// sizeLeft -= bytesWritten;
// data += bytesWritten;
// }
}
void MainWindow::dragEnterEvent(QDragEnterEvent* event) {

View File

@ -5,6 +5,7 @@
#include <QMainWindow>
#include <memory>
#include "audiobuffer.h"
#include "timer.h"
#include "ui_mainwindow.h"
@ -50,9 +51,9 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat
std::mutex machineMutex;
std::unique_ptr<QAudioOutput> audioOutput;
QIODevice *audioIODevice = nullptr;
bool audioIs8bit = false, audioIsStereo = false;
void speaker_did_complete_samples(Outputs::Speaker::Speaker *speaker, const std::vector<int16_t> &buffer) override;
AudioBuffer audioBuffer;
bool processEvent(QKeyEvent *);