@@ -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__ | #define __JackOSSDriver__ | ||||
#include "JackAudioDriver.h" | #include "JackAudioDriver.h" | ||||
#include "JackOSSChannel.h" | |||||
namespace Jack | namespace Jack | ||||
{ | { | ||||
@@ -44,9 +45,6 @@ class JackOSSDriver : public JackAudioDriver | |||||
{ | { | ||||
private: | private: | ||||
int fInFD; | |||||
int fOutFD; | |||||
int fBits; | int fBits; | ||||
int fNperiods; | int fNperiods; | ||||
bool fCapture; | bool fCapture; | ||||
@@ -54,42 +52,15 @@ class JackOSSDriver : public JackAudioDriver | |||||
bool fExcl; | bool fExcl; | ||||
bool fIgnoreHW; | 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(); | int OpenAux(); | ||||
void CloseAux(); | void CloseAux(); | ||||
void DisplayDeviceInfo(); | |||||
int ProbeInBlockSize(); | |||||
int ProbeOutBlockSize(); | |||||
int Discard(jack_nframes_t frames); | |||||
int WriteSilence(jack_nframes_t frames); | |||||
int WaitAndSync(); | |||||
protected: | protected: | ||||
virtual void UpdateLatencies(); | virtual void UpdateLatencies(); | ||||
@@ -98,16 +69,9 @@ class JackOSSDriver : public JackAudioDriver | |||||
JackOSSDriver(const char* name, const char* alias, JackLockedEngine* engine, JackSynchro* table) | JackOSSDriver(const char* name, const char* alias, JackLockedEngine* engine, JackSynchro* table) | ||||
: JackAudioDriver(name, alias, engine, table), | : JackAudioDriver(name, alias, engine, table), | ||||
fInFD(-1), fOutFD(-1), fBits(0), | |||||
fBits(0), | |||||
fNperiods(0), fCapture(false), fPlayback(false), fExcl(false), fIgnoreHW(true), | 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() | 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 = [ | freebsd_oss_src = [ | ||||
'common/memops.c', | 'common/memops.c', | ||||
'freebsd/oss/JackOSSChannel.cpp', | |||||
'freebsd/oss/JackOSSDriver.cpp' | 'freebsd/oss/JackOSSDriver.cpp' | ||||
] | ] | ||||