mirror of
https://github.com/clangen/musikcube.git
synced 2024-10-02 04:52:32 +00:00
Experimental SndioOut changes.
This commit is contained in:
parent
ad79be4c77
commit
4c2da06441
@ -39,11 +39,48 @@
|
||||
#include <math.h>
|
||||
#include <limits.h>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
|
||||
#define DUMPSTATE() std::cerr << "handle=" << this->handle << " state=" << this->state << "\n";
|
||||
#define BUFFER_COUNT 16
|
||||
#define ERROR(str) std::cerr << "SndioOut Error: " << str << "\n";
|
||||
#define INFO(str) std::cerr << "SndioOut Info: " << str << "\n";
|
||||
#define LOCK() std::unique_lock<std::recursive_mutex> lock(this->mutex);
|
||||
#define LOCK() std::unique_lock<std::mutex> lock(this->mutex);
|
||||
#define WAIT() this->threadEvent.wait(lock);
|
||||
#define NOTIFY() this->threadEvent.notify_all();
|
||||
|
||||
#define STOP() \
|
||||
if(started) { \
|
||||
if (!sio_stop(handle)) { \
|
||||
INFO("failed to stop sndio") \
|
||||
quit = true; \
|
||||
continue; \
|
||||
} \
|
||||
else { \
|
||||
INFO("stopped handle") \
|
||||
started = false; \
|
||||
} \
|
||||
} \
|
||||
else { \
|
||||
INFO("already stopped") \
|
||||
}
|
||||
|
||||
#define START() \
|
||||
if(!started) { \
|
||||
if (!sio_start(handle)) { \
|
||||
INFO("failed to start sndio") \
|
||||
quit = true; \
|
||||
continue; \
|
||||
} \
|
||||
else { \
|
||||
INFO("started handle") \
|
||||
started = true; \
|
||||
} \
|
||||
} \
|
||||
else { \
|
||||
INFO("handle already started") \
|
||||
}
|
||||
|
||||
#define PREF_DEVICE_ID "device_id"
|
||||
|
||||
using namespace musik::core::sdk;
|
||||
@ -61,63 +98,70 @@ extern "C" void SetPreferences(musik::core::sdk::IPreferences* prefs) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool waitForDevice(sio_hdl* hdl) {
|
||||
INFO("waiting for device")
|
||||
int nfds, revents;
|
||||
struct pollfd pfds[1];
|
||||
do {
|
||||
nfds = sio_pollfd(hdl, pfds, POLLOUT);
|
||||
if (nfds > 0) {
|
||||
if (poll(pfds, nfds, -1) < 0) {
|
||||
INFO("poll failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
revents = sio_revents(hdl, pfds);
|
||||
if (revents & POLLHUP) {
|
||||
INFO("device disappeared");
|
||||
return false;
|
||||
}
|
||||
} while (!(revents & POLLOUT));
|
||||
INFO("done waiting for device")
|
||||
return true;
|
||||
}
|
||||
|
||||
SndioOut::SndioOut() {
|
||||
INFO("---------- sndout.ctor ----------")
|
||||
this->volume = 1.0f;
|
||||
this->state = StateStopped;
|
||||
this->handle = nullptr;
|
||||
this->buffer = nullptr;
|
||||
this->bufferSamples = 0;
|
||||
this->latency = 0.0;
|
||||
this->pars = { 0 };
|
||||
this->ditherState = 0.0;
|
||||
this->writeThread.reset(new std::thread(
|
||||
std::bind(&SndioOut::WriteLoop, this)));
|
||||
}
|
||||
|
||||
SndioOut::~SndioOut() {
|
||||
this->Stop();
|
||||
delete[] this->buffer;
|
||||
this->bufferSamples = 0;
|
||||
this->PushCommand(Command::Quit);
|
||||
|
||||
INFO("joining thread")
|
||||
this->writeThread->join();
|
||||
INFO("thread finished")
|
||||
}
|
||||
|
||||
void SndioOut::Release() {
|
||||
delete this;
|
||||
}
|
||||
|
||||
void SndioOut::Pause() {
|
||||
INFO("Pause()")
|
||||
LOCK()
|
||||
if (this->handle && this->state == StatePlaying) {
|
||||
if (!sio_stop(this->handle)) {
|
||||
ERROR("pause failed")
|
||||
this->Stop();
|
||||
}
|
||||
else {
|
||||
INFO("paused")
|
||||
this->state = StatePaused;
|
||||
}
|
||||
void SndioOut::PushCommand(Command command) {
|
||||
INFO("PushCommand.start")
|
||||
{
|
||||
LOCK()
|
||||
commands.push_back(command);
|
||||
}
|
||||
NOTIFY()
|
||||
INFO("PushCommand.end")
|
||||
}
|
||||
|
||||
void SndioOut::Pause() {
|
||||
this->PushCommand(Command::Pause);
|
||||
}
|
||||
|
||||
void SndioOut::Resume() {
|
||||
INFO("Resume()")
|
||||
LOCK()
|
||||
if (this->handle && this->state == StatePaused) {
|
||||
if (!sio_start(this->handle)) {
|
||||
ERROR("resume failed")
|
||||
this->Stop();
|
||||
}
|
||||
else {
|
||||
INFO("playing")
|
||||
}
|
||||
}
|
||||
this->state = StatePlaying;
|
||||
this->PushCommand(Command::Resume);
|
||||
}
|
||||
|
||||
void SndioOut::SetVolume(double volume) {
|
||||
this->volume = volume;
|
||||
|
||||
if (this->handle) {
|
||||
sio_setvol(this->handle, lround(volume * SIO_MAXVOL));
|
||||
}
|
||||
this->PushCommand(Command::SetVolume);
|
||||
}
|
||||
|
||||
double SndioOut::GetVolume() {
|
||||
@ -125,24 +169,11 @@ double SndioOut::GetVolume() {
|
||||
}
|
||||
|
||||
void SndioOut::Stop() {
|
||||
INFO("Stop()")
|
||||
LOCK()
|
||||
if (this->handle) {
|
||||
sio_close(this->handle);
|
||||
}
|
||||
this->handle = nullptr;
|
||||
this->pars = { 0 };
|
||||
this->latency = 0;
|
||||
this->state = StateStopped;
|
||||
this->PushCommand(Command::Stop);
|
||||
}
|
||||
|
||||
void SndioOut::Drain() {
|
||||
INFO("Drain()")
|
||||
LOCK()
|
||||
if (this->handle) {
|
||||
sio_stop(this->handle);
|
||||
sio_start(this->handle);
|
||||
}
|
||||
this->PushCommand(Command::Drain);
|
||||
}
|
||||
|
||||
IDeviceList* SndioOut::GetDeviceList() {
|
||||
@ -157,122 +188,254 @@ IDevice* SndioOut::GetDefaultDevice() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SndioOut::InitDevice(IBuffer *buffer) {
|
||||
const char* device = (::deviceId && strlen(::deviceId)) ? deviceId : nullptr;
|
||||
|
||||
this->handle = sio_open(device, SIO_PLAY, 0);
|
||||
|
||||
if (this->handle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int n = 1; bool littleEndian = *(char *) &n == 1;
|
||||
|
||||
sio_initpar(&this->pars);
|
||||
this->pars.pchan = buffer->Channels();
|
||||
this->pars.rate = buffer->SampleRate();
|
||||
this->pars.sig = 1;
|
||||
this->pars.le = !!littleEndian;
|
||||
this->pars.bits = 16;
|
||||
|
||||
/* stolen from cmus; presumeably they've already iterated
|
||||
this value and it should be a reasonable default */
|
||||
this->pars.appbufsz = pars.rate * 300 / 1000;
|
||||
|
||||
if (!sio_setpar(this->handle, &this->pars)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sio_getpar(this->handle, &this->pars)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sio_start(this->handle)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->latency = (double)
|
||||
this->pars.bufsz /
|
||||
this->pars.pchan /
|
||||
this->pars.rate;
|
||||
|
||||
this->SetVolume(this->volume);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int SndioOut::Play(IBuffer *buffer, IBufferProvider *provider) {
|
||||
//DUMPSTATE()
|
||||
std::this_thread::yield();
|
||||
|
||||
if (this->handle == nullptr) {
|
||||
INFO("initializing device...");
|
||||
if (!this->InitDevice(buffer)) {
|
||||
ERROR("failed to initialize device")
|
||||
return OutputInvalidState;
|
||||
}
|
||||
INFO("initialized");
|
||||
this->state = StateStopped;
|
||||
}
|
||||
|
||||
if (!this->handle || this->state == StatePaused) {
|
||||
if (this->state != StatePlaying) {
|
||||
return OutputInvalidState;
|
||||
}
|
||||
|
||||
this->state = StatePlaying;
|
||||
|
||||
/* convert to 16-bit PCM */
|
||||
long samples = buffer->Samples();
|
||||
if (!this->buffer || samples > this->bufferSamples) {
|
||||
delete[] this->buffer;
|
||||
this->buffer = new short[samples];
|
||||
this->bufferSamples = samples;
|
||||
{
|
||||
LOCK()
|
||||
if (this->CountBuffersWithProvider(provider) >= BUFFER_COUNT) {
|
||||
return OutputBufferFull;
|
||||
}
|
||||
this->buffers.push_back(BufferContext{provider, buffer});
|
||||
}
|
||||
|
||||
float* src = buffer->BufferPointer();
|
||||
short* dst = this->buffer;
|
||||
for (long i = 0; i < samples; i++) {
|
||||
float sample = *src;
|
||||
if (sample > 1.0f) sample = 1.0f;
|
||||
if (sample < -1.0f) sample = -1.0f;
|
||||
sample *= SHRT_MAX;
|
||||
NOTIFY()
|
||||
return OutputBufferWritten;
|
||||
}
|
||||
|
||||
/* triangle (high pass) dither, based on Audacity's
|
||||
implementation */
|
||||
float r = (rand() / (float) RAND_MAX - 0.5f);
|
||||
sample = sample + r - this->ditherState;
|
||||
this->ditherState = r;
|
||||
void SndioOut::WriteLoop() {
|
||||
bool started = false;
|
||||
std::list<BufferContext> toNotify;
|
||||
sio_hdl* handle = nullptr;
|
||||
sio_par pars = { 0 };
|
||||
short* pcm = nullptr;
|
||||
long pcmSamples = 0;
|
||||
float ditherState = 0.0;
|
||||
bool quit = false;
|
||||
BufferContext next {nullptr, nullptr};
|
||||
|
||||
*dst = sample;
|
||||
++dst; ++src;
|
||||
/* open the output device. we only do this once */
|
||||
const char* device = (::deviceId && strlen(::deviceId)) ? deviceId : nullptr;
|
||||
handle = sio_open(device, SIO_PLAY, 0);
|
||||
if (handle == nullptr) {
|
||||
INFO("failed to init device")
|
||||
quit = true;
|
||||
}
|
||||
else {
|
||||
if (!sio_start(handle)) {
|
||||
INFO("device opened, but couldn't start")
|
||||
quit = true;
|
||||
}
|
||||
else {
|
||||
started = true;
|
||||
INFO("device handle initialized")
|
||||
}
|
||||
}
|
||||
|
||||
/* write the entire output buffer. this may require multiple passes;
|
||||
that's ok, just loop until we're done */
|
||||
char* data = (char*) this->buffer;
|
||||
size_t dataLength = samples * sizeof(short);
|
||||
size_t totalWritten = 0;
|
||||
|
||||
while (totalWritten < dataLength && this->state == StatePlaying) {
|
||||
size_t remaining = dataLength - totalWritten;
|
||||
size_t written = 0;
|
||||
while (!quit) {
|
||||
/* drain any old buffers (we do this outside of the critical section */
|
||||
if (toNotify.size()) {
|
||||
INFO("cleaning up dead buffers")
|
||||
for (auto& it : toNotify) {
|
||||
it.provider->OnBufferProcessed(it.buffer);
|
||||
}
|
||||
toNotify.clear();
|
||||
}
|
||||
|
||||
{
|
||||
/* we wait until we have commands to process or samples to play */
|
||||
LOCK()
|
||||
written = sio_write(this->handle, data, remaining);
|
||||
while (!quit && !this->commands.size() &&
|
||||
(this->state != StatePlaying || !this->buffers.size()))
|
||||
{
|
||||
INFO("waiting")
|
||||
WAIT()
|
||||
INFO("done waiting")
|
||||
}
|
||||
|
||||
if (quit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* process commands */
|
||||
for (auto command : this->commands) {
|
||||
switch (command) {
|
||||
case Command::Pause: {
|
||||
INFO("command.pause")
|
||||
STOP()
|
||||
this->state = StatePaused;
|
||||
} break;
|
||||
case Command::Resume: {
|
||||
INFO("command.resume")
|
||||
START()
|
||||
this->state = StatePlaying;
|
||||
} break;
|
||||
case Command::Stop: {
|
||||
INFO("command.stop")
|
||||
STOP()
|
||||
std::swap(toNotify, this->buffers);
|
||||
this->state = StateStopped;
|
||||
} break;
|
||||
case Command::SetVolume: {
|
||||
INFO("command.setvolume")
|
||||
if (handle) {
|
||||
sio_setvol(handle, lround(this->volume * SIO_MAXVOL));
|
||||
}
|
||||
} break;
|
||||
case Command::Drain: {
|
||||
INFO("command.drain")
|
||||
STOP()
|
||||
START()
|
||||
} break;
|
||||
case Command::Quit: {
|
||||
quit = true;
|
||||
continue;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
this->commands.clear();
|
||||
|
||||
/* play the next buffer, if it exists */
|
||||
if (started &&
|
||||
this->state == StatePlaying &&
|
||||
this->buffers.size())
|
||||
{
|
||||
next = this->buffers.front();
|
||||
this->buffers.pop_front();
|
||||
}
|
||||
else {
|
||||
INFO(std::string("state=") + std::to_string((int) this->state));
|
||||
INFO(std::string("count=") + std::to_string((int) this->buffers.size()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (written == 0) {
|
||||
break;
|
||||
}
|
||||
auto buffer = next.buffer;
|
||||
|
||||
totalWritten += written;
|
||||
data += written;
|
||||
if (buffer) {
|
||||
/* ensure our output params are setup properly based on this
|
||||
buffer's params */
|
||||
if (pars.pchan != buffer->Channels() ||
|
||||
pars.rate != buffer->SampleRate())
|
||||
{
|
||||
INFO("updating output params")
|
||||
|
||||
STOP()
|
||||
|
||||
sio_initpar(&pars);
|
||||
pars.pchan = buffer->Channels();
|
||||
pars.rate = buffer->SampleRate();
|
||||
pars.sig = 1;
|
||||
pars.le = SIO_LE_NATIVE;
|
||||
pars.bits = 16;
|
||||
pars.appbufsz = (pars.rate * 250) / 1000;
|
||||
|
||||
if (!sio_setpar(handle, &pars)) {
|
||||
INFO("sio_setpar() failed");
|
||||
quit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sio_getpar(handle, &pars)) {
|
||||
INFO("sio_getpar() failed");
|
||||
quit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
START()
|
||||
|
||||
this->latency = (double) pars.bufsz / pars.pchan / pars.rate;
|
||||
}
|
||||
|
||||
/* allocate PCM buffer, if needed. most of the time we'll be
|
||||
able to re-use the previously allocated one. */
|
||||
long samples = buffer->Samples();
|
||||
if (!pcm || samples > pcmSamples) {
|
||||
delete[] pcm;
|
||||
pcm = new short[samples];
|
||||
pcmSamples = samples;
|
||||
}
|
||||
|
||||
/* convert to 16-bit PCM */
|
||||
float* src = buffer->BufferPointer();
|
||||
short* dst = pcm;
|
||||
for (long i = 0; i < samples; i++) {
|
||||
float sample = *src;
|
||||
if (sample > 1.0f) sample = 1.0f;
|
||||
if (sample < -1.0f) sample = -1.0f;
|
||||
sample *= SHRT_MAX;
|
||||
|
||||
/* triangle (high pass) dither, based on Audacity's
|
||||
implementation */
|
||||
float r = (rand() / (float) RAND_MAX - 0.5f);
|
||||
sample = sample + r - ditherState;
|
||||
ditherState = r;
|
||||
|
||||
*dst = sample;
|
||||
++dst; ++src;
|
||||
}
|
||||
|
||||
/* write the entire output buffer. this may require multiple passes;
|
||||
that's ok, just loop until we're done */
|
||||
char* data = (char*) pcm;
|
||||
size_t dataLength = samples * sizeof(short);
|
||||
size_t totalWritten = 0;
|
||||
|
||||
{
|
||||
// LOCK()
|
||||
// INFO("start write")
|
||||
while (totalWritten < dataLength) {
|
||||
size_t remaining = dataLength - totalWritten;
|
||||
size_t written = sio_write(handle, data, remaining);
|
||||
if (written == 0) { INFO("failed to write!") break; }
|
||||
// else { std::cerr << "wrote " << written << " of " << remaining << " bytes\n"; }
|
||||
totalWritten += written;
|
||||
data += written;
|
||||
}
|
||||
// INFO("end write")
|
||||
}
|
||||
|
||||
next.provider->OnBufferProcessed(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
provider->OnBufferProcessed(buffer);
|
||||
return OutputBufferWritten;
|
||||
/* done, free remaining buffers and close the handle */
|
||||
{
|
||||
LOCK()
|
||||
std::swap(toNotify, this->buffers);
|
||||
this->buffers.clear();
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
INFO("closing")
|
||||
sio_close(handle);
|
||||
handle = nullptr;
|
||||
}
|
||||
|
||||
for (auto& it : toNotify) {
|
||||
INFO("cleaning up dead buffer")
|
||||
it.provider->OnBufferProcessed(it.buffer);
|
||||
}
|
||||
|
||||
delete[] pcm;
|
||||
}
|
||||
|
||||
double SndioOut::Latency() {
|
||||
return this->latency;
|
||||
}
|
||||
|
||||
size_t SndioOut::CountBuffersWithProvider(IBufferProvider* provider) {
|
||||
size_t count = 0;
|
||||
for (auto& it : this->buffers) {
|
||||
if (it.provider == provider) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,9 @@
|
||||
#include <core/sdk/IOutput.h>
|
||||
#include <sndio.h>
|
||||
#include <mutex>
|
||||
#include <list>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
|
||||
using namespace musik::core::sdk;
|
||||
|
||||
@ -62,21 +65,32 @@ class SndioOut : public IOutput {
|
||||
virtual IDevice* GetDefaultDevice() override;
|
||||
|
||||
private:
|
||||
bool InitDevice(IBuffer *buffer);
|
||||
|
||||
enum State {
|
||||
StateStopped,
|
||||
StatePaused,
|
||||
StatePlaying
|
||||
enum class Command: int {
|
||||
Pause, Resume, Stop, SetVolume, Drain, Quit
|
||||
};
|
||||
|
||||
struct BufferContext {
|
||||
IBufferProvider* provider;
|
||||
IBuffer* buffer;
|
||||
};
|
||||
|
||||
enum State {
|
||||
StateStopped, StatePaused, StatePlaying
|
||||
};
|
||||
|
||||
size_t CountBuffersWithProvider(IBufferProvider* provider);
|
||||
void WriteLoop();
|
||||
void PushCommand(Command command);
|
||||
|
||||
/* audio */
|
||||
State state;
|
||||
double volume;
|
||||
sio_hdl* handle;
|
||||
sio_par pars;
|
||||
short* buffer;
|
||||
long bufferSamples;
|
||||
double latency;
|
||||
float ditherState;
|
||||
std::recursive_mutex mutex;
|
||||
|
||||
/* threading */
|
||||
std::list<Command> commands;
|
||||
std::list<BufferContext> buffers;
|
||||
std::unique_ptr<std::thread> writeThread;
|
||||
std::condition_variable threadEvent;
|
||||
std::mutex mutex;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user