-
Notifications
You must be signed in to change notification settings - Fork 374
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FreeBSD: Add synced OSS operation (sosso) headers.
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
Showing
9 changed files
with
2,152 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.