Skip to content

Commit

Permalink
FreeBSD: Add synced OSS operation (sosso) headers.
Browse files Browse the repository at this point in the history
As a header-only C++ library, synced OSS operation (sosso) is used to
clean up and separate the low-level handling of FreeBSD OSS devices
in JACK. Features include:

 * Supports both read() / write() and mmap() IO operation.
 * Adaptive polling, better suited for low-latency requirements.
 * Internal double buffer to avoid troubles with OSS buffer resize.
 * Closely monitors progress and drift for each channel (+/- 1ms).
 * Facilitates drift correction through buffer offsets.
 * Depends on C++ standard library and system headers, nothing else.

Although the sosso library is somewhat tailored to the needs of JACK,
it was developed separately and will eventually be published and / or
used in other projects. Therefore the headers follow a different
formatting style and are more liberally licensed (ISC).
  • Loading branch information
0EVSG committed Jul 5, 2023
1 parent b93a1d8 commit 2d31051
Show file tree
Hide file tree
Showing 9 changed files with 2,152 additions and 0 deletions.
153 changes: 153 additions & 0 deletions freebsd/sosso/Buffer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright (c) 2023 Florian Walpen <[email protected]>
*
* 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 changes: 210 additions & 0 deletions freebsd/sosso/Channel.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright (c) 2023 Florian Walpen <[email protected]>
*
* 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
Loading

0 comments on commit 2d31051

Please sign in to comment.