@@ -0,0 +1,492 @@ | |||
/* | |||
Copyright (C) 2023 Florian Walpen <dev@submerge.ch> | |||
This program is free software; you can redistribute it and/or modify | |||
it under the terms of the GNU General Public License as published by | |||
the Free Software Foundation; either version 2 of the License, or | |||
(at your option) any later version. | |||
This program is distributed in the hope that it will be useful, | |||
but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
GNU General Public License for more details. | |||
You should have received a copy of the GNU General Public License | |||
along with this program; if not, write to the Free Software | |||
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | |||
*/ | |||
#include "JackOSSChannel.h" | |||
#include "JackError.h" | |||
#include "JackThread.h" | |||
#include "memops.h" | |||
#include <cstdint> | |||
#include <sys/ioctl.h> | |||
#include <sys/soundcard.h> | |||
#include <fcntl.h> | |||
#include <iostream> | |||
#include <assert.h> | |||
#include <stdio.h> | |||
typedef jack_default_audio_sample_t jack_sample_t; | |||
namespace | |||
{ | |||
int SuggestSampleFormat(int bits) | |||
{ | |||
switch(bits) { | |||
// Native-endian signed 32 bit samples. | |||
case 32: | |||
return AFMT_S32_NE; | |||
// Native-endian signed 24 bit (packed) samples. | |||
case 24: | |||
return AFMT_S24_NE; | |||
// Native-endian signed 16 bit samples, used by default. | |||
case 16: | |||
default: | |||
return AFMT_S16_NE; | |||
} | |||
} | |||
bool SupportedSampleFormat(int format) | |||
{ | |||
switch(format) { | |||
// Only signed sample formats are supported by the conversion functions. | |||
case AFMT_S16_NE: | |||
case AFMT_S16_OE: | |||
case AFMT_S24_NE: | |||
case AFMT_S24_OE: | |||
case AFMT_S32_NE: | |||
case AFMT_S32_OE: | |||
return true; | |||
} | |||
return false; | |||
} | |||
void CopyAndConvertIn(jack_sample_t *dst, char *src, size_t nframes, int channel, int chcount, int format) | |||
{ | |||
switch (format) { | |||
case AFMT_S16_NE: | |||
src += channel * 2; | |||
sample_move_dS_s16(dst, src, nframes, chcount * 2); | |||
break; | |||
case AFMT_S16_OE: | |||
src += channel * 2; | |||
sample_move_dS_s16s(dst, src, nframes, chcount * 2); | |||
break; | |||
case AFMT_S24_NE: | |||
src += channel * 3; | |||
sample_move_dS_s24(dst, src, nframes, chcount * 3); | |||
break; | |||
case AFMT_S24_OE: | |||
src += channel * 3; | |||
sample_move_dS_s24s(dst, src, nframes, chcount * 3); | |||
break; | |||
case AFMT_S32_NE: | |||
src += channel * 4; | |||
sample_move_dS_s32(dst, src, nframes, chcount * 4); | |||
break; | |||
case AFMT_S32_OE: | |||
src += channel * 4; | |||
sample_move_dS_s32s(dst, src, nframes, chcount * 4); | |||
break; | |||
} | |||
} | |||
void CopyAndConvertOut(char *dst, jack_sample_t *src, size_t nframes, int channel, int chcount, int format) | |||
{ | |||
switch (format) { | |||
case AFMT_S16_NE: | |||
dst += channel * 2; | |||
sample_move_d16_sS(dst, src, nframes, chcount * 2, NULL); | |||
break; | |||
case AFMT_S16_OE: | |||
dst += channel * 2; | |||
sample_move_d16_sSs(dst, src, nframes, chcount * 2, NULL); | |||
break; | |||
case AFMT_S24_NE: | |||
dst += channel * 3; | |||
sample_move_d24_sS(dst, src, nframes, chcount * 3, NULL); | |||
break; | |||
case AFMT_S24_OE: | |||
dst += channel * 3; | |||
sample_move_d24_sSs(dst, src, nframes, chcount * 3, NULL); | |||
break; | |||
case AFMT_S32_NE: | |||
dst += channel * 4; | |||
sample_move_d32_sS(dst, src, nframes, chcount * 4, NULL); | |||
break; | |||
case AFMT_S32_OE: | |||
dst += channel * 4; | |||
sample_move_d32_sSs(dst, src, nframes, chcount * 4, NULL); | |||
break; | |||
} | |||
} | |||
} | |||
void sosso::Log::log(sosso::SourceLocation location, const char* message) { | |||
jack_log(message); | |||
} | |||
void sosso::Log::info(sosso::SourceLocation location, const char* message) { | |||
jack_info(message); | |||
} | |||
void sosso::Log::warn(sosso::SourceLocation location, const char* message) { | |||
jack_error(message); | |||
} | |||
namespace Jack | |||
{ | |||
bool JackOSSChannel::InitialSetup(unsigned int sample_rate) | |||
{ | |||
fFrameStamp = 0; | |||
fCorrection.clear(); | |||
return fFrameClock.set_sample_rate(sample_rate); | |||
} | |||
bool JackOSSChannel::OpenCapture(const char *device, bool exclusive, int bits, int &channels) | |||
{ | |||
if (channels == 0) channels = 2; | |||
int sample_format = SuggestSampleFormat(bits); | |||
if (!fReadChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) { | |||
jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", sample_format); | |||
return false; | |||
} | |||
if (!fReadChannel.open(device, exclusive)) { | |||
return false; | |||
} | |||
if (fReadChannel.sample_rate() != fFrameClock.sample_rate()) { | |||
jack_error("JackOSSChannel::OpenCapture driver forced sample rate %ld", fReadChannel.sample_rate()); | |||
fReadChannel.close(); | |||
return false; | |||
} | |||
if (!SupportedSampleFormat(fReadChannel.sample_format())) { | |||
jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", fReadChannel.sample_format()); | |||
fReadChannel.close(); | |||
return false; | |||
} | |||
jack_log("JackOSSChannel::OpenCapture capture file descriptor = %d", fReadChannel.file_descriptor()); | |||
if (fReadChannel.channels() != channels) { | |||
channels = fReadChannel.channels(); | |||
jack_info("JackOSSChannel::OpenCapture driver forced the number of capture channels %ld", channels); | |||
} | |||
fReadChannel.memory_map(); | |||
return true; | |||
} | |||
bool JackOSSChannel::OpenPlayback(const char *device, bool exclusive, int bits, int &channels) | |||
{ | |||
if (channels == 0) channels = 2; | |||
int sample_format = SuggestSampleFormat(bits); | |||
if (!fWriteChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) { | |||
jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", sample_format); | |||
return false; | |||
} | |||
if (!fWriteChannel.open(device, exclusive)) { | |||
return false; | |||
} | |||
if (fWriteChannel.sample_rate() != fFrameClock.sample_rate()) { | |||
jack_error("JackOSSChannel::OpenPlayback driver forced sample rate %ld", fWriteChannel.sample_rate()); | |||
fWriteChannel.close(); | |||
return false; | |||
} | |||
if (!SupportedSampleFormat(fWriteChannel.sample_format())) { | |||
jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", fWriteChannel.sample_format()); | |||
fWriteChannel.close(); | |||
return false; | |||
} | |||
jack_log("JackOSSChannel::OpenPlayback playback file descriptor = %d", fWriteChannel.file_descriptor()); | |||
if (fWriteChannel.channels() != channels) { | |||
channels = fWriteChannel.channels(); | |||
jack_info("JackOSSChannel::OpenPlayback driver forced the number of playback channels %ld", channels); | |||
} | |||
fWriteChannel.memory_map(); | |||
return true; | |||
} | |||
bool JackOSSChannel::Read(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end) | |||
{ | |||
if (fReadChannel.recording()) { | |||
// Get buffer from read channel. | |||
sosso::Buffer buffer = fReadChannel.take_buffer(); | |||
// Get recording audio data and then clear buffer. | |||
for (unsigned i = 0; i < fReadChannel.channels(); i++) { | |||
if (sample_buffers[i]) { | |||
CopyAndConvertIn(sample_buffers[i], buffer.data(), length, i, fReadChannel.channels(), fReadChannel.sample_format()); | |||
} | |||
} | |||
buffer.reset(); | |||
// Put buffer back to capture at requested end position. | |||
fReadChannel.set_buffer(std::move(buffer), end); | |||
SignalWork(); | |||
return true; | |||
} | |||
return false; | |||
} | |||
bool JackOSSChannel::Write(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end) | |||
{ | |||
if (fWriteChannel.playback()) { | |||
// Get buffer from write channel. | |||
sosso::Buffer buffer = fWriteChannel.take_buffer(); | |||
// Clear buffer and write new playback audio data. | |||
memset(buffer.data(), 0, buffer.length()); | |||
buffer.reset(); | |||
for (unsigned i = 0; i < fWriteChannel.channels(); i++) { | |||
if (sample_buffers[i]) { | |||
CopyAndConvertOut(buffer.data(), sample_buffers[i], length, i, fWriteChannel.channels(), fWriteChannel.sample_format()); | |||
} | |||
} | |||
// Put buffer back to playback at requested end position. | |||
end += PlaybackCorrection(); | |||
fWriteChannel.set_buffer(std::move(buffer), end); | |||
SignalWork(); | |||
return true; | |||
} | |||
return false; | |||
} | |||
bool JackOSSChannel::StartChannels(unsigned int buffer_frames) | |||
{ | |||
int group_id = 0; | |||
if (fReadChannel.recording()) { | |||
// Allocate two recording buffers for double buffering. | |||
size_t buffer_size = buffer_frames * fReadChannel.frame_size(); | |||
sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size); | |||
assert(buffer.data()); | |||
fReadChannel.set_buffer(std::move(buffer), 0); | |||
buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size); | |||
assert(buffer.data()); | |||
fReadChannel.set_buffer(std::move(buffer), buffer_frames); | |||
// Add recording channel to synced start group. | |||
fReadChannel.add_to_sync_group(group_id); | |||
} | |||
if (fWriteChannel.playback()) { | |||
// Allocate two playback buffers for double buffering. | |||
size_t buffer_size = buffer_frames * fWriteChannel.frame_size(); | |||
sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size); | |||
assert(buffer.data()); | |||
fWriteChannel.set_buffer(std::move(buffer), 0); | |||
buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size); | |||
assert(buffer.data()); | |||
fWriteChannel.set_buffer(std::move(buffer), buffer_frames); | |||
// Add playback channel to synced start group. | |||
fWriteChannel.add_to_sync_group(group_id); | |||
} | |||
// Start both channels in sync if supported. | |||
if (fReadChannel.recording()) { | |||
fReadChannel.start_sync_group(group_id); | |||
} else { | |||
fWriteChannel.start_sync_group(group_id); | |||
} | |||
// Init frame clock here to mark start time. | |||
if (!fFrameClock.init_clock(fFrameClock.sample_rate())) { | |||
return false; | |||
} | |||
// Small drift corrections to keep latency whithin +/- 1ms. | |||
std::int64_t limit = fFrameClock.sample_rate() / 1000; | |||
fCorrection.set_drift_limits(-limit, limit); | |||
// Drastic corrections when drift exceeds half a period. | |||
limit = std::max<std::int64_t>(limit, buffer_frames / 2); | |||
fCorrection.set_loss_limits(-limit, limit); | |||
SignalWork(); | |||
return true; | |||
} | |||
bool JackOSSChannel::StopChannels() | |||
{ | |||
if (fReadChannel.recording()) { | |||
free(fReadChannel.take_buffer().data()); | |||
free(fReadChannel.take_buffer().data()); | |||
fReadChannel.memory_unmap(); | |||
fReadChannel.close(); | |||
} | |||
if (fWriteChannel.playback()) { | |||
free(fWriteChannel.take_buffer().data()); | |||
free(fWriteChannel.take_buffer().data()); | |||
fWriteChannel.memory_unmap(); | |||
fWriteChannel.close(); | |||
} | |||
return true; | |||
} | |||
bool JackOSSChannel::StartAssistThread(bool realtime, int priority) | |||
{ | |||
if (fAssistThread.Start() >= 0) { | |||
if (realtime && fAssistThread.AcquireRealTime(priority) != 0) { | |||
jack_error("JackOSSChannel::StartAssistThread realtime priority failed."); | |||
} | |||
return true; | |||
} | |||
return false; | |||
} | |||
bool JackOSSChannel::StopAssistThread() | |||
{ | |||
if (fAssistThread.GetStatus() != JackThread::kIdle) { | |||
fAssistThread.SetStatus(JackThread::kIdle); | |||
SignalWork(); | |||
fAssistThread.Kill(); | |||
} | |||
return true; | |||
} | |||
bool JackOSSChannel::CheckTimeAndRun() | |||
{ | |||
// Check current frame time. | |||
if (!fFrameClock.now(fFrameStamp)) { | |||
jack_error("JackOSSChannel::CheckTimeAndRun(): Frame clock failed."); | |||
return false; | |||
} | |||
std::int64_t now = fFrameStamp; | |||
// Process read channel if wakeup time passed, or OSS buffer data available. | |||
if (fReadChannel.recording() && !fReadChannel.total_finished(now)) { | |||
if (now >= fReadChannel.wakeup_time(now)) { | |||
if (!fReadChannel.process(now)) { | |||
jack_error("JackOSSChannel::CheckTimeAndRun(): Read process failed."); | |||
return false; | |||
} | |||
} | |||
} | |||
// Process write channel if wakeup time passed, or OSS buffer space available. | |||
if (fWriteChannel.playback() && !fWriteChannel.total_finished(now)) { | |||
if (now >= fWriteChannel.wakeup_time(now)) { | |||
if (!fWriteChannel.process(now)) { | |||
jack_error("JackOSSChannel::CheckTimeAndRun(): Write process failed."); | |||
return false; | |||
} | |||
} | |||
} | |||
return true; | |||
} | |||
bool JackOSSChannel::Sleep() const | |||
{ | |||
std::int64_t wakeup = NextWakeup(); | |||
if (wakeup > fFrameStamp) { | |||
return fFrameClock.sleep(wakeup); | |||
} | |||
return true; | |||
} | |||
bool JackOSSChannel::CaptureFinished() const | |||
{ | |||
return fReadChannel.finished(fFrameStamp); | |||
} | |||
bool JackOSSChannel::PlaybackFinished() const | |||
{ | |||
return fWriteChannel.finished(fFrameStamp); | |||
} | |||
std::int64_t JackOSSChannel::PlaybackCorrection() | |||
{ | |||
std::int64_t correction = 0; | |||
// If both channels are used, correct drift relative to recording balance. | |||
if (fReadChannel.recording() && fWriteChannel.playback()) { | |||
std::int64_t previous = fCorrection.correction(); | |||
correction = fCorrection.correct(fWriteChannel.balance(), fReadChannel.balance()); | |||
if (correction != previous) { | |||
jack_info("Playback correction changed from %lld to %lld.", previous, correction); | |||
jack_info("Read balance %lld vs write balance %lld.", fReadChannel.balance(), fWriteChannel.balance()); | |||
} | |||
} | |||
return correction; | |||
} | |||
bool JackOSSChannel::Init() | |||
{ | |||
return true; | |||
} | |||
bool JackOSSChannel::Execute() | |||
{ | |||
if (Lock()) { | |||
if (fAssistThread.GetStatus() != JackThread::kIdle) { | |||
if (!CheckTimeAndRun()) { | |||
return Unlock() && false; | |||
} | |||
std::int64_t wakeup = NextWakeup(); | |||
if (fReadChannel.total_finished(fFrameStamp) && fWriteChannel.total_finished(fFrameStamp)) { | |||
// Nothing to do, wait on the mutex for work. | |||
jack_info("JackOSSChannel::Execute waiting for work."); | |||
fMutex.TimedWait(1000000); | |||
jack_info("JackOSSChannel::Execute resuming work."); | |||
} else if (fFrameStamp < wakeup) { | |||
// Unlock mutex before going to sleep, let others process. | |||
return Unlock() && fFrameClock.sleep(wakeup); | |||
} | |||
} | |||
return Unlock(); | |||
} | |||
return false; | |||
} | |||
std::int64_t JackOSSChannel::XRunGap() const | |||
{ | |||
// Compute processing gap in case we are late. | |||
std::int64_t max_end = std::max(fReadChannel.total_end(), fWriteChannel.total_end()); | |||
if (max_end < fFrameStamp) { | |||
return fFrameStamp - max_end; | |||
} | |||
return 0; | |||
} | |||
void JackOSSChannel::ResetBuffers(std::int64_t offset) | |||
{ | |||
// Clear buffers and offset their positions, after processing gaps. | |||
if (fReadChannel.recording()) { | |||
fReadChannel.reset_buffers(fReadChannel.end_frames() + offset); | |||
} | |||
if (fWriteChannel.playback()) { | |||
fWriteChannel.reset_buffers(fWriteChannel.end_frames() + offset); | |||
} | |||
} | |||
std::int64_t JackOSSChannel::NextWakeup() const | |||
{ | |||
return std::min(fReadChannel.wakeup_time(fFrameStamp), fWriteChannel.wakeup_time(fFrameStamp)); | |||
} | |||
} // end of namespace |
@@ -0,0 +1,132 @@ | |||
/* | |||
Copyright (C) 2003-2007 Jussi Laako <jussi@sonarnerd.net> | |||
Copyright (C) 2008 Grame & RTL 2008 | |||
This program is free software; you can redistribute it and/or modify | |||
it under the terms of the GNU General Public License as published by | |||
the Free Software Foundation; either version 2 of the License, or | |||
(at your option) any later version. | |||
This program is distributed in the hope that it will be useful, | |||
but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
GNU General Public License for more details. | |||
You should have received a copy of the GNU General Public License | |||
along with this program; if not, write to the Free Software | |||
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | |||
*/ | |||
#ifndef __JackOSSChannel__ | |||
#define __JackOSSChannel__ | |||
#include "JackMutex.h" | |||
#include "JackThread.h" | |||
#include "sosso/Correction.hpp" | |||
#include "sosso/DoubleBuffer.hpp" | |||
#include "sosso/FrameClock.hpp" | |||
#include "sosso/ReadChannel.hpp" | |||
#include "sosso/WriteChannel.hpp" | |||
namespace Jack | |||
{ | |||
typedef jack_default_audio_sample_t jack_sample_t; | |||
/*! | |||
\brief The OSS driver. | |||
*/ | |||
class JackOSSChannel : public JackRunnableInterface | |||
{ | |||
private: | |||
JackThread fAssistThread; | |||
JackProcessSync fMutex; | |||
sosso::FrameClock fFrameClock; | |||
sosso::DoubleBuffer<sosso::ReadChannel> fReadChannel; | |||
sosso::DoubleBuffer<sosso::WriteChannel> fWriteChannel; | |||
sosso::Correction fCorrection; | |||
std::int64_t fFrameStamp = 0; | |||
public: | |||
JackOSSChannel() : fAssistThread(this) | |||
{} | |||
virtual ~JackOSSChannel() | |||
{} | |||
sosso::DoubleBuffer<sosso::ReadChannel> &Capture() | |||
{ | |||
return fReadChannel; | |||
} | |||
sosso::DoubleBuffer<sosso::WriteChannel> &Playback() | |||
{ | |||
return fWriteChannel; | |||
} | |||
sosso::FrameClock &FrameClock() | |||
{ | |||
return fFrameClock; | |||
} | |||
bool Lock() | |||
{ | |||
return fMutex.Lock(); | |||
} | |||
bool Unlock() | |||
{ | |||
return fMutex.Unlock(); | |||
} | |||
void SignalWork() | |||
{ | |||
fMutex.SignalAll(); | |||
} | |||
bool InitialSetup(unsigned sample_rate); | |||
bool OpenCapture(const char* device, bool exclusive, int bits, int &channels); | |||
bool OpenPlayback(const char* device, bool exclusive, int bits, int &channels); | |||
bool Read(jack_sample_t** sample_buffers, jack_nframes_t length, std::int64_t end); | |||
bool Write(jack_sample_t** sample_buffers, jack_nframes_t length, std::int64_t end); | |||
bool StartChannels(unsigned buffer_frames); | |||
bool StopChannels(); | |||
bool StartAssistThread(bool realtime, int priority); | |||
bool StopAssistThread(); | |||
bool CheckTimeAndRun(); | |||
bool Sleep() const; | |||
bool CaptureFinished() const; | |||
bool PlaybackFinished() const; | |||
std::int64_t PlaybackCorrection(); | |||
virtual bool Init(); | |||
virtual bool Execute(); | |||
std::int64_t XRunGap() const; | |||
void ResetBuffers(std::int64_t offset); | |||
std::int64_t FrameStamp() const | |||
{ | |||
return fFrameStamp; | |||
} | |||
std::int64_t NextWakeup() const; | |||
}; | |||
} // end of namespace | |||
#endif |
@@ -22,6 +22,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | |||
#define __JackOSSDriver__ | |||
#include "JackAudioDriver.h" | |||
#include "JackOSSChannel.h" | |||
namespace Jack | |||
{ | |||
@@ -44,9 +45,6 @@ class JackOSSDriver : public JackAudioDriver | |||
{ | |||
private: | |||
int fInFD; | |||
int fOutFD; | |||
int fBits; | |||
int fNperiods; | |||
bool fCapture; | |||
@@ -54,42 +52,15 @@ class JackOSSDriver : public JackAudioDriver | |||
bool fExcl; | |||
bool fIgnoreHW; | |||
unsigned int fInSampleSize; | |||
unsigned int fOutSampleSize; | |||
unsigned int fInputBufferSize; | |||
unsigned int fOutputBufferSize; | |||
void* fInputBuffer; | |||
void* fOutputBuffer; | |||
jack_nframes_t fInBlockSize; | |||
jack_nframes_t fOutBlockSize; | |||
jack_nframes_t fInMeanStep; | |||
jack_nframes_t fOutMeanStep; | |||
jack_nframes_t fOSSInBuffer; | |||
jack_nframes_t fOSSOutBuffer; | |||
jack_time_t fOSSReadSync; | |||
long long fOSSReadOffset; | |||
jack_time_t fOSSWriteSync; | |||
long long fOSSWriteOffset; | |||
std::int64_t fCycleEnd; | |||
std::int64_t fLastRun; | |||
std::int64_t fMaxRunGap; | |||
jack_sample_t** fSampleBuffers; | |||
// Buffer balance and sync correction | |||
long long fBufferBalance; | |||
bool fForceBalancing; | |||
bool fForceSync; | |||
JackOSSChannel fChannel; | |||
int OpenInput(); | |||
int OpenOutput(); | |||
int OpenAux(); | |||
void CloseAux(); | |||
void DisplayDeviceInfo(); | |||
int ProbeInBlockSize(); | |||
int ProbeOutBlockSize(); | |||
int Discard(jack_nframes_t frames); | |||
int WriteSilence(jack_nframes_t frames); | |||
int WaitAndSync(); | |||
protected: | |||
virtual void UpdateLatencies(); | |||
@@ -98,16 +69,9 @@ class JackOSSDriver : public JackAudioDriver | |||
JackOSSDriver(const char* name, const char* alias, JackLockedEngine* engine, JackSynchro* table) | |||
: JackAudioDriver(name, alias, engine, table), | |||
fInFD(-1), fOutFD(-1), fBits(0), | |||
fBits(0), | |||
fNperiods(0), fCapture(false), fPlayback(false), fExcl(false), fIgnoreHW(true), | |||
fInSampleSize(0), fOutSampleSize(0), | |||
fInputBufferSize(0), fOutputBufferSize(0), | |||
fInputBuffer(NULL), fOutputBuffer(NULL), | |||
fInBlockSize(1), fOutBlockSize(1), | |||
fInMeanStep(0), fOutMeanStep(0), | |||
fOSSInBuffer(0), fOSSOutBuffer(0), | |||
fOSSReadSync(0), fOSSReadOffset(0), fOSSWriteSync(0), fOSSWriteOffset(0), | |||
fBufferBalance(0), fForceBalancing(false), fForceSync(false) | |||
fCycleEnd(0), fLastRun(0), fMaxRunGap(0), fSampleBuffers(nullptr) | |||
{} | |||
virtual ~JackOSSDriver() | |||
@@ -0,0 +1,153 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_BUFFER_HPP | |||
#define SOSSO_BUFFER_HPP | |||
#include <cstring> | |||
namespace sosso { | |||
/*! | |||
* \brief Buffer Management | |||
* | |||
* Provides means to access and manipulate externally allocated buffer memory. | |||
* It stores a memory pointer, length and a read / write position. The buffer | |||
* memory can be passed from one Buffer instance to another, through move | |||
* constructor and move assignment. This prevents multiple Buffer instances from | |||
* referencing the same memory. | |||
*/ | |||
class Buffer { | |||
public: | |||
//! Construct an empty and invalid Buffer. | |||
Buffer() = default; | |||
/*! | |||
* \brief Construct Buffer operating on given memory. | |||
* \param buffer Pointer to the externally allocated memory. | |||
* \param length Length of the memory dedicated to this Buffer. | |||
*/ | |||
Buffer(char *buffer, std::size_t length) | |||
: _data(buffer), _length(length), _position(0) {} | |||
/*! | |||
* \brief Move construct a buffer. | |||
* \param other Adopt memory from this Buffer, leaving it empty. | |||
*/ | |||
Buffer(Buffer &&other) noexcept | |||
: _data(other._data), _length(other._length), _position(other._position) { | |||
other._data = nullptr; | |||
other._position = 0; | |||
other._length = 0; | |||
} | |||
/*! | |||
* \brief Move assign memory from another Buffer. | |||
* \param other Adopt memory from this Buffer, leaving it empty. | |||
* \return This newly assigned Buffer. | |||
*/ | |||
Buffer &operator=(Buffer &&other) { | |||
_data = other._data; | |||
_position = other._position; | |||
_length = other._length; | |||
other._data = nullptr; | |||
other._position = 0; | |||
other._length = 0; | |||
return *this; | |||
} | |||
//! Buffer is valid if the memory is accessable. | |||
bool valid() const { return (_data != nullptr) && (_length > 0); } | |||
//! Access the underlying memory, null if invalid. | |||
char *data() const { return _data; } | |||
//! Length of the underlying memory in bytes, 0 if invalid. | |||
std::size_t length() const { return _length; } | |||
//! Access buffer memory at read / write position. | |||
char *position() const { return _data + _position; } | |||
//! Get read / write progress from buffer start, in bytes. | |||
std::size_t progress() const { return _position; } | |||
//! Remaining buffer memory in bytes. | |||
std::size_t remaining() const { return _length - _position; } | |||
/*! | |||
* \brief Cap given progress by remaining buffer memory. | |||
* \param progress Progress in bytes. | |||
* \return Progress limited by the remaining buffer memory. | |||
*/ | |||
std::size_t remaining(std::size_t progress) const { | |||
if (progress > remaining()) { | |||
progress = remaining(); | |||
} | |||
return progress; | |||
} | |||
//! Indicate that the buffer is fully processed. | |||
bool done() const { return _position == _length; } | |||
//! Advance the buffer read / write position. | |||
std::size_t advance(std::size_t progress) { | |||
progress = remaining(progress); | |||
_position += progress; | |||
return progress; | |||
} | |||
//! Rewind the buffer read / write position. | |||
std::size_t rewind(std::size_t progress) { | |||
if (progress > _position) { | |||
progress = _position; | |||
} | |||
_position -= progress; | |||
return progress; | |||
} | |||
/*! | |||
* \brief Erase an already processed part, rewind. | |||
* \param begin Start position of the region to be erased. | |||
* \param end End position of the region to be erased. | |||
* \return The number of bytes that were effectively erased. | |||
*/ | |||
std::size_t erase(std::size_t begin, std::size_t end) { | |||
if (begin < _position && begin < end) { | |||
if (end > _position) { | |||
end = _position; | |||
} | |||
std::size_t copy = _position - end; | |||
if (copy > 0) { | |||
std::memmove(_data + begin, _data + end, copy); | |||
} | |||
_position -= (end - begin); | |||
return (end - begin); | |||
} | |||
return 0; | |||
} | |||
//! Reset the buffer position to zero. | |||
void reset() { _position = 0; } | |||
private: | |||
char *_data = nullptr; // External buffer memory, null if invalid. | |||
std::size_t _length = 0; // Total length of the buffer memory. | |||
std::size_t _position = 0; // Current read / write position. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_BUFFER_HPP |
@@ -0,0 +1,210 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_CHANNEL_HPP | |||
#define SOSSO_CHANNEL_HPP | |||
#include "sosso/Device.hpp" | |||
#include <algorithm> | |||
namespace sosso { | |||
/*! | |||
* \brief Audio Channel of a Device | |||
* | |||
* As a base class for read and write channels, this class provides generic | |||
* handling of progress, loss and wakeup times. Progress here means the OSS | |||
* device captures or consumes audio data, in frames. When progress is detected | |||
* within a short wakeup interval, this counts as a sync where we can exactly | |||
* match the device progress to current time. | |||
* The balance indicates the drift between device progress and external time, | |||
* usually taken from FrameClock. | |||
* At device start and after loss, device progress can be irregular and is | |||
* temporarily decoupled from Channel progress (freewheel). Sync events are | |||
* required to change into normal mode which strictly follows device progress. | |||
*/ | |||
class Channel : public Device { | |||
public: | |||
/*! | |||
* \brief Open the device, initialize Channel | |||
* \param device Full device path. | |||
* \param mode Open mode (read / write). | |||
* \return True if successful. | |||
*/ | |||
bool open(const char *device, int mode) { | |||
// Reset all internal statistics from last run. | |||
_last_processing = 0; | |||
_last_sync = 0; | |||
_last_progress = 0; | |||
_balance = 0; | |||
_min_progress = 0; | |||
_max_progress = 0; | |||
_total_loss = 0; | |||
_sync_level = 8; | |||
return Device::open(device, mode); | |||
} | |||
//! Total progress of the device since start. | |||
std::int64_t last_progress() const { return _last_progress; } | |||
//! Balance (drift) compared to external time. | |||
std::int64_t balance() const { return _balance; } | |||
//! Last time there was a successful sync. | |||
std::int64_t last_sync() const { return _last_sync; } | |||
//! Last time the Channel was processed (mark_progress()). | |||
std::int64_t last_processing() const { return _last_processing; } | |||
//! Maximum progress step encountered. | |||
std::int64_t max_progress() const { return _max_progress; } | |||
//! Minimum progress step encountered. | |||
std::int64_t min_progress() const { return _min_progress; } | |||
//! Current number of syncs required to change to normal mode. | |||
unsigned sync_level() const { return _sync_level; } | |||
//! Indicate Channel progress decoupled from device progress. | |||
bool freewheel() const { return _sync_level > 4; } | |||
//! Indicate a full resync with small wakeup steps is required. | |||
bool full_resync() const { return _sync_level > 2; } | |||
//! Indicate a resync is required. | |||
bool resync() const { return _sync_level > 0; } | |||
//! Total number of frames lost due to over- or underruns. | |||
std::int64_t total_loss() const { return _total_loss; } | |||
//! Next time a device progress could be expected. | |||
std::int64_t next_min_progress() const { | |||
return _last_progress + _min_progress + _balance; | |||
} | |||
//! Calculate safe wakeup time to avoid over- or underruns. | |||
std::int64_t safe_wakeup(std::int64_t oss_available) const { | |||
return next_min_progress() + buffer_frames() - oss_available - | |||
max_progress(); | |||
} | |||
//! Estimate the time to expect over- or underruns. | |||
std::int64_t estimated_dropout(std::int64_t oss_available) const { | |||
return _last_progress + _balance + buffer_frames() - oss_available; | |||
} | |||
/*! | |||
* \brief Calculate next wakeup time. | |||
* \param sync_target External wakeup target like the next buffer end. | |||
* \param oss_available Number of frames available in OSS buffer. | |||
* \return Next wakeup time in external frame time. | |||
*/ | |||
std::int64_t wakeup_time(std::int64_t sync_target, | |||
std::int64_t oss_available) const { | |||
// Use one sync step by default. | |||
std::int64_t wakeup = _last_processing + Device::stepping(); | |||
if (freewheel() || full_resync()) { | |||
// Small steps when doing a full resync. | |||
} else if (resync() || wakeup + max_progress() > sync_target) { | |||
// Sync required, wake up prior to next progress if possible. | |||
if (next_min_progress() > wakeup) { | |||
wakeup = next_min_progress() - Device::stepping(); | |||
} else if (next_min_progress() > _last_processing) { | |||
wakeup = next_min_progress(); | |||
} | |||
} else { | |||
// Sleep until prior to sync target, then sync again. | |||
wakeup = sync_target - max_progress(); | |||
} | |||
// Make sure we wake up at sync target. | |||
if (sync_target > _last_processing && sync_target < wakeup) { | |||
wakeup = sync_target; | |||
} | |||
// Make sure we don't sleep into an OSS under- or overrun. | |||
if (safe_wakeup(oss_available) < wakeup) { | |||
wakeup = std::max(safe_wakeup(oss_available), | |||
_last_processing + Device::stepping()); | |||
} | |||
return wakeup; | |||
} | |||
protected: | |||
// Account for progress detected, at current time. | |||
void mark_progress(std::int64_t progress, std::int64_t now) { | |||
if (progress > 0) { | |||
if (freewheel()) { | |||
// Some cards show irregular progress at the beginning, correct that. | |||
// Also correct loss after under- and overruns, assume same balance. | |||
_last_progress = now - progress - _balance; | |||
// Require a sync before transition back to normal processing. | |||
if (now <= _last_processing + stepping()) { | |||
_sync_level -= 1; | |||
} | |||
} else if (now <= _last_processing + stepping()) { | |||
// Successful sync on progress within small processing steps. | |||
_balance = now - (_last_progress + progress); | |||
_last_sync = now; | |||
if (_sync_level > 0) { | |||
_sync_level -= 1; | |||
} | |||
if (progress < _min_progress || _min_progress == 0) { | |||
_min_progress = progress; | |||
} | |||
if (progress > _max_progress) { | |||
_max_progress = progress; | |||
} | |||
} else { | |||
// Big step with progress but no sync, requires a resync. | |||
_sync_level += 1; | |||
} | |||
_last_progress += progress; | |||
} | |||
_last_processing = now; | |||
} | |||
// Account for loss given progress and current time. | |||
std::int64_t mark_loss(std::int64_t progress, std::int64_t now) { | |||
// Estimate frames lost due to over- or underrun. | |||
std::int64_t loss = (now - _balance) - (_last_progress + progress); | |||
return mark_loss(loss); | |||
} | |||
// Account for loss. | |||
std::int64_t mark_loss(std::int64_t loss) { | |||
if (loss > 0) { | |||
_total_loss += loss; | |||
// Resync OSS progress to frame time (now) to recover from loss. | |||
_sync_level = std::max(_sync_level, 6U); | |||
} else { | |||
loss = 0; | |||
} | |||
return loss; | |||
} | |||
private: | |||
std::int64_t _last_processing = 0; // Last processing time. | |||
std::int64_t _last_sync = 0; // Last sync time. | |||
std::int64_t _last_progress = 0; // Total device progress. | |||
std::int64_t _balance = 0; // Channel drift. | |||
std::int64_t _min_progress = 0; // Minimum progress step encountered. | |||
std::int64_t _max_progress = 0; // Maximum progress step encountered. | |||
std::int64_t _total_loss = 0; // Total loss due to over- or underruns. | |||
unsigned _sync_level = 0; // Syncs required. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_CHANNEL_HPP |
@@ -0,0 +1,112 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_CORRECTION_HPP | |||
#define SOSSO_CORRECTION_HPP | |||
#include <cstdint> | |||
namespace sosso { | |||
/*! | |||
* \brief Drift Correction | |||
* | |||
* Calculates drift correction for a channel, relative to another channel if | |||
* required. Usually the playback channel is corrected relative to the recording | |||
* channel, if in use. | |||
* It keeps track of the correction parameter (in frames), and also the | |||
* threshhold values which determine the amount of correction. Above these | |||
* threshholds, either single frame correction is applied for smaller drift, | |||
* or rigorous correction in case of large discrepance. The idea is that single | |||
* frame corrections typically go unnoticed, but it may not be sufficient to | |||
* correct something more grave like packet loss on a USB audio interface. | |||
*/ | |||
class Correction { | |||
public: | |||
//! Default constructor, threshhold values are set separately. | |||
Correction() = default; | |||
/*! | |||
* \brief Set thresholds for small drift correction. | |||
* \param drift_min Limit for negative drift balance. | |||
* \param drift_max Limit for positive drift balance. | |||
*/ | |||
void set_drift_limits(std::int64_t drift_min, std::int64_t drift_max) { | |||
if (drift_min < drift_max) { | |||
_drift_min = drift_min; | |||
_drift_max = drift_max; | |||
} else { | |||
_drift_min = drift_max; | |||
_drift_max = drift_min; | |||
} | |||
} | |||
/*! | |||
* \brief Set thresholds for rigorous large discrepance correction. | |||
* \param loss_min Limit for negative discrepance balance. | |||
* \param loss_max Limit for positive discrepance balance. | |||
*/ | |||
void set_loss_limits(std::int64_t loss_min, std::int64_t loss_max) { | |||
if (loss_min < loss_max) { | |||
_loss_min = loss_min; | |||
_loss_max = loss_max; | |||
} else { | |||
_loss_min = loss_max; | |||
_loss_max = loss_min; | |||
} | |||
} | |||
//! Get current correction parameter. | |||
std::int64_t correction() const { return _correction; } | |||
/*! | |||
* \brief Calculate a new correction parameter. | |||
* \param balance Balance of the corrected channel, compared to FrameClock. | |||
* \param target Balance of a master channel which acts as reference. | |||
* \return Current correction parameter. | |||
*/ | |||
std::int64_t correct(std::int64_t balance, std::int64_t target = 0) { | |||
std::int64_t corrected_balance = balance - target + _correction; | |||
if (corrected_balance > _loss_max) { | |||
// Large positive discrepance, rigorous correction. | |||
_correction -= corrected_balance - _loss_max; | |||
} else if (corrected_balance < _loss_min) { | |||
// Large negative discrepance, rigorous correction. | |||
_correction += _loss_min - corrected_balance; | |||
} else if (corrected_balance > _drift_max) { | |||
// Small positive drift, correct by a single frame. | |||
_correction -= 1; | |||
} else if (corrected_balance < _drift_min) { | |||
// Small negative drift, correct by a single frame. | |||
_correction += 1; | |||
} | |||
return _correction; | |||
} | |||
//! Clear the current correction parameter, but not the thresholds. | |||
void clear() { _correction = 0; } | |||
private: | |||
std::int64_t _loss_min = -128; // Negative threshold for rigorous correction. | |||
std::int64_t _loss_max = 128; // Positive threshold for rigorous correction. | |||
std::int64_t _drift_min = -64; // Negative threshold for drift correction. | |||
std::int64_t _drift_max = 64; // Positive threshold for drift correction. | |||
std::int64_t _correction = 0; // Correction parameter. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_CORRECTION_HPP |
@@ -0,0 +1,678 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_DEVICE_HPP | |||
#define SOSSO_DEVICE_HPP | |||
#include "sosso/Logging.hpp" | |||
#include <cstdint> | |||
#include <cstring> | |||
#include <fcntl.h> | |||
#include <sys/errno.h> | |||
#include <sys/ioctl.h> | |||
#include <sys/mman.h> | |||
#include <sys/soundcard.h> | |||
#include <unistd.h> | |||
namespace sosso { | |||
/*! | |||
* \brief Manage OSS devices. | |||
* | |||
* Encapsulates all the low-level handling of a FreeBSD OSS pcm device. Due to | |||
* restrictions of the OSS API, the device can be opened for either playback or | |||
* recording, not both. For duplex operation, separate instances of Device have | |||
* to be opened. | |||
* By default a Device opens 2 channels of 32 bit samples at 48 kHz, but the | |||
* OSS API will force that to be whatever is supported by the hardware. | |||
* Different default parameters can be set via set_parameters() prior to opening | |||
* the Device. Always check the effective parameters before any use. | |||
*/ | |||
class Device { | |||
public: | |||
/*! | |||
* \brief Translate OSS sample formats to sample size. | |||
* \param format OSS sample format, see sys/soundcard.h header. | |||
* \return Sample size in bytes, 0 if unsupported. | |||
*/ | |||
static std::size_t bytes_per_sample(int format) { | |||
switch (format) { | |||
case AFMT_S16_LE: | |||
case AFMT_S16_BE: | |||
return 2; | |||
case AFMT_S24_LE: | |||
case AFMT_S24_BE: | |||
return 3; | |||
case AFMT_S32_LE: | |||
case AFMT_S32_BE: | |||
return 4; | |||
default: | |||
return 0; | |||
} | |||
} | |||
//! Always close device before destruction. | |||
~Device() { close(); } | |||
//! Effective OSS sample format, see sys/soundcard.h header. | |||
int sample_format() const { return _sample_format; } | |||
//! Effective sample size in bytes. | |||
std::size_t bytes_per_sample() const { | |||
return bytes_per_sample(_sample_format); | |||
} | |||
//! Indicate that the device is open. | |||
bool is_open() const { return _fd >= 0; } | |||
//! Indicate that the device is opened in playback mode. | |||
bool playback() const { return _fd >= 0 && (_file_mode & O_WRONLY); } | |||
//! Indicate that the device is opened in recording mode. | |||
bool recording() const { return _fd >= 0 && !playback(); } | |||
//! Get the file descriptor of the device, -1 if not open. | |||
int file_descriptor() const { return _fd; } | |||
//! Effective number of audio channels. | |||
unsigned channels() const { return _channels; } | |||
//! Effective frame size, one sample for each channel. | |||
std::size_t frame_size() const { return _channels * bytes_per_sample(); } | |||
//! Effective OSS buffer size in bytes. | |||
std::size_t buffer_size() const { return _fragments * _fragment_size; } | |||
//! Effective OSS buffer size in frames, samples per channel. | |||
unsigned buffer_frames() const { return buffer_size() / frame_size(); } | |||
//! Effective sample rate in Hz. | |||
unsigned sample_rate() const { return _sample_rate; } | |||
//! Suggested minimal polling step, in frames. | |||
unsigned stepping() const { return 16U * (1U + (_sample_rate / 50000)); } | |||
//! Indicate that the OSS buffer can be memory mapped. | |||
bool can_memory_map() const { return has_capability(PCM_CAP_MMAP); } | |||
//! A pointer to the memory mapped OSS buffer, null if not mapped. | |||
char *map() const { return static_cast<char *>(_map); } | |||
//! Current read / write position in the mapped OSS buffer. | |||
unsigned map_pointer() const { return _map_progress % buffer_size(); } | |||
//! Total progress of the mapped OSS buffer, in frames. | |||
std::int64_t map_progress() const { return _map_progress / frame_size(); } | |||
/*! | |||
* \brief Set preferred audio parameters before opening device. | |||
* \param format OSS sample formet, see sys/soundcard.h header. | |||
* \param rate Sample rate in Hz. | |||
* \param channels Number of recording / playback channels. | |||
* \return True if successful, false means unsupported parameters. | |||
*/ | |||
bool set_parameters(int format, int rate, int channels) { | |||
if (bytes_per_sample(format) && channels > 0) { | |||
_sample_format = format; | |||
_sample_rate = rate; | |||
_channels = channels; | |||
return true; | |||
} | |||
return false; | |||
} | |||
/*! | |||
* \brief Open the device for either recording or playback. | |||
* \param device Path to the OSS device (e.g. "/dev/dsp1"). | |||
* \param mode Open mode read or write, optional exclusive and non-blocking. | |||
* \return True if successful. | |||
*/ | |||
bool open(const char *device, int mode) { | |||
if (mode & O_RDWR) { | |||
Log::warn(SOSSO_LOC, "Only one direction allowed, open %s in read mode.", | |||
device); | |||
mode = O_RDONLY | (mode & O_EXCL) | (mode & O_NONBLOCK); | |||
} | |||
_fd = ::open(device, mode); | |||
if (_fd >= 0) { | |||
_file_mode = mode; | |||
if (bitperfect_mode(_fd) && set_sample_format(_fd) && set_channels(_fd) && | |||
set_sample_rate(_fd) && get_buffer_info() && get_capabilities()) { | |||
return true; | |||
} | |||
} | |||
Log::warn(SOSSO_LOC, "Unable to open device %s, errno %d.", device, errno); | |||
close(); | |||
return false; | |||
} | |||
//! Close the device. | |||
void close() { | |||
if (map()) { | |||
memory_unmap(); | |||
} | |||
if (_fd >= 0) { | |||
::close(_fd); | |||
_fd = -1; | |||
} | |||
} | |||
/*! | |||
* \brief Request a specific OSS buffer size. | |||
* \param fragments Number of fragments. | |||
* \param fragment_size Size of the fragments in bytes. | |||
* \return True if successful. | |||
* \warning Due to OSS API limitations, resulting buffer sizes are not really | |||
* predictable and may cause problems with some soundcards. | |||
*/ | |||
bool set_buffer_size(unsigned fragments, unsigned fragment_size) { | |||
int frg = 0; | |||
while ((1U << frg) < fragment_size) { | |||
++frg; | |||
} | |||
frg |= (fragments << 16); | |||
Log::info(SOSSO_LOC, "Request %d fragments of %u bytes.", (frg >> 16), | |||
(1U << (frg & 0xffff))); | |||
if (ioctl(_fd, SNDCTL_DSP_SETFRAGMENT, &frg) != 0) { | |||
Log::warn(SOSSO_LOC, "Set fragments failed with %d.", errno); | |||
return false; | |||
} | |||
return get_buffer_info(); | |||
} | |||
/*! | |||
* \brief Request a specific OSS buffer size. | |||
* \param total_size Total size of all buffer fragments. | |||
* \return True if successful. | |||
* \warning Due to OSS API limitations, resulting buffer sizes are not really | |||
* predictable and may cause problems with some soundcards. | |||
*/ | |||
bool set_buffer_size(unsigned total_size) { | |||
if (_fragment_size > 0) { | |||
unsigned fragments = (total_size + _fragment_size - 1) / _fragment_size; | |||
return set_buffer_size(fragments, _fragment_size); | |||
} | |||
return false; | |||
} | |||
/*! | |||
* \brief Read recorded audio data from OSS buffer. | |||
* \param buffer Pointer to destination buffer. | |||
* \param length Maximum read length in bytes. | |||
* \param count Byte counter, increased by effective read length. | |||
* \return True if successful or if nothing to do. | |||
*/ | |||
bool read_io(char *buffer, std::size_t length, std::size_t &count) { | |||
if (buffer && length > 0 && recording()) { | |||
ssize_t result = ::read(_fd, buffer, length); | |||
if (result >= 0) { | |||
count += result; | |||
} else if (errno == EAGAIN) { | |||
count += 0; | |||
} else { | |||
Log::warn(SOSSO_LOC, "Data read failed with %d.", errno); | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
/*! | |||
* \brief Read recorded audio data from memory mapped OSS buffer. | |||
* \param buffer Pointer to destination buffer. | |||
* \param offset Read offset into the OSS buffer, in bytes. | |||
* \param length Maximum read length in bytes. | |||
* \return The number of bytes read. | |||
*/ | |||
std::size_t read_map(char *buffer, std::size_t offset, std::size_t length) { | |||
std::size_t bytes_read = 0; | |||
if (length > 0 && map()) { | |||
// Sanitize offset and length parameters. | |||
offset = offset % buffer_size(); | |||
if (length > buffer_size()) { | |||
length = buffer_size(); | |||
} | |||
// Check if the read length spans across an OSS buffer cycle. | |||
if (offset + length > buffer_size()) { | |||
// Read until buffer end first. | |||
bytes_read = read_map(buffer, offset, buffer_size() - offset); | |||
length -= bytes_read; | |||
buffer += bytes_read; | |||
offset = 0; | |||
} | |||
// Read remaining data. | |||
std::memcpy(buffer, map() + offset, length); | |||
bytes_read += length; | |||
} | |||
return bytes_read; | |||
} | |||
/*! | |||
* \brief Write audio data to OSS buffer. | |||
* \param buffer Pointer to source buffer. | |||
* \param length Maximum write length in bytes. | |||
* \param count Byte counter, increased by effective write length. | |||
* \return True if successful or if nothing to do. | |||
*/ | |||
bool write_io(char *buffer, std::size_t length, std::size_t &count) { | |||
if (buffer && length > 0 && playback()) { | |||
ssize_t result = ::write(file_descriptor(), buffer, length); | |||
if (result >= 0) { | |||
count += result; | |||
} else if (errno == EAGAIN) { | |||
count += 0; | |||
} else { | |||
Log::warn(SOSSO_LOC, "Data write failed with %d.", errno); | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
/*! | |||
* \brief Write audio data to a memory mapped OSS buffer. | |||
* \param buffer Pointer to source buffer, null writes zeros to OSS buffer. | |||
* \param offset Write offset into the OSS buffer, in bytes. | |||
* \param length Maximum write length in bytes. | |||
* \return The number of bytes written. | |||
*/ | |||
std::size_t write_map(const char *buffer, std::size_t offset, | |||
std::size_t length) { | |||
std::size_t bytes_written = 0; | |||
if (length > 0 && map()) { | |||
// Sanitize pointer and length parameters. | |||
offset = offset % buffer_size(); | |||
if (length > buffer_size()) { | |||
length = buffer_size(); | |||
} | |||
// Check if the write length spans across an OSS buffer cycle. | |||
if (offset + length > buffer_size()) { | |||
// Write until buffer end first. | |||
bytes_written += write_map(buffer, offset, buffer_size() - offset); | |||
length -= bytes_written; | |||
if (buffer) { | |||
buffer += bytes_written; | |||
} | |||
offset = 0; | |||
} | |||
// Write source if available, otherwise clear the buffer. | |||
if (buffer) { | |||
std::memcpy(map() + offset, buffer, length); | |||
} else { | |||
std::memset(map() + offset, 0, length); | |||
} | |||
bytes_written += length; | |||
} | |||
return bytes_written; | |||
} | |||
/*! | |||
* \brief Query number of frames in the OSS buffer (non-mapped). | |||
* \return Number of frames, 0 if not successful. | |||
*/ | |||
int queued_samples() { | |||
unsigned long request = | |||
playback() ? SNDCTL_DSP_CURRENT_OPTR : SNDCTL_DSP_CURRENT_IPTR; | |||
oss_count_t ptr; | |||
if (ioctl(_fd, request, &ptr) == 0) { | |||
return ptr.fifo_samples; | |||
} | |||
return 0; | |||
} | |||
//! Indicate that the device can be triggered to start. | |||
bool can_trigger() const { return has_capability(PCM_CAP_TRIGGER); } | |||
//! Trigger the device to start recording / playback. | |||
bool start() const { | |||
if (!can_trigger()) { | |||
Log::warn(SOSSO_LOC, "Trigger start not supported by device."); | |||
return false; | |||
} | |||
int trigger = recording() ? PCM_ENABLE_INPUT : PCM_ENABLE_OUTPUT; | |||
if (ioctl(file_descriptor(), SNDCTL_DSP_SETTRIGGER, &trigger) != 0) { | |||
const char *direction = recording() ? "recording" : "playback"; | |||
Log::warn(SOSSO_LOC, "Starting %s channel failed with error %d.", | |||
direction, errno); | |||
return false; | |||
} | |||
return true; | |||
} | |||
/*! | |||
* \brief Add device to a sync group for synchronized start. | |||
* \param id Id of the sync group, 0 will initialize a new group. | |||
* \return True if successful. | |||
*/ | |||
bool add_to_sync_group(int &id) { | |||
oss_syncgroup sync_group = {0, 0, {0}}; | |||
sync_group.id = id; | |||
sync_group.mode |= (recording() ? PCM_ENABLE_INPUT : PCM_ENABLE_OUTPUT); | |||
if (ioctl(file_descriptor(), SNDCTL_DSP_SYNCGROUP, &sync_group) == 0 && | |||
(id == 0 || sync_group.id == id)) { | |||
id = sync_group.id; | |||
return true; | |||
} | |||
Log::warn(SOSSO_LOC, "Sync grouping channel failed with error %d.", errno); | |||
return false; | |||
} | |||
/*! | |||
* \brief Synchronized start of all devices in the sync group. | |||
* \param id Id of the sync group. | |||
* \return True if successful. | |||
*/ | |||
bool start_sync_group(int id) { | |||
if (ioctl(file_descriptor(), SNDCTL_DSP_SYNCSTART, &id) == 0) { | |||
return true; | |||
} | |||
Log::warn(SOSSO_LOC, "Start of sync group failed with error %d.", errno); | |||
return false; | |||
} | |||
//! Query the number of playback underruns since last called. | |||
int get_play_underruns() { | |||
int play_underruns = 0; | |||
int rec_overruns = 0; | |||
get_errors(play_underruns, rec_overruns); | |||
return play_underruns; | |||
} | |||
//! Query the number of recording overruns since last called. | |||
int get_rec_overruns() { | |||
int play_underruns = 0; | |||
int rec_overruns = 0; | |||
get_errors(play_underruns, rec_overruns); | |||
return rec_overruns; | |||
} | |||
//! Update current playback position for memory mapped OSS buffer. | |||
bool get_play_pointer() { | |||
count_info info = {}; | |||
if (ioctl(file_descriptor(), SNDCTL_DSP_GETOPTR, &info) == 0) { | |||
if (info.ptr >= 0 && static_cast<unsigned>(info.ptr) < buffer_size() && | |||
(info.ptr % frame_size()) == 0 && info.blocks >= 0) { | |||
// Calculate pointer delta without complete buffer cycles. | |||
unsigned delta = | |||
(info.ptr + buffer_size() - map_pointer()) % buffer_size(); | |||
// Get upper bound on progress from blocks info. | |||
unsigned max_bytes = (info.blocks + 1) * _fragment_size - 1; | |||
if (max_bytes >= delta) { | |||
// Estimate cycle part and round it down to buffer cycles. | |||
unsigned cycles = max_bytes - delta; | |||
cycles -= (cycles % buffer_size()); | |||
delta += cycles; | |||
} | |||
int fragments = delta / _fragment_size; | |||
if (info.blocks < fragments || info.blocks > fragments + 1) { | |||
Log::warn(SOSSO_LOC, "Play pointer blocks: %u - %d, %d, %d.", | |||
map_pointer(), info.ptr, info.blocks, info.bytes); | |||
} | |||
_map_progress += delta; | |||
return true; | |||
} | |||
Log::warn(SOSSO_LOC, "Play pointer out of bounds: %d, %d blocks.", | |||
info.ptr, info.blocks); | |||
} else { | |||
Log::warn(SOSSO_LOC, "Play pointer failed with error: %d.", errno); | |||
} | |||
return false; | |||
} | |||
//! Update current recording position for memory mapped OSS buffer. | |||
bool get_rec_pointer() { | |||
count_info info = {}; | |||
if (ioctl(file_descriptor(), SNDCTL_DSP_GETIPTR, &info) == 0) { | |||
if (info.ptr >= 0 && static_cast<unsigned>(info.ptr) < buffer_size() && | |||
(info.ptr % frame_size()) == 0 && info.blocks >= 0) { | |||
// Calculate pointer delta without complete buffer cycles. | |||
unsigned delta = | |||
(info.ptr + buffer_size() - map_pointer()) % buffer_size(); | |||
// Get upper bound on progress from blocks info. | |||
unsigned max_bytes = (info.blocks + 1) * _fragment_size - 1; | |||
if (max_bytes >= delta) { | |||
// Estimate cycle part and round it down to buffer cycles. | |||
unsigned cycles = max_bytes - delta; | |||
cycles -= (cycles % buffer_size()); | |||
delta += cycles; | |||
} | |||
int fragments = delta / _fragment_size; | |||
if (info.blocks < fragments || info.blocks > fragments + 1) { | |||
Log::warn(SOSSO_LOC, "Rec pointer blocks: %u - %d, %d, %d.", | |||
map_pointer(), info.ptr, info.blocks, info.bytes); | |||
} | |||
_map_progress += delta; | |||
return true; | |||
} | |||
Log::warn(SOSSO_LOC, "Rec pointer out of bounds: %d, %d blocks.", | |||
info.ptr, info.blocks); | |||
} else { | |||
Log::warn(SOSSO_LOC, "Rec pointer failed with error: %d.", errno); | |||
} | |||
return false; | |||
} | |||
//! Memory map the OSS buffer. | |||
bool memory_map() { | |||
if (!can_memory_map()) { | |||
Log::warn(SOSSO_LOC, "Memory map not supported by device."); | |||
return false; | |||
} | |||
int protection = PROT_NONE; | |||
if (playback()) { | |||
protection = PROT_WRITE; | |||
} | |||
if (recording()) { | |||
protection = PROT_READ; | |||
} | |||
if (_map == nullptr && protection != PROT_NONE) { | |||
_map = mmap(NULL, buffer_size(), protection, MAP_SHARED, | |||
file_descriptor(), 0); | |||
if (_map == MAP_FAILED) { | |||
Log::warn(SOSSO_LOC, "Memory map failed with error %d.", errno); | |||
_map = nullptr; | |||
} | |||
} | |||
return (_map != nullptr); | |||
} | |||
//! Unmap a previously memory mapped OSS buffer. | |||
bool memory_unmap() { | |||
if (_map) { | |||
if (munmap(_map, buffer_size()) != 0) { | |||
Log::warn(SOSSO_LOC, "Memory unmap failed with error %d.", errno); | |||
return false; | |||
} | |||
_map = nullptr; | |||
} | |||
return true; | |||
} | |||
/*! | |||
* \brief Check device capabilities. | |||
* \param capabilities Device capabilities, see sys/soundcard.h header. | |||
* \return True if the device has the capabilities in question. | |||
*/ | |||
bool has_capability(int capabilities) const { | |||
return (_capabilities & capabilities) == capabilities; | |||
} | |||
//! Print device info to user information log. | |||
void log_device_info() const { | |||
if (!is_open()) { | |||
return; | |||
} | |||
const char *direction = (recording() ? "Recording" : "Playback"); | |||
Log::info(SOSSO_LOC, "%s device is %u channels at %u Hz, %lu bits.", | |||
direction, _channels, _sample_rate, bytes_per_sample() * 8); | |||
Log::info(SOSSO_LOC, "Device buffer is %u fragments of size %u, %u frames.", | |||
_fragments, _fragment_size, buffer_frames()); | |||
oss_sysinfo sys_info = {}; | |||
if (ioctl(_fd, SNDCTL_SYSINFO, &sys_info) == 0) { | |||
Log::info(SOSSO_LOC, "OSS version %s number %d on %s.", sys_info.version, | |||
sys_info.versionnum, sys_info.product); | |||
} | |||
Log::info(SOSSO_LOC, "PCM capabilities:"); | |||
if (has_capability(PCM_CAP_TRIGGER)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_TRIGGER (Trigger start)"); | |||
if (has_capability(PCM_CAP_MMAP)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_MMAP (Memory map)"); | |||
if (has_capability(PCM_CAP_MULTI)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_MULTI (Multiple open)"); | |||
if (has_capability(PCM_CAP_INPUT)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_INPUT (Recording)"); | |||
if (has_capability(PCM_CAP_OUTPUT)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_OUTPUT (Playback)"); | |||
if (has_capability(PCM_CAP_VIRTUAL)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_VIRTUAL (Virtual device)"); | |||
if (has_capability(PCM_CAP_ANALOGIN)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_ANALOGIN (Analog input)"); | |||
if (has_capability(PCM_CAP_ANALOGOUT)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_ANALOGOUT (Analog output)"); | |||
if (has_capability(PCM_CAP_DIGITALIN)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_DIGITALIN (Digital input)"); | |||
if (has_capability(PCM_CAP_DIGITALOUT)) | |||
Log::info(SOSSO_LOC, " PCM_CAP_DIGITALOUT (Digital output)"); | |||
} | |||
private: | |||
// Disable auto-conversion (bitperfect) when opened in exclusive mode. | |||
bool bitperfect_mode(int fd) { | |||
if (_file_mode & O_EXCL) { | |||
int flags = 0; | |||
int result = ioctl(fd, SNDCTL_DSP_COOKEDMODE, &flags); | |||
if (result < 0) { | |||
Log::warn(SOSSO_LOC, "Unable to set cooked mode."); | |||
} | |||
return result >= 0; | |||
} | |||
return true; | |||
} | |||
// Set sample format and the check the result. | |||
bool set_sample_format(int fd) { | |||
int format = _sample_format; | |||
int result = ioctl(fd, SNDCTL_DSP_SETFMT, &format); | |||
if (result != 0) { | |||
Log::warn(SOSSO_LOC, "Unable to set sample format, error %d.", errno); | |||
return false; | |||
} else if (bytes_per_sample(format) == 0) { | |||
Log::warn(SOSSO_LOC, "Unsupported sample format %d.", format); | |||
return false; | |||
} else if (format != _sample_format) { | |||
Log::warn( | |||
SOSSO_LOC, "Driver changed the sample format, %lu bit vs %lu bit.", | |||
bytes_per_sample(format) * 8, bytes_per_sample(_sample_format) * 8); | |||
} | |||
_sample_format = format; | |||
return true; | |||
} | |||
// Set sample rate and then check the result. | |||
bool set_sample_rate(int fd) { | |||
int rate = _sample_rate; | |||
if (ioctl(fd, SNDCTL_DSP_SPEED, &rate) == 0) { | |||
if (rate != _sample_rate) { | |||
Log::warn(SOSSO_LOC, "Driver changed the sample rate, %d vs %d.", rate, | |||
_sample_rate); | |||
_sample_rate = rate; | |||
} | |||
return true; | |||
} | |||
Log::warn(SOSSO_LOC, "Unable to set sample rate, error %d.", errno); | |||
return false; | |||
} | |||
// Set the number of channels and then check the result. | |||
bool set_channels(int fd) { | |||
int channels = _channels; | |||
if (ioctl(fd, SNDCTL_DSP_CHANNELS, &channels) == 0) { | |||
if (channels != _channels) { | |||
Log::warn(SOSSO_LOC, "Driver changed number of channels, %d vs %d.", | |||
channels, _channels); | |||
_channels = channels; | |||
} | |||
return true; | |||
} | |||
Log::warn(SOSSO_LOC, "Unable to set channels, error %d.", errno); | |||
return false; | |||
} | |||
// Query fragments and size of the OSS buffer. | |||
bool get_buffer_info() { | |||
audio_buf_info info = {0, 0, 0, 0}; | |||
unsigned long request = | |||
playback() ? SNDCTL_DSP_GETOSPACE : SNDCTL_DSP_GETISPACE; | |||
if (ioctl(_fd, request, &info) >= 0) { | |||
_fragments = info.fragstotal; | |||
_fragment_size = info.fragsize; | |||
return true; | |||
} else { | |||
Log::warn(SOSSO_LOC, "Unable to get buffer info."); | |||
return false; | |||
} | |||
} | |||
// Query capabilities of the device. | |||
bool get_capabilities() { | |||
if (ioctl(_fd, SNDCTL_DSP_GETCAPS, &_capabilities) == 0) { | |||
oss_sysinfo sysinfo = {}; | |||
if (ioctl(_fd, OSS_SYSINFO, &sysinfo) == 0) { | |||
if (std::strncmp(sysinfo.version, "1302000", 7) < 0) { | |||
// Memory map on FreeBSD prior to 13.2 may use wrong buffer size. | |||
Log::warn(SOSSO_LOC, | |||
"Disable memory map, workaround OSS bug on FreeBSD < 13.2"); | |||
_capabilities &= ~PCM_CAP_MMAP; | |||
} | |||
return true; | |||
} else { | |||
Log::warn(SOSSO_LOC, "Unable to get system info, error %d.", errno); | |||
} | |||
} else { | |||
Log::warn(SOSSO_LOC, "Unable to get device capabilities, error %d.", | |||
errno); | |||
_capabilities = 0; | |||
} | |||
return false; | |||
} | |||
// Query error information from the device. | |||
bool get_errors(int &play_underruns, int &rec_overruns) { | |||
audio_errinfo error_info = {}; | |||
if (ioctl(file_descriptor(), SNDCTL_DSP_GETERROR, &error_info) == 0) { | |||
play_underruns = error_info.play_underruns; | |||
rec_overruns = error_info.rec_overruns; | |||
return true; | |||
} | |||
return false; | |||
} | |||
private: | |||
int _fd = -1; // File descriptor. | |||
int _file_mode = O_RDONLY; // File open mode. | |||
void *_map = nullptr; // Memory map pointer. | |||
std::uint64_t _map_progress = 0; // Memory map progress. | |||
int _channels = 2; // Number of channels. | |||
int _capabilities = 0; // Device capabilities. | |||
int _sample_format = AFMT_S32_NE; // Sample format. | |||
int _sample_rate = 48000; // Sample rate. | |||
unsigned _fragments = 0; // Number of OSS buffer fragments. | |||
unsigned _fragment_size = 0; // OSS buffer fragment size. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_DEVICE_HPP |
@@ -0,0 +1,218 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_DOUBLEBUFFER_HPP | |||
#define SOSSO_DOUBLEBUFFER_HPP | |||
#include "sosso/Buffer.hpp" | |||
#include "sosso/Logging.hpp" | |||
#include <algorithm> | |||
#include <limits> | |||
namespace sosso { | |||
/*! | |||
* \brief Double Buffering for Channel | |||
* | |||
* Manages double buffering on top of a ReadChannel or WriteChannel. It takes | |||
* two buffers with corresponding end positions. One of these is selected | |||
* for processing, depending on the buffer and channel positions. The buffers | |||
* can be overlapping or have gaps in between. | |||
* A buffer is marked as finished when all buffer data was processed and the | |||
* channel progress has reached the buffer end. This provides steady buffer | |||
* replacement times, synchronized with channel progress. | |||
* The wakeup times for processing are adapted to available channel data and | |||
* work pending (unprocessed buffer data). | |||
*/ | |||
template <class Channel> class DoubleBuffer : public Channel { | |||
/*! | |||
* \brief Store a buffer and its end position. | |||
* | |||
* The end position of the buffer corresponds to channel progress in frames. | |||
* Marking the end position allows to map the buffer content to the matching | |||
* channel data, independent of read and write positions. | |||
*/ | |||
struct BufferRecord { | |||
Buffer buffer; // External buffer, may be empty. | |||
std::int64_t end_frames = 0; // Buffer end position in frames. | |||
}; | |||
public: | |||
//! Indicate that buffer is ready for processing. | |||
bool ready() const { return _buffer_a.buffer.valid(); } | |||
/*! | |||
* \brief Set the next consecutive buffer to be processed. | |||
* \param buffer External buffer ready for processing. | |||
* \param end_frames End position of the buffer in frames. | |||
* \return True if successful, false means there are already two buffers. | |||
*/ | |||
bool set_buffer(Buffer &&buffer, std::int64_t end_frames) { | |||
// Set secondary buffer if available. | |||
if (!_buffer_b.buffer.valid()) { | |||
_buffer_b.buffer = std::move(buffer); | |||
_buffer_b.end_frames = end_frames; | |||
// Promote secondary buffer to primary if primary is not set. | |||
if (!_buffer_a.buffer.valid()) { | |||
std::swap(_buffer_b, _buffer_a); | |||
} | |||
return ready(); | |||
} | |||
return false; | |||
} | |||
/*! | |||
* \brief Reset the buffer end positions in case of over- and underruns. | |||
* \param end_frames New end position of the primary buffer. | |||
* \return True if ready to proceed. | |||
*/ | |||
bool reset_buffers(std::int64_t end_frames) { | |||
// Reset primary buffer. | |||
if (_buffer_a.buffer.valid()) { | |||
std::memset(_buffer_a.buffer.data(), 0, _buffer_a.buffer.length()); | |||
_buffer_a.buffer.reset(); | |||
Log::info(SOSSO_LOC, "Primary buffer reset from %lld to %lld.", | |||
_buffer_a.end_frames, end_frames); | |||
_buffer_a.end_frames = end_frames; | |||
} | |||
// Reset secondary buffer. | |||
if (_buffer_b.buffer.valid()) { | |||
std::memset(_buffer_b.buffer.data(), 0, _buffer_b.buffer.length()); | |||
_buffer_b.buffer.reset(); | |||
end_frames += _buffer_b.buffer.length() / Channel::frame_size(); | |||
Log::info(SOSSO_LOC, "Secondary buffer reset from %lld to %lld.", | |||
_buffer_b.end_frames, end_frames); | |||
_buffer_b.end_frames = end_frames; | |||
} | |||
return ready(); | |||
} | |||
//! Retrieve the primary buffer, may be empty. | |||
Buffer &&take_buffer() { | |||
std::swap(_buffer_a, _buffer_b); | |||
return std::move(_buffer_b.buffer); | |||
} | |||
/*! | |||
* \brief Process channel with given buffers to read or write. | |||
* \param now Time offset from channel start in frames, see FrameClock. | |||
* \return True if there were no processing errors. | |||
*/ | |||
bool process(std::int64_t now) { | |||
// Round frame time down to steppings, ignore timing jitter. | |||
now = now - now % Channel::stepping(); | |||
// Always process primary buffer, No-Op if already done. | |||
bool ok = Channel::process(_buffer_a.buffer, _buffer_a.end_frames, now); | |||
// Process secondary buffer when primary is done. | |||
if (ok && _buffer_a.buffer.done() && _buffer_b.buffer.valid()) { | |||
ok = Channel::process(_buffer_b.buffer, _buffer_b.end_frames, now); | |||
} | |||
return ok; | |||
} | |||
//! End position of the primary buffer. | |||
std::int64_t end_frames() const { | |||
if (ready()) { | |||
return _buffer_a.end_frames; | |||
} | |||
return 0; | |||
} | |||
//! Expected frame time when primary buffer is finished. | |||
std::int64_t period_end() const { | |||
if (ready()) { | |||
return end_frames() + Channel::balance(); | |||
} | |||
return 0; | |||
} | |||
//! Expected frame time when both buffers are finished. | |||
std::int64_t total_end() const { | |||
if (ready()) { | |||
if (_buffer_b.buffer.valid()) { | |||
return _buffer_b.end_frames + Channel::balance(); | |||
} | |||
return end_frames() + Channel::balance(); | |||
} | |||
return 0; | |||
} | |||
/*! | |||
* \brief Calculate next wakeup time for processing. | |||
* \param now Current frame time as offset from channel start, see FrameClock. | |||
* \return Next suggested wakeup in frame time. | |||
*/ | |||
std::int64_t wakeup_time(std::int64_t now) const { | |||
// No need to wake up if channel is not running. | |||
if (!Channel::is_open()) { | |||
return std::numeric_limits<std::int64_t>::max(); | |||
} | |||
// Wakeup immediately if there's more work to do now. | |||
if (Channel::oss_available() > 0 && | |||
(!_buffer_a.buffer.done() || !_buffer_b.buffer.done())) { | |||
Log::log(SOSSO_LOC, "Immediate wakeup at %lld for more work.", now); | |||
return now; | |||
} | |||
// Get upcoming buffer end and compute next channel wakeup time. | |||
std::int64_t sync_frames = now; | |||
if (_buffer_a.buffer.valid() && !finished(now)) { | |||
sync_frames = period_end(); | |||
} else if (_buffer_b.buffer.valid() && !total_finished(now)) { | |||
sync_frames = _buffer_b.end_frames + Channel::balance(); | |||
} else { | |||
sync_frames = std::numeric_limits<std::int64_t>::max(); | |||
} | |||
return Channel::wakeup_time(sync_frames); | |||
} | |||
//! Indicate progress on processing the primary buffer, in frames. | |||
std::int64_t buffer_progress() const { | |||
return _buffer_a.buffer.progress() / Channel::frame_size(); | |||
} | |||
//! Indicate that primary buffer is finished at current frame time. | |||
bool finished(std::int64_t now) const { | |||
return period_end() <= now && _buffer_a.buffer.done(); | |||
} | |||
//! Indicate that both buffers are finished at current frame time. | |||
bool total_finished(std::int64_t now) const { | |||
return total_end() <= now && _buffer_a.buffer.done() && | |||
_buffer_b.buffer.done(); | |||
} | |||
//! Print channel state as user information, at current frame time. | |||
void log_state(std::int64_t now) const { | |||
const char *direction = Channel::playback() ? "Out" : "In"; | |||
const char *sync = (Channel::last_sync() == now) ? "sync" : "frame"; | |||
std::int64_t buf_a = _buffer_a.buffer.progress() / Channel::frame_size(); | |||
std::int64_t buf_b = _buffer_b.buffer.progress() / Channel::frame_size(); | |||
Log::log(SOSSO_LOC, | |||
"%s %s, %lld bal %lld, buf A %lld B %lld OSS %lld, %lld left, " | |||
"req %u min %lld", | |||
direction, sync, now, Channel::balance(), buf_a, buf_b, | |||
Channel::oss_available(), period_end() - now, | |||
Channel::sync_level(), Channel::min_progress()); | |||
} | |||
private: | |||
BufferRecord _buffer_a; // Primary buffer, may be empty. | |||
BufferRecord _buffer_b; // Secondary buffer, may be empty. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_DOUBLEBUFFER_HPP |
@@ -0,0 +1,141 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_FRAMECLOCK_HPP | |||
#define SOSSO_FRAMECLOCK_HPP | |||
#include "sosso/Logging.hpp" | |||
#include <sys/errno.h> | |||
#include <time.h> | |||
namespace sosso { | |||
/*! | |||
* \brief Clock using audio frames as time unit. | |||
* | |||
* Provides time as an offset from an initial time zero, usually when the audio | |||
* device was started. Instead of nanoseconds it measures time in frames | |||
* (samples per channel), and thus needs to know the sample rate. | |||
* It also lets a thread sleep until a specified wakeup time, again in frames. | |||
*/ | |||
class FrameClock { | |||
public: | |||
/*! | |||
* \brief Initialize the clock, set time zero. | |||
* \param sample_rate Sample rate in Hz, for time to frame conversion. | |||
* \return True if successful, false means an error occurred. | |||
*/ | |||
bool init_clock(unsigned sample_rate) { | |||
return set_sample_rate(sample_rate) && init_zero_time(); | |||
} | |||
/*! | |||
* \brief Get current frame time. | |||
* \param result Set to current frame time, as offset from time zero. | |||
* \return True if successful, false means an error occurred. | |||
*/ | |||
bool now(std::int64_t &result) const { | |||
std::int64_t time_ns = 0; | |||
if (get_time_offset(time_ns)) { | |||
result = time_to_frames(time_ns); | |||
return true; | |||
} | |||
return false; | |||
} | |||
/*! | |||
* \brief Let the thread sleep until wakeup time. | |||
* \param wakeup_frame Wakeup time in frames since time zero. | |||
* \return True if successful, false means an error occurred. | |||
*/ | |||
bool sleep(std::int64_t wakeup_frame) const { | |||
std::int64_t time_ns = frames_to_time(wakeup_frame); | |||
return sleep_until(time_ns); | |||
} | |||
//! Convert frames to time in nanoseconds. | |||
std::int64_t frames_to_time(std::int64_t frames) const { | |||
return (frames * 1000000000) / _sample_rate; | |||
} | |||
//! Convert time in nanoseconds to frames. | |||
std::int64_t time_to_frames(std::int64_t time_ns) const { | |||
return (time_ns * _sample_rate) / 1000000000; | |||
} | |||
//! Convert frames to system clock time in microseconds. | |||
std::int64_t frames_to_absolute_us(std::int64_t frames) const { | |||
return _zero.tv_sec * 1000000ULL + _zero.tv_nsec / 1000 + | |||
frames_to_time(frames) / 1000; | |||
} | |||
//! Currently used sample rate in Hz. | |||
unsigned sample_rate() const { return _sample_rate; } | |||
//! Set the sample rate in Hz, used for time to frame conversion. | |||
bool set_sample_rate(unsigned sample_rate) { | |||
if (sample_rate > 0) { | |||
_sample_rate = sample_rate; | |||
return true; | |||
} | |||
return false; | |||
} | |||
//! Suggested minimal wakeup step in frames. | |||
unsigned stepping() const { return 16U * (1U + (_sample_rate / 50000)); } | |||
private: | |||
// Initialize time zero now. | |||
bool init_zero_time() { return gettime(_zero); } | |||
// Get current time in nanoseconds, as offset from time zero. | |||
bool get_time_offset(std::int64_t &result) const { | |||
timespec now; | |||
if (gettime(now)) { | |||
result = ((now.tv_sec - _zero.tv_sec) * 1000000000) + now.tv_nsec - | |||
_zero.tv_nsec; | |||
return true; | |||
} | |||
return false; | |||
} | |||
// Let thread sleep until wakeup time, in nanoseconds since time zero. | |||
bool sleep_until(std::int64_t offset_ns) const { | |||
timespec wakeup = {_zero.tv_sec + (_zero.tv_nsec + offset_ns) / 1000000000, | |||
(_zero.tv_nsec + offset_ns) % 1000000000}; | |||
if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &wakeup, NULL) != 0) { | |||
Log::warn(SOSSO_LOC, "Sleep failed with error %d.", errno); | |||
return false; | |||
} | |||
return true; | |||
} | |||
// Get current time in nanosecons, as a timespec struct. | |||
bool gettime(timespec &result) const { | |||
if (clock_gettime(CLOCK_MONOTONIC, &result) != 0) { | |||
Log::warn(SOSSO_LOC, "Get time failed with error %d.", errno); | |||
return false; | |||
} | |||
return true; | |||
} | |||
timespec _zero = {0, 0}; // Time zero as a timespec struct. | |||
unsigned _sample_rate = 48000; // Sample rate used for frame conversion. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_FRAMECLOCK_HPP |
@@ -0,0 +1,105 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_LOGGING_HPP | |||
#define SOSSO_LOGGING_HPP | |||
#include <cstdint> | |||
#include <cstdio> | |||
namespace sosso { | |||
/*! | |||
* \brief Store the source location for logging. | |||
* | |||
* Keep its implementation close to C++20 std::source_location. | |||
* It will be replaced by that when C++20 is widely available. | |||
*/ | |||
struct SourceLocation { | |||
//! Get the line number in the source file. | |||
std::uint_least32_t line() const { return _line; } | |||
//! Get the column in the source file, not implemented. | |||
std::uint_least32_t column() const { return _column; } | |||
//! Get the file name of the source file. | |||
const char *file_name() const { return _file_name; } | |||
//! Get the function context in the source file. | |||
const char *function_name() const { return _function_name; } | |||
std::uint_least32_t _line; | |||
std::uint_least32_t _column; | |||
const char *_file_name; | |||
const char *_function_name; | |||
}; | |||
/// Capture source location in place of this macro. | |||
#define SOSSO_LOC \ | |||
SourceLocation { __LINE__, 0, __FILE__, __func__ } | |||
/*! | |||
* \brief Static logging functions. | |||
* | |||
* There are three log levels: | |||
* - warn() indicates warnings and errors. | |||
* - info() provides general information to the user. | |||
* - log() is for low-level information and debugging purposes. | |||
* | |||
* The single message static logging functions have to be implemented in the | |||
* application, so they output to the appropriate places. Otherwise there will | |||
* be a linking error at build time. To give some context for debugging, the | |||
* source location is given. | |||
* | |||
* For printf-style message composition use the corresponding variable argument | |||
* function templates, limited to 255 character length. | |||
*/ | |||
class Log { | |||
public: | |||
//! Single message low-level log, implement this in the application. | |||
static void log(SourceLocation location, const char *message); | |||
//! Compose printf-style low-level log messages. | |||
template <typename... Args> | |||
static void log(SourceLocation location, const char *message, Args... args) { | |||
char formatted[256]; | |||
std::snprintf(formatted, 256, message, args...); | |||
log(location, formatted); | |||
} | |||
//! Single message user information, implement this in the application. | |||
static void info(SourceLocation location, const char *message); | |||
//! Compose printf-style user information messages. | |||
template <typename... Args> | |||
static void info(SourceLocation location, const char *message, Args... args) { | |||
char formatted[256]; | |||
std::snprintf(formatted, 256, message, args...); | |||
info(location, formatted); | |||
} | |||
//! Single message warning, implement this in the application. | |||
static void warn(SourceLocation location, const char *message); | |||
//! Compose printf-style warning messages. | |||
template <typename... Args> | |||
static void warn(SourceLocation location, const char *message, Args... args) { | |||
char formatted[256]; | |||
std::snprintf(formatted, 256, message, args...); | |||
warn(location, formatted); | |||
} | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_LOGGING_HPP |
@@ -0,0 +1,255 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_READCHANNEL_HPP | |||
#define SOSSO_READCHANNEL_HPP | |||
#include "sosso/Buffer.hpp" | |||
#include "sosso/Channel.hpp" | |||
#include "sosso/Logging.hpp" | |||
#include <fcntl.h> | |||
namespace sosso { | |||
/*! | |||
* \brief Recording Channel | |||
* | |||
* Specializes the generic Channel class into a recording channel. It keeps | |||
* track of the OSS recording progress, and reads the available audio data to an | |||
* external buffer. If the OSS buffer is memory mapped, the audio data is copied | |||
* from there. Otherwise I/O read() system calls are used. | |||
*/ | |||
class ReadChannel : public Channel { | |||
public: | |||
/*! | |||
* \brief Open a device for recording. | |||
* \param device Path to the device, e.g. "/dev/dsp1". | |||
* \param exclusive Try to get exclusive access to the device. | |||
* \return True if the device was opened successfully. | |||
*/ | |||
bool open(const char *device, bool exclusive = true) { | |||
int mode = O_RDONLY | O_NONBLOCK; | |||
if (exclusive) { | |||
mode |= O_EXCL; | |||
} | |||
return Channel::open(device, mode); | |||
} | |||
//! Available audio data to be read, in frames. | |||
std::int64_t oss_available() const { | |||
std::int64_t result = last_progress() - _read_position; | |||
if (result < 0) { | |||
result = 0; | |||
} else if (result > buffer_frames()) { | |||
result = buffer_frames(); | |||
} | |||
return result; | |||
} | |||
/*! | |||
* \brief Calculate next wakeup time. | |||
* \param sync_frames Required sync event (e.g. buffer end), in frame time. | |||
* \return Suggested and safe wakeup time for next process(), in frame time. | |||
*/ | |||
std::int64_t wakeup_time(std::int64_t sync_frames) const { | |||
return Channel::wakeup_time(sync_frames, oss_available()); | |||
} | |||
/*! | |||
* \brief Check OSS progress and read recorded audio to the buffer. | |||
* \param buffer Buffer to write to, untouched if invalid. | |||
* \param end Buffer end position, matching channel progress. | |||
* \param now Current time in frame time, see FrameClock. | |||
* \return True if successful, false means there was an error. | |||
*/ | |||
bool process(Buffer &buffer, std::int64_t end, std::int64_t now) { | |||
if (map()) { | |||
return (progress_done(now) || check_map_progress(now)) && | |||
(buffer_done(buffer, end) || process_mapped(buffer, end, now)); | |||
} else { | |||
return (progress_done(now) || check_read_progress(now)) && | |||
(buffer_done(buffer, end) || process_read(buffer, end, now)); | |||
} | |||
} | |||
protected: | |||
// Indicate that OSS progress has already been checked. | |||
bool progress_done(std::int64_t now) { return (last_processing() == now); } | |||
// Check OSS progress in case of memory mapped buffer. | |||
bool check_map_progress(std::int64_t now) { | |||
// Get OSS progress through map pointer. | |||
if (get_rec_pointer()) { | |||
std::int64_t progress = map_progress() - _oss_progress; | |||
_oss_progress += progress; | |||
std::int64_t available = last_progress() + progress - _read_position; | |||
std::int64_t loss = mark_loss(available - buffer_frames()); | |||
mark_progress(progress, now); | |||
if (loss > 0) { | |||
Log::warn(SOSSO_LOC, "OSS recording buffer overrun, %lld lost.", loss); | |||
_read_position = last_progress() - buffer_frames(); | |||
} | |||
} | |||
return progress_done(now); | |||
} | |||
// Read recorded audio data to buffer, in case of memory mapped OSS buffer. | |||
bool process_mapped(Buffer &buffer, std::int64_t end, std::int64_t now) { | |||
// Calculate current read buffer position. | |||
std::int64_t position = buffer_position(buffer, end); | |||
// Only read what is available until OSS captured its complete buffer. | |||
std::int64_t oldest = last_progress() - buffer_frames(); | |||
if (_oss_progress < buffer_frames()) { | |||
oldest = last_progress() - _oss_progress; | |||
} | |||
if (std::int64_t skip = buffer_advance(buffer, oldest - position)) { | |||
// First part of the read buffer already passed, fill it up. | |||
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer late by %lld, skip %lld.", | |||
now, end, oldest - position, skip); | |||
position += skip; | |||
} else if (position != _read_position) { | |||
// Position mismatch, reread what is available. | |||
if (std::int64_t rewind = buffer_rewind(buffer, position - oldest)) { | |||
Log::info(SOSSO_LOC, | |||
"@%lld - %lld Read position mismatch, reread %lld.", now, end, | |||
rewind); | |||
position -= rewind; | |||
} | |||
} | |||
if (position >= oldest && position < last_progress() && !buffer.done()) { | |||
// Read from offset up to current position, if read buffer can hold it. | |||
std::int64_t offset = last_progress() - position; | |||
std::size_t length = buffer.remaining(offset * frame_size()); | |||
unsigned pointer = (_oss_progress - offset) % buffer_frames(); | |||
length = read_map(buffer.position(), pointer * frame_size(), length); | |||
buffer.advance(length); | |||
_read_position = buffer_position(buffer, end); | |||
} | |||
_read_position += freewheel_finish(buffer, end, now); | |||
return true; | |||
} | |||
// Check progress when using I/O read() system call. | |||
bool check_read_progress(std::int64_t now) { | |||
// Check for OSS buffer overruns. | |||
std::int64_t overdue = now - estimated_dropout(oss_available()); | |||
if ((overdue > 0 && get_rec_overruns() > 0) || overdue > max_progress()) { | |||
std::int64_t progress = buffer_frames() - oss_available(); | |||
std::int64_t loss = mark_loss(progress, now); | |||
Log::warn(SOSSO_LOC, "OSS recording buffer overrun, %lld lost.", loss); | |||
mark_progress(progress + loss, now); | |||
_read_position = last_progress() - buffer_frames(); | |||
} else { | |||
// Infer progress from OSS queue changes. | |||
std::int64_t queued = queued_samples(); | |||
std::int64_t progress = queued - (last_progress() - _read_position); | |||
mark_progress(progress, now); | |||
_read_position = last_progress() - queued; | |||
} | |||
return progress_done(now); | |||
} | |||
// Read recorded audio data to buffer, using I/O read() syscall. | |||
bool process_read(Buffer &buffer, std::int64_t end, std::int64_t now) { | |||
bool ok = true; | |||
std::int64_t position = buffer_position(buffer, end); | |||
if (std::int64_t skip = buffer_advance(buffer, _read_position - position)) { | |||
// Overlapping buffers, skip the overlapping part. | |||
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer overlap %lld, skip %lld.", | |||
now, end, _read_position - position, skip); | |||
position += skip; | |||
} else if (std::int64_t rewind = | |||
buffer_rewind(buffer, position - _read_position)) { | |||
// Gap between reads, try to rewind to last read position. | |||
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer gap %lld, rewind %lld.", | |||
now, end, position - _read_position, rewind); | |||
position -= rewind; | |||
} | |||
if (oss_available() == 0) { | |||
// OSS buffer is empty, nothing to do. | |||
} else if (position > _read_position) { | |||
// Read and omit data of remaining gap, drain OSS buffer. | |||
std::int64_t gap = position - _read_position; | |||
std::size_t read_limit = buffer.remaining(gap * frame_size()); | |||
std::size_t bytes_read = 0; | |||
ok = read_io(buffer.position(), read_limit, bytes_read); | |||
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer gap %lld, drain %lu.", now, | |||
end, gap, bytes_read / frame_size()); | |||
_read_position += bytes_read / frame_size(); | |||
} else if (position == _read_position) { | |||
// Read as much as currently available. | |||
std::size_t bytes_read = 0; | |||
ok = read_io(buffer.position(), buffer.remaining(), bytes_read); | |||
_read_position += bytes_read / frame_size(); | |||
buffer.advance(bytes_read); | |||
} | |||
freewheel_finish(buffer, end, now); | |||
return ok; | |||
} | |||
private: | |||
// Calculate read position of the remaining buffer. | |||
std::int64_t buffer_position(const Buffer &buffer, std::int64_t end) const { | |||
return end - extra_latency() - (buffer.remaining() / frame_size()); | |||
} | |||
// Indicate that a buffer doesn't need further processing. | |||
bool buffer_done(const Buffer &buffer, std::int64_t end) const { | |||
return buffer.done() && buffer_position(buffer, end) <= _read_position; | |||
} | |||
// Extra latency to always finish on time, regardless of OSS progress steps. | |||
std::int64_t extra_latency() const { return max_progress(); } | |||
// Avoid stalled buffers with irregular OSS progress in freewheel mode. | |||
std::int64_t freewheel_finish(Buffer &buffer, std::int64_t end, | |||
std::int64_t now) { | |||
std::int64_t advance = 0; | |||
if (freewheel() && now >= end + balance() && !buffer.done()) { | |||
// Buffer is overdue in freewheel sync mode, finish immediately. | |||
std::memset(buffer.position(), 0, buffer.remaining()); | |||
advance = buffer.advance(buffer.remaining()) / frame_size(); | |||
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer overdue, fill by %lu.", | |||
now, end, advance); | |||
} | |||
return advance; | |||
} | |||
// Skip reading part of the buffer to match OSS read position. | |||
std::int64_t buffer_advance(Buffer &buffer, std::int64_t frames) { | |||
if (frames > 0) { | |||
std::size_t skip = buffer.remaining(frames * frame_size()); | |||
std::memset(buffer.position(), 0, skip); | |||
return buffer.advance(skip) / frame_size(); | |||
} | |||
return 0; | |||
} | |||
// Rewind part of the buffer to match OSS read position. | |||
std::int64_t buffer_rewind(Buffer &buffer, std::int64_t frames) { | |||
if (frames > 0) { | |||
return buffer.rewind(frames * frame_size()) / frame_size(); | |||
} | |||
return 0; | |||
} | |||
std::int64_t _oss_progress = 0; // Last memory mapped OSS progress. | |||
std::int64_t _read_position = 0; // Current read position of channel. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_READCHANNEL_HPP |
@@ -0,0 +1,280 @@ | |||
/* | |||
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch> | |||
* | |||
* Permission to use, copy, modify, and distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef SOSSO_WRITECHANNEL_HPP | |||
#define SOSSO_WRITECHANNEL_HPP | |||
#include "sosso/Buffer.hpp" | |||
#include "sosso/Channel.hpp" | |||
#include "sosso/Logging.hpp" | |||
#include <fcntl.h> | |||
namespace sosso { | |||
/*! | |||
* \brief Playback Channel | |||
* | |||
* Specializes the generic Channel class into a playback channel. It keeps track | |||
* of the OSS playback progress, and writes audio data from an external buffer | |||
* to the available OSS buffer. If the OSS buffer is memory mapped, the audio | |||
* data is copied there. Otherwise I/O write() system calls are used. | |||
*/ | |||
class WriteChannel : public Channel { | |||
public: | |||
/*! | |||
* \brief Open a device for playback. | |||
* \param device Path to the device, e.g. "/dev/dsp1". | |||
* \param exclusive Try to get exclusive access to the device. | |||
* \return True if the device was opened successfully. | |||
*/ | |||
bool open(const char *device, bool exclusive = true) { | |||
int mode = O_WRONLY | O_NONBLOCK; | |||
if (exclusive) { | |||
mode |= O_EXCL; | |||
} | |||
return Channel::open(device, mode); | |||
} | |||
//! Available OSS buffer space for writing, in frames. | |||
std::int64_t oss_available() const { | |||
std::int64_t result = last_progress() + buffer_frames() - _write_position; | |||
if (result < 0) { | |||
result = 0; | |||
} else if (result > buffer_frames()) { | |||
result = buffer_frames(); | |||
} | |||
return result; | |||
} | |||
/*! | |||
* \brief Calculate next wakeup time. | |||
* \param sync_frames Required sync event (e.g. buffer end), in frame time. | |||
* \return Suggested and safe wakeup time for next process(), in frame time. | |||
*/ | |||
std::int64_t wakeup_time(std::int64_t sync_frames) const { | |||
return Channel::wakeup_time(sync_frames, oss_available()); | |||
} | |||
/*! | |||
* \brief Check OSS progress and write playback audio to the OSS buffer. | |||
* \param buffer Buffer of playback audio data, untouched if invalid. | |||
* \param end Buffer end position, matching channel progress. | |||
* \param now Current time in frame time, see FrameClock. | |||
* \return True if successful, false means there was an error. | |||
*/ | |||
bool process(Buffer &buffer, std::int64_t end, std::int64_t now) { | |||
if (map()) { | |||
return (progress_done(now) || check_map_progress(now)) && | |||
(buffer_done(buffer, end) || process_mapped(buffer, end, now)); | |||
} else { | |||
return (progress_done(now) || check_write_progress(now)) && | |||
(buffer_done(buffer, end) || process_write(buffer, end, now)); | |||
} | |||
} | |||
protected: | |||
// Indicate that OSS progress has already been checked. | |||
bool progress_done(std::int64_t now) { return (last_processing() == now); } | |||
// Check OSS progress in case of memory mapped buffer. | |||
bool check_map_progress(std::int64_t now) { | |||
// Get OSS progress through map pointer. | |||
if (get_play_pointer()) { | |||
std::int64_t progress = map_progress() - _oss_progress; | |||
if (progress > 0) { | |||
// Sometimes OSS playback starts with a bogus extra buffer cycle. | |||
if (progress > buffer_frames() && | |||
now - last_processing() < buffer_frames() / 2) { | |||
Log::warn(SOSSO_LOC, | |||
"OSS playback bogus buffer cycle, %lld frames in %lld.", | |||
progress, now - last_processing()); | |||
progress = progress % buffer_frames(); | |||
} | |||
// Clear obsolete audio data in the buffer. | |||
write_map(nullptr, (_oss_progress % buffer_frames()) * frame_size(), | |||
progress * frame_size()); | |||
_oss_progress = map_progress(); | |||
} | |||
std::int64_t loss = | |||
mark_loss(last_progress() + progress - _write_position); | |||
mark_progress(progress, now); | |||
if (loss > 0) { | |||
Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss); | |||
_write_position = last_progress(); | |||
} | |||
} | |||
return progress_done(now); | |||
} | |||
// Write playback audio data to a memory mapped OSS buffer. | |||
bool process_mapped(Buffer &buffer, std::int64_t end, std::int64_t now) { | |||
// Buffer position should be between OSS progress and last write position. | |||
std::int64_t position = buffer_position(buffer.remaining(), end); | |||
if (std::int64_t skip = | |||
buffer_advance(buffer, last_progress() - position)) { | |||
// First part of the buffer already played, skip it. | |||
Log::info(SOSSO_LOC, "@%lld - %lld Write %lld already played, skip %lld.", | |||
now, end, last_progress() - position, skip); | |||
position += skip; | |||
} else if (position != _write_position) { | |||
// Position mismatch, rewrite as much as possible. | |||
if (std::int64_t rewind = | |||
buffer_rewind(buffer, position - last_progress())) { | |||
Log::info(SOSSO_LOC, | |||
"@%lld - %lld Write position mismatch, rewrite %lld.", now, | |||
end, rewind); | |||
position -= rewind; | |||
} | |||
} | |||
// The writable window is the whole buffer, starting from OSS progress. | |||
if (!buffer.done() && position >= last_progress() && | |||
position < last_progress() + buffer_frames()) { | |||
if (_write_position < position && _write_position + 8 >= position) { | |||
// Small remaining gap between writes, fill in a replay patch. | |||
std::int64_t offset = _write_position - last_progress(); | |||
unsigned pointer = (_oss_progress + offset) % buffer_frames(); | |||
std::size_t length = (position - _write_position) * frame_size(); | |||
length = buffer.remaining(length); | |||
std::size_t written = | |||
write_map(buffer.position(), pointer * frame_size(), length); | |||
Log::info(SOSSO_LOC, "@%lld - %lld Write small gap %lld, replay %lld.", | |||
now, end, position - _write_position, written / frame_size()); | |||
} | |||
// Write from buffer offset up to either OSS or write buffer end. | |||
std::int64_t offset = position - last_progress(); | |||
unsigned pointer = (_oss_progress + offset) % buffer_frames(); | |||
std::size_t length = (buffer_frames() - offset) * frame_size(); | |||
length = buffer.remaining(length); | |||
std::size_t written = | |||
write_map(buffer.position(), pointer * frame_size(), length); | |||
buffer.advance(written); | |||
_write_position = buffer_position(buffer.remaining(), end); | |||
} | |||
_write_position += freewheel_finish(buffer, end, now); | |||
return true; | |||
} | |||
// Check progress when using I/O write() system call. | |||
bool check_write_progress(std::int64_t now) { | |||
// Check for OSS buffer underruns. | |||
std::int64_t overdue = now - estimated_dropout(oss_available()); | |||
if ((overdue > 0 && get_play_underruns() > 0) || overdue > max_progress()) { | |||
// OSS buffer underrun, estimate loss and progress from time. | |||
std::int64_t progress = _write_position - last_progress(); | |||
std::int64_t loss = mark_loss(progress, now); | |||
Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss); | |||
mark_progress(progress + loss, now); | |||
_write_position = last_progress(); | |||
} else { | |||
// Infer progress from OSS queue changes. | |||
std::int64_t queued = queued_samples(); | |||
std::int64_t progress = (_write_position - last_progress()) - queued; | |||
mark_progress(progress, now); | |||
_write_position = last_progress() + queued; | |||
} | |||
return progress_done(now); | |||
} | |||
// Write playback audio data to OSS buffer using I/O write() system call. | |||
bool process_write(Buffer &buffer, std::int64_t end, std::int64_t now) { | |||
bool ok = true; | |||
// Adjust buffer position to OSS write position, if possible. | |||
std::int64_t position = buffer_position(buffer.remaining(), end); | |||
if (std::int64_t rewind = | |||
buffer_rewind(buffer, position - _write_position)) { | |||
// Gap between buffers, replay parts to fill it up. | |||
Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, replay %lld.", | |||
now, end, position - _write_position, rewind); | |||
position -= rewind; | |||
} else if (std::int64_t skip = | |||
buffer_advance(buffer, _write_position - position)) { | |||
// Overlapping buffers, skip the overlapping part. | |||
Log::info(SOSSO_LOC, "@%lld - %lld Write buffer overlap %lld, skip %lld.", | |||
now, end, _write_position - position, skip); | |||
position += skip; | |||
} | |||
if (oss_available() == 0) { | |||
// OSS buffer is full, nothing to do. | |||
} else if (position > _write_position) { | |||
// Replay to fill remaining gap, limit the write to just fill the gap. | |||
std::int64_t gap = position - _write_position; | |||
std::size_t write_limit = buffer.remaining(gap * frame_size()); | |||
std::size_t bytes_written = 0; | |||
ok = write_io(buffer.position(), write_limit, bytes_written); | |||
Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, fill %lld.", | |||
now, end, gap, bytes_written / frame_size()); | |||
_write_position += bytes_written / frame_size(); | |||
} else if (position == _write_position) { | |||
// Write as much as currently possible. | |||
std::size_t write_limit = buffer.remaining(); | |||
std::size_t bytes_written = 0; | |||
ok = write_io(buffer.position(), write_limit, bytes_written); | |||
_write_position += bytes_written / frame_size(); | |||
buffer.advance(bytes_written); | |||
} | |||
// Make sure buffers finish in time, despite irregular progress (freewheel). | |||
freewheel_finish(buffer, end, now); | |||
return ok; | |||
} | |||
private: | |||
// Calculate write position of the remaining buffer. | |||
std::int64_t buffer_position(std::size_t remaining, std::int64_t end) const { | |||
return end - (remaining / frame_size()); | |||
} | |||
// Indicate that a buffer doesn't need further processing. | |||
bool buffer_done(const Buffer &buffer, std::int64_t end) const { | |||
return buffer.done() && end <= _write_position; | |||
} | |||
// Avoid stalled buffers with irregular OSS progress in freewheel mode. | |||
std::int64_t freewheel_finish(Buffer &buffer, std::int64_t end, | |||
std::int64_t now) { | |||
std::int64_t advance = 0; | |||
// Make sure buffers finish in time, despite irregular progress (freewheel). | |||
if (freewheel() && now >= end + balance() && !buffer.done()) { | |||
advance = buffer.advance(buffer.remaining()) / frame_size(); | |||
Log::info(SOSSO_LOC, | |||
"@%lld - %lld Write freewheel finish remaining buffer %lld.", | |||
now, end, advance); | |||
} | |||
return advance; | |||
} | |||
// Skip writing part of the buffer to match OSS write position. | |||
std::int64_t buffer_advance(Buffer &buffer, std::int64_t frames) { | |||
if (frames > 0) { | |||
return buffer.advance(frames * frame_size()) / frame_size(); | |||
} | |||
return 0; | |||
} | |||
// Rewind part of the buffer to match OSS write postion. | |||
std::int64_t buffer_rewind(Buffer &buffer, std::int64_t frames) { | |||
if (frames > 0) { | |||
return buffer.rewind(frames * frame_size()) / frame_size(); | |||
} | |||
return 0; | |||
} | |||
std::int64_t _oss_progress = 0; // Last memory mapped OSS progress. | |||
std::int64_t _write_position = 0; // Current write position of the channel. | |||
}; | |||
} // namespace sosso | |||
#endif // SOSSO_WRITECHANNEL_HPP |
@@ -695,6 +695,7 @@ def build_drivers(bld): | |||
freebsd_oss_src = [ | |||
'common/memops.c', | |||
'freebsd/oss/JackOSSChannel.cpp', | |||
'freebsd/oss/JackOSSDriver.cpp' | |||
] | |||