Browse Source

Merge 0762eac06a into 43e1173e30

pull/943/merge
0EVSG GitHub 7 months ago
parent
commit
70c053ee3e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
14 changed files with 2932 additions and 1032 deletions
  1. +492
    -0
      freebsd/oss/JackOSSChannel.cpp
  2. +132
    -0
      freebsd/oss/JackOSSChannel.h
  3. +147
    -988
      freebsd/oss/JackOSSDriver.cpp
  4. +8
    -44
      freebsd/oss/JackOSSDriver.h
  5. +153
    -0
      freebsd/sosso/Buffer.hpp
  6. +210
    -0
      freebsd/sosso/Channel.hpp
  7. +112
    -0
      freebsd/sosso/Correction.hpp
  8. +678
    -0
      freebsd/sosso/Device.hpp
  9. +218
    -0
      freebsd/sosso/DoubleBuffer.hpp
  10. +141
    -0
      freebsd/sosso/FrameClock.hpp
  11. +105
    -0
      freebsd/sosso/Logging.hpp
  12. +255
    -0
      freebsd/sosso/ReadChannel.hpp
  13. +280
    -0
      freebsd/sosso/WriteChannel.hpp
  14. +1
    -0
      wscript

+ 492
- 0
freebsd/oss/JackOSSChannel.cpp View File

@@ -0,0 +1,492 @@
/*
Copyright (C) 2023 Florian Walpen <dev@submerge.ch>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include "JackOSSChannel.h"
#include "JackError.h"
#include "JackThread.h"
#include "memops.h"

#include <cstdint>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <fcntl.h>
#include <iostream>
#include <assert.h>
#include <stdio.h>

typedef jack_default_audio_sample_t jack_sample_t;

namespace
{

int SuggestSampleFormat(int bits)
{
switch(bits) {
// Native-endian signed 32 bit samples.
case 32:
return AFMT_S32_NE;
// Native-endian signed 24 bit (packed) samples.
case 24:
return AFMT_S24_NE;
// Native-endian signed 16 bit samples, used by default.
case 16:
default:
return AFMT_S16_NE;
}
}

bool SupportedSampleFormat(int format)
{
switch(format) {
// Only signed sample formats are supported by the conversion functions.
case AFMT_S16_NE:
case AFMT_S16_OE:
case AFMT_S24_NE:
case AFMT_S24_OE:
case AFMT_S32_NE:
case AFMT_S32_OE:
return true;
}
return false;
}

void CopyAndConvertIn(jack_sample_t *dst, char *src, size_t nframes, int channel, int chcount, int format)
{
switch (format) {

case AFMT_S16_NE:
src += channel * 2;
sample_move_dS_s16(dst, src, nframes, chcount * 2);
break;
case AFMT_S16_OE:
src += channel * 2;
sample_move_dS_s16s(dst, src, nframes, chcount * 2);
break;
case AFMT_S24_NE:
src += channel * 3;
sample_move_dS_s24(dst, src, nframes, chcount * 3);
break;
case AFMT_S24_OE:
src += channel * 3;
sample_move_dS_s24s(dst, src, nframes, chcount * 3);
break;
case AFMT_S32_NE:
src += channel * 4;
sample_move_dS_s32(dst, src, nframes, chcount * 4);
break;
case AFMT_S32_OE:
src += channel * 4;
sample_move_dS_s32s(dst, src, nframes, chcount * 4);
break;
}
}

void CopyAndConvertOut(char *dst, jack_sample_t *src, size_t nframes, int channel, int chcount, int format)
{
switch (format) {

case AFMT_S16_NE:
dst += channel * 2;
sample_move_d16_sS(dst, src, nframes, chcount * 2, NULL);
break;
case AFMT_S16_OE:
dst += channel * 2;
sample_move_d16_sSs(dst, src, nframes, chcount * 2, NULL);
break;
case AFMT_S24_NE:
dst += channel * 3;
sample_move_d24_sS(dst, src, nframes, chcount * 3, NULL);
break;
case AFMT_S24_OE:
dst += channel * 3;
sample_move_d24_sSs(dst, src, nframes, chcount * 3, NULL);
break;
case AFMT_S32_NE:
dst += channel * 4;
sample_move_d32_sS(dst, src, nframes, chcount * 4, NULL);
break;
case AFMT_S32_OE:
dst += channel * 4;
sample_move_d32_sSs(dst, src, nframes, chcount * 4, NULL);
break;
}
}

}

void sosso::Log::log(sosso::SourceLocation location, const char* message) {
jack_log(message);
}

void sosso::Log::info(sosso::SourceLocation location, const char* message) {
jack_info(message);
}

void sosso::Log::warn(sosso::SourceLocation location, const char* message) {
jack_error(message);
}

namespace Jack
{

bool JackOSSChannel::InitialSetup(unsigned int sample_rate)
{
fFrameStamp = 0;
fCorrection.clear();
return fFrameClock.set_sample_rate(sample_rate);
}

bool JackOSSChannel::OpenCapture(const char *device, bool exclusive, int bits, int &channels)
{
if (channels == 0) channels = 2;

int sample_format = SuggestSampleFormat(bits);

if (!fReadChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) {
jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", sample_format);
return false;
}

if (!fReadChannel.open(device, exclusive)) {
return false;
}

if (fReadChannel.sample_rate() != fFrameClock.sample_rate()) {
jack_error("JackOSSChannel::OpenCapture driver forced sample rate %ld", fReadChannel.sample_rate());
fReadChannel.close();
return false;
}

if (!SupportedSampleFormat(fReadChannel.sample_format())) {
jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", fReadChannel.sample_format());
fReadChannel.close();
return false;
}

jack_log("JackOSSChannel::OpenCapture capture file descriptor = %d", fReadChannel.file_descriptor());

if (fReadChannel.channels() != channels) {
channels = fReadChannel.channels();
jack_info("JackOSSChannel::OpenCapture driver forced the number of capture channels %ld", channels);
}

fReadChannel.memory_map();

return true;
}

bool JackOSSChannel::OpenPlayback(const char *device, bool exclusive, int bits, int &channels)
{
if (channels == 0) channels = 2;

int sample_format = SuggestSampleFormat(bits);

if (!fWriteChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) {
jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", sample_format);
return false;
}

if (!fWriteChannel.open(device, exclusive)) {
return false;
}

if (fWriteChannel.sample_rate() != fFrameClock.sample_rate()) {
jack_error("JackOSSChannel::OpenPlayback driver forced sample rate %ld", fWriteChannel.sample_rate());
fWriteChannel.close();
return false;
}

if (!SupportedSampleFormat(fWriteChannel.sample_format())) {
jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", fWriteChannel.sample_format());
fWriteChannel.close();
return false;
}

jack_log("JackOSSChannel::OpenPlayback playback file descriptor = %d", fWriteChannel.file_descriptor());

if (fWriteChannel.channels() != channels) {
channels = fWriteChannel.channels();
jack_info("JackOSSChannel::OpenPlayback driver forced the number of playback channels %ld", channels);
}

fWriteChannel.memory_map();

return true;
}

bool JackOSSChannel::Read(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end)
{
if (fReadChannel.recording()) {
// Get buffer from read channel.
sosso::Buffer buffer = fReadChannel.take_buffer();

// Get recording audio data and then clear buffer.
for (unsigned i = 0; i < fReadChannel.channels(); i++) {
if (sample_buffers[i]) {
CopyAndConvertIn(sample_buffers[i], buffer.data(), length, i, fReadChannel.channels(), fReadChannel.sample_format());
}
}
buffer.reset();

// Put buffer back to capture at requested end position.
fReadChannel.set_buffer(std::move(buffer), end);
SignalWork();
return true;
}
return false;
}

bool JackOSSChannel::Write(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end)
{
if (fWriteChannel.playback()) {
// Get buffer from write channel.
sosso::Buffer buffer = fWriteChannel.take_buffer();

// Clear buffer and write new playback audio data.
memset(buffer.data(), 0, buffer.length());
buffer.reset();
for (unsigned i = 0; i < fWriteChannel.channels(); i++) {
if (sample_buffers[i]) {
CopyAndConvertOut(buffer.data(), sample_buffers[i], length, i, fWriteChannel.channels(), fWriteChannel.sample_format());
}
}

// Put buffer back to playback at requested end position.
end += PlaybackCorrection();
fWriteChannel.set_buffer(std::move(buffer), end);
SignalWork();
return true;
}
return false;
}

bool JackOSSChannel::StartChannels(unsigned int buffer_frames)
{
int group_id = 0;

if (fReadChannel.recording()) {
// Allocate two recording buffers for double buffering.
size_t buffer_size = buffer_frames * fReadChannel.frame_size();
sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size);
assert(buffer.data());
fReadChannel.set_buffer(std::move(buffer), 0);
buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size);
assert(buffer.data());
fReadChannel.set_buffer(std::move(buffer), buffer_frames);
// Add recording channel to synced start group.
fReadChannel.add_to_sync_group(group_id);
}

if (fWriteChannel.playback()) {
// Allocate two playback buffers for double buffering.
size_t buffer_size = buffer_frames * fWriteChannel.frame_size();
sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size);
assert(buffer.data());
fWriteChannel.set_buffer(std::move(buffer), 0);
buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size);
assert(buffer.data());
fWriteChannel.set_buffer(std::move(buffer), buffer_frames);
// Add playback channel to synced start group.
fWriteChannel.add_to_sync_group(group_id);
}

// Start both channels in sync if supported.
if (fReadChannel.recording()) {
fReadChannel.start_sync_group(group_id);
} else {
fWriteChannel.start_sync_group(group_id);
}

// Init frame clock here to mark start time.
if (!fFrameClock.init_clock(fFrameClock.sample_rate())) {
return false;
}

// Small drift corrections to keep latency whithin +/- 1ms.
std::int64_t limit = fFrameClock.sample_rate() / 1000;
fCorrection.set_drift_limits(-limit, limit);
// Drastic corrections when drift exceeds half a period.
limit = std::max<std::int64_t>(limit, buffer_frames / 2);
fCorrection.set_loss_limits(-limit, limit);

SignalWork();

return true;
}

bool JackOSSChannel::StopChannels()
{
if (fReadChannel.recording()) {
free(fReadChannel.take_buffer().data());
free(fReadChannel.take_buffer().data());
fReadChannel.memory_unmap();
fReadChannel.close();
}

if (fWriteChannel.playback()) {
free(fWriteChannel.take_buffer().data());
free(fWriteChannel.take_buffer().data());
fWriteChannel.memory_unmap();
fWriteChannel.close();
}

return true;
}

bool JackOSSChannel::StartAssistThread(bool realtime, int priority)
{
if (fAssistThread.Start() >= 0) {
if (realtime && fAssistThread.AcquireRealTime(priority) != 0) {
jack_error("JackOSSChannel::StartAssistThread realtime priority failed.");
}
return true;
}
return false;
}

bool JackOSSChannel::StopAssistThread()
{
if (fAssistThread.GetStatus() != JackThread::kIdle) {
fAssistThread.SetStatus(JackThread::kIdle);
SignalWork();
fAssistThread.Kill();
}
return true;
}

bool JackOSSChannel::CheckTimeAndRun()
{
// Check current frame time.
if (!fFrameClock.now(fFrameStamp)) {
jack_error("JackOSSChannel::CheckTimeAndRun(): Frame clock failed.");
return false;
}
std::int64_t now = fFrameStamp;

// Process read channel if wakeup time passed, or OSS buffer data available.
if (fReadChannel.recording() && !fReadChannel.total_finished(now)) {
if (now >= fReadChannel.wakeup_time(now)) {
if (!fReadChannel.process(now)) {
jack_error("JackOSSChannel::CheckTimeAndRun(): Read process failed.");
return false;
}
}
}
// Process write channel if wakeup time passed, or OSS buffer space available.
if (fWriteChannel.playback() && !fWriteChannel.total_finished(now)) {
if (now >= fWriteChannel.wakeup_time(now)) {
if (!fWriteChannel.process(now)) {
jack_error("JackOSSChannel::CheckTimeAndRun(): Write process failed.");
return false;
}
}
}

return true;
}

bool JackOSSChannel::Sleep() const
{
std::int64_t wakeup = NextWakeup();
if (wakeup > fFrameStamp) {
return fFrameClock.sleep(wakeup);
}
return true;
}

bool JackOSSChannel::CaptureFinished() const
{
return fReadChannel.finished(fFrameStamp);
}

bool JackOSSChannel::PlaybackFinished() const
{
return fWriteChannel.finished(fFrameStamp);
}

std::int64_t JackOSSChannel::PlaybackCorrection()
{
std::int64_t correction = 0;
// If both channels are used, correct drift relative to recording balance.
if (fReadChannel.recording() && fWriteChannel.playback()) {
std::int64_t previous = fCorrection.correction();
correction = fCorrection.correct(fWriteChannel.balance(), fReadChannel.balance());
if (correction != previous) {
jack_info("Playback correction changed from %lld to %lld.", previous, correction);
jack_info("Read balance %lld vs write balance %lld.", fReadChannel.balance(), fWriteChannel.balance());
}
}
return correction;
}

bool JackOSSChannel::Init()
{
return true;
}

bool JackOSSChannel::Execute()
{
if (Lock()) {
if (fAssistThread.GetStatus() != JackThread::kIdle) {
if (!CheckTimeAndRun()) {
return Unlock() && false;
}
std::int64_t wakeup = NextWakeup();
if (fReadChannel.total_finished(fFrameStamp) && fWriteChannel.total_finished(fFrameStamp)) {
// Nothing to do, wait on the mutex for work.
jack_info("JackOSSChannel::Execute waiting for work.");
fMutex.TimedWait(1000000);
jack_info("JackOSSChannel::Execute resuming work.");
} else if (fFrameStamp < wakeup) {
// Unlock mutex before going to sleep, let others process.
return Unlock() && fFrameClock.sleep(wakeup);
}
}
return Unlock();
}
return false;
}

std::int64_t JackOSSChannel::XRunGap() const
{
// Compute processing gap in case we are late.
std::int64_t max_end = std::max(fReadChannel.total_end(), fWriteChannel.total_end());
if (max_end < fFrameStamp) {
return fFrameStamp - max_end;
}
return 0;
}

void JackOSSChannel::ResetBuffers(std::int64_t offset)
{
// Clear buffers and offset their positions, after processing gaps.
if (fReadChannel.recording()) {
fReadChannel.reset_buffers(fReadChannel.end_frames() + offset);
}
if (fWriteChannel.playback()) {
fWriteChannel.reset_buffers(fWriteChannel.end_frames() + offset);
}
}

std::int64_t JackOSSChannel::NextWakeup() const
{
return std::min(fReadChannel.wakeup_time(fFrameStamp), fWriteChannel.wakeup_time(fFrameStamp));
}

} // end of namespace

+ 132
- 0
freebsd/oss/JackOSSChannel.h View File

@@ -0,0 +1,132 @@
/*
Copyright (C) 2003-2007 Jussi Laako <jussi@sonarnerd.net>
Copyright (C) 2008 Grame & RTL 2008

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#ifndef __JackOSSChannel__
#define __JackOSSChannel__

#include "JackMutex.h"
#include "JackThread.h"
#include "sosso/Correction.hpp"
#include "sosso/DoubleBuffer.hpp"
#include "sosso/FrameClock.hpp"
#include "sosso/ReadChannel.hpp"
#include "sosso/WriteChannel.hpp"

namespace Jack
{

typedef jack_default_audio_sample_t jack_sample_t;

/*!
\brief The OSS driver.
*/

class JackOSSChannel : public JackRunnableInterface
{

private:
JackThread fAssistThread;
JackProcessSync fMutex;
sosso::FrameClock fFrameClock;
sosso::DoubleBuffer<sosso::ReadChannel> fReadChannel;
sosso::DoubleBuffer<sosso::WriteChannel> fWriteChannel;
sosso::Correction fCorrection;

std::int64_t fFrameStamp = 0;

public:

JackOSSChannel() : fAssistThread(this)
{}
virtual ~JackOSSChannel()
{}

sosso::DoubleBuffer<sosso::ReadChannel> &Capture()
{
return fReadChannel;
}

sosso::DoubleBuffer<sosso::WriteChannel> &Playback()
{
return fWriteChannel;
}

sosso::FrameClock &FrameClock()
{
return fFrameClock;
}

bool Lock()
{
return fMutex.Lock();
}

bool Unlock()
{
return fMutex.Unlock();
}

void SignalWork()
{
fMutex.SignalAll();
}

bool InitialSetup(unsigned sample_rate);

bool OpenCapture(const char* device, bool exclusive, int bits, int &channels);
bool OpenPlayback(const char* device, bool exclusive, int bits, int &channels);

bool Read(jack_sample_t** sample_buffers, jack_nframes_t length, std::int64_t end);
bool Write(jack_sample_t** sample_buffers, jack_nframes_t length, std::int64_t end);

bool StartChannels(unsigned buffer_frames);
bool StopChannels();

bool StartAssistThread(bool realtime, int priority);
bool StopAssistThread();

bool CheckTimeAndRun();

bool Sleep() const;

bool CaptureFinished() const;
bool PlaybackFinished() const;

std::int64_t PlaybackCorrection();

virtual bool Init();

virtual bool Execute();

std::int64_t XRunGap() const;

void ResetBuffers(std::int64_t offset);

std::int64_t FrameStamp() const
{
return fFrameStamp;
}

std::int64_t NextWakeup() const;
};

} // end of namespace

#endif

+ 147
- 988
freebsd/oss/JackOSSDriver.cpp
File diff suppressed because it is too large
View File


+ 8
- 44
freebsd/oss/JackOSSDriver.h View File

@@ -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()


+ 153
- 0
freebsd/sosso/Buffer.hpp View File

@@ -0,0 +1,153 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_BUFFER_HPP
#define SOSSO_BUFFER_HPP

#include <cstring>

namespace sosso {

/*!
* \brief Buffer Management
*
* Provides means to access and manipulate externally allocated buffer memory.
* It stores a memory pointer, length and a read / write position. The buffer
* memory can be passed from one Buffer instance to another, through move
* constructor and move assignment. This prevents multiple Buffer instances from
* referencing the same memory.
*/
class Buffer {
public:
//! Construct an empty and invalid Buffer.
Buffer() = default;

/*!
* \brief Construct Buffer operating on given memory.
* \param buffer Pointer to the externally allocated memory.
* \param length Length of the memory dedicated to this Buffer.
*/
Buffer(char *buffer, std::size_t length)
: _data(buffer), _length(length), _position(0) {}

/*!
* \brief Move construct a buffer.
* \param other Adopt memory from this Buffer, leaving it empty.
*/
Buffer(Buffer &&other) noexcept
: _data(other._data), _length(other._length), _position(other._position) {
other._data = nullptr;
other._position = 0;
other._length = 0;
}

/*!
* \brief Move assign memory from another Buffer.
* \param other Adopt memory from this Buffer, leaving it empty.
* \return This newly assigned Buffer.
*/
Buffer &operator=(Buffer &&other) {
_data = other._data;
_position = other._position;
_length = other._length;
other._data = nullptr;
other._position = 0;
other._length = 0;
return *this;
}

//! Buffer is valid if the memory is accessable.
bool valid() const { return (_data != nullptr) && (_length > 0); }

//! Access the underlying memory, null if invalid.
char *data() const { return _data; }

//! Length of the underlying memory in bytes, 0 if invalid.
std::size_t length() const { return _length; }

//! Access buffer memory at read / write position.
char *position() const { return _data + _position; }

//! Get read / write progress from buffer start, in bytes.
std::size_t progress() const { return _position; }

//! Remaining buffer memory in bytes.
std::size_t remaining() const { return _length - _position; }

/*!
* \brief Cap given progress by remaining buffer memory.
* \param progress Progress in bytes.
* \return Progress limited by the remaining buffer memory.
*/
std::size_t remaining(std::size_t progress) const {
if (progress > remaining()) {
progress = remaining();
}
return progress;
}

//! Indicate that the buffer is fully processed.
bool done() const { return _position == _length; }

//! Advance the buffer read / write position.
std::size_t advance(std::size_t progress) {
progress = remaining(progress);
_position += progress;
return progress;
}

//! Rewind the buffer read / write position.
std::size_t rewind(std::size_t progress) {
if (progress > _position) {
progress = _position;
}
_position -= progress;
return progress;
}

/*!
* \brief Erase an already processed part, rewind.
* \param begin Start position of the region to be erased.
* \param end End position of the region to be erased.
* \return The number of bytes that were effectively erased.
*/
std::size_t erase(std::size_t begin, std::size_t end) {
if (begin < _position && begin < end) {
if (end > _position) {
end = _position;
}
std::size_t copy = _position - end;
if (copy > 0) {
std::memmove(_data + begin, _data + end, copy);
}
_position -= (end - begin);
return (end - begin);
}
return 0;
}

//! Reset the buffer position to zero.
void reset() { _position = 0; }

private:
char *_data = nullptr; // External buffer memory, null if invalid.
std::size_t _length = 0; // Total length of the buffer memory.
std::size_t _position = 0; // Current read / write position.
};

} // namespace sosso

#endif // SOSSO_BUFFER_HPP

+ 210
- 0
freebsd/sosso/Channel.hpp View File

@@ -0,0 +1,210 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_CHANNEL_HPP
#define SOSSO_CHANNEL_HPP

#include "sosso/Device.hpp"
#include <algorithm>

namespace sosso {

/*!
* \brief Audio Channel of a Device
*
* As a base class for read and write channels, this class provides generic
* handling of progress, loss and wakeup times. Progress here means the OSS
* device captures or consumes audio data, in frames. When progress is detected
* within a short wakeup interval, this counts as a sync where we can exactly
* match the device progress to current time.
* The balance indicates the drift between device progress and external time,
* usually taken from FrameClock.
* At device start and after loss, device progress can be irregular and is
* temporarily decoupled from Channel progress (freewheel). Sync events are
* required to change into normal mode which strictly follows device progress.
*/
class Channel : public Device {
public:
/*!
* \brief Open the device, initialize Channel
* \param device Full device path.
* \param mode Open mode (read / write).
* \return True if successful.
*/
bool open(const char *device, int mode) {
// Reset all internal statistics from last run.
_last_processing = 0;
_last_sync = 0;
_last_progress = 0;
_balance = 0;
_min_progress = 0;
_max_progress = 0;
_total_loss = 0;
_sync_level = 8;
return Device::open(device, mode);
}

//! Total progress of the device since start.
std::int64_t last_progress() const { return _last_progress; }

//! Balance (drift) compared to external time.
std::int64_t balance() const { return _balance; }

//! Last time there was a successful sync.
std::int64_t last_sync() const { return _last_sync; }

//! Last time the Channel was processed (mark_progress()).
std::int64_t last_processing() const { return _last_processing; }

//! Maximum progress step encountered.
std::int64_t max_progress() const { return _max_progress; }

//! Minimum progress step encountered.
std::int64_t min_progress() const { return _min_progress; }

//! Current number of syncs required to change to normal mode.
unsigned sync_level() const { return _sync_level; }

//! Indicate Channel progress decoupled from device progress.
bool freewheel() const { return _sync_level > 4; }

//! Indicate a full resync with small wakeup steps is required.
bool full_resync() const { return _sync_level > 2; }

//! Indicate a resync is required.
bool resync() const { return _sync_level > 0; }

//! Total number of frames lost due to over- or underruns.
std::int64_t total_loss() const { return _total_loss; }

//! Next time a device progress could be expected.
std::int64_t next_min_progress() const {
return _last_progress + _min_progress + _balance;
}

//! Calculate safe wakeup time to avoid over- or underruns.
std::int64_t safe_wakeup(std::int64_t oss_available) const {
return next_min_progress() + buffer_frames() - oss_available -
max_progress();
}

//! Estimate the time to expect over- or underruns.
std::int64_t estimated_dropout(std::int64_t oss_available) const {
return _last_progress + _balance + buffer_frames() - oss_available;
}

/*!
* \brief Calculate next wakeup time.
* \param sync_target External wakeup target like the next buffer end.
* \param oss_available Number of frames available in OSS buffer.
* \return Next wakeup time in external frame time.
*/
std::int64_t wakeup_time(std::int64_t sync_target,
std::int64_t oss_available) const {
// Use one sync step by default.
std::int64_t wakeup = _last_processing + Device::stepping();
if (freewheel() || full_resync()) {
// Small steps when doing a full resync.
} else if (resync() || wakeup + max_progress() > sync_target) {
// Sync required, wake up prior to next progress if possible.
if (next_min_progress() > wakeup) {
wakeup = next_min_progress() - Device::stepping();
} else if (next_min_progress() > _last_processing) {
wakeup = next_min_progress();
}
} else {
// Sleep until prior to sync target, then sync again.
wakeup = sync_target - max_progress();
}
// Make sure we wake up at sync target.
if (sync_target > _last_processing && sync_target < wakeup) {
wakeup = sync_target;
}
// Make sure we don't sleep into an OSS under- or overrun.
if (safe_wakeup(oss_available) < wakeup) {
wakeup = std::max(safe_wakeup(oss_available),
_last_processing + Device::stepping());
}
return wakeup;
}

protected:
// Account for progress detected, at current time.
void mark_progress(std::int64_t progress, std::int64_t now) {
if (progress > 0) {
if (freewheel()) {
// Some cards show irregular progress at the beginning, correct that.
// Also correct loss after under- and overruns, assume same balance.
_last_progress = now - progress - _balance;
// Require a sync before transition back to normal processing.
if (now <= _last_processing + stepping()) {
_sync_level -= 1;
}
} else if (now <= _last_processing + stepping()) {
// Successful sync on progress within small processing steps.
_balance = now - (_last_progress + progress);
_last_sync = now;
if (_sync_level > 0) {
_sync_level -= 1;
}
if (progress < _min_progress || _min_progress == 0) {
_min_progress = progress;
}
if (progress > _max_progress) {
_max_progress = progress;
}
} else {
// Big step with progress but no sync, requires a resync.
_sync_level += 1;
}
_last_progress += progress;
}
_last_processing = now;
}

// Account for loss given progress and current time.
std::int64_t mark_loss(std::int64_t progress, std::int64_t now) {
// Estimate frames lost due to over- or underrun.
std::int64_t loss = (now - _balance) - (_last_progress + progress);
return mark_loss(loss);
}

// Account for loss.
std::int64_t mark_loss(std::int64_t loss) {
if (loss > 0) {
_total_loss += loss;
// Resync OSS progress to frame time (now) to recover from loss.
_sync_level = std::max(_sync_level, 6U);
} else {
loss = 0;
}
return loss;
}

private:
std::int64_t _last_processing = 0; // Last processing time.
std::int64_t _last_sync = 0; // Last sync time.
std::int64_t _last_progress = 0; // Total device progress.
std::int64_t _balance = 0; // Channel drift.
std::int64_t _min_progress = 0; // Minimum progress step encountered.
std::int64_t _max_progress = 0; // Maximum progress step encountered.
std::int64_t _total_loss = 0; // Total loss due to over- or underruns.
unsigned _sync_level = 0; // Syncs required.
};

} // namespace sosso

#endif // SOSSO_CHANNEL_HPP

+ 112
- 0
freebsd/sosso/Correction.hpp View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_CORRECTION_HPP
#define SOSSO_CORRECTION_HPP

#include <cstdint>

namespace sosso {

/*!
* \brief Drift Correction
*
* Calculates drift correction for a channel, relative to another channel if
* required. Usually the playback channel is corrected relative to the recording
* channel, if in use.
* It keeps track of the correction parameter (in frames), and also the
* threshhold values which determine the amount of correction. Above these
* threshholds, either single frame correction is applied for smaller drift,
* or rigorous correction in case of large discrepance. The idea is that single
* frame corrections typically go unnoticed, but it may not be sufficient to
* correct something more grave like packet loss on a USB audio interface.
*/
class Correction {
public:
//! Default constructor, threshhold values are set separately.
Correction() = default;

/*!
* \brief Set thresholds for small drift correction.
* \param drift_min Limit for negative drift balance.
* \param drift_max Limit for positive drift balance.
*/
void set_drift_limits(std::int64_t drift_min, std::int64_t drift_max) {
if (drift_min < drift_max) {
_drift_min = drift_min;
_drift_max = drift_max;
} else {
_drift_min = drift_max;
_drift_max = drift_min;
}
}

/*!
* \brief Set thresholds for rigorous large discrepance correction.
* \param loss_min Limit for negative discrepance balance.
* \param loss_max Limit for positive discrepance balance.
*/
void set_loss_limits(std::int64_t loss_min, std::int64_t loss_max) {
if (loss_min < loss_max) {
_loss_min = loss_min;
_loss_max = loss_max;
} else {
_loss_min = loss_max;
_loss_max = loss_min;
}
}

//! Get current correction parameter.
std::int64_t correction() const { return _correction; }

/*!
* \brief Calculate a new correction parameter.
* \param balance Balance of the corrected channel, compared to FrameClock.
* \param target Balance of a master channel which acts as reference.
* \return Current correction parameter.
*/
std::int64_t correct(std::int64_t balance, std::int64_t target = 0) {
std::int64_t corrected_balance = balance - target + _correction;
if (corrected_balance > _loss_max) {
// Large positive discrepance, rigorous correction.
_correction -= corrected_balance - _loss_max;
} else if (corrected_balance < _loss_min) {
// Large negative discrepance, rigorous correction.
_correction += _loss_min - corrected_balance;
} else if (corrected_balance > _drift_max) {
// Small positive drift, correct by a single frame.
_correction -= 1;
} else if (corrected_balance < _drift_min) {
// Small negative drift, correct by a single frame.
_correction += 1;
}
return _correction;
}

//! Clear the current correction parameter, but not the thresholds.
void clear() { _correction = 0; }

private:
std::int64_t _loss_min = -128; // Negative threshold for rigorous correction.
std::int64_t _loss_max = 128; // Positive threshold for rigorous correction.
std::int64_t _drift_min = -64; // Negative threshold for drift correction.
std::int64_t _drift_max = 64; // Positive threshold for drift correction.
std::int64_t _correction = 0; // Correction parameter.
};

} // namespace sosso

#endif // SOSSO_CORRECTION_HPP

+ 678
- 0
freebsd/sosso/Device.hpp View File

@@ -0,0 +1,678 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_DEVICE_HPP
#define SOSSO_DEVICE_HPP

#include "sosso/Logging.hpp"
#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <sys/errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/soundcard.h>
#include <unistd.h>

namespace sosso {

/*!
* \brief Manage OSS devices.
*
* Encapsulates all the low-level handling of a FreeBSD OSS pcm device. Due to
* restrictions of the OSS API, the device can be opened for either playback or
* recording, not both. For duplex operation, separate instances of Device have
* to be opened.
* By default a Device opens 2 channels of 32 bit samples at 48 kHz, but the
* OSS API will force that to be whatever is supported by the hardware.
* Different default parameters can be set via set_parameters() prior to opening
* the Device. Always check the effective parameters before any use.
*/
class Device {
public:
/*!
* \brief Translate OSS sample formats to sample size.
* \param format OSS sample format, see sys/soundcard.h header.
* \return Sample size in bytes, 0 if unsupported.
*/
static std::size_t bytes_per_sample(int format) {
switch (format) {
case AFMT_S16_LE:
case AFMT_S16_BE:
return 2;
case AFMT_S24_LE:
case AFMT_S24_BE:
return 3;
case AFMT_S32_LE:
case AFMT_S32_BE:
return 4;
default:
return 0;
}
}

//! Always close device before destruction.
~Device() { close(); }

//! Effective OSS sample format, see sys/soundcard.h header.
int sample_format() const { return _sample_format; }

//! Effective sample size in bytes.
std::size_t bytes_per_sample() const {
return bytes_per_sample(_sample_format);
}

//! Indicate that the device is open.
bool is_open() const { return _fd >= 0; }

//! Indicate that the device is opened in playback mode.
bool playback() const { return _fd >= 0 && (_file_mode & O_WRONLY); }

//! Indicate that the device is opened in recording mode.
bool recording() const { return _fd >= 0 && !playback(); }

//! Get the file descriptor of the device, -1 if not open.
int file_descriptor() const { return _fd; }

//! Effective number of audio channels.
unsigned channels() const { return _channels; }

//! Effective frame size, one sample for each channel.
std::size_t frame_size() const { return _channels * bytes_per_sample(); }

//! Effective OSS buffer size in bytes.
std::size_t buffer_size() const { return _fragments * _fragment_size; }

//! Effective OSS buffer size in frames, samples per channel.
unsigned buffer_frames() const { return buffer_size() / frame_size(); }

//! Effective sample rate in Hz.
unsigned sample_rate() const { return _sample_rate; }

//! Suggested minimal polling step, in frames.
unsigned stepping() const { return 16U * (1U + (_sample_rate / 50000)); }

//! Indicate that the OSS buffer can be memory mapped.
bool can_memory_map() const { return has_capability(PCM_CAP_MMAP); }

//! A pointer to the memory mapped OSS buffer, null if not mapped.
char *map() const { return static_cast<char *>(_map); }

//! Current read / write position in the mapped OSS buffer.
unsigned map_pointer() const { return _map_progress % buffer_size(); }

//! Total progress of the mapped OSS buffer, in frames.
std::int64_t map_progress() const { return _map_progress / frame_size(); }

/*!
* \brief Set preferred audio parameters before opening device.
* \param format OSS sample formet, see sys/soundcard.h header.
* \param rate Sample rate in Hz.
* \param channels Number of recording / playback channels.
* \return True if successful, false means unsupported parameters.
*/
bool set_parameters(int format, int rate, int channels) {
if (bytes_per_sample(format) && channels > 0) {
_sample_format = format;
_sample_rate = rate;
_channels = channels;
return true;
}
return false;
}

/*!
* \brief Open the device for either recording or playback.
* \param device Path to the OSS device (e.g. "/dev/dsp1").
* \param mode Open mode read or write, optional exclusive and non-blocking.
* \return True if successful.
*/
bool open(const char *device, int mode) {
if (mode & O_RDWR) {
Log::warn(SOSSO_LOC, "Only one direction allowed, open %s in read mode.",
device);
mode = O_RDONLY | (mode & O_EXCL) | (mode & O_NONBLOCK);
}
_fd = ::open(device, mode);
if (_fd >= 0) {
_file_mode = mode;
if (bitperfect_mode(_fd) && set_sample_format(_fd) && set_channels(_fd) &&
set_sample_rate(_fd) && get_buffer_info() && get_capabilities()) {
return true;
}
}
Log::warn(SOSSO_LOC, "Unable to open device %s, errno %d.", device, errno);
close();
return false;
}

//! Close the device.
void close() {
if (map()) {
memory_unmap();
}
if (_fd >= 0) {
::close(_fd);
_fd = -1;
}
}

/*!
* \brief Request a specific OSS buffer size.
* \param fragments Number of fragments.
* \param fragment_size Size of the fragments in bytes.
* \return True if successful.
* \warning Due to OSS API limitations, resulting buffer sizes are not really
* predictable and may cause problems with some soundcards.
*/
bool set_buffer_size(unsigned fragments, unsigned fragment_size) {
int frg = 0;
while ((1U << frg) < fragment_size) {
++frg;
}
frg |= (fragments << 16);
Log::info(SOSSO_LOC, "Request %d fragments of %u bytes.", (frg >> 16),
(1U << (frg & 0xffff)));
if (ioctl(_fd, SNDCTL_DSP_SETFRAGMENT, &frg) != 0) {
Log::warn(SOSSO_LOC, "Set fragments failed with %d.", errno);
return false;
}
return get_buffer_info();
}

/*!
* \brief Request a specific OSS buffer size.
* \param total_size Total size of all buffer fragments.
* \return True if successful.
* \warning Due to OSS API limitations, resulting buffer sizes are not really
* predictable and may cause problems with some soundcards.
*/
bool set_buffer_size(unsigned total_size) {
if (_fragment_size > 0) {
unsigned fragments = (total_size + _fragment_size - 1) / _fragment_size;
return set_buffer_size(fragments, _fragment_size);
}
return false;
}

/*!
* \brief Read recorded audio data from OSS buffer.
* \param buffer Pointer to destination buffer.
* \param length Maximum read length in bytes.
* \param count Byte counter, increased by effective read length.
* \return True if successful or if nothing to do.
*/
bool read_io(char *buffer, std::size_t length, std::size_t &count) {
if (buffer && length > 0 && recording()) {
ssize_t result = ::read(_fd, buffer, length);
if (result >= 0) {
count += result;
} else if (errno == EAGAIN) {
count += 0;
} else {
Log::warn(SOSSO_LOC, "Data read failed with %d.", errno);
return false;
}
}
return true;
}

/*!
* \brief Read recorded audio data from memory mapped OSS buffer.
* \param buffer Pointer to destination buffer.
* \param offset Read offset into the OSS buffer, in bytes.
* \param length Maximum read length in bytes.
* \return The number of bytes read.
*/
std::size_t read_map(char *buffer, std::size_t offset, std::size_t length) {
std::size_t bytes_read = 0;
if (length > 0 && map()) {
// Sanitize offset and length parameters.
offset = offset % buffer_size();
if (length > buffer_size()) {
length = buffer_size();
}
// Check if the read length spans across an OSS buffer cycle.
if (offset + length > buffer_size()) {
// Read until buffer end first.
bytes_read = read_map(buffer, offset, buffer_size() - offset);
length -= bytes_read;
buffer += bytes_read;
offset = 0;
}
// Read remaining data.
std::memcpy(buffer, map() + offset, length);
bytes_read += length;
}
return bytes_read;
}

/*!
* \brief Write audio data to OSS buffer.
* \param buffer Pointer to source buffer.
* \param length Maximum write length in bytes.
* \param count Byte counter, increased by effective write length.
* \return True if successful or if nothing to do.
*/
bool write_io(char *buffer, std::size_t length, std::size_t &count) {
if (buffer && length > 0 && playback()) {
ssize_t result = ::write(file_descriptor(), buffer, length);
if (result >= 0) {
count += result;
} else if (errno == EAGAIN) {
count += 0;
} else {
Log::warn(SOSSO_LOC, "Data write failed with %d.", errno);
return false;
}
}
return true;
}

/*!
* \brief Write audio data to a memory mapped OSS buffer.
* \param buffer Pointer to source buffer, null writes zeros to OSS buffer.
* \param offset Write offset into the OSS buffer, in bytes.
* \param length Maximum write length in bytes.
* \return The number of bytes written.
*/
std::size_t write_map(const char *buffer, std::size_t offset,
std::size_t length) {
std::size_t bytes_written = 0;
if (length > 0 && map()) {
// Sanitize pointer and length parameters.
offset = offset % buffer_size();
if (length > buffer_size()) {
length = buffer_size();
}
// Check if the write length spans across an OSS buffer cycle.
if (offset + length > buffer_size()) {
// Write until buffer end first.
bytes_written += write_map(buffer, offset, buffer_size() - offset);
length -= bytes_written;
if (buffer) {
buffer += bytes_written;
}
offset = 0;
}
// Write source if available, otherwise clear the buffer.
if (buffer) {
std::memcpy(map() + offset, buffer, length);
} else {
std::memset(map() + offset, 0, length);
}
bytes_written += length;
}
return bytes_written;
}

/*!
* \brief Query number of frames in the OSS buffer (non-mapped).
* \return Number of frames, 0 if not successful.
*/
int queued_samples() {
unsigned long request =
playback() ? SNDCTL_DSP_CURRENT_OPTR : SNDCTL_DSP_CURRENT_IPTR;
oss_count_t ptr;
if (ioctl(_fd, request, &ptr) == 0) {
return ptr.fifo_samples;
}
return 0;
}

//! Indicate that the device can be triggered to start.
bool can_trigger() const { return has_capability(PCM_CAP_TRIGGER); }

//! Trigger the device to start recording / playback.
bool start() const {
if (!can_trigger()) {
Log::warn(SOSSO_LOC, "Trigger start not supported by device.");
return false;
}
int trigger = recording() ? PCM_ENABLE_INPUT : PCM_ENABLE_OUTPUT;
if (ioctl(file_descriptor(), SNDCTL_DSP_SETTRIGGER, &trigger) != 0) {
const char *direction = recording() ? "recording" : "playback";
Log::warn(SOSSO_LOC, "Starting %s channel failed with error %d.",
direction, errno);
return false;
}
return true;
}

/*!
* \brief Add device to a sync group for synchronized start.
* \param id Id of the sync group, 0 will initialize a new group.
* \return True if successful.
*/
bool add_to_sync_group(int &id) {
oss_syncgroup sync_group = {0, 0, {0}};
sync_group.id = id;
sync_group.mode |= (recording() ? PCM_ENABLE_INPUT : PCM_ENABLE_OUTPUT);
if (ioctl(file_descriptor(), SNDCTL_DSP_SYNCGROUP, &sync_group) == 0 &&
(id == 0 || sync_group.id == id)) {
id = sync_group.id;
return true;
}
Log::warn(SOSSO_LOC, "Sync grouping channel failed with error %d.", errno);
return false;
}

/*!
* \brief Synchronized start of all devices in the sync group.
* \param id Id of the sync group.
* \return True if successful.
*/
bool start_sync_group(int id) {
if (ioctl(file_descriptor(), SNDCTL_DSP_SYNCSTART, &id) == 0) {
return true;
}
Log::warn(SOSSO_LOC, "Start of sync group failed with error %d.", errno);
return false;
}

//! Query the number of playback underruns since last called.
int get_play_underruns() {
int play_underruns = 0;
int rec_overruns = 0;
get_errors(play_underruns, rec_overruns);
return play_underruns;
}

//! Query the number of recording overruns since last called.
int get_rec_overruns() {
int play_underruns = 0;
int rec_overruns = 0;
get_errors(play_underruns, rec_overruns);
return rec_overruns;
}

//! Update current playback position for memory mapped OSS buffer.
bool get_play_pointer() {
count_info info = {};
if (ioctl(file_descriptor(), SNDCTL_DSP_GETOPTR, &info) == 0) {
if (info.ptr >= 0 && static_cast<unsigned>(info.ptr) < buffer_size() &&
(info.ptr % frame_size()) == 0 && info.blocks >= 0) {
// Calculate pointer delta without complete buffer cycles.
unsigned delta =
(info.ptr + buffer_size() - map_pointer()) % buffer_size();
// Get upper bound on progress from blocks info.
unsigned max_bytes = (info.blocks + 1) * _fragment_size - 1;
if (max_bytes >= delta) {
// Estimate cycle part and round it down to buffer cycles.
unsigned cycles = max_bytes - delta;
cycles -= (cycles % buffer_size());
delta += cycles;
}
int fragments = delta / _fragment_size;
if (info.blocks < fragments || info.blocks > fragments + 1) {
Log::warn(SOSSO_LOC, "Play pointer blocks: %u - %d, %d, %d.",
map_pointer(), info.ptr, info.blocks, info.bytes);
}
_map_progress += delta;
return true;
}
Log::warn(SOSSO_LOC, "Play pointer out of bounds: %d, %d blocks.",
info.ptr, info.blocks);
} else {
Log::warn(SOSSO_LOC, "Play pointer failed with error: %d.", errno);
}
return false;
}

//! Update current recording position for memory mapped OSS buffer.
bool get_rec_pointer() {
count_info info = {};
if (ioctl(file_descriptor(), SNDCTL_DSP_GETIPTR, &info) == 0) {
if (info.ptr >= 0 && static_cast<unsigned>(info.ptr) < buffer_size() &&
(info.ptr % frame_size()) == 0 && info.blocks >= 0) {
// Calculate pointer delta without complete buffer cycles.
unsigned delta =
(info.ptr + buffer_size() - map_pointer()) % buffer_size();
// Get upper bound on progress from blocks info.
unsigned max_bytes = (info.blocks + 1) * _fragment_size - 1;
if (max_bytes >= delta) {
// Estimate cycle part and round it down to buffer cycles.
unsigned cycles = max_bytes - delta;
cycles -= (cycles % buffer_size());
delta += cycles;
}
int fragments = delta / _fragment_size;
if (info.blocks < fragments || info.blocks > fragments + 1) {
Log::warn(SOSSO_LOC, "Rec pointer blocks: %u - %d, %d, %d.",
map_pointer(), info.ptr, info.blocks, info.bytes);
}
_map_progress += delta;
return true;
}
Log::warn(SOSSO_LOC, "Rec pointer out of bounds: %d, %d blocks.",
info.ptr, info.blocks);
} else {
Log::warn(SOSSO_LOC, "Rec pointer failed with error: %d.", errno);
}
return false;
}

//! Memory map the OSS buffer.
bool memory_map() {
if (!can_memory_map()) {
Log::warn(SOSSO_LOC, "Memory map not supported by device.");
return false;
}
int protection = PROT_NONE;
if (playback()) {
protection = PROT_WRITE;
}
if (recording()) {
protection = PROT_READ;
}
if (_map == nullptr && protection != PROT_NONE) {
_map = mmap(NULL, buffer_size(), protection, MAP_SHARED,
file_descriptor(), 0);
if (_map == MAP_FAILED) {
Log::warn(SOSSO_LOC, "Memory map failed with error %d.", errno);
_map = nullptr;
}
}
return (_map != nullptr);
}

//! Unmap a previously memory mapped OSS buffer.
bool memory_unmap() {
if (_map) {
if (munmap(_map, buffer_size()) != 0) {
Log::warn(SOSSO_LOC, "Memory unmap failed with error %d.", errno);
return false;
}
_map = nullptr;
}
return true;
}

/*!
* \brief Check device capabilities.
* \param capabilities Device capabilities, see sys/soundcard.h header.
* \return True if the device has the capabilities in question.
*/
bool has_capability(int capabilities) const {
return (_capabilities & capabilities) == capabilities;
}

//! Print device info to user information log.
void log_device_info() const {
if (!is_open()) {
return;
}
const char *direction = (recording() ? "Recording" : "Playback");
Log::info(SOSSO_LOC, "%s device is %u channels at %u Hz, %lu bits.",
direction, _channels, _sample_rate, bytes_per_sample() * 8);
Log::info(SOSSO_LOC, "Device buffer is %u fragments of size %u, %u frames.",
_fragments, _fragment_size, buffer_frames());
oss_sysinfo sys_info = {};
if (ioctl(_fd, SNDCTL_SYSINFO, &sys_info) == 0) {
Log::info(SOSSO_LOC, "OSS version %s number %d on %s.", sys_info.version,
sys_info.versionnum, sys_info.product);
}
Log::info(SOSSO_LOC, "PCM capabilities:");
if (has_capability(PCM_CAP_TRIGGER))
Log::info(SOSSO_LOC, " PCM_CAP_TRIGGER (Trigger start)");
if (has_capability(PCM_CAP_MMAP))
Log::info(SOSSO_LOC, " PCM_CAP_MMAP (Memory map)");
if (has_capability(PCM_CAP_MULTI))
Log::info(SOSSO_LOC, " PCM_CAP_MULTI (Multiple open)");
if (has_capability(PCM_CAP_INPUT))
Log::info(SOSSO_LOC, " PCM_CAP_INPUT (Recording)");
if (has_capability(PCM_CAP_OUTPUT))
Log::info(SOSSO_LOC, " PCM_CAP_OUTPUT (Playback)");
if (has_capability(PCM_CAP_VIRTUAL))
Log::info(SOSSO_LOC, " PCM_CAP_VIRTUAL (Virtual device)");
if (has_capability(PCM_CAP_ANALOGIN))
Log::info(SOSSO_LOC, " PCM_CAP_ANALOGIN (Analog input)");
if (has_capability(PCM_CAP_ANALOGOUT))
Log::info(SOSSO_LOC, " PCM_CAP_ANALOGOUT (Analog output)");
if (has_capability(PCM_CAP_DIGITALIN))
Log::info(SOSSO_LOC, " PCM_CAP_DIGITALIN (Digital input)");
if (has_capability(PCM_CAP_DIGITALOUT))
Log::info(SOSSO_LOC, " PCM_CAP_DIGITALOUT (Digital output)");
}

private:
// Disable auto-conversion (bitperfect) when opened in exclusive mode.
bool bitperfect_mode(int fd) {
if (_file_mode & O_EXCL) {
int flags = 0;
int result = ioctl(fd, SNDCTL_DSP_COOKEDMODE, &flags);
if (result < 0) {
Log::warn(SOSSO_LOC, "Unable to set cooked mode.");
}
return result >= 0;
}
return true;
}

// Set sample format and the check the result.
bool set_sample_format(int fd) {
int format = _sample_format;
int result = ioctl(fd, SNDCTL_DSP_SETFMT, &format);
if (result != 0) {
Log::warn(SOSSO_LOC, "Unable to set sample format, error %d.", errno);
return false;
} else if (bytes_per_sample(format) == 0) {
Log::warn(SOSSO_LOC, "Unsupported sample format %d.", format);
return false;
} else if (format != _sample_format) {
Log::warn(
SOSSO_LOC, "Driver changed the sample format, %lu bit vs %lu bit.",
bytes_per_sample(format) * 8, bytes_per_sample(_sample_format) * 8);
}
_sample_format = format;
return true;
}

// Set sample rate and then check the result.
bool set_sample_rate(int fd) {
int rate = _sample_rate;
if (ioctl(fd, SNDCTL_DSP_SPEED, &rate) == 0) {
if (rate != _sample_rate) {
Log::warn(SOSSO_LOC, "Driver changed the sample rate, %d vs %d.", rate,
_sample_rate);
_sample_rate = rate;
}
return true;
}
Log::warn(SOSSO_LOC, "Unable to set sample rate, error %d.", errno);
return false;
}

// Set the number of channels and then check the result.
bool set_channels(int fd) {
int channels = _channels;
if (ioctl(fd, SNDCTL_DSP_CHANNELS, &channels) == 0) {
if (channels != _channels) {
Log::warn(SOSSO_LOC, "Driver changed number of channels, %d vs %d.",
channels, _channels);
_channels = channels;
}
return true;
}
Log::warn(SOSSO_LOC, "Unable to set channels, error %d.", errno);
return false;
}

// Query fragments and size of the OSS buffer.
bool get_buffer_info() {
audio_buf_info info = {0, 0, 0, 0};
unsigned long request =
playback() ? SNDCTL_DSP_GETOSPACE : SNDCTL_DSP_GETISPACE;
if (ioctl(_fd, request, &info) >= 0) {
_fragments = info.fragstotal;
_fragment_size = info.fragsize;
return true;
} else {
Log::warn(SOSSO_LOC, "Unable to get buffer info.");
return false;
}
}

// Query capabilities of the device.
bool get_capabilities() {
if (ioctl(_fd, SNDCTL_DSP_GETCAPS, &_capabilities) == 0) {
oss_sysinfo sysinfo = {};
if (ioctl(_fd, OSS_SYSINFO, &sysinfo) == 0) {
if (std::strncmp(sysinfo.version, "1302000", 7) < 0) {
// Memory map on FreeBSD prior to 13.2 may use wrong buffer size.
Log::warn(SOSSO_LOC,
"Disable memory map, workaround OSS bug on FreeBSD < 13.2");
_capabilities &= ~PCM_CAP_MMAP;
}
return true;
} else {
Log::warn(SOSSO_LOC, "Unable to get system info, error %d.", errno);
}
} else {
Log::warn(SOSSO_LOC, "Unable to get device capabilities, error %d.",
errno);
_capabilities = 0;
}
return false;
}

// Query error information from the device.
bool get_errors(int &play_underruns, int &rec_overruns) {
audio_errinfo error_info = {};
if (ioctl(file_descriptor(), SNDCTL_DSP_GETERROR, &error_info) == 0) {
play_underruns = error_info.play_underruns;
rec_overruns = error_info.rec_overruns;
return true;
}
return false;
}

private:
int _fd = -1; // File descriptor.
int _file_mode = O_RDONLY; // File open mode.
void *_map = nullptr; // Memory map pointer.
std::uint64_t _map_progress = 0; // Memory map progress.
int _channels = 2; // Number of channels.
int _capabilities = 0; // Device capabilities.
int _sample_format = AFMT_S32_NE; // Sample format.
int _sample_rate = 48000; // Sample rate.
unsigned _fragments = 0; // Number of OSS buffer fragments.
unsigned _fragment_size = 0; // OSS buffer fragment size.
};

} // namespace sosso

#endif // SOSSO_DEVICE_HPP

+ 218
- 0
freebsd/sosso/DoubleBuffer.hpp View File

@@ -0,0 +1,218 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_DOUBLEBUFFER_HPP
#define SOSSO_DOUBLEBUFFER_HPP

#include "sosso/Buffer.hpp"
#include "sosso/Logging.hpp"
#include <algorithm>
#include <limits>

namespace sosso {

/*!
* \brief Double Buffering for Channel
*
* Manages double buffering on top of a ReadChannel or WriteChannel. It takes
* two buffers with corresponding end positions. One of these is selected
* for processing, depending on the buffer and channel positions. The buffers
* can be overlapping or have gaps in between.
* A buffer is marked as finished when all buffer data was processed and the
* channel progress has reached the buffer end. This provides steady buffer
* replacement times, synchronized with channel progress.
* The wakeup times for processing are adapted to available channel data and
* work pending (unprocessed buffer data).
*/
template <class Channel> class DoubleBuffer : public Channel {
/*!
* \brief Store a buffer and its end position.
*
* The end position of the buffer corresponds to channel progress in frames.
* Marking the end position allows to map the buffer content to the matching
* channel data, independent of read and write positions.
*/
struct BufferRecord {
Buffer buffer; // External buffer, may be empty.
std::int64_t end_frames = 0; // Buffer end position in frames.
};

public:
//! Indicate that buffer is ready for processing.
bool ready() const { return _buffer_a.buffer.valid(); }

/*!
* \brief Set the next consecutive buffer to be processed.
* \param buffer External buffer ready for processing.
* \param end_frames End position of the buffer in frames.
* \return True if successful, false means there are already two buffers.
*/
bool set_buffer(Buffer &&buffer, std::int64_t end_frames) {
// Set secondary buffer if available.
if (!_buffer_b.buffer.valid()) {
_buffer_b.buffer = std::move(buffer);
_buffer_b.end_frames = end_frames;
// Promote secondary buffer to primary if primary is not set.
if (!_buffer_a.buffer.valid()) {
std::swap(_buffer_b, _buffer_a);
}
return ready();
}
return false;
}

/*!
* \brief Reset the buffer end positions in case of over- and underruns.
* \param end_frames New end position of the primary buffer.
* \return True if ready to proceed.
*/
bool reset_buffers(std::int64_t end_frames) {
// Reset primary buffer.
if (_buffer_a.buffer.valid()) {
std::memset(_buffer_a.buffer.data(), 0, _buffer_a.buffer.length());
_buffer_a.buffer.reset();
Log::info(SOSSO_LOC, "Primary buffer reset from %lld to %lld.",
_buffer_a.end_frames, end_frames);
_buffer_a.end_frames = end_frames;
}
// Reset secondary buffer.
if (_buffer_b.buffer.valid()) {
std::memset(_buffer_b.buffer.data(), 0, _buffer_b.buffer.length());
_buffer_b.buffer.reset();
end_frames += _buffer_b.buffer.length() / Channel::frame_size();
Log::info(SOSSO_LOC, "Secondary buffer reset from %lld to %lld.",
_buffer_b.end_frames, end_frames);
_buffer_b.end_frames = end_frames;
}
return ready();
}

//! Retrieve the primary buffer, may be empty.
Buffer &&take_buffer() {
std::swap(_buffer_a, _buffer_b);
return std::move(_buffer_b.buffer);
}

/*!
* \brief Process channel with given buffers to read or write.
* \param now Time offset from channel start in frames, see FrameClock.
* \return True if there were no processing errors.
*/
bool process(std::int64_t now) {
// Round frame time down to steppings, ignore timing jitter.
now = now - now % Channel::stepping();
// Always process primary buffer, No-Op if already done.
bool ok = Channel::process(_buffer_a.buffer, _buffer_a.end_frames, now);
// Process secondary buffer when primary is done.
if (ok && _buffer_a.buffer.done() && _buffer_b.buffer.valid()) {
ok = Channel::process(_buffer_b.buffer, _buffer_b.end_frames, now);
}
return ok;
}

//! End position of the primary buffer.
std::int64_t end_frames() const {
if (ready()) {
return _buffer_a.end_frames;
}
return 0;
}

//! Expected frame time when primary buffer is finished.
std::int64_t period_end() const {
if (ready()) {
return end_frames() + Channel::balance();
}
return 0;
}

//! Expected frame time when both buffers are finished.
std::int64_t total_end() const {
if (ready()) {
if (_buffer_b.buffer.valid()) {
return _buffer_b.end_frames + Channel::balance();
}
return end_frames() + Channel::balance();
}
return 0;
}

/*!
* \brief Calculate next wakeup time for processing.
* \param now Current frame time as offset from channel start, see FrameClock.
* \return Next suggested wakeup in frame time.
*/
std::int64_t wakeup_time(std::int64_t now) const {
// No need to wake up if channel is not running.
if (!Channel::is_open()) {
return std::numeric_limits<std::int64_t>::max();
}
// Wakeup immediately if there's more work to do now.
if (Channel::oss_available() > 0 &&
(!_buffer_a.buffer.done() || !_buffer_b.buffer.done())) {
Log::log(SOSSO_LOC, "Immediate wakeup at %lld for more work.", now);
return now;
}
// Get upcoming buffer end and compute next channel wakeup time.
std::int64_t sync_frames = now;
if (_buffer_a.buffer.valid() && !finished(now)) {
sync_frames = period_end();
} else if (_buffer_b.buffer.valid() && !total_finished(now)) {
sync_frames = _buffer_b.end_frames + Channel::balance();
} else {
sync_frames = std::numeric_limits<std::int64_t>::max();
}
return Channel::wakeup_time(sync_frames);
}

//! Indicate progress on processing the primary buffer, in frames.
std::int64_t buffer_progress() const {
return _buffer_a.buffer.progress() / Channel::frame_size();
}

//! Indicate that primary buffer is finished at current frame time.
bool finished(std::int64_t now) const {
return period_end() <= now && _buffer_a.buffer.done();
}

//! Indicate that both buffers are finished at current frame time.
bool total_finished(std::int64_t now) const {
return total_end() <= now && _buffer_a.buffer.done() &&
_buffer_b.buffer.done();
}

//! Print channel state as user information, at current frame time.
void log_state(std::int64_t now) const {
const char *direction = Channel::playback() ? "Out" : "In";
const char *sync = (Channel::last_sync() == now) ? "sync" : "frame";
std::int64_t buf_a = _buffer_a.buffer.progress() / Channel::frame_size();
std::int64_t buf_b = _buffer_b.buffer.progress() / Channel::frame_size();
Log::log(SOSSO_LOC,
"%s %s, %lld bal %lld, buf A %lld B %lld OSS %lld, %lld left, "
"req %u min %lld",
direction, sync, now, Channel::balance(), buf_a, buf_b,
Channel::oss_available(), period_end() - now,
Channel::sync_level(), Channel::min_progress());
}

private:
BufferRecord _buffer_a; // Primary buffer, may be empty.
BufferRecord _buffer_b; // Secondary buffer, may be empty.
};

} // namespace sosso

#endif // SOSSO_DOUBLEBUFFER_HPP

+ 141
- 0
freebsd/sosso/FrameClock.hpp View File

@@ -0,0 +1,141 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_FRAMECLOCK_HPP
#define SOSSO_FRAMECLOCK_HPP

#include "sosso/Logging.hpp"
#include <sys/errno.h>
#include <time.h>

namespace sosso {

/*!
* \brief Clock using audio frames as time unit.
*
* Provides time as an offset from an initial time zero, usually when the audio
* device was started. Instead of nanoseconds it measures time in frames
* (samples per channel), and thus needs to know the sample rate.
* It also lets a thread sleep until a specified wakeup time, again in frames.
*/
class FrameClock {
public:
/*!
* \brief Initialize the clock, set time zero.
* \param sample_rate Sample rate in Hz, for time to frame conversion.
* \return True if successful, false means an error occurred.
*/
bool init_clock(unsigned sample_rate) {
return set_sample_rate(sample_rate) && init_zero_time();
}

/*!
* \brief Get current frame time.
* \param result Set to current frame time, as offset from time zero.
* \return True if successful, false means an error occurred.
*/
bool now(std::int64_t &result) const {
std::int64_t time_ns = 0;
if (get_time_offset(time_ns)) {
result = time_to_frames(time_ns);
return true;
}
return false;
}

/*!
* \brief Let the thread sleep until wakeup time.
* \param wakeup_frame Wakeup time in frames since time zero.
* \return True if successful, false means an error occurred.
*/
bool sleep(std::int64_t wakeup_frame) const {
std::int64_t time_ns = frames_to_time(wakeup_frame);
return sleep_until(time_ns);
}

//! Convert frames to time in nanoseconds.
std::int64_t frames_to_time(std::int64_t frames) const {
return (frames * 1000000000) / _sample_rate;
}

//! Convert time in nanoseconds to frames.
std::int64_t time_to_frames(std::int64_t time_ns) const {
return (time_ns * _sample_rate) / 1000000000;
}

//! Convert frames to system clock time in microseconds.
std::int64_t frames_to_absolute_us(std::int64_t frames) const {
return _zero.tv_sec * 1000000ULL + _zero.tv_nsec / 1000 +
frames_to_time(frames) / 1000;
}

//! Currently used sample rate in Hz.
unsigned sample_rate() const { return _sample_rate; }

//! Set the sample rate in Hz, used for time to frame conversion.
bool set_sample_rate(unsigned sample_rate) {
if (sample_rate > 0) {
_sample_rate = sample_rate;
return true;
}
return false;
}

//! Suggested minimal wakeup step in frames.
unsigned stepping() const { return 16U * (1U + (_sample_rate / 50000)); }

private:
// Initialize time zero now.
bool init_zero_time() { return gettime(_zero); }

// Get current time in nanoseconds, as offset from time zero.
bool get_time_offset(std::int64_t &result) const {
timespec now;
if (gettime(now)) {
result = ((now.tv_sec - _zero.tv_sec) * 1000000000) + now.tv_nsec -
_zero.tv_nsec;
return true;
}
return false;
}

// Let thread sleep until wakeup time, in nanoseconds since time zero.
bool sleep_until(std::int64_t offset_ns) const {
timespec wakeup = {_zero.tv_sec + (_zero.tv_nsec + offset_ns) / 1000000000,
(_zero.tv_nsec + offset_ns) % 1000000000};
if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &wakeup, NULL) != 0) {
Log::warn(SOSSO_LOC, "Sleep failed with error %d.", errno);
return false;
}
return true;
}

// Get current time in nanosecons, as a timespec struct.
bool gettime(timespec &result) const {
if (clock_gettime(CLOCK_MONOTONIC, &result) != 0) {
Log::warn(SOSSO_LOC, "Get time failed with error %d.", errno);
return false;
}
return true;
}

timespec _zero = {0, 0}; // Time zero as a timespec struct.
unsigned _sample_rate = 48000; // Sample rate used for frame conversion.
};

} // namespace sosso

#endif // SOSSO_FRAMECLOCK_HPP

+ 105
- 0
freebsd/sosso/Logging.hpp View File

@@ -0,0 +1,105 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_LOGGING_HPP
#define SOSSO_LOGGING_HPP

#include <cstdint>
#include <cstdio>

namespace sosso {

/*!
* \brief Store the source location for logging.
*
* Keep its implementation close to C++20 std::source_location.
* It will be replaced by that when C++20 is widely available.
*/
struct SourceLocation {
//! Get the line number in the source file.
std::uint_least32_t line() const { return _line; }
//! Get the column in the source file, not implemented.
std::uint_least32_t column() const { return _column; }
//! Get the file name of the source file.
const char *file_name() const { return _file_name; }
//! Get the function context in the source file.
const char *function_name() const { return _function_name; }

std::uint_least32_t _line;
std::uint_least32_t _column;
const char *_file_name;
const char *_function_name;
};

/// Capture source location in place of this macro.
#define SOSSO_LOC \
SourceLocation { __LINE__, 0, __FILE__, __func__ }

/*!
* \brief Static logging functions.
*
* There are three log levels:
* - warn() indicates warnings and errors.
* - info() provides general information to the user.
* - log() is for low-level information and debugging purposes.
*
* The single message static logging functions have to be implemented in the
* application, so they output to the appropriate places. Otherwise there will
* be a linking error at build time. To give some context for debugging, the
* source location is given.
*
* For printf-style message composition use the corresponding variable argument
* function templates, limited to 255 character length.
*/
class Log {
public:
//! Single message low-level log, implement this in the application.
static void log(SourceLocation location, const char *message);

//! Compose printf-style low-level log messages.
template <typename... Args>
static void log(SourceLocation location, const char *message, Args... args) {
char formatted[256];
std::snprintf(formatted, 256, message, args...);
log(location, formatted);
}

//! Single message user information, implement this in the application.
static void info(SourceLocation location, const char *message);

//! Compose printf-style user information messages.
template <typename... Args>
static void info(SourceLocation location, const char *message, Args... args) {
char formatted[256];
std::snprintf(formatted, 256, message, args...);
info(location, formatted);
}

//! Single message warning, implement this in the application.
static void warn(SourceLocation location, const char *message);

//! Compose printf-style warning messages.
template <typename... Args>
static void warn(SourceLocation location, const char *message, Args... args) {
char formatted[256];
std::snprintf(formatted, 256, message, args...);
warn(location, formatted);
}
};

} // namespace sosso

#endif // SOSSO_LOGGING_HPP

+ 255
- 0
freebsd/sosso/ReadChannel.hpp View File

@@ -0,0 +1,255 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_READCHANNEL_HPP
#define SOSSO_READCHANNEL_HPP

#include "sosso/Buffer.hpp"
#include "sosso/Channel.hpp"
#include "sosso/Logging.hpp"
#include <fcntl.h>

namespace sosso {

/*!
* \brief Recording Channel
*
* Specializes the generic Channel class into a recording channel. It keeps
* track of the OSS recording progress, and reads the available audio data to an
* external buffer. If the OSS buffer is memory mapped, the audio data is copied
* from there. Otherwise I/O read() system calls are used.
*/
class ReadChannel : public Channel {
public:
/*!
* \brief Open a device for recording.
* \param device Path to the device, e.g. "/dev/dsp1".
* \param exclusive Try to get exclusive access to the device.
* \return True if the device was opened successfully.
*/
bool open(const char *device, bool exclusive = true) {
int mode = O_RDONLY | O_NONBLOCK;
if (exclusive) {
mode |= O_EXCL;
}
return Channel::open(device, mode);
}

//! Available audio data to be read, in frames.
std::int64_t oss_available() const {
std::int64_t result = last_progress() - _read_position;
if (result < 0) {
result = 0;
} else if (result > buffer_frames()) {
result = buffer_frames();
}
return result;
}

/*!
* \brief Calculate next wakeup time.
* \param sync_frames Required sync event (e.g. buffer end), in frame time.
* \return Suggested and safe wakeup time for next process(), in frame time.
*/
std::int64_t wakeup_time(std::int64_t sync_frames) const {
return Channel::wakeup_time(sync_frames, oss_available());
}

/*!
* \brief Check OSS progress and read recorded audio to the buffer.
* \param buffer Buffer to write to, untouched if invalid.
* \param end Buffer end position, matching channel progress.
* \param now Current time in frame time, see FrameClock.
* \return True if successful, false means there was an error.
*/
bool process(Buffer &buffer, std::int64_t end, std::int64_t now) {
if (map()) {
return (progress_done(now) || check_map_progress(now)) &&
(buffer_done(buffer, end) || process_mapped(buffer, end, now));
} else {
return (progress_done(now) || check_read_progress(now)) &&
(buffer_done(buffer, end) || process_read(buffer, end, now));
}
}

protected:
// Indicate that OSS progress has already been checked.
bool progress_done(std::int64_t now) { return (last_processing() == now); }

// Check OSS progress in case of memory mapped buffer.
bool check_map_progress(std::int64_t now) {
// Get OSS progress through map pointer.
if (get_rec_pointer()) {
std::int64_t progress = map_progress() - _oss_progress;
_oss_progress += progress;
std::int64_t available = last_progress() + progress - _read_position;
std::int64_t loss = mark_loss(available - buffer_frames());
mark_progress(progress, now);
if (loss > 0) {
Log::warn(SOSSO_LOC, "OSS recording buffer overrun, %lld lost.", loss);
_read_position = last_progress() - buffer_frames();
}
}
return progress_done(now);
}

// Read recorded audio data to buffer, in case of memory mapped OSS buffer.
bool process_mapped(Buffer &buffer, std::int64_t end, std::int64_t now) {
// Calculate current read buffer position.
std::int64_t position = buffer_position(buffer, end);
// Only read what is available until OSS captured its complete buffer.
std::int64_t oldest = last_progress() - buffer_frames();
if (_oss_progress < buffer_frames()) {
oldest = last_progress() - _oss_progress;
}
if (std::int64_t skip = buffer_advance(buffer, oldest - position)) {
// First part of the read buffer already passed, fill it up.
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer late by %lld, skip %lld.",
now, end, oldest - position, skip);
position += skip;
} else if (position != _read_position) {
// Position mismatch, reread what is available.
if (std::int64_t rewind = buffer_rewind(buffer, position - oldest)) {
Log::info(SOSSO_LOC,
"@%lld - %lld Read position mismatch, reread %lld.", now, end,
rewind);
position -= rewind;
}
}
if (position >= oldest && position < last_progress() && !buffer.done()) {
// Read from offset up to current position, if read buffer can hold it.
std::int64_t offset = last_progress() - position;
std::size_t length = buffer.remaining(offset * frame_size());
unsigned pointer = (_oss_progress - offset) % buffer_frames();
length = read_map(buffer.position(), pointer * frame_size(), length);
buffer.advance(length);
_read_position = buffer_position(buffer, end);
}
_read_position += freewheel_finish(buffer, end, now);
return true;
}

// Check progress when using I/O read() system call.
bool check_read_progress(std::int64_t now) {
// Check for OSS buffer overruns.
std::int64_t overdue = now - estimated_dropout(oss_available());
if ((overdue > 0 && get_rec_overruns() > 0) || overdue > max_progress()) {
std::int64_t progress = buffer_frames() - oss_available();
std::int64_t loss = mark_loss(progress, now);
Log::warn(SOSSO_LOC, "OSS recording buffer overrun, %lld lost.", loss);
mark_progress(progress + loss, now);
_read_position = last_progress() - buffer_frames();
} else {
// Infer progress from OSS queue changes.
std::int64_t queued = queued_samples();
std::int64_t progress = queued - (last_progress() - _read_position);
mark_progress(progress, now);
_read_position = last_progress() - queued;
}
return progress_done(now);
}

// Read recorded audio data to buffer, using I/O read() syscall.
bool process_read(Buffer &buffer, std::int64_t end, std::int64_t now) {
bool ok = true;
std::int64_t position = buffer_position(buffer, end);
if (std::int64_t skip = buffer_advance(buffer, _read_position - position)) {
// Overlapping buffers, skip the overlapping part.
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer overlap %lld, skip %lld.",
now, end, _read_position - position, skip);
position += skip;
} else if (std::int64_t rewind =
buffer_rewind(buffer, position - _read_position)) {
// Gap between reads, try to rewind to last read position.
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer gap %lld, rewind %lld.",
now, end, position - _read_position, rewind);
position -= rewind;
}
if (oss_available() == 0) {
// OSS buffer is empty, nothing to do.
} else if (position > _read_position) {
// Read and omit data of remaining gap, drain OSS buffer.
std::int64_t gap = position - _read_position;
std::size_t read_limit = buffer.remaining(gap * frame_size());
std::size_t bytes_read = 0;
ok = read_io(buffer.position(), read_limit, bytes_read);
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer gap %lld, drain %lu.", now,
end, gap, bytes_read / frame_size());
_read_position += bytes_read / frame_size();
} else if (position == _read_position) {
// Read as much as currently available.
std::size_t bytes_read = 0;
ok = read_io(buffer.position(), buffer.remaining(), bytes_read);
_read_position += bytes_read / frame_size();
buffer.advance(bytes_read);
}
freewheel_finish(buffer, end, now);
return ok;
}

private:
// Calculate read position of the remaining buffer.
std::int64_t buffer_position(const Buffer &buffer, std::int64_t end) const {
return end - extra_latency() - (buffer.remaining() / frame_size());
}

// Indicate that a buffer doesn't need further processing.
bool buffer_done(const Buffer &buffer, std::int64_t end) const {
return buffer.done() && buffer_position(buffer, end) <= _read_position;
}

// Extra latency to always finish on time, regardless of OSS progress steps.
std::int64_t extra_latency() const { return max_progress(); }

// Avoid stalled buffers with irregular OSS progress in freewheel mode.
std::int64_t freewheel_finish(Buffer &buffer, std::int64_t end,
std::int64_t now) {
std::int64_t advance = 0;
if (freewheel() && now >= end + balance() && !buffer.done()) {
// Buffer is overdue in freewheel sync mode, finish immediately.
std::memset(buffer.position(), 0, buffer.remaining());
advance = buffer.advance(buffer.remaining()) / frame_size();
Log::info(SOSSO_LOC, "@%lld - %lld Read buffer overdue, fill by %lu.",
now, end, advance);
}
return advance;
}

// Skip reading part of the buffer to match OSS read position.
std::int64_t buffer_advance(Buffer &buffer, std::int64_t frames) {
if (frames > 0) {
std::size_t skip = buffer.remaining(frames * frame_size());
std::memset(buffer.position(), 0, skip);
return buffer.advance(skip) / frame_size();
}
return 0;
}

// Rewind part of the buffer to match OSS read position.
std::int64_t buffer_rewind(Buffer &buffer, std::int64_t frames) {
if (frames > 0) {
return buffer.rewind(frames * frame_size()) / frame_size();
}
return 0;
}

std::int64_t _oss_progress = 0; // Last memory mapped OSS progress.
std::int64_t _read_position = 0; // Current read position of channel.
};

} // namespace sosso

#endif // SOSSO_READCHANNEL_HPP

+ 280
- 0
freebsd/sosso/WriteChannel.hpp View File

@@ -0,0 +1,280 @@
/*
* Copyright (c) 2023 Florian Walpen <dev@submerge.ch>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#ifndef SOSSO_WRITECHANNEL_HPP
#define SOSSO_WRITECHANNEL_HPP

#include "sosso/Buffer.hpp"
#include "sosso/Channel.hpp"
#include "sosso/Logging.hpp"
#include <fcntl.h>

namespace sosso {

/*!
* \brief Playback Channel
*
* Specializes the generic Channel class into a playback channel. It keeps track
* of the OSS playback progress, and writes audio data from an external buffer
* to the available OSS buffer. If the OSS buffer is memory mapped, the audio
* data is copied there. Otherwise I/O write() system calls are used.
*/
class WriteChannel : public Channel {
public:
/*!
* \brief Open a device for playback.
* \param device Path to the device, e.g. "/dev/dsp1".
* \param exclusive Try to get exclusive access to the device.
* \return True if the device was opened successfully.
*/
bool open(const char *device, bool exclusive = true) {
int mode = O_WRONLY | O_NONBLOCK;
if (exclusive) {
mode |= O_EXCL;
}
return Channel::open(device, mode);
}

//! Available OSS buffer space for writing, in frames.
std::int64_t oss_available() const {
std::int64_t result = last_progress() + buffer_frames() - _write_position;
if (result < 0) {
result = 0;
} else if (result > buffer_frames()) {
result = buffer_frames();
}
return result;
}

/*!
* \brief Calculate next wakeup time.
* \param sync_frames Required sync event (e.g. buffer end), in frame time.
* \return Suggested and safe wakeup time for next process(), in frame time.
*/
std::int64_t wakeup_time(std::int64_t sync_frames) const {
return Channel::wakeup_time(sync_frames, oss_available());
}

/*!
* \brief Check OSS progress and write playback audio to the OSS buffer.
* \param buffer Buffer of playback audio data, untouched if invalid.
* \param end Buffer end position, matching channel progress.
* \param now Current time in frame time, see FrameClock.
* \return True if successful, false means there was an error.
*/
bool process(Buffer &buffer, std::int64_t end, std::int64_t now) {
if (map()) {
return (progress_done(now) || check_map_progress(now)) &&
(buffer_done(buffer, end) || process_mapped(buffer, end, now));
} else {
return (progress_done(now) || check_write_progress(now)) &&
(buffer_done(buffer, end) || process_write(buffer, end, now));
}
}

protected:
// Indicate that OSS progress has already been checked.
bool progress_done(std::int64_t now) { return (last_processing() == now); }

// Check OSS progress in case of memory mapped buffer.
bool check_map_progress(std::int64_t now) {
// Get OSS progress through map pointer.
if (get_play_pointer()) {
std::int64_t progress = map_progress() - _oss_progress;
if (progress > 0) {
// Sometimes OSS playback starts with a bogus extra buffer cycle.
if (progress > buffer_frames() &&
now - last_processing() < buffer_frames() / 2) {
Log::warn(SOSSO_LOC,
"OSS playback bogus buffer cycle, %lld frames in %lld.",
progress, now - last_processing());
progress = progress % buffer_frames();
}
// Clear obsolete audio data in the buffer.
write_map(nullptr, (_oss_progress % buffer_frames()) * frame_size(),
progress * frame_size());
_oss_progress = map_progress();
}
std::int64_t loss =
mark_loss(last_progress() + progress - _write_position);
mark_progress(progress, now);
if (loss > 0) {
Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss);
_write_position = last_progress();
}
}
return progress_done(now);
}

// Write playback audio data to a memory mapped OSS buffer.
bool process_mapped(Buffer &buffer, std::int64_t end, std::int64_t now) {
// Buffer position should be between OSS progress and last write position.
std::int64_t position = buffer_position(buffer.remaining(), end);
if (std::int64_t skip =
buffer_advance(buffer, last_progress() - position)) {
// First part of the buffer already played, skip it.
Log::info(SOSSO_LOC, "@%lld - %lld Write %lld already played, skip %lld.",
now, end, last_progress() - position, skip);
position += skip;
} else if (position != _write_position) {
// Position mismatch, rewrite as much as possible.
if (std::int64_t rewind =
buffer_rewind(buffer, position - last_progress())) {
Log::info(SOSSO_LOC,
"@%lld - %lld Write position mismatch, rewrite %lld.", now,
end, rewind);
position -= rewind;
}
}
// The writable window is the whole buffer, starting from OSS progress.
if (!buffer.done() && position >= last_progress() &&
position < last_progress() + buffer_frames()) {
if (_write_position < position && _write_position + 8 >= position) {
// Small remaining gap between writes, fill in a replay patch.
std::int64_t offset = _write_position - last_progress();
unsigned pointer = (_oss_progress + offset) % buffer_frames();
std::size_t length = (position - _write_position) * frame_size();
length = buffer.remaining(length);
std::size_t written =
write_map(buffer.position(), pointer * frame_size(), length);
Log::info(SOSSO_LOC, "@%lld - %lld Write small gap %lld, replay %lld.",
now, end, position - _write_position, written / frame_size());
}
// Write from buffer offset up to either OSS or write buffer end.
std::int64_t offset = position - last_progress();
unsigned pointer = (_oss_progress + offset) % buffer_frames();
std::size_t length = (buffer_frames() - offset) * frame_size();
length = buffer.remaining(length);
std::size_t written =
write_map(buffer.position(), pointer * frame_size(), length);
buffer.advance(written);
_write_position = buffer_position(buffer.remaining(), end);
}
_write_position += freewheel_finish(buffer, end, now);
return true;
}

// Check progress when using I/O write() system call.
bool check_write_progress(std::int64_t now) {
// Check for OSS buffer underruns.
std::int64_t overdue = now - estimated_dropout(oss_available());
if ((overdue > 0 && get_play_underruns() > 0) || overdue > max_progress()) {
// OSS buffer underrun, estimate loss and progress from time.
std::int64_t progress = _write_position - last_progress();
std::int64_t loss = mark_loss(progress, now);
Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss);
mark_progress(progress + loss, now);
_write_position = last_progress();
} else {
// Infer progress from OSS queue changes.
std::int64_t queued = queued_samples();
std::int64_t progress = (_write_position - last_progress()) - queued;
mark_progress(progress, now);
_write_position = last_progress() + queued;
}
return progress_done(now);
}

// Write playback audio data to OSS buffer using I/O write() system call.
bool process_write(Buffer &buffer, std::int64_t end, std::int64_t now) {
bool ok = true;
// Adjust buffer position to OSS write position, if possible.
std::int64_t position = buffer_position(buffer.remaining(), end);
if (std::int64_t rewind =
buffer_rewind(buffer, position - _write_position)) {
// Gap between buffers, replay parts to fill it up.
Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, replay %lld.",
now, end, position - _write_position, rewind);
position -= rewind;
} else if (std::int64_t skip =
buffer_advance(buffer, _write_position - position)) {
// Overlapping buffers, skip the overlapping part.
Log::info(SOSSO_LOC, "@%lld - %lld Write buffer overlap %lld, skip %lld.",
now, end, _write_position - position, skip);
position += skip;
}
if (oss_available() == 0) {
// OSS buffer is full, nothing to do.
} else if (position > _write_position) {
// Replay to fill remaining gap, limit the write to just fill the gap.
std::int64_t gap = position - _write_position;
std::size_t write_limit = buffer.remaining(gap * frame_size());
std::size_t bytes_written = 0;
ok = write_io(buffer.position(), write_limit, bytes_written);
Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, fill %lld.",
now, end, gap, bytes_written / frame_size());
_write_position += bytes_written / frame_size();
} else if (position == _write_position) {
// Write as much as currently possible.
std::size_t write_limit = buffer.remaining();
std::size_t bytes_written = 0;
ok = write_io(buffer.position(), write_limit, bytes_written);
_write_position += bytes_written / frame_size();
buffer.advance(bytes_written);
}
// Make sure buffers finish in time, despite irregular progress (freewheel).
freewheel_finish(buffer, end, now);
return ok;
}

private:
// Calculate write position of the remaining buffer.
std::int64_t buffer_position(std::size_t remaining, std::int64_t end) const {
return end - (remaining / frame_size());
}

// Indicate that a buffer doesn't need further processing.
bool buffer_done(const Buffer &buffer, std::int64_t end) const {
return buffer.done() && end <= _write_position;
}

// Avoid stalled buffers with irregular OSS progress in freewheel mode.
std::int64_t freewheel_finish(Buffer &buffer, std::int64_t end,
std::int64_t now) {
std::int64_t advance = 0;
// Make sure buffers finish in time, despite irregular progress (freewheel).
if (freewheel() && now >= end + balance() && !buffer.done()) {
advance = buffer.advance(buffer.remaining()) / frame_size();
Log::info(SOSSO_LOC,
"@%lld - %lld Write freewheel finish remaining buffer %lld.",
now, end, advance);
}
return advance;
}

// Skip writing part of the buffer to match OSS write position.
std::int64_t buffer_advance(Buffer &buffer, std::int64_t frames) {
if (frames > 0) {
return buffer.advance(frames * frame_size()) / frame_size();
}
return 0;
}

// Rewind part of the buffer to match OSS write postion.
std::int64_t buffer_rewind(Buffer &buffer, std::int64_t frames) {
if (frames > 0) {
return buffer.rewind(frames * frame_size()) / frame_size();
}
return 0;
}

std::int64_t _oss_progress = 0; // Last memory mapped OSS progress.
std::int64_t _write_position = 0; // Current write position of the channel.
};

} // namespace sosso

#endif // SOSSO_WRITECHANNEL_HPP

+ 1
- 0
wscript View File

@@ -695,6 +695,7 @@ def build_drivers(bld):

freebsd_oss_src = [
'common/memops.c',
'freebsd/oss/JackOSSChannel.cpp',
'freebsd/oss/JackOSSDriver.cpp'
]



Loading…
Cancel
Save