diff --git a/freebsd/oss/JackOSSChannel.cpp b/freebsd/oss/JackOSSChannel.cpp new file mode 100644 index 000000000..416e0828a --- /dev/null +++ b/freebsd/oss/JackOSSChannel.cpp @@ -0,0 +1,492 @@ +/* +Copyright (C) 2023 Florian Walpen + +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 +#include +#include +#include +#include +#include +#include + +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(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 diff --git a/freebsd/oss/JackOSSChannel.h b/freebsd/oss/JackOSSChannel.h new file mode 100644 index 000000000..d1a4a969e --- /dev/null +++ b/freebsd/oss/JackOSSChannel.h @@ -0,0 +1,132 @@ +/* +Copyright (C) 2003-2007 Jussi Laako +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 fReadChannel; + sosso::DoubleBuffer fWriteChannel; + sosso::Correction fCorrection; + + std::int64_t fFrameStamp = 0; + + public: + + JackOSSChannel() : fAssistThread(this) + {} + virtual ~JackOSSChannel() + {} + + sosso::DoubleBuffer &Capture() + { + return fReadChannel; + } + + sosso::DoubleBuffer &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 diff --git a/freebsd/oss/JackOSSDriver.cpp b/freebsd/oss/JackOSSDriver.cpp index d6f3e86cf..b2b90d5d1 100644 --- a/freebsd/oss/JackOSSDriver.cpp +++ b/freebsd/oss/JackOSSDriver.cpp @@ -20,103 +20,17 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #include "driver_interface.h" #include "JackThreadedDriver.h" -#include "JackDriverLoader.h" #include "JackOSSDriver.h" #include "JackEngineControl.h" #include "JackGraphManager.h" #include "JackError.h" #include "JackTime.h" -#include "JackShmMem.h" -#include "memops.h" - -#include -#include -#include -#include -#include + +#include #include using namespace std; -namespace -{ - -inline jack_nframes_t TimeToFrames(jack_time_t time, jack_nframes_t sample_rate) { - return ((time * sample_rate) + 500000ULL) / 1000000ULL; -} - -inline long long TimeToOffset(jack_time_t time1, jack_time_t time2, jack_nframes_t sample_rate) -{ - if (time2 > time1) { - return TimeToFrames(time2 - time1, sample_rate); - } else { - return 0LL - TimeToFrames(time1 - time2, sample_rate); - } -} - -inline jack_time_t FramesToTime(jack_nframes_t frames, jack_nframes_t sample_rate) { - return ((frames * 1000000ULL) + (sample_rate / 2ULL)) / sample_rate; -} - -inline jack_nframes_t RoundUp(jack_nframes_t frames, jack_nframes_t block) { - if (block > 0) { - frames += (block - 1); - frames -= (frames % block); - } - return frames; -} - -inline jack_time_t RoundDown(jack_time_t time, jack_time_t interval) { - if (interval > 0) { - time -= (time % interval); - } - return time; -} - -int GetSampleFormat(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; - } -} - -unsigned int GetSampleSize(int format) -{ - switch(format) { - // Native-endian signed 32 bit samples. - case AFMT_S32_NE: - return 4; - // Native-endian signed 24 bit (packed) samples. - case AFMT_S24_NE: - return 3; - // Native-endian signed 16 bit samples. - case AFMT_S16_NE: - return 2; - // Unsupported sample format. - default: - return 0; - } -} - -inline int UpToPower2(int x) -{ - int r = 0; - while ((1 << r) < x) - r++; - return r; -} - -} - namespace Jack { @@ -144,707 +58,6 @@ int gCycleCount = 0; #endif -static inline void CopyAndConvertIn(jack_sample_t *dst, void *src, size_t nframes, int channel, int chcount, int bits) -{ - switch (bits) { - - case 16: { - signed short *s16src = (signed short*)src; - s16src += channel; - sample_move_dS_s16(dst, (char*)s16src, nframes, chcount<<1); - break; - } - case 24: { - char *s24src = (char*)src; - s24src += channel * 3; - sample_move_dS_s24(dst, s24src, nframes, chcount*3); - break; - } - case 32: { - signed int *s32src = (signed int*)src; - s32src += channel; - sample_move_dS_s32u24(dst, (char*)s32src, nframes, chcount<<2); - break; - } - } -} - -static inline void CopyAndConvertOut(void *dst, jack_sample_t *src, size_t nframes, int channel, int chcount, int bits) -{ - switch (bits) { - - case 16: { - signed short *s16dst = (signed short*)dst; - s16dst += channel; - sample_move_d16_sS((char*)s16dst, src, nframes, chcount<<1, NULL); // No dithering for now... - break; - } - case 24: { - char *s24dst = (char*)dst; - s24dst += channel * 3; - sample_move_d24_sS(s24dst, src, nframes, chcount*3, NULL); - break; - } - case 32: { - signed int *s32dst = (signed int*)dst; - s32dst += channel; - sample_move_d32u24_sS((char*)s32dst, src, nframes, chcount<<2, NULL); - break; - } - } -} - -void JackOSSDriver::DisplayDeviceInfo() -{ - audio_buf_info info; - memset(&info, 0, sizeof(audio_buf_info)); - int cap = 0; - - // Duplex cards : http://manuals.opensound.com/developer/full_duplex.html - jack_info("Audio Interface Description :"); - jack_info("Sampling Frequency : %d, Sample Size : %d", fEngineControl->fSampleRate, fInSampleSize * 8); - - if (fPlayback) { - - oss_sysinfo si; - if (ioctl(fOutFD, OSS_SYSINFO, &si) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo OSS_SYSINFO failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("OSS product %s", si.product); - jack_info("OSS version %s", si.version); - jack_info("OSS version num %d", si.versionnum); - jack_info("OSS numaudios %d", si.numaudios); - jack_info("OSS numaudioengines %d", si.numaudioengines); - jack_info("OSS numcards %d", si.numcards); - } - - jack_info("Output capabilities - %d channels : ", fPlaybackChannels); - jack_info("Output block size = %d", fOutputBufferSize); - - if (ioctl(fOutFD, SNDCTL_DSP_GETOSPACE, &info) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETOSPACE failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("output space info: fragments = %d, fragstotal = %d, fragsize = %d, bytes = %d", - info.fragments, info.fragstotal, info.fragsize, info.bytes); - } - - if (ioctl(fOutFD, SNDCTL_DSP_GETCAPS, &cap) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETCAPS failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - if (cap & DSP_CAP_DUPLEX) jack_info(" DSP_CAP_DUPLEX"); - if (cap & DSP_CAP_REALTIME) jack_info(" DSP_CAP_REALTIME"); - if (cap & DSP_CAP_BATCH) jack_info(" DSP_CAP_BATCH"); - if (cap & DSP_CAP_COPROC) jack_info(" DSP_CAP_COPROC"); - if (cap & DSP_CAP_TRIGGER) jack_info(" DSP_CAP_TRIGGER"); - if (cap & DSP_CAP_MMAP) jack_info(" DSP_CAP_MMAP"); - if (cap & DSP_CAP_MULTI) jack_info(" DSP_CAP_MULTI"); - if (cap & DSP_CAP_BIND) jack_info(" DSP_CAP_BIND"); - } - } - - if (fCapture) { - - oss_sysinfo si; - if (ioctl(fInFD, OSS_SYSINFO, &si) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo OSS_SYSINFO failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("OSS product %s", si.product); - jack_info("OSS version %s", si.version); - jack_info("OSS version num %d", si.versionnum); - jack_info("OSS numaudios %d", si.numaudios); - jack_info("OSS numaudioengines %d", si.numaudioengines); - jack_info("OSS numcards %d", si.numcards); - } - - jack_info("Input capabilities - %d channels : ", fCaptureChannels); - jack_info("Input block size = %d", fInputBufferSize); - - if (ioctl(fInFD, SNDCTL_DSP_GETISPACE, &info) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETOSPACE failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("input space info: fragments = %d, fragstotal = %d, fragsize = %d, bytes = %d", - info.fragments, info.fragstotal, info.fragsize, info.bytes); - } - - if (ioctl(fInFD, SNDCTL_DSP_GETCAPS, &cap) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETCAPS failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - if (cap & DSP_CAP_DUPLEX) jack_info(" DSP_CAP_DUPLEX"); - if (cap & DSP_CAP_REALTIME) jack_info(" DSP_CAP_REALTIME"); - if (cap & DSP_CAP_BATCH) jack_info(" DSP_CAP_BATCH"); - if (cap & DSP_CAP_COPROC) jack_info(" DSP_CAP_COPROC"); - if (cap & DSP_CAP_TRIGGER) jack_info(" DSP_CAP_TRIGGER"); - if (cap & DSP_CAP_MMAP) jack_info(" DSP_CAP_MMAP"); - if (cap & DSP_CAP_MULTI) jack_info(" DSP_CAP_MULTI"); - if (cap & DSP_CAP_BIND) jack_info(" DSP_CAP_BIND"); - } - } -} - -int JackOSSDriver::ProbeInBlockSize() -{ - jack_nframes_t blocks[8] = {0, 0, 0, 0, 0, 0, 0, 0}; - int probes = 0; - int ret = 0; - // Default values in case of an error. - fInMeanStep = fEngineControl->fBufferSize; - fInBlockSize = 1; - - if (fInFD > 0) { - // Read one frame into a new hardware block so we can check its size. - // Repeat that for multiple probes, sometimes the first reads differ. - jack_nframes_t frames = 1; - for (int p = 0; p < 8 && frames > 0; ++p) { - ret = Discard(frames); - frames = 0; - if (ret == 0) { - oss_count_t ptr; - if (ioctl(fInFD, SNDCTL_DSP_CURRENT_IPTR, &ptr) == 0 && ptr.fifo_samples > 0) { - // Success, store probed hardware block size for later. - blocks[p] = 1U + ptr.fifo_samples; - ++probes; - // Proceed by reading one frame into the next hardware block. - frames = blocks[p]; - } - } else { - // Read error - abort. - jack_error("JackOSSDriver::ProbeInBlockSize read failed with %d", ret); - } - } - - // Stop recording. - ioctl(fInFD, SNDCTL_DSP_HALT_INPUT, NULL); - } - - if (probes == 8) { - // Compute mean block size of the last six probes. - jack_nframes_t sum = 0; - for (int p = 2; p < 8; ++p) { - jack_log("JackOSSDriver::ProbeInBlockSize read block of %d frames", blocks[p]); - sum += blocks[p]; - } - fInMeanStep = sum / 6; - - // Check that none of the probed block sizes deviates too much. - jack_nframes_t slack = fInMeanStep / 16; - bool strict = true; - for (int p = 2; p < 8; ++p) { - strict = strict && (blocks[p] > fInMeanStep - slack) && (blocks[p] < fInMeanStep + slack); - } - - if (strict && fInMeanStep <= fEngineControl->fBufferSize) { - // Regular hardware block size, use it for rounding. - jack_info("JackOSSDriver::ProbeInBlockSize read blocks are %d frames", fInMeanStep); - fInBlockSize = fInMeanStep; - } else { - jack_info("JackOSSDriver::ProbeInBlockSize irregular read block sizes"); - jack_info("JackOSSDriver::ProbeInBlockSize mean read block was %d frames", fInMeanStep); - } - - if (fInBlockSize > fEngineControl->fBufferSize / 2) { - jack_info("JackOSSDriver::ProbeInBlockSize less than two read blocks per cycle"); - jack_info("JackOSSDriver::ProbeInBlockSize for best results make period a multiple of %d", fInBlockSize); - } - - if (fInMeanStep > fEngineControl->fBufferSize) { - jack_error("JackOSSDriver::ProbeInBlockSize period is too small, minimum is %d frames", fInMeanStep); - return -1; - } - } - - return ret; -} - -int JackOSSDriver::ProbeOutBlockSize() -{ - jack_nframes_t blocks[8] = {0, 0, 0, 0, 0, 0, 0, 0}; - int probes = 0; - int ret = 0; - // Default values in case of an error. - fOutMeanStep = fEngineControl->fBufferSize; - fOutBlockSize = 1; - - if (fOutFD) { - // Write one frame over the low water mark, then check the consumed block size. - // Repeat that for multiple probes, sometimes the initial ones differ. - jack_nframes_t mark = fNperiods * fEngineControl->fBufferSize; - WriteSilence(mark + 1); - for (int p = 0; p < 8 && ret >= 0; ++p) { - pollfd poll_fd; - poll_fd.fd = fOutFD; - poll_fd.events = POLLOUT; - ret = poll(&poll_fd, 1, 500); - if (ret < 0) { - jack_error("JackOSSDriver::ProbeOutBlockSize poll failed with %d", ret); - break; - } - if (poll_fd.revents & POLLOUT) { - oss_count_t ptr; - if (ioctl(fOutFD, SNDCTL_DSP_CURRENT_OPTR, &ptr) != -1 && ptr.fifo_samples >= 0) { - // Success, store probed hardware block size for later. - blocks[p] = mark + 1 - ptr.fifo_samples; - ++probes; - // Proceed by writing one frame over the low water mark. - WriteSilence(blocks[p]); - } - poll_fd.revents = 0; - } - } - - // Stop playback. - ioctl(fOutFD, SNDCTL_DSP_HALT_INPUT, NULL); - } - - if (probes == 8) { - // Compute mean and maximum block size of the last six probes. - jack_nframes_t sum = 0; - for (int p = 2; p < 8; ++p) { - jack_log("JackOSSDriver::ProbeOutBlockSize write block of %d frames", blocks[p]); - sum += blocks[p]; - } - fOutMeanStep = sum / 6; - - // Check that none of the probed block sizes deviates too much. - jack_nframes_t slack = fOutMeanStep / 16; - bool strict = true; - for (int p = 2; p < 8; ++p) { - strict = strict && (blocks[p] > fOutMeanStep - slack) && (blocks[p] < fOutMeanStep + slack); - } - - if (strict && fOutMeanStep <= fEngineControl->fBufferSize) { - // Regular hardware block size, use it for rounding. - jack_info("JackOSSDriver::ProbeOutBlockSize write blocks are %d frames", fOutMeanStep); - fOutBlockSize = fOutMeanStep; - } else { - jack_info("JackOSSDriver::ProbeOutBlockSize irregular write block sizes"); - jack_info("JackOSSDriver::ProbeOutBlockSize mean write block was %d frames", fOutMeanStep); - } - - if (fOutBlockSize > fEngineControl->fBufferSize / 2) { - jack_info("JackOSSDriver::ProbeOutBlockSize less than two write blocks per cycle"); - jack_info("JackOSSDriver::ProbeOutBlockSize for best results make period a multiple of %d", fOutBlockSize); - } - - if (fOutMeanStep > fEngineControl->fBufferSize) { - jack_error("JackOSSDriver::ProbeOutBlockSize period is too small, minimum is %d frames", fOutMeanStep); - return -1; - } - } - - return ret; -} - -int JackOSSDriver::Discard(jack_nframes_t frames) -{ - if (fInFD < 0) { - return -1; - } - - // Read frames from OSS capture buffer to be discarded. - ssize_t size = frames * fInSampleSize * fCaptureChannels; - while (size > 0) { - ssize_t chunk = (size > fInputBufferSize) ? fInputBufferSize : size; - ssize_t count = ::read(fInFD, fInputBuffer, chunk); - if (count <= 0) { - jack_error("JackOSSDriver::Discard error bytes read = %ld", count); - return -1; - } - fOSSReadOffset += count / (fInSampleSize * fCaptureChannels); - size -= count; - } - return 0; -} - -int JackOSSDriver::WriteSilence(jack_nframes_t frames) -{ - if (fOutFD < 0) { - return -1; - } - - // Fill OSS playback buffer, write some periods of silence. - memset(fOutputBuffer, 0, fOutputBufferSize); - ssize_t size = frames * fOutSampleSize * fPlaybackChannels; - while (size > 0) { - ssize_t chunk = (size > fOutputBufferSize) ? fOutputBufferSize : size; - ssize_t count = ::write(fOutFD, fOutputBuffer, chunk); - if (count <= 0) { - jack_error("JackOSSDriver::WriteSilence error bytes written = %ld", count); - return -1; - } - fOSSWriteOffset += (count / (fOutSampleSize * fPlaybackChannels)); - size -= count; - } - return 0; -} - -int JackOSSDriver::WaitAndSync() -{ - oss_count_t ptr = {0, 0, {0}}; - if (fInFD > 0 && fOSSReadSync != 0) { - // Predict time of next capture sync (poll() return). - if (fOSSReadOffset + fEngineControl->fBufferSize > 0) { - jack_nframes_t frames = fOSSReadOffset + fEngineControl->fBufferSize; - jack_nframes_t rounded = RoundUp(frames, fInBlockSize); - fOSSReadSync += FramesToTime(rounded, fEngineControl->fSampleRate); - fOSSReadOffset -= rounded; - } - } - if (fOutFD > 0 && fOSSWriteSync != 0) { - // Predict time of next playback sync (poll() return). - if (fOSSWriteOffset > fNperiods * fEngineControl->fBufferSize) { - jack_nframes_t frames = fOSSWriteOffset - fNperiods * fEngineControl->fBufferSize; - jack_nframes_t rounded = RoundUp(frames, fOutBlockSize); - fOSSWriteSync += FramesToTime(rounded, fEngineControl->fSampleRate); - fOSSWriteOffset -= rounded; - } - } - jack_time_t poll_start = GetMicroSeconds(); - // Poll until recording and playback buffer are ready for this cycle. - pollfd poll_fd[2]; - poll_fd[0].fd = fInFD; - if (fInFD > 0 && (fForceSync || poll_start < fOSSReadSync)) { - poll_fd[0].events = POLLIN; - } else { - poll_fd[0].events = 0; - } - poll_fd[1].fd = fOutFD; - if (fOutFD > 0 && (fForceSync || poll_start < fOSSWriteSync)) { - poll_fd[1].events = POLLOUT; - } else { - poll_fd[1].events = 0; - } - while (poll_fd[0].events != 0 || poll_fd[1].events != 0) { - poll_fd[0].revents = 0; - poll_fd[1].revents = 0; - int ret = poll(poll_fd, 2, 500); - jack_time_t now = GetMicroSeconds(); - if (ret <= 0) { - jack_error("JackOSSDriver::WaitAndSync poll failed with %d after %ld us", ret, now - poll_start); - return ret; - } - if (poll_fd[0].revents & POLLIN) { - // Check the excess recording frames. - if (ioctl(fInFD, SNDCTL_DSP_CURRENT_IPTR, &ptr) != -1 && ptr.fifo_samples >= 0) { - if (fInBlockSize <= 1) { - // Irregular block size, let sync time converge slowly when late. - fOSSReadSync = min(fOSSReadSync, now) / 2 + now / 2; - fOSSReadOffset = -ptr.fifo_samples; - } else if (ptr.fifo_samples - fEngineControl->fBufferSize >= fInBlockSize) { - // Too late for a reliable sync, make sure sync time is not in the future. - if (now < fOSSReadSync) { - fOSSReadOffset = -ptr.fifo_samples; - jack_info("JackOSSDriver::WaitAndSync capture sync %ld us early, %ld frames", fOSSReadSync - now, fOSSReadOffset); - fOSSReadSync = now; - } - } else if (fForceSync) { - // Uncertain previous sync, just use sync time directly. - fOSSReadSync = now; - fOSSReadOffset = -ptr.fifo_samples; - } else { - // Adapt expected sync time when early or late - in whole block intervals. - // Account for some speed drift, but otherwise round down to earlier interval. - jack_time_t interval = FramesToTime(fInBlockSize, fEngineControl->fSampleRate); - jack_time_t remainder = fOSSReadSync % interval; - jack_time_t max_drift = interval / 4; - jack_time_t rounded = RoundDown((now - remainder) + max_drift, interval) + remainder; - // Let sync time converge slowly when late, prefer earlier sync times. - fOSSReadSync = min(rounded, now) / 2 + now / 2; - fOSSReadOffset = -ptr.fifo_samples; - } - } - poll_fd[0].events = 0; - } - if (poll_fd[1].revents & POLLOUT) { - // Check the remaining playback frames. - if (ioctl(fOutFD, SNDCTL_DSP_CURRENT_OPTR, &ptr) != -1 && ptr.fifo_samples >= 0) { - if (fOutBlockSize <= 1) { - // Irregular block size, let sync time converge slowly when late. - fOSSWriteSync = min(fOSSWriteSync, now) / 2 + now / 2; - fOSSWriteOffset = ptr.fifo_samples; - } else if (ptr.fifo_samples + fOutBlockSize <= fNperiods * fEngineControl->fBufferSize) { - // Too late for a reliable sync, make sure sync time is not in the future. - if (now < fOSSWriteSync) { - fOSSWriteOffset = ptr.fifo_samples; - jack_info("JackOSSDriver::WaitAndSync playback sync %ld us early, %ld frames", fOSSWriteSync - now, fOSSWriteOffset); - fOSSWriteSync = now; - } - } else if (fForceSync) { - // Uncertain previous sync, just use sync time directly. - fOSSWriteSync = now; - fOSSWriteOffset = ptr.fifo_samples; - } else { - // Adapt expected sync time when early or late - in whole block intervals. - // Account for some speed drift, but otherwise round down to earlier interval. - jack_time_t interval = FramesToTime(fOutBlockSize, fEngineControl->fSampleRate); - jack_time_t remainder = fOSSWriteSync % interval; - jack_time_t max_drift = interval / 4; - jack_time_t rounded = RoundDown((now - remainder) + max_drift, interval) + remainder; - // Let sync time converge slowly when late, prefer earlier sync times. - fOSSWriteSync = min(rounded, now) / 2 + now / 2; - fOSSWriteOffset = ptr.fifo_samples; - } - } - poll_fd[1].events = 0; - } - } - - fForceSync = false; - - // Compute balance of read and write buffers combined. - fBufferBalance = 0; - if (fInFD > 0 && fOutFD > 0) { - // Compare actual buffer content with target of (1 + n) * period. - fBufferBalance += ((1 + fNperiods) * fEngineControl->fBufferSize); - fBufferBalance -= (fOSSWriteOffset - fOSSReadOffset); - fBufferBalance += TimeToOffset(fOSSWriteSync, fOSSReadSync, fEngineControl->fSampleRate); - - // Force balancing if sync times deviate too much. - jack_time_t slack = FramesToTime((fEngineControl->fBufferSize * 2) / 3, fEngineControl->fSampleRate); - fForceBalancing = fForceBalancing || (fOSSReadSync > fOSSWriteSync + slack); - fForceBalancing = fForceBalancing || (fOSSWriteSync > fOSSReadSync + slack); - // Force balancing if buffer is badly balanced. - fForceBalancing = fForceBalancing || (abs(fBufferBalance) > max(fInMeanStep, fOutMeanStep)); - } - - // Print debug info every 10 seconds. - if (ptr.samples > 0 && (ptr.samples % (10 * fEngineControl->fSampleRate)) < fEngineControl->fBufferSize) { - jack_log("JackOSSDriver::Read buffer balance is %ld frames", fBufferBalance); - jack_time_t now = GetMicroSeconds(); - jack_log("JackOSSDriver::Read recording sync %ld frames %ld us ago", fOSSReadOffset, now - fOSSReadSync); - jack_log("JackOSSDriver::Read playback sync %ld frames %ld us ago", fOSSWriteOffset, now - fOSSWriteSync); - } - - return 0; -} - -int JackOSSDriver::OpenInput() -{ - int flags = 0; - int gFragFormat; - int cur_capture_channels; - int cur_sample_format; - jack_nframes_t cur_sample_rate; - audio_buf_info info; - - if (fCaptureChannels == 0) fCaptureChannels = 2; - - if ((fInFD = open(fCaptureDriverName, O_RDONLY | ((fExcl) ? O_EXCL : 0))) < 0) { - jack_error("JackOSSDriver::OpenInput failed to open device : %s@%i, errno = %d", __FILE__, __LINE__, errno); - return -1; - } - - jack_log("JackOSSDriver::OpenInput input fInFD = %d", fInFD); - - if (fExcl) { - if (ioctl(fInFD, SNDCTL_DSP_COOKEDMODE, &flags) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set cooked mode : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - } - - cur_sample_format = GetSampleFormat(fBits); - if (ioctl(fInFD, SNDCTL_DSP_SETFMT, &cur_sample_format) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set format : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fInSampleSize = GetSampleSize(cur_sample_format); - if (cur_sample_format != GetSampleFormat(fBits)) { - if (fInSampleSize > 0) { - jack_info("JackOSSDriver::OpenInput driver forced %d bit sample format", fInSampleSize * 8); - } else { - jack_error("JackOSSDriver::OpenInput unsupported sample format %#x", cur_sample_format); - goto error; - } - } - - cur_capture_channels = fCaptureChannels; - if (ioctl(fInFD, SNDCTL_DSP_CHANNELS, &fCaptureChannels) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set channels : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_capture_channels != fCaptureChannels) { - jack_info("JackOSSDriver::OpenInput driver forced the number of capture channels %ld", fCaptureChannels); - } - - cur_sample_rate = fEngineControl->fSampleRate; - if (ioctl(fInFD, SNDCTL_DSP_SPEED, &fEngineControl->fSampleRate) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set sample rate : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_sample_rate != fEngineControl->fSampleRate) { - jack_info("JackOSSDriver::OpenInput driver forced the sample rate %ld", fEngineControl->fSampleRate); - } - - // Internal buffer size required for one period. - fInputBufferSize = fEngineControl->fBufferSize * fInSampleSize * fCaptureChannels; - - // Get the total size of the OSS recording buffer, in sample frames. - info = {0, 0, 0, 0}; - if (ioctl(fInFD, SNDCTL_DSP_GETISPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenInput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSInBuffer = info.fragstotal * info.fragsize / (fInSampleSize * fCaptureChannels); - - if (fOSSInBuffer < fEngineControl->fBufferSize * (1 + fNperiods)) { - // Total size of the OSS recording buffer is too small, resize it. - unsigned int buf_size = fInputBufferSize * (1 + fNperiods); - // Keep current fragment size if possible - respect OSS latency settings. - gFragFormat = UpToPower2(info.fragsize); - unsigned int frag_size = 1U << gFragFormat; - gFragFormat |= ((buf_size + frag_size - 1) / frag_size) << 16; - jack_info("JackOSSDriver::OpenInput request %d fragments of %d", (gFragFormat >> 16), frag_size); - if (ioctl(fInFD, SNDCTL_DSP_SETFRAGMENT, &gFragFormat) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set fragments : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - // Check the new OSS recording buffer size. - info = {0, 0, 0, 0}; - if (ioctl(fInFD, SNDCTL_DSP_GETISPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenInput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSInBuffer = info.fragstotal * info.fragsize / (fInSampleSize * fCaptureChannels); - } - - if (fOSSInBuffer > fEngineControl->fBufferSize) { - int mark = fInputBufferSize; - if (ioctl(fInFD, SNDCTL_DSP_LOW_WATER, &mark) != 0) { - jack_error("JackOSSDriver::OpenInput failed to set low water mark : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - jack_info("JackOSSDriver::OpenInput set low water mark to %d", mark); - } - - fInputBuffer = (void*)calloc(fInputBufferSize, 1); - assert(fInputBuffer); - - if (ProbeInBlockSize() < 0) { - goto error; - } - - return 0; - -error: - ::close(fInFD); - return -1; -} - -int JackOSSDriver::OpenOutput() -{ - int flags = 0; - int gFragFormat; - int cur_sample_format; - int cur_playback_channels; - jack_nframes_t cur_sample_rate; - audio_buf_info info; - - if (fPlaybackChannels == 0) fPlaybackChannels = 2; - - if ((fOutFD = open(fPlaybackDriverName, O_WRONLY | ((fExcl) ? O_EXCL : 0))) < 0) { - jack_error("JackOSSDriver::OpenOutput failed to open device : %s@%i, errno = %d", __FILE__, __LINE__, errno); - return -1; - } - - jack_log("JackOSSDriver::OpenOutput output fOutFD = %d", fOutFD); - - if (fExcl) { - if (ioctl(fOutFD, SNDCTL_DSP_COOKEDMODE, &flags) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set cooked mode : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - } - - cur_sample_format = GetSampleFormat(fBits); - if (ioctl(fOutFD, SNDCTL_DSP_SETFMT, &cur_sample_format) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set format : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOutSampleSize = GetSampleSize(cur_sample_format); - if (cur_sample_format != GetSampleFormat(fBits)) { - if (fOutSampleSize > 0) { - jack_info("JackOSSDriver::OpenOutput driver forced %d bit sample format", fOutSampleSize * 8); - } else { - jack_error("JackOSSDriver::OpenOutput unsupported sample format %#x", cur_sample_format); - goto error; - } - } - - cur_playback_channels = fPlaybackChannels; - if (ioctl(fOutFD, SNDCTL_DSP_CHANNELS, &fPlaybackChannels) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set channels : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_playback_channels != fPlaybackChannels) { - jack_info("JackOSSDriver::OpenOutput driver forced the number of playback channels %ld", fPlaybackChannels); - } - - cur_sample_rate = fEngineControl->fSampleRate; - if (ioctl(fOutFD, SNDCTL_DSP_SPEED, &fEngineControl->fSampleRate) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set sample rate : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_sample_rate != fEngineControl->fSampleRate) { - jack_info("JackOSSDriver::OpenInput driver forced the sample rate %ld", fEngineControl->fSampleRate); - } - - // Internal buffer size required for one period. - fOutputBufferSize = fEngineControl->fBufferSize * fOutSampleSize * fPlaybackChannels; - - // Get the total size of the OSS playback buffer, in sample frames. - info = {0, 0, 0, 0}; - if (ioctl(fOutFD, SNDCTL_DSP_GETOSPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenOutput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSOutBuffer = info.fragstotal * info.fragsize / (fOutSampleSize * fPlaybackChannels); - - if (fOSSOutBuffer < fEngineControl->fBufferSize * (1 + fNperiods)) { - // Total size of the OSS playback buffer is too small, resize it. - unsigned int buf_size = fOutputBufferSize * (1 + fNperiods); - // Keep current fragment size if possible - respect OSS latency settings. - // Some sound cards like Intel HDA may stutter when changing the fragment size. - gFragFormat = UpToPower2(info.fragsize); - unsigned int frag_size = 1U << gFragFormat; - gFragFormat |= ((buf_size + frag_size - 1) / frag_size) << 16; - jack_info("JackOSSDriver::OpenOutput request %d fragments of %d", (gFragFormat >> 16), frag_size); - if (ioctl(fOutFD, SNDCTL_DSP_SETFRAGMENT, &gFragFormat) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set fragments : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - // Check the new OSS playback buffer size. - info = {0, 0, 0, 0}; - if (ioctl(fOutFD, SNDCTL_DSP_GETOSPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenOutput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSOutBuffer = info.fragstotal * info.fragsize / (fOutSampleSize * fPlaybackChannels); - } - - if (fOSSOutBuffer > fEngineControl->fBufferSize * fNperiods) { - jack_nframes_t low = fOSSOutBuffer - (fNperiods * fEngineControl->fBufferSize); - int mark = low * fOutSampleSize * fPlaybackChannels; - if (ioctl(fOutFD, SNDCTL_DSP_LOW_WATER, &mark) != 0) { - jack_error("JackOSSDriver::OpenOutput failed to set low water mark : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - jack_info("JackOSSDriver::OpenOutput set low water mark to %d", mark); - } - - fOutputBuffer = (void*)calloc(fOutputBufferSize, 1); - assert(fOutputBuffer); - - if (ProbeOutBlockSize() < 0) { - goto error; - } - - return 0; - -error: - ::close(fOutFD); - return -1; -} - int JackOSSDriver::Open(jack_nframes_t nframes, int user_nperiods, jack_nframes_t samplerate, @@ -884,6 +97,7 @@ int JackOSSDriver::Open(jack_nframes_t nframes, Close(); return -1; } else { + fChannel.StartAssistThread(fEngineControl->fRealTime, fEngineControl->fServerPriority); return 0; } } @@ -938,6 +152,11 @@ int JackOSSDriver::Close() fclose(file); } #endif + + fChannel.Lock(); + fChannel.StopAssistThread(); + fChannel.Unlock(); + int res = JackAudioDriver::Close(); CloseAux(); return res; @@ -946,286 +165,225 @@ int JackOSSDriver::Close() int JackOSSDriver::OpenAux() { - // (Re-)Initialize runtime variables. - fInSampleSize = fOutSampleSize = 0; - fInputBufferSize = fOutputBufferSize = 0; - fInBlockSize = fOutBlockSize = 1; - fInMeanStep = fOutMeanStep = 0; - fOSSInBuffer = fOSSOutBuffer = 0; - fOSSReadSync = fOSSWriteSync = 0; - fOSSReadOffset = fOSSWriteOffset = 0; - fBufferBalance = 0; - fForceBalancing = false; - fForceSync = false; - - if (fCapture && (OpenInput() < 0)) { + if (!fChannel.Lock()) { return -1; } - if (fPlayback && (OpenOutput() < 0)) { + // (Re-)Initialize runtime variables. + fCycleEnd = 0; + fLastRun = 0; + fMaxRunGap = 0; + + if (!fChannel.InitialSetup(fEngineControl->fSampleRate)) { + fChannel.Unlock(); return -1; } - DisplayDeviceInfo(); - return 0; -} + if (fCapture) { + if (!fChannel.OpenCapture(fCaptureDriverName, fExcl, fBits, fCaptureChannels)) { + fChannel.Unlock(); + return -1; + } + } -void JackOSSDriver::CloseAux() -{ - if (fCapture && fInFD > 0) { - close(fInFD); - fInFD = -1; + if (fPlayback) { + if (!fChannel.OpenPlayback(fPlaybackDriverName, fExcl, fBits, fPlaybackChannels)) { + fChannel.Unlock(); + return -1; + } } - if (fPlayback && fOutFD > 0) { - close(fOutFD); - fOutFD = -1; + if (!fChannel.StartChannels(fEngineControl->fBufferSize)) { + fChannel.Unlock(); + return -1; } - if (fInputBuffer) - free(fInputBuffer); - fInputBuffer = NULL; + if (fCapture) { + fChannel.Capture().log_device_info(); + } + if (fPlayback) { + fChannel.Playback().log_device_info(); + } + + if (size_t max_channels = std::max(fCaptureChannels, fPlaybackChannels)) { + fSampleBuffers = new jack_sample_t * [max_channels]; + } + + if (!fChannel.Unlock()) { + return -1; + } - if (fOutputBuffer) - free(fOutputBuffer); - fOutputBuffer = NULL; + return 0; } -int JackOSSDriver::Read() +void JackOSSDriver::CloseAux() { - if (fInFD > 0 && fOSSReadSync == 0) { - // First cycle, account for leftover samples from previous reads. - fOSSReadOffset = 0; - oss_count_t ptr; - if (ioctl(fInFD, SNDCTL_DSP_CURRENT_IPTR, &ptr) == 0 && ptr.fifo_samples > 0) { - jack_log("JackOSSDriver::Read pre recording samples = %ld, fifo_samples = %d", ptr.samples, ptr.fifo_samples); - fOSSReadOffset = -ptr.fifo_samples; - } + fChannel.Lock(); - // Start capture by reading a new hardware block., - jack_nframes_t discard = fInMeanStep - fOSSReadOffset; - // Let half a block or at most 1ms remain in buffer, avoid drift issues at start. - discard -= min(TimeToFrames(1000, fEngineControl->fSampleRate), (fInMeanStep / 2)); - jack_log("JackOSSDriver::Read start recording discard %ld frames", discard); - fOSSReadSync = GetMicroSeconds(); - Discard(discard); + fChannel.StopChannels(); - fForceSync = true; - fForceBalancing = true; + if (fSampleBuffers) { + delete[] fSampleBuffers; + fSampleBuffers = nullptr; } - if (fOutFD > 0 && fOSSWriteSync == 0) { - // First cycle, account for leftover samples from previous writes. - fOSSWriteOffset = 0; - oss_count_t ptr; - if (ioctl(fOutFD, SNDCTL_DSP_CURRENT_OPTR, &ptr) == 0 && ptr.fifo_samples > 0) { - jack_log("JackOSSDriver::Read pre playback samples = %ld, fifo_samples = %d", ptr.samples, ptr.fifo_samples); - fOSSWriteOffset = ptr.fifo_samples; - } - - // Start playback with silence, target latency as given by the user. - jack_nframes_t silence = (fNperiods + 1) * fEngineControl->fBufferSize; - // Minus half a block or at most 1ms of frames, avoid drift issues at start. - silence -= min(TimeToFrames(1000, fEngineControl->fSampleRate), (fOutMeanStep / 2)); - silence = max(silence - fOSSWriteOffset, 1LL); - jack_log("JackOSSDriver::Read start playback with %ld frames of silence", silence); - fOSSWriteSync = GetMicroSeconds(); - WriteSilence(silence); - - fForceSync = true; - fForceBalancing = true; - } + fChannel.Unlock(); +} +int JackOSSDriver::Read() +{ #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fBeforeRead = GetMicroSeconds(); #endif - if (WaitAndSync() < 0) { + if (!fChannel.Lock()) { return -1; } - // Keep begin cycle time - JackDriver::CycleTakeBeginTime(); + // Mark the end time of this cycle, in frames. + fCycleEnd += fEngineControl->fBufferSize; - if (fInFD < 0) { - return 0; + // Process read and write channels at least once. + std::int64_t channel_stamp = fChannel.FrameStamp(); + if (!fChannel.CheckTimeAndRun()) { + fChannel.Unlock(); + return -1; + } + if (fChannel.FrameStamp() - fLastRun > fMaxRunGap) { + fMaxRunGap = fChannel.FrameStamp() - fLastRun; + std::int64_t channel_gap = fChannel.FrameStamp() - channel_stamp; + jack_log("JackOSSDriver::Read max run gap %lld frames vs channel %lld.", fMaxRunGap, channel_gap); } - // Try to read multiple times in case of short reads. - size_t count = 0; - for (int i = 0; i < 3 && count < fInputBufferSize; ++i) { - ssize_t ret = ::read(fInFD, ((char*)fInputBuffer) + count, fInputBufferSize - count); - if (ret < 0) { - jack_error("JackOSSDriver::Read error = %s", strerror(errno)); - return -1; - } - count += ret; + // Check for over- and underruns. + if (fChannel.XRunGap() > 0) { + std::int64_t skip = fChannel.XRunGap() + fEngineControl->fBufferSize; + NotifyXRun(GetMicroSeconds(), (float) (fChannel.FrameClock().frames_to_time(skip) / 1000)); + fCycleEnd += skip; + jack_error("JackOSSDriver::Read(): XRun, late by %lld frames.", skip); + fChannel.ResetBuffers(skip); } - // Read offset accounting and overrun detection. - if (count > 0) { - jack_time_t now = GetMicroSeconds(); - jack_time_t sync = max(fOSSReadSync, fOSSWriteSync); - if (now - sync > 1000) { - // Blocking read() may indicate sample loss in OSS - force resync. - jack_log("JackOSSDriver::Read long read duration of %ld us", now - sync); - fForceSync = true; - } - long long passed = TimeToFrames(now - fOSSReadSync, fEngineControl->fSampleRate); - passed -= (passed % fInBlockSize); - if (passed > fOSSReadOffset + fOSSInBuffer) { - // Overrun, adjust read and write position. - long long missed = passed - (fOSSReadOffset + fOSSInBuffer); - jack_error("JackOSSDriver::Read missed %ld frames by overrun, passed=%ld, sync=%ld, now=%ld", missed, passed, fOSSReadSync, now); - fOSSReadOffset += missed; - fOSSWriteOffset += missed; - NotifyXRun(now, float(FramesToTime(missed, fEngineControl->fSampleRate))); + // Wait and process channels until read, or else write, buffer is finished. + while ((fCapture && !fChannel.CaptureFinished()) || + (!fCapture && !fChannel.PlaybackFinished())) { + if (!(fChannel.Sleep() && fChannel.CheckTimeAndRun())) { + fChannel.Unlock(); + return -1; } - fOSSReadOffset += count / (fInSampleSize * fCaptureChannels); } -#ifdef JACK_MONITOR - if (count > 0 && count != (int)fInputBufferSize) - jack_log("JackOSSDriver::Read count = %ld", count / (fInSampleSize * fCaptureChannels)); - gCycleTable.fTable[gCycleCount].fAfterRead = GetMicroSeconds(); -#endif - - // Check and clear OSS errors. - audio_errinfo ei_in; - if (ioctl(fInFD, SNDCTL_DSP_GETERROR, &ei_in) == 0) { - - // Not reliable for overrun detection, virtual_oss doesn't implement it. - if (ei_in.rec_overruns > 0 ) { - jack_error("JackOSSDriver::Read %d overrun events", ei_in.rec_overruns); - } + // Keep begin cycle time + JackDriver::CycleTakeBeginTime(); - if (ei_in.rec_errorcount > 0 && ei_in.rec_lasterror != 0) { - jack_error("%d OSS rec event(s), last=%05d:%d", ei_in.rec_errorcount, ei_in.rec_lasterror, ei_in.rec_errorparm); + if (!fCapture) { + if (!fChannel.Unlock()) { + return -1; } + return 0; } - if (count < fInputBufferSize) { - jack_error("JackOSSDriver::Read incomplete read of %ld bytes", count); - return -1; + if ((fChannel.FrameStamp() / fEngineControl->fBufferSize) % ((5 * fEngineControl->fSampleRate) / fEngineControl->fBufferSize) == 0) { + fChannel.Capture().log_state(fChannel.FrameStamp()); + fMaxRunGap = 0; } +#ifdef JACK_MONITOR + gCycleTable.fTable[gCycleCount].fAfterRead = GetMicroSeconds(); +#endif + for (int i = 0; i < fCaptureChannels; i++) { + fSampleBuffers[i] = nullptr; if (fGraphManager->GetConnectionsNum(fCapturePortList[i]) > 0) { - CopyAndConvertIn(GetInputBuffer(i), fInputBuffer, fEngineControl->fBufferSize, i, fCaptureChannels, fInSampleSize * 8); + fSampleBuffers[i] = GetInputBuffer(i); } } + std::int64_t buffer_end = fCycleEnd + fEngineControl->fBufferSize; + if (!fChannel.Read(fSampleBuffers, fEngineControl->fBufferSize, buffer_end)) { + fChannel.Unlock(); + return -1; + } #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fAfterReadConvert = GetMicroSeconds(); #endif + if (!fChannel.CheckTimeAndRun() || !fChannel.Unlock()) { + return -1; + } + fLastRun = fChannel.FrameStamp(); + return 0; } int JackOSSDriver::Write() { - if (fOutFD < 0) { + if (!fPlayback) { return 0; } - unsigned int skip = 0; - jack_time_t start = GetMicroSeconds(); - - if (fOSSWriteSync > 0) { - // Check for underruns, rounded to hardware block size if available. - long long passed = TimeToFrames(start - fOSSWriteSync, fEngineControl->fSampleRate); - long long consumed = passed - (passed % fOutBlockSize); - long long tolerance = (fOutBlockSize > 1) ? 0 : fOutMeanStep; - long long overdue = 0; - if (consumed > fOSSWriteOffset + tolerance) { - // Skip playback data that already passed. - overdue = consumed - fOSSWriteOffset - tolerance; - jack_error("JackOSSDriver::Write underrun, late by %ld, skip %ld frames", passed - fOSSWriteOffset, overdue); - jack_log("JackOSSDriver::Write playback offset %ld frames synced %ld us ago", fOSSWriteOffset, start - fOSSWriteSync); - // Also consider buffer balance, there was a gap in playback anyway. - fForceBalancing = true; - } - // Account for buffer balance if needed. - long long progress = fEngineControl->fBufferSize; - if (fForceBalancing) { - fForceBalancing = false; - progress = max(progress + fBufferBalance, 0LL); - jack_info("JackOSSDriver::Write buffer balancing %ld frames", fBufferBalance); - jack_log("JackOSSDriver::Write recording sync %ld frames %ld us ago", fOSSReadOffset, start - fOSSReadSync); - jack_log("JackOSSDriver::Write playback sync %ld frames %ld us ago", fOSSWriteOffset, start - fOSSWriteSync); - } - // How many samples to skip or prepend due to underrun and balancing. - long long write_length = progress - overdue; - if (write_length <= 0) { - skip += fOutputBufferSize; - fOSSWriteOffset += progress; - } else if (write_length < fEngineControl->fBufferSize) { - skip += (fEngineControl->fBufferSize - write_length) * fOutSampleSize * fPlaybackChannels; - fOSSWriteOffset += overdue; - } else if (write_length > fEngineControl->fBufferSize) { - jack_nframes_t fill = write_length - fEngineControl->fBufferSize; - WriteSilence(fill); + if (!fChannel.Lock()) { + return -1; + } + + // Process read and write channels at least once. + std::int64_t channel_stamp = fChannel.FrameStamp(); + if (!fChannel.CheckTimeAndRun()) { + fChannel.Unlock(); + return -1; + } + if (fChannel.FrameStamp() - fLastRun > fMaxRunGap) { + fMaxRunGap = fChannel.FrameStamp() - fLastRun; + std::int64_t channel_gap = fChannel.FrameStamp() - channel_stamp; + jack_log("JackOSSDriver::Write max run gap %lld frames vs channel %lld.", fMaxRunGap, channel_gap); + } + + // Wait and process channels until write buffer is finished. + while (!fChannel.PlaybackFinished()) { + if (!(fChannel.Sleep() && fChannel.CheckTimeAndRun())) { + fChannel.Unlock(); + return -1; } } + if ((fChannel.FrameStamp() / fEngineControl->fBufferSize) % ((5 * fEngineControl->fSampleRate) / fEngineControl->fBufferSize) == 0) { + fChannel.Playback().log_state(fChannel.FrameStamp()); + fMaxRunGap = 0; + } + #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fBeforeWriteConvert = GetMicroSeconds(); #endif - memset(fOutputBuffer, 0, fOutputBufferSize); for (int i = 0; i < fPlaybackChannels; i++) { + fSampleBuffers[i] = nullptr; if (fGraphManager->GetConnectionsNum(fPlaybackPortList[i]) > 0) { - CopyAndConvertOut(fOutputBuffer, GetOutputBuffer(i), fEngineControl->fBufferSize, i, fPlaybackChannels, fOutSampleSize * 8); + fSampleBuffers[i] = GetOutputBuffer(i); } } + std::int64_t buffer_end = fCycleEnd + fEngineControl->fBufferSize; + if (!fChannel.Write(fSampleBuffers, fEngineControl->fBufferSize, buffer_end)) { + fChannel.Unlock(); + return -1; + } #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fBeforeWrite = GetMicroSeconds(); #endif - // Try multiple times in case of short writes. - ssize_t count = skip; - for (int i = 0; i < 3 && count < fOutputBufferSize; ++i) { - ssize_t ret = ::write(fOutFD, ((char*)fOutputBuffer) + count, fOutputBufferSize - count); - if (ret < 0) { - jack_error("JackOSSDriver::Write error = %s", strerror(errno)); - return -1; - } - count += ret; - } - - fOSSWriteOffset += ((count - skip) / (fOutSampleSize * fPlaybackChannels)); - - jack_time_t duration = GetMicroSeconds() - start; - if (duration > 1000) { - // Blocking write() may indicate sample loss in OSS - force resync. - jack_log("JackOSSDriver::Write long write duration of %ld us", duration); - fForceSync = true; + // Do a processing step here. + if (!fChannel.CheckTimeAndRun()) { + fChannel.Unlock(); + return -1; } + fLastRun = fChannel.FrameStamp(); #ifdef JACK_MONITOR - if (count > 0 && count != (int)fOutputBufferSize) - jack_log("JackOSSDriver::Write count = %ld", (count - skip) / (fOutSampleSize * fPlaybackChannels)); gCycleTable.fTable[gCycleCount].fAfterWrite = GetMicroSeconds(); gCycleCount = (gCycleCount == CYCLE_POINTS - 1) ? gCycleCount: gCycleCount + 1; #endif - // Check and clear OSS errors. - audio_errinfo ei_out; - if (ioctl(fOutFD, SNDCTL_DSP_GETERROR, &ei_out) == 0) { - - // Not reliable for underrun detection, virtual_oss does not implement it. - if (ei_out.play_underruns > 0) { - jack_error("JackOSSDriver::Write %d underrun events", ei_out.play_underruns); - } - - if (ei_out.play_errorcount > 0 && ei_out.play_lasterror != 0) { - jack_error("%d OSS play event(s), last=%05d:%d",ei_out.play_errorcount, ei_out.play_lasterror, ei_out.play_errorparm); - } - } - - if (count < (int)fOutputBufferSize) { - jack_error("JackOSSDriver::Write incomplete write of %ld bytes", count - skip); + if (!fChannel.Unlock()) { return -1; } @@ -1245,6 +403,7 @@ void JackOSSDriver::UpdateLatencies() } for (int i = 0; i < fPlaybackChannels; i++) { + // TODO: Move this half period to capture latency. output_range.max = (fEngineControl->fBufferSize / 2) + fPlaybackLatency; // Additional latency introduced by the OSS buffer. output_range.max += fNperiods * fEngineControl->fBufferSize; diff --git a/freebsd/oss/JackOSSDriver.h b/freebsd/oss/JackOSSDriver.h index c5449b376..4cdb160fe 100644 --- a/freebsd/oss/JackOSSDriver.h +++ b/freebsd/oss/JackOSSDriver.h @@ -22,6 +22,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #define __JackOSSDriver__ #include "JackAudioDriver.h" +#include "JackOSSChannel.h" namespace Jack { @@ -44,9 +45,6 @@ class JackOSSDriver : public JackAudioDriver { private: - int fInFD; - int fOutFD; - int fBits; int fNperiods; bool fCapture; @@ -54,42 +52,15 @@ class JackOSSDriver : public JackAudioDriver bool fExcl; bool fIgnoreHW; - unsigned int fInSampleSize; - unsigned int fOutSampleSize; - - unsigned int fInputBufferSize; - unsigned int fOutputBufferSize; - - void* fInputBuffer; - void* fOutputBuffer; - - jack_nframes_t fInBlockSize; - jack_nframes_t fOutBlockSize; - jack_nframes_t fInMeanStep; - jack_nframes_t fOutMeanStep; - jack_nframes_t fOSSInBuffer; - jack_nframes_t fOSSOutBuffer; - - jack_time_t fOSSReadSync; - long long fOSSReadOffset; - jack_time_t fOSSWriteSync; - long long fOSSWriteOffset; + std::int64_t fCycleEnd; + std::int64_t fLastRun; + std::int64_t fMaxRunGap; + jack_sample_t** fSampleBuffers; - // Buffer balance and sync correction - long long fBufferBalance; - bool fForceBalancing; - bool fForceSync; + JackOSSChannel fChannel; - int OpenInput(); - int OpenOutput(); int OpenAux(); void CloseAux(); - void DisplayDeviceInfo(); - int ProbeInBlockSize(); - int ProbeOutBlockSize(); - int Discard(jack_nframes_t frames); - int WriteSilence(jack_nframes_t frames); - int WaitAndSync(); protected: virtual void UpdateLatencies(); @@ -98,16 +69,9 @@ class JackOSSDriver : public JackAudioDriver JackOSSDriver(const char* name, const char* alias, JackLockedEngine* engine, JackSynchro* table) : JackAudioDriver(name, alias, engine, table), - fInFD(-1), fOutFD(-1), fBits(0), + fBits(0), fNperiods(0), fCapture(false), fPlayback(false), fExcl(false), fIgnoreHW(true), - fInSampleSize(0), fOutSampleSize(0), - fInputBufferSize(0), fOutputBufferSize(0), - fInputBuffer(NULL), fOutputBuffer(NULL), - fInBlockSize(1), fOutBlockSize(1), - fInMeanStep(0), fOutMeanStep(0), - fOSSInBuffer(0), fOSSOutBuffer(0), - fOSSReadSync(0), fOSSReadOffset(0), fOSSWriteSync(0), fOSSWriteOffset(0), - fBufferBalance(0), fForceBalancing(false), fForceSync(false) + fCycleEnd(0), fLastRun(0), fMaxRunGap(0), fSampleBuffers(nullptr) {} virtual ~JackOSSDriver() diff --git a/freebsd/sosso/Buffer.hpp b/freebsd/sosso/Buffer.hpp new file mode 100644 index 000000000..1c9e7bbf1 --- /dev/null +++ b/freebsd/sosso/Buffer.hpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 + +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 diff --git a/freebsd/sosso/Channel.hpp b/freebsd/sosso/Channel.hpp new file mode 100644 index 000000000..2b29fe33a --- /dev/null +++ b/freebsd/sosso/Channel.hpp @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 + +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 diff --git a/freebsd/sosso/Correction.hpp b/freebsd/sosso/Correction.hpp new file mode 100644 index 000000000..2c3c304ab --- /dev/null +++ b/freebsd/sosso/Correction.hpp @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 + +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 diff --git a/freebsd/sosso/Device.hpp b/freebsd/sosso/Device.hpp new file mode 100644 index 000000000..c815fefae --- /dev/null +++ b/freebsd/sosso/Device.hpp @@ -0,0 +1,678 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 +#include +#include +#include +#include +#include +#include +#include + +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(_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(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(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 diff --git a/freebsd/sosso/DoubleBuffer.hpp b/freebsd/sosso/DoubleBuffer.hpp new file mode 100644 index 000000000..b966dbd0b --- /dev/null +++ b/freebsd/sosso/DoubleBuffer.hpp @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 +#include + +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 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::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::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 diff --git a/freebsd/sosso/FrameClock.hpp b/freebsd/sosso/FrameClock.hpp new file mode 100644 index 000000000..335846439 --- /dev/null +++ b/freebsd/sosso/FrameClock.hpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 +#include + +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 diff --git a/freebsd/sosso/Logging.hpp b/freebsd/sosso/Logging.hpp new file mode 100644 index 000000000..8a31df24a --- /dev/null +++ b/freebsd/sosso/Logging.hpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 +#include + +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 + 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 + 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 + 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 diff --git a/freebsd/sosso/ReadChannel.hpp b/freebsd/sosso/ReadChannel.hpp new file mode 100644 index 000000000..970131957 --- /dev/null +++ b/freebsd/sosso/ReadChannel.hpp @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 + +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 diff --git a/freebsd/sosso/WriteChannel.hpp b/freebsd/sosso/WriteChannel.hpp new file mode 100644 index 000000000..8e5b04663 --- /dev/null +++ b/freebsd/sosso/WriteChannel.hpp @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * 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 + +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 diff --git a/wscript b/wscript index f0a09c876..8a081cbec 100644 --- a/wscript +++ b/wscript @@ -695,6 +695,7 @@ def build_drivers(bld): freebsd_oss_src = [ 'common/memops.c', + 'freebsd/oss/JackOSSChannel.cpp', 'freebsd/oss/JackOSSDriver.cpp' ]