diff --git a/freebsd/sosso/Buffer.hpp b/freebsd/sosso/Buffer.hpp new file mode 100644 index 000000000..1c9e7bbf1 --- /dev/null +++ b/freebsd/sosso/Buffer.hpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_BUFFER_HPP +#define SOSSO_BUFFER_HPP + +#include + +namespace sosso { + +/*! + * \brief Buffer Management + * + * Provides means to access and manipulate externally allocated buffer memory. + * It stores a memory pointer, length and a read / write position. The buffer + * memory can be passed from one Buffer instance to another, through move + * constructor and move assignment. This prevents multiple Buffer instances from + * referencing the same memory. + */ +class Buffer { +public: + //! Construct an empty and invalid Buffer. + Buffer() = default; + + /*! + * \brief Construct Buffer operating on given memory. + * \param buffer Pointer to the externally allocated memory. + * \param length Length of the memory dedicated to this Buffer. + */ + Buffer(char *buffer, std::size_t length) + : _data(buffer), _length(length), _position(0) {} + + /*! + * \brief Move construct a buffer. + * \param other Adopt memory from this Buffer, leaving it empty. + */ + Buffer(Buffer &&other) noexcept + : _data(other._data), _length(other._length), _position(other._position) { + other._data = nullptr; + other._position = 0; + other._length = 0; + } + + /*! + * \brief Move assign memory from another Buffer. + * \param other Adopt memory from this Buffer, leaving it empty. + * \return This newly assigned Buffer. + */ + Buffer &operator=(Buffer &&other) { + _data = other._data; + _position = other._position; + _length = other._length; + other._data = nullptr; + other._position = 0; + other._length = 0; + return *this; + } + + //! Buffer is valid if the memory is accessable. + bool valid() const { return (_data != nullptr) && (_length > 0); } + + //! Access the underlying memory, null if invalid. + char *data() const { return _data; } + + //! Length of the underlying memory in bytes, 0 if invalid. + std::size_t length() const { return _length; } + + //! Access buffer memory at read / write position. + char *position() const { return _data + _position; } + + //! Get read / write progress from buffer start, in bytes. + std::size_t progress() const { return _position; } + + //! Remaining buffer memory in bytes. + std::size_t remaining() const { return _length - _position; } + + /*! + * \brief Cap given progress by remaining buffer memory. + * \param progress Progress in bytes. + * \return Progress limited by the remaining buffer memory. + */ + std::size_t remaining(std::size_t progress) const { + if (progress > remaining()) { + progress = remaining(); + } + return progress; + } + + //! Indicate that the buffer is fully processed. + bool done() const { return _position == _length; } + + //! Advance the buffer read / write position. + std::size_t advance(std::size_t progress) { + progress = remaining(progress); + _position += progress; + return progress; + } + + //! Rewind the buffer read / write position. + std::size_t rewind(std::size_t progress) { + if (progress > _position) { + progress = _position; + } + _position -= progress; + return progress; + } + + /*! + * \brief Erase an already processed part, rewind. + * \param begin Start position of the region to be erased. + * \param end End position of the region to be erased. + * \return The number of bytes that were effectively erased. + */ + std::size_t erase(std::size_t begin, std::size_t end) { + if (begin < _position && begin < end) { + if (end > _position) { + end = _position; + } + std::size_t copy = _position - end; + if (copy > 0) { + std::memmove(_data + begin, _data + end, copy); + } + _position -= (end - begin); + return (end - begin); + } + return 0; + } + + //! Reset the buffer position to zero. + void reset() { _position = 0; } + +private: + char *_data = nullptr; // External buffer memory, null if invalid. + std::size_t _length = 0; // Total length of the buffer memory. + std::size_t _position = 0; // Current read / write position. +}; + +} // namespace sosso + +#endif // SOSSO_BUFFER_HPP diff --git a/freebsd/sosso/Channel.hpp b/freebsd/sosso/Channel.hpp new file mode 100644 index 000000000..2b29fe33a --- /dev/null +++ b/freebsd/sosso/Channel.hpp @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_CHANNEL_HPP +#define SOSSO_CHANNEL_HPP + +#include "sosso/Device.hpp" +#include + +namespace sosso { + +/*! + * \brief Audio Channel of a Device + * + * As a base class for read and write channels, this class provides generic + * handling of progress, loss and wakeup times. Progress here means the OSS + * device captures or consumes audio data, in frames. When progress is detected + * within a short wakeup interval, this counts as a sync where we can exactly + * match the device progress to current time. + * The balance indicates the drift between device progress and external time, + * usually taken from FrameClock. + * At device start and after loss, device progress can be irregular and is + * temporarily decoupled from Channel progress (freewheel). Sync events are + * required to change into normal mode which strictly follows device progress. + */ +class Channel : public Device { +public: + /*! + * \brief Open the device, initialize Channel + * \param device Full device path. + * \param mode Open mode (read / write). + * \return True if successful. + */ + bool open(const char *device, int mode) { + // Reset all internal statistics from last run. + _last_processing = 0; + _last_sync = 0; + _last_progress = 0; + _balance = 0; + _min_progress = 0; + _max_progress = 0; + _total_loss = 0; + _sync_level = 8; + return Device::open(device, mode); + } + + //! Total progress of the device since start. + std::int64_t last_progress() const { return _last_progress; } + + //! Balance (drift) compared to external time. + std::int64_t balance() const { return _balance; } + + //! Last time there was a successful sync. + std::int64_t last_sync() const { return _last_sync; } + + //! Last time the Channel was processed (mark_progress()). + std::int64_t last_processing() const { return _last_processing; } + + //! Maximum progress step encountered. + std::int64_t max_progress() const { return _max_progress; } + + //! Minimum progress step encountered. + std::int64_t min_progress() const { return _min_progress; } + + //! Current number of syncs required to change to normal mode. + unsigned sync_level() const { return _sync_level; } + + //! Indicate Channel progress decoupled from device progress. + bool freewheel() const { return _sync_level > 4; } + + //! Indicate a full resync with small wakeup steps is required. + bool full_resync() const { return _sync_level > 2; } + + //! Indicate a resync is required. + bool resync() const { return _sync_level > 0; } + + //! Total number of frames lost due to over- or underruns. + std::int64_t total_loss() const { return _total_loss; } + + //! Next time a device progress could be expected. + std::int64_t next_min_progress() const { + return _last_progress + _min_progress + _balance; + } + + //! Calculate safe wakeup time to avoid over- or underruns. + std::int64_t safe_wakeup(std::int64_t oss_available) const { + return next_min_progress() + buffer_frames() - oss_available - + max_progress(); + } + + //! Estimate the time to expect over- or underruns. + std::int64_t estimated_dropout(std::int64_t oss_available) const { + return _last_progress + _balance + buffer_frames() - oss_available; + } + + /*! + * \brief Calculate next wakeup time. + * \param sync_target External wakeup target like the next buffer end. + * \param oss_available Number of frames available in OSS buffer. + * \return Next wakeup time in external frame time. + */ + std::int64_t wakeup_time(std::int64_t sync_target, + std::int64_t oss_available) const { + // Use one sync step by default. + std::int64_t wakeup = _last_processing + Device::stepping(); + if (freewheel() || full_resync()) { + // Small steps when doing a full resync. + } else if (resync() || wakeup + max_progress() > sync_target) { + // Sync required, wake up prior to next progress if possible. + if (next_min_progress() > wakeup) { + wakeup = next_min_progress() - Device::stepping(); + } else if (next_min_progress() > _last_processing) { + wakeup = next_min_progress(); + } + } else { + // Sleep until prior to sync target, then sync again. + wakeup = sync_target - max_progress(); + } + // Make sure we wake up at sync target. + if (sync_target > _last_processing && sync_target < wakeup) { + wakeup = sync_target; + } + // Make sure we don't sleep into an OSS under- or overrun. + if (safe_wakeup(oss_available) < wakeup) { + wakeup = std::max(safe_wakeup(oss_available), + _last_processing + Device::stepping()); + } + return wakeup; + } + +protected: + // Account for progress detected, at current time. + void mark_progress(std::int64_t progress, std::int64_t now) { + if (progress > 0) { + if (freewheel()) { + // Some cards show irregular progress at the beginning, correct that. + // Also correct loss after under- and overruns, assume same balance. + _last_progress = now - progress - _balance; + // Require a sync before transition back to normal processing. + if (now <= _last_processing + stepping()) { + _sync_level -= 1; + } + } else if (now <= _last_processing + stepping()) { + // Successful sync on progress within small processing steps. + _balance = now - (_last_progress + progress); + _last_sync = now; + if (_sync_level > 0) { + _sync_level -= 1; + } + if (progress < _min_progress || _min_progress == 0) { + _min_progress = progress; + } + if (progress > _max_progress) { + _max_progress = progress; + } + } else { + // Big step with progress but no sync, requires a resync. + _sync_level += 1; + } + _last_progress += progress; + } + _last_processing = now; + } + + // Account for loss given progress and current time. + std::int64_t mark_loss(std::int64_t progress, std::int64_t now) { + // Estimate frames lost due to over- or underrun. + std::int64_t loss = (now - _balance) - (_last_progress + progress); + return mark_loss(loss); + } + + // Account for loss. + std::int64_t mark_loss(std::int64_t loss) { + if (loss > 0) { + _total_loss += loss; + // Resync OSS progress to frame time (now) to recover from loss. + _sync_level = std::max(_sync_level, 6U); + } else { + loss = 0; + } + return loss; + } + +private: + std::int64_t _last_processing = 0; // Last processing time. + std::int64_t _last_sync = 0; // Last sync time. + std::int64_t _last_progress = 0; // Total device progress. + std::int64_t _balance = 0; // Channel drift. + std::int64_t _min_progress = 0; // Minimum progress step encountered. + std::int64_t _max_progress = 0; // Maximum progress step encountered. + std::int64_t _total_loss = 0; // Total loss due to over- or underruns. + unsigned _sync_level = 0; // Syncs required. +}; + +} // namespace sosso + +#endif // SOSSO_CHANNEL_HPP diff --git a/freebsd/sosso/Correction.hpp b/freebsd/sosso/Correction.hpp new file mode 100644 index 000000000..2c3c304ab --- /dev/null +++ b/freebsd/sosso/Correction.hpp @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_CORRECTION_HPP +#define SOSSO_CORRECTION_HPP + +#include + +namespace sosso { + +/*! + * \brief Drift Correction + * + * Calculates drift correction for a channel, relative to another channel if + * required. Usually the playback channel is corrected relative to the recording + * channel, if in use. + * It keeps track of the correction parameter (in frames), and also the + * threshhold values which determine the amount of correction. Above these + * threshholds, either single frame correction is applied for smaller drift, + * or rigorous correction in case of large discrepance. The idea is that single + * frame corrections typically go unnoticed, but it may not be sufficient to + * correct something more grave like packet loss on a USB audio interface. + */ +class Correction { +public: + //! Default constructor, threshhold values are set separately. + Correction() = default; + + /*! + * \brief Set thresholds for small drift correction. + * \param drift_min Limit for negative drift balance. + * \param drift_max Limit for positive drift balance. + */ + void set_drift_limits(std::int64_t drift_min, std::int64_t drift_max) { + if (drift_min < drift_max) { + _drift_min = drift_min; + _drift_max = drift_max; + } else { + _drift_min = drift_max; + _drift_max = drift_min; + } + } + + /*! + * \brief Set thresholds for rigorous large discrepance correction. + * \param loss_min Limit for negative discrepance balance. + * \param loss_max Limit for positive discrepance balance. + */ + void set_loss_limits(std::int64_t loss_min, std::int64_t loss_max) { + if (loss_min < loss_max) { + _loss_min = loss_min; + _loss_max = loss_max; + } else { + _loss_min = loss_max; + _loss_max = loss_min; + } + } + + //! Get current correction parameter. + std::int64_t correction() const { return _correction; } + + /*! + * \brief Calculate a new correction parameter. + * \param balance Balance of the corrected channel, compared to FrameClock. + * \param target Balance of a master channel which acts as reference. + * \return Current correction parameter. + */ + std::int64_t correct(std::int64_t balance, std::int64_t target = 0) { + std::int64_t corrected_balance = balance - target + _correction; + if (corrected_balance > _loss_max) { + // Large positive discrepance, rigorous correction. + _correction -= corrected_balance - _loss_max; + } else if (corrected_balance < _loss_min) { + // Large negative discrepance, rigorous correction. + _correction += _loss_min - corrected_balance; + } else if (corrected_balance > _drift_max) { + // Small positive drift, correct by a single frame. + _correction -= 1; + } else if (corrected_balance < _drift_min) { + // Small negative drift, correct by a single frame. + _correction += 1; + } + return _correction; + } + + //! Clear the current correction parameter, but not the thresholds. + void clear() { _correction = 0; } + +private: + std::int64_t _loss_min = -128; // Negative threshold for rigorous correction. + std::int64_t _loss_max = 128; // Positive threshold for rigorous correction. + std::int64_t _drift_min = -64; // Negative threshold for drift correction. + std::int64_t _drift_max = 64; // Positive threshold for drift correction. + std::int64_t _correction = 0; // Correction parameter. +}; + +} // namespace sosso + +#endif // SOSSO_CORRECTION_HPP diff --git a/freebsd/sosso/Device.hpp b/freebsd/sosso/Device.hpp new file mode 100644 index 000000000..c815fefae --- /dev/null +++ b/freebsd/sosso/Device.hpp @@ -0,0 +1,678 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_DEVICE_HPP +#define SOSSO_DEVICE_HPP + +#include "sosso/Logging.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sosso { + +/*! + * \brief Manage OSS devices. + * + * Encapsulates all the low-level handling of a FreeBSD OSS pcm device. Due to + * restrictions of the OSS API, the device can be opened for either playback or + * recording, not both. For duplex operation, separate instances of Device have + * to be opened. + * By default a Device opens 2 channels of 32 bit samples at 48 kHz, but the + * OSS API will force that to be whatever is supported by the hardware. + * Different default parameters can be set via set_parameters() prior to opening + * the Device. Always check the effective parameters before any use. + */ +class Device { +public: + /*! + * \brief Translate OSS sample formats to sample size. + * \param format OSS sample format, see sys/soundcard.h header. + * \return Sample size in bytes, 0 if unsupported. + */ + static std::size_t bytes_per_sample(int format) { + switch (format) { + case AFMT_S16_LE: + case AFMT_S16_BE: + return 2; + case AFMT_S24_LE: + case AFMT_S24_BE: + return 3; + case AFMT_S32_LE: + case AFMT_S32_BE: + return 4; + default: + return 0; + } + } + + //! Always close device before destruction. + ~Device() { close(); } + + //! Effective OSS sample format, see sys/soundcard.h header. + int sample_format() const { return _sample_format; } + + //! Effective sample size in bytes. + std::size_t bytes_per_sample() const { + return bytes_per_sample(_sample_format); + } + + //! Indicate that the device is open. + bool is_open() const { return _fd >= 0; } + + //! Indicate that the device is opened in playback mode. + bool playback() const { return _fd >= 0 && (_file_mode & O_WRONLY); } + + //! Indicate that the device is opened in recording mode. + bool recording() const { return _fd >= 0 && !playback(); } + + //! Get the file descriptor of the device, -1 if not open. + int file_descriptor() const { return _fd; } + + //! Effective number of audio channels. + unsigned channels() const { return _channels; } + + //! Effective frame size, one sample for each channel. + std::size_t frame_size() const { return _channels * bytes_per_sample(); } + + //! Effective OSS buffer size in bytes. + std::size_t buffer_size() const { return _fragments * _fragment_size; } + + //! Effective OSS buffer size in frames, samples per channel. + unsigned buffer_frames() const { return buffer_size() / frame_size(); } + + //! Effective sample rate in Hz. + unsigned sample_rate() const { return _sample_rate; } + + //! Suggested minimal polling step, in frames. + unsigned stepping() const { return 16U * (1U + (_sample_rate / 50000)); } + + //! Indicate that the OSS buffer can be memory mapped. + bool can_memory_map() const { return has_capability(PCM_CAP_MMAP); } + + //! A pointer to the memory mapped OSS buffer, null if not mapped. + char *map() const { return static_cast(_map); } + + //! Current read / write position in the mapped OSS buffer. + unsigned map_pointer() const { return _map_progress % buffer_size(); } + + //! Total progress of the mapped OSS buffer, in frames. + std::int64_t map_progress() const { return _map_progress / frame_size(); } + + /*! + * \brief Set preferred audio parameters before opening device. + * \param format OSS sample formet, see sys/soundcard.h header. + * \param rate Sample rate in Hz. + * \param channels Number of recording / playback channels. + * \return True if successful, false means unsupported parameters. + */ + bool set_parameters(int format, int rate, int channels) { + if (bytes_per_sample(format) && channels > 0) { + _sample_format = format; + _sample_rate = rate; + _channels = channels; + return true; + } + return false; + } + + /*! + * \brief Open the device for either recording or playback. + * \param device Path to the OSS device (e.g. "/dev/dsp1"). + * \param mode Open mode read or write, optional exclusive and non-blocking. + * \return True if successful. + */ + bool open(const char *device, int mode) { + if (mode & O_RDWR) { + Log::warn(SOSSO_LOC, "Only one direction allowed, open %s in read mode.", + device); + mode = O_RDONLY | (mode & O_EXCL) | (mode & O_NONBLOCK); + } + _fd = ::open(device, mode); + if (_fd >= 0) { + _file_mode = mode; + if (bitperfect_mode(_fd) && set_sample_format(_fd) && set_channels(_fd) && + set_sample_rate(_fd) && get_buffer_info() && get_capabilities()) { + return true; + } + } + Log::warn(SOSSO_LOC, "Unable to open device %s, errno %d.", device, errno); + close(); + return false; + } + + //! Close the device. + void close() { + if (map()) { + memory_unmap(); + } + if (_fd >= 0) { + ::close(_fd); + _fd = -1; + } + } + + /*! + * \brief Request a specific OSS buffer size. + * \param fragments Number of fragments. + * \param fragment_size Size of the fragments in bytes. + * \return True if successful. + * \warning Due to OSS API limitations, resulting buffer sizes are not really + * predictable and may cause problems with some soundcards. + */ + bool set_buffer_size(unsigned fragments, unsigned fragment_size) { + int frg = 0; + while ((1U << frg) < fragment_size) { + ++frg; + } + frg |= (fragments << 16); + Log::info(SOSSO_LOC, "Request %d fragments of %u bytes.", (frg >> 16), + (1U << (frg & 0xffff))); + if (ioctl(_fd, SNDCTL_DSP_SETFRAGMENT, &frg) != 0) { + Log::warn(SOSSO_LOC, "Set fragments failed with %d.", errno); + return false; + } + return get_buffer_info(); + } + + /*! + * \brief Request a specific OSS buffer size. + * \param total_size Total size of all buffer fragments. + * \return True if successful. + * \warning Due to OSS API limitations, resulting buffer sizes are not really + * predictable and may cause problems with some soundcards. + */ + bool set_buffer_size(unsigned total_size) { + if (_fragment_size > 0) { + unsigned fragments = (total_size + _fragment_size - 1) / _fragment_size; + return set_buffer_size(fragments, _fragment_size); + } + return false; + } + + /*! + * \brief Read recorded audio data from OSS buffer. + * \param buffer Pointer to destination buffer. + * \param length Maximum read length in bytes. + * \param count Byte counter, increased by effective read length. + * \return True if successful or if nothing to do. + */ + bool read_io(char *buffer, std::size_t length, std::size_t &count) { + if (buffer && length > 0 && recording()) { + ssize_t result = ::read(_fd, buffer, length); + if (result >= 0) { + count += result; + } else if (errno == EAGAIN) { + count += 0; + } else { + Log::warn(SOSSO_LOC, "Data read failed with %d.", errno); + return false; + } + } + return true; + } + + /*! + * \brief Read recorded audio data from memory mapped OSS buffer. + * \param buffer Pointer to destination buffer. + * \param offset Read offset into the OSS buffer, in bytes. + * \param length Maximum read length in bytes. + * \return The number of bytes read. + */ + std::size_t read_map(char *buffer, std::size_t offset, std::size_t length) { + std::size_t bytes_read = 0; + if (length > 0 && map()) { + // Sanitize offset and length parameters. + offset = offset % buffer_size(); + if (length > buffer_size()) { + length = buffer_size(); + } + // Check if the read length spans across an OSS buffer cycle. + if (offset + length > buffer_size()) { + // Read until buffer end first. + bytes_read = read_map(buffer, offset, buffer_size() - offset); + length -= bytes_read; + buffer += bytes_read; + offset = 0; + } + // Read remaining data. + std::memcpy(buffer, map() + offset, length); + bytes_read += length; + } + return bytes_read; + } + + /*! + * \brief Write audio data to OSS buffer. + * \param buffer Pointer to source buffer. + * \param length Maximum write length in bytes. + * \param count Byte counter, increased by effective write length. + * \return True if successful or if nothing to do. + */ + bool write_io(char *buffer, std::size_t length, std::size_t &count) { + if (buffer && length > 0 && playback()) { + ssize_t result = ::write(file_descriptor(), buffer, length); + if (result >= 0) { + count += result; + } else if (errno == EAGAIN) { + count += 0; + } else { + Log::warn(SOSSO_LOC, "Data write failed with %d.", errno); + return false; + } + } + return true; + } + + /*! + * \brief Write audio data to a memory mapped OSS buffer. + * \param buffer Pointer to source buffer, null writes zeros to OSS buffer. + * \param offset Write offset into the OSS buffer, in bytes. + * \param length Maximum write length in bytes. + * \return The number of bytes written. + */ + std::size_t write_map(const char *buffer, std::size_t offset, + std::size_t length) { + std::size_t bytes_written = 0; + if (length > 0 && map()) { + // Sanitize pointer and length parameters. + offset = offset % buffer_size(); + if (length > buffer_size()) { + length = buffer_size(); + } + // Check if the write length spans across an OSS buffer cycle. + if (offset + length > buffer_size()) { + // Write until buffer end first. + bytes_written += write_map(buffer, offset, buffer_size() - offset); + length -= bytes_written; + if (buffer) { + buffer += bytes_written; + } + offset = 0; + } + // Write source if available, otherwise clear the buffer. + if (buffer) { + std::memcpy(map() + offset, buffer, length); + } else { + std::memset(map() + offset, 0, length); + } + bytes_written += length; + } + return bytes_written; + } + + /*! + * \brief Query number of frames in the OSS buffer (non-mapped). + * \return Number of frames, 0 if not successful. + */ + int queued_samples() { + unsigned long request = + playback() ? SNDCTL_DSP_CURRENT_OPTR : SNDCTL_DSP_CURRENT_IPTR; + oss_count_t ptr; + if (ioctl(_fd, request, &ptr) == 0) { + return ptr.fifo_samples; + } + return 0; + } + + //! Indicate that the device can be triggered to start. + bool can_trigger() const { return has_capability(PCM_CAP_TRIGGER); } + + //! Trigger the device to start recording / playback. + bool start() const { + if (!can_trigger()) { + Log::warn(SOSSO_LOC, "Trigger start not supported by device."); + return false; + } + int trigger = recording() ? PCM_ENABLE_INPUT : PCM_ENABLE_OUTPUT; + if (ioctl(file_descriptor(), SNDCTL_DSP_SETTRIGGER, &trigger) != 0) { + const char *direction = recording() ? "recording" : "playback"; + Log::warn(SOSSO_LOC, "Starting %s channel failed with error %d.", + direction, errno); + return false; + } + return true; + } + + /*! + * \brief Add device to a sync group for synchronized start. + * \param id Id of the sync group, 0 will initialize a new group. + * \return True if successful. + */ + bool add_to_sync_group(int &id) { + oss_syncgroup sync_group = {0, 0, {0}}; + sync_group.id = id; + sync_group.mode |= (recording() ? PCM_ENABLE_INPUT : PCM_ENABLE_OUTPUT); + if (ioctl(file_descriptor(), SNDCTL_DSP_SYNCGROUP, &sync_group) == 0 && + (id == 0 || sync_group.id == id)) { + id = sync_group.id; + return true; + } + Log::warn(SOSSO_LOC, "Sync grouping channel failed with error %d.", errno); + return false; + } + + /*! + * \brief Synchronized start of all devices in the sync group. + * \param id Id of the sync group. + * \return True if successful. + */ + bool start_sync_group(int id) { + if (ioctl(file_descriptor(), SNDCTL_DSP_SYNCSTART, &id) == 0) { + return true; + } + Log::warn(SOSSO_LOC, "Start of sync group failed with error %d.", errno); + return false; + } + + //! Query the number of playback underruns since last called. + int get_play_underruns() { + int play_underruns = 0; + int rec_overruns = 0; + get_errors(play_underruns, rec_overruns); + return play_underruns; + } + + //! Query the number of recording overruns since last called. + int get_rec_overruns() { + int play_underruns = 0; + int rec_overruns = 0; + get_errors(play_underruns, rec_overruns); + return rec_overruns; + } + + //! Update current playback position for memory mapped OSS buffer. + bool get_play_pointer() { + count_info info = {}; + if (ioctl(file_descriptor(), SNDCTL_DSP_GETOPTR, &info) == 0) { + if (info.ptr >= 0 && static_cast(info.ptr) < buffer_size() && + (info.ptr % frame_size()) == 0 && info.blocks >= 0) { + // Calculate pointer delta without complete buffer cycles. + unsigned delta = + (info.ptr + buffer_size() - map_pointer()) % buffer_size(); + // Get upper bound on progress from blocks info. + unsigned max_bytes = (info.blocks + 1) * _fragment_size - 1; + if (max_bytes >= delta) { + // Estimate cycle part and round it down to buffer cycles. + unsigned cycles = max_bytes - delta; + cycles -= (cycles % buffer_size()); + delta += cycles; + } + int fragments = delta / _fragment_size; + if (info.blocks < fragments || info.blocks > fragments + 1) { + Log::warn(SOSSO_LOC, "Play pointer blocks: %u - %d, %d, %d.", + map_pointer(), info.ptr, info.blocks, info.bytes); + } + _map_progress += delta; + return true; + } + Log::warn(SOSSO_LOC, "Play pointer out of bounds: %d, %d blocks.", + info.ptr, info.blocks); + } else { + Log::warn(SOSSO_LOC, "Play pointer failed with error: %d.", errno); + } + return false; + } + + //! Update current recording position for memory mapped OSS buffer. + bool get_rec_pointer() { + count_info info = {}; + if (ioctl(file_descriptor(), SNDCTL_DSP_GETIPTR, &info) == 0) { + if (info.ptr >= 0 && static_cast(info.ptr) < buffer_size() && + (info.ptr % frame_size()) == 0 && info.blocks >= 0) { + // Calculate pointer delta without complete buffer cycles. + unsigned delta = + (info.ptr + buffer_size() - map_pointer()) % buffer_size(); + // Get upper bound on progress from blocks info. + unsigned max_bytes = (info.blocks + 1) * _fragment_size - 1; + if (max_bytes >= delta) { + // Estimate cycle part and round it down to buffer cycles. + unsigned cycles = max_bytes - delta; + cycles -= (cycles % buffer_size()); + delta += cycles; + } + int fragments = delta / _fragment_size; + if (info.blocks < fragments || info.blocks > fragments + 1) { + Log::warn(SOSSO_LOC, "Rec pointer blocks: %u - %d, %d, %d.", + map_pointer(), info.ptr, info.blocks, info.bytes); + } + _map_progress += delta; + return true; + } + Log::warn(SOSSO_LOC, "Rec pointer out of bounds: %d, %d blocks.", + info.ptr, info.blocks); + } else { + Log::warn(SOSSO_LOC, "Rec pointer failed with error: %d.", errno); + } + return false; + } + + //! Memory map the OSS buffer. + bool memory_map() { + if (!can_memory_map()) { + Log::warn(SOSSO_LOC, "Memory map not supported by device."); + return false; + } + int protection = PROT_NONE; + if (playback()) { + protection = PROT_WRITE; + } + if (recording()) { + protection = PROT_READ; + } + if (_map == nullptr && protection != PROT_NONE) { + _map = mmap(NULL, buffer_size(), protection, MAP_SHARED, + file_descriptor(), 0); + if (_map == MAP_FAILED) { + Log::warn(SOSSO_LOC, "Memory map failed with error %d.", errno); + _map = nullptr; + } + } + return (_map != nullptr); + } + + //! Unmap a previously memory mapped OSS buffer. + bool memory_unmap() { + if (_map) { + if (munmap(_map, buffer_size()) != 0) { + Log::warn(SOSSO_LOC, "Memory unmap failed with error %d.", errno); + return false; + } + _map = nullptr; + } + return true; + } + + /*! + * \brief Check device capabilities. + * \param capabilities Device capabilities, see sys/soundcard.h header. + * \return True if the device has the capabilities in question. + */ + bool has_capability(int capabilities) const { + return (_capabilities & capabilities) == capabilities; + } + + //! Print device info to user information log. + void log_device_info() const { + if (!is_open()) { + return; + } + const char *direction = (recording() ? "Recording" : "Playback"); + Log::info(SOSSO_LOC, "%s device is %u channels at %u Hz, %lu bits.", + direction, _channels, _sample_rate, bytes_per_sample() * 8); + Log::info(SOSSO_LOC, "Device buffer is %u fragments of size %u, %u frames.", + _fragments, _fragment_size, buffer_frames()); + oss_sysinfo sys_info = {}; + if (ioctl(_fd, SNDCTL_SYSINFO, &sys_info) == 0) { + Log::info(SOSSO_LOC, "OSS version %s number %d on %s.", sys_info.version, + sys_info.versionnum, sys_info.product); + } + Log::info(SOSSO_LOC, "PCM capabilities:"); + if (has_capability(PCM_CAP_TRIGGER)) + Log::info(SOSSO_LOC, " PCM_CAP_TRIGGER (Trigger start)"); + if (has_capability(PCM_CAP_MMAP)) + Log::info(SOSSO_LOC, " PCM_CAP_MMAP (Memory map)"); + if (has_capability(PCM_CAP_MULTI)) + Log::info(SOSSO_LOC, " PCM_CAP_MULTI (Multiple open)"); + if (has_capability(PCM_CAP_INPUT)) + Log::info(SOSSO_LOC, " PCM_CAP_INPUT (Recording)"); + if (has_capability(PCM_CAP_OUTPUT)) + Log::info(SOSSO_LOC, " PCM_CAP_OUTPUT (Playback)"); + if (has_capability(PCM_CAP_VIRTUAL)) + Log::info(SOSSO_LOC, " PCM_CAP_VIRTUAL (Virtual device)"); + if (has_capability(PCM_CAP_ANALOGIN)) + Log::info(SOSSO_LOC, " PCM_CAP_ANALOGIN (Analog input)"); + if (has_capability(PCM_CAP_ANALOGOUT)) + Log::info(SOSSO_LOC, " PCM_CAP_ANALOGOUT (Analog output)"); + if (has_capability(PCM_CAP_DIGITALIN)) + Log::info(SOSSO_LOC, " PCM_CAP_DIGITALIN (Digital input)"); + if (has_capability(PCM_CAP_DIGITALOUT)) + Log::info(SOSSO_LOC, " PCM_CAP_DIGITALOUT (Digital output)"); + } + +private: + // Disable auto-conversion (bitperfect) when opened in exclusive mode. + bool bitperfect_mode(int fd) { + if (_file_mode & O_EXCL) { + int flags = 0; + int result = ioctl(fd, SNDCTL_DSP_COOKEDMODE, &flags); + if (result < 0) { + Log::warn(SOSSO_LOC, "Unable to set cooked mode."); + } + return result >= 0; + } + return true; + } + + // Set sample format and the check the result. + bool set_sample_format(int fd) { + int format = _sample_format; + int result = ioctl(fd, SNDCTL_DSP_SETFMT, &format); + if (result != 0) { + Log::warn(SOSSO_LOC, "Unable to set sample format, error %d.", errno); + return false; + } else if (bytes_per_sample(format) == 0) { + Log::warn(SOSSO_LOC, "Unsupported sample format %d.", format); + return false; + } else if (format != _sample_format) { + Log::warn( + SOSSO_LOC, "Driver changed the sample format, %lu bit vs %lu bit.", + bytes_per_sample(format) * 8, bytes_per_sample(_sample_format) * 8); + } + _sample_format = format; + return true; + } + + // Set sample rate and then check the result. + bool set_sample_rate(int fd) { + int rate = _sample_rate; + if (ioctl(fd, SNDCTL_DSP_SPEED, &rate) == 0) { + if (rate != _sample_rate) { + Log::warn(SOSSO_LOC, "Driver changed the sample rate, %d vs %d.", rate, + _sample_rate); + _sample_rate = rate; + } + return true; + } + Log::warn(SOSSO_LOC, "Unable to set sample rate, error %d.", errno); + return false; + } + + // Set the number of channels and then check the result. + bool set_channels(int fd) { + int channels = _channels; + if (ioctl(fd, SNDCTL_DSP_CHANNELS, &channels) == 0) { + if (channels != _channels) { + Log::warn(SOSSO_LOC, "Driver changed number of channels, %d vs %d.", + channels, _channels); + _channels = channels; + } + return true; + } + Log::warn(SOSSO_LOC, "Unable to set channels, error %d.", errno); + return false; + } + + // Query fragments and size of the OSS buffer. + bool get_buffer_info() { + audio_buf_info info = {0, 0, 0, 0}; + unsigned long request = + playback() ? SNDCTL_DSP_GETOSPACE : SNDCTL_DSP_GETISPACE; + if (ioctl(_fd, request, &info) >= 0) { + _fragments = info.fragstotal; + _fragment_size = info.fragsize; + return true; + } else { + Log::warn(SOSSO_LOC, "Unable to get buffer info."); + return false; + } + } + + // Query capabilities of the device. + bool get_capabilities() { + if (ioctl(_fd, SNDCTL_DSP_GETCAPS, &_capabilities) == 0) { + oss_sysinfo sysinfo = {}; + if (ioctl(_fd, OSS_SYSINFO, &sysinfo) == 0) { + if (std::strncmp(sysinfo.version, "1302000", 7) < 0) { + // Memory map on FreeBSD prior to 13.2 may use wrong buffer size. + Log::warn(SOSSO_LOC, + "Disable memory map, workaround OSS bug on FreeBSD < 13.2"); + _capabilities &= ~PCM_CAP_MMAP; + } + return true; + } else { + Log::warn(SOSSO_LOC, "Unable to get system info, error %d.", errno); + } + } else { + Log::warn(SOSSO_LOC, "Unable to get device capabilities, error %d.", + errno); + _capabilities = 0; + } + return false; + } + + // Query error information from the device. + bool get_errors(int &play_underruns, int &rec_overruns) { + audio_errinfo error_info = {}; + if (ioctl(file_descriptor(), SNDCTL_DSP_GETERROR, &error_info) == 0) { + play_underruns = error_info.play_underruns; + rec_overruns = error_info.rec_overruns; + return true; + } + return false; + } + +private: + int _fd = -1; // File descriptor. + int _file_mode = O_RDONLY; // File open mode. + void *_map = nullptr; // Memory map pointer. + std::uint64_t _map_progress = 0; // Memory map progress. + int _channels = 2; // Number of channels. + int _capabilities = 0; // Device capabilities. + int _sample_format = AFMT_S32_NE; // Sample format. + int _sample_rate = 48000; // Sample rate. + unsigned _fragments = 0; // Number of OSS buffer fragments. + unsigned _fragment_size = 0; // OSS buffer fragment size. +}; + +} // namespace sosso + +#endif // SOSSO_DEVICE_HPP diff --git a/freebsd/sosso/DoubleBuffer.hpp b/freebsd/sosso/DoubleBuffer.hpp new file mode 100644 index 000000000..b966dbd0b --- /dev/null +++ b/freebsd/sosso/DoubleBuffer.hpp @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_DOUBLEBUFFER_HPP +#define SOSSO_DOUBLEBUFFER_HPP + +#include "sosso/Buffer.hpp" +#include "sosso/Logging.hpp" +#include +#include + +namespace sosso { + +/*! + * \brief Double Buffering for Channel + * + * Manages double buffering on top of a ReadChannel or WriteChannel. It takes + * two buffers with corresponding end positions. One of these is selected + * for processing, depending on the buffer and channel positions. The buffers + * can be overlapping or have gaps in between. + * A buffer is marked as finished when all buffer data was processed and the + * channel progress has reached the buffer end. This provides steady buffer + * replacement times, synchronized with channel progress. + * The wakeup times for processing are adapted to available channel data and + * work pending (unprocessed buffer data). + */ +template class DoubleBuffer : public Channel { + /*! + * \brief Store a buffer and its end position. + * + * The end position of the buffer corresponds to channel progress in frames. + * Marking the end position allows to map the buffer content to the matching + * channel data, independent of read and write positions. + */ + struct BufferRecord { + Buffer buffer; // External buffer, may be empty. + std::int64_t end_frames = 0; // Buffer end position in frames. + }; + +public: + //! Indicate that buffer is ready for processing. + bool ready() const { return _buffer_a.buffer.valid(); } + + /*! + * \brief Set the next consecutive buffer to be processed. + * \param buffer External buffer ready for processing. + * \param end_frames End position of the buffer in frames. + * \return True if successful, false means there are already two buffers. + */ + bool set_buffer(Buffer &&buffer, std::int64_t end_frames) { + // Set secondary buffer if available. + if (!_buffer_b.buffer.valid()) { + _buffer_b.buffer = std::move(buffer); + _buffer_b.end_frames = end_frames; + // Promote secondary buffer to primary if primary is not set. + if (!_buffer_a.buffer.valid()) { + std::swap(_buffer_b, _buffer_a); + } + return ready(); + } + return false; + } + + /*! + * \brief Reset the buffer end positions in case of over- and underruns. + * \param end_frames New end position of the primary buffer. + * \return True if ready to proceed. + */ + bool reset_buffers(std::int64_t end_frames) { + // Reset primary buffer. + if (_buffer_a.buffer.valid()) { + std::memset(_buffer_a.buffer.data(), 0, _buffer_a.buffer.length()); + _buffer_a.buffer.reset(); + Log::info(SOSSO_LOC, "Primary buffer reset from %lld to %lld.", + _buffer_a.end_frames, end_frames); + _buffer_a.end_frames = end_frames; + } + // Reset secondary buffer. + if (_buffer_b.buffer.valid()) { + std::memset(_buffer_b.buffer.data(), 0, _buffer_b.buffer.length()); + _buffer_b.buffer.reset(); + end_frames += _buffer_b.buffer.length() / Channel::frame_size(); + Log::info(SOSSO_LOC, "Secondary buffer reset from %lld to %lld.", + _buffer_b.end_frames, end_frames); + _buffer_b.end_frames = end_frames; + } + return ready(); + } + + //! Retrieve the primary buffer, may be empty. + Buffer &&take_buffer() { + std::swap(_buffer_a, _buffer_b); + return std::move(_buffer_b.buffer); + } + + /*! + * \brief Process channel with given buffers to read or write. + * \param now Time offset from channel start in frames, see FrameClock. + * \return True if there were no processing errors. + */ + bool process(std::int64_t now) { + // Round frame time down to steppings, ignore timing jitter. + now = now - now % Channel::stepping(); + // Always process primary buffer, No-Op if already done. + bool ok = Channel::process(_buffer_a.buffer, _buffer_a.end_frames, now); + // Process secondary buffer when primary is done. + if (ok && _buffer_a.buffer.done() && _buffer_b.buffer.valid()) { + ok = Channel::process(_buffer_b.buffer, _buffer_b.end_frames, now); + } + return ok; + } + + //! End position of the primary buffer. + std::int64_t end_frames() const { + if (ready()) { + return _buffer_a.end_frames; + } + return 0; + } + + //! Expected frame time when primary buffer is finished. + std::int64_t period_end() const { + if (ready()) { + return end_frames() + Channel::balance(); + } + return 0; + } + + //! Expected frame time when both buffers are finished. + std::int64_t total_end() const { + if (ready()) { + if (_buffer_b.buffer.valid()) { + return _buffer_b.end_frames + Channel::balance(); + } + return end_frames() + Channel::balance(); + } + return 0; + } + + /*! + * \brief Calculate next wakeup time for processing. + * \param now Current frame time as offset from channel start, see FrameClock. + * \return Next suggested wakeup in frame time. + */ + std::int64_t wakeup_time(std::int64_t now) const { + // No need to wake up if channel is not running. + if (!Channel::is_open()) { + return std::numeric_limits::max(); + } + // Wakeup immediately if there's more work to do now. + if (Channel::oss_available() > 0 && + (!_buffer_a.buffer.done() || !_buffer_b.buffer.done())) { + Log::log(SOSSO_LOC, "Immediate wakeup at %lld for more work.", now); + return now; + } + // Get upcoming buffer end and compute next channel wakeup time. + std::int64_t sync_frames = now; + if (_buffer_a.buffer.valid() && !finished(now)) { + sync_frames = period_end(); + } else if (_buffer_b.buffer.valid() && !total_finished(now)) { + sync_frames = _buffer_b.end_frames + Channel::balance(); + } else { + sync_frames = std::numeric_limits::max(); + } + return Channel::wakeup_time(sync_frames); + } + + //! Indicate progress on processing the primary buffer, in frames. + std::int64_t buffer_progress() const { + return _buffer_a.buffer.progress() / Channel::frame_size(); + } + + //! Indicate that primary buffer is finished at current frame time. + bool finished(std::int64_t now) const { + return period_end() <= now && _buffer_a.buffer.done(); + } + + //! Indicate that both buffers are finished at current frame time. + bool total_finished(std::int64_t now) const { + return total_end() <= now && _buffer_a.buffer.done() && + _buffer_b.buffer.done(); + } + + //! Print channel state as user information, at current frame time. + void log_state(std::int64_t now) const { + const char *direction = Channel::playback() ? "Out" : "In"; + const char *sync = (Channel::last_sync() == now) ? "sync" : "frame"; + std::int64_t buf_a = _buffer_a.buffer.progress() / Channel::frame_size(); + std::int64_t buf_b = _buffer_b.buffer.progress() / Channel::frame_size(); + Log::log(SOSSO_LOC, + "%s %s, %lld bal %lld, buf A %lld B %lld OSS %lld, %lld left, " + "req %u min %lld", + direction, sync, now, Channel::balance(), buf_a, buf_b, + Channel::oss_available(), period_end() - now, + Channel::sync_level(), Channel::min_progress()); + } + +private: + BufferRecord _buffer_a; // Primary buffer, may be empty. + BufferRecord _buffer_b; // Secondary buffer, may be empty. +}; + +} // namespace sosso + +#endif // SOSSO_DOUBLEBUFFER_HPP diff --git a/freebsd/sosso/FrameClock.hpp b/freebsd/sosso/FrameClock.hpp new file mode 100644 index 000000000..335846439 --- /dev/null +++ b/freebsd/sosso/FrameClock.hpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_FRAMECLOCK_HPP +#define SOSSO_FRAMECLOCK_HPP + +#include "sosso/Logging.hpp" +#include +#include + +namespace sosso { + +/*! + * \brief Clock using audio frames as time unit. + * + * Provides time as an offset from an initial time zero, usually when the audio + * device was started. Instead of nanoseconds it measures time in frames + * (samples per channel), and thus needs to know the sample rate. + * It also lets a thread sleep until a specified wakeup time, again in frames. + */ +class FrameClock { +public: + /*! + * \brief Initialize the clock, set time zero. + * \param sample_rate Sample rate in Hz, for time to frame conversion. + * \return True if successful, false means an error occurred. + */ + bool init_clock(unsigned sample_rate) { + return set_sample_rate(sample_rate) && init_zero_time(); + } + + /*! + * \brief Get current frame time. + * \param result Set to current frame time, as offset from time zero. + * \return True if successful, false means an error occurred. + */ + bool now(std::int64_t &result) const { + std::int64_t time_ns = 0; + if (get_time_offset(time_ns)) { + result = time_to_frames(time_ns); + return true; + } + return false; + } + + /*! + * \brief Let the thread sleep until wakeup time. + * \param wakeup_frame Wakeup time in frames since time zero. + * \return True if successful, false means an error occurred. + */ + bool sleep(std::int64_t wakeup_frame) const { + std::int64_t time_ns = frames_to_time(wakeup_frame); + return sleep_until(time_ns); + } + + //! Convert frames to time in nanoseconds. + std::int64_t frames_to_time(std::int64_t frames) const { + return (frames * 1000000000) / _sample_rate; + } + + //! Convert time in nanoseconds to frames. + std::int64_t time_to_frames(std::int64_t time_ns) const { + return (time_ns * _sample_rate) / 1000000000; + } + + //! Convert frames to system clock time in microseconds. + std::int64_t frames_to_absolute_us(std::int64_t frames) const { + return _zero.tv_sec * 1000000ULL + _zero.tv_nsec / 1000 + + frames_to_time(frames) / 1000; + } + + //! Currently used sample rate in Hz. + unsigned sample_rate() const { return _sample_rate; } + + //! Set the sample rate in Hz, used for time to frame conversion. + bool set_sample_rate(unsigned sample_rate) { + if (sample_rate > 0) { + _sample_rate = sample_rate; + return true; + } + return false; + } + + //! Suggested minimal wakeup step in frames. + unsigned stepping() const { return 16U * (1U + (_sample_rate / 50000)); } + +private: + // Initialize time zero now. + bool init_zero_time() { return gettime(_zero); } + + // Get current time in nanoseconds, as offset from time zero. + bool get_time_offset(std::int64_t &result) const { + timespec now; + if (gettime(now)) { + result = ((now.tv_sec - _zero.tv_sec) * 1000000000) + now.tv_nsec - + _zero.tv_nsec; + return true; + } + return false; + } + + // Let thread sleep until wakeup time, in nanoseconds since time zero. + bool sleep_until(std::int64_t offset_ns) const { + timespec wakeup = {_zero.tv_sec + (_zero.tv_nsec + offset_ns) / 1000000000, + (_zero.tv_nsec + offset_ns) % 1000000000}; + if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &wakeup, NULL) != 0) { + Log::warn(SOSSO_LOC, "Sleep failed with error %d.", errno); + return false; + } + return true; + } + + // Get current time in nanosecons, as a timespec struct. + bool gettime(timespec &result) const { + if (clock_gettime(CLOCK_MONOTONIC, &result) != 0) { + Log::warn(SOSSO_LOC, "Get time failed with error %d.", errno); + return false; + } + return true; + } + + timespec _zero = {0, 0}; // Time zero as a timespec struct. + unsigned _sample_rate = 48000; // Sample rate used for frame conversion. +}; + +} // namespace sosso + +#endif // SOSSO_FRAMECLOCK_HPP diff --git a/freebsd/sosso/Logging.hpp b/freebsd/sosso/Logging.hpp new file mode 100644 index 000000000..8a31df24a --- /dev/null +++ b/freebsd/sosso/Logging.hpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_LOGGING_HPP +#define SOSSO_LOGGING_HPP + +#include +#include + +namespace sosso { + +/*! + * \brief Store the source location for logging. + * + * Keep its implementation close to C++20 std::source_location. + * It will be replaced by that when C++20 is widely available. + */ +struct SourceLocation { + //! Get the line number in the source file. + std::uint_least32_t line() const { return _line; } + //! Get the column in the source file, not implemented. + std::uint_least32_t column() const { return _column; } + //! Get the file name of the source file. + const char *file_name() const { return _file_name; } + //! Get the function context in the source file. + const char *function_name() const { return _function_name; } + + std::uint_least32_t _line; + std::uint_least32_t _column; + const char *_file_name; + const char *_function_name; +}; + +/// Capture source location in place of this macro. +#define SOSSO_LOC \ + SourceLocation { __LINE__, 0, __FILE__, __func__ } + +/*! + * \brief Static logging functions. + * + * There are three log levels: + * - warn() indicates warnings and errors. + * - info() provides general information to the user. + * - log() is for low-level information and debugging purposes. + * + * The single message static logging functions have to be implemented in the + * application, so they output to the appropriate places. Otherwise there will + * be a linking error at build time. To give some context for debugging, the + * source location is given. + * + * For printf-style message composition use the corresponding variable argument + * function templates, limited to 255 character length. + */ +class Log { +public: + //! Single message low-level log, implement this in the application. + static void log(SourceLocation location, const char *message); + + //! Compose printf-style low-level log messages. + template + static void log(SourceLocation location, const char *message, Args... args) { + char formatted[256]; + std::snprintf(formatted, 256, message, args...); + log(location, formatted); + } + + //! Single message user information, implement this in the application. + static void info(SourceLocation location, const char *message); + + //! Compose printf-style user information messages. + template + static void info(SourceLocation location, const char *message, Args... args) { + char formatted[256]; + std::snprintf(formatted, 256, message, args...); + info(location, formatted); + } + + //! Single message warning, implement this in the application. + static void warn(SourceLocation location, const char *message); + + //! Compose printf-style warning messages. + template + static void warn(SourceLocation location, const char *message, Args... args) { + char formatted[256]; + std::snprintf(formatted, 256, message, args...); + warn(location, formatted); + } +}; + +} // namespace sosso + +#endif // SOSSO_LOGGING_HPP diff --git a/freebsd/sosso/ReadChannel.hpp b/freebsd/sosso/ReadChannel.hpp new file mode 100644 index 000000000..970131957 --- /dev/null +++ b/freebsd/sosso/ReadChannel.hpp @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_READCHANNEL_HPP +#define SOSSO_READCHANNEL_HPP + +#include "sosso/Buffer.hpp" +#include "sosso/Channel.hpp" +#include "sosso/Logging.hpp" +#include + +namespace sosso { + +/*! + * \brief Recording Channel + * + * Specializes the generic Channel class into a recording channel. It keeps + * track of the OSS recording progress, and reads the available audio data to an + * external buffer. If the OSS buffer is memory mapped, the audio data is copied + * from there. Otherwise I/O read() system calls are used. + */ +class ReadChannel : public Channel { +public: + /*! + * \brief Open a device for recording. + * \param device Path to the device, e.g. "/dev/dsp1". + * \param exclusive Try to get exclusive access to the device. + * \return True if the device was opened successfully. + */ + bool open(const char *device, bool exclusive = true) { + int mode = O_RDONLY | O_NONBLOCK; + if (exclusive) { + mode |= O_EXCL; + } + return Channel::open(device, mode); + } + + //! Available audio data to be read, in frames. + std::int64_t oss_available() const { + std::int64_t result = last_progress() - _read_position; + if (result < 0) { + result = 0; + } else if (result > buffer_frames()) { + result = buffer_frames(); + } + return result; + } + + /*! + * \brief Calculate next wakeup time. + * \param sync_frames Required sync event (e.g. buffer end), in frame time. + * \return Suggested and safe wakeup time for next process(), in frame time. + */ + std::int64_t wakeup_time(std::int64_t sync_frames) const { + return Channel::wakeup_time(sync_frames, oss_available()); + } + + /*! + * \brief Check OSS progress and read recorded audio to the buffer. + * \param buffer Buffer to write to, untouched if invalid. + * \param end Buffer end position, matching channel progress. + * \param now Current time in frame time, see FrameClock. + * \return True if successful, false means there was an error. + */ + bool process(Buffer &buffer, std::int64_t end, std::int64_t now) { + if (map()) { + return (progress_done(now) || check_map_progress(now)) && + (buffer_done(buffer, end) || process_mapped(buffer, end, now)); + } else { + return (progress_done(now) || check_read_progress(now)) && + (buffer_done(buffer, end) || process_read(buffer, end, now)); + } + } + +protected: + // Indicate that OSS progress has already been checked. + bool progress_done(std::int64_t now) { return (last_processing() == now); } + + // Check OSS progress in case of memory mapped buffer. + bool check_map_progress(std::int64_t now) { + // Get OSS progress through map pointer. + if (get_rec_pointer()) { + std::int64_t progress = map_progress() - _oss_progress; + _oss_progress += progress; + std::int64_t available = last_progress() + progress - _read_position; + std::int64_t loss = mark_loss(available - buffer_frames()); + mark_progress(progress, now); + if (loss > 0) { + Log::warn(SOSSO_LOC, "OSS recording buffer overrun, %lld lost.", loss); + _read_position = last_progress() - buffer_frames(); + } + } + return progress_done(now); + } + + // Read recorded audio data to buffer, in case of memory mapped OSS buffer. + bool process_mapped(Buffer &buffer, std::int64_t end, std::int64_t now) { + // Calculate current read buffer position. + std::int64_t position = buffer_position(buffer, end); + // Only read what is available until OSS captured its complete buffer. + std::int64_t oldest = last_progress() - buffer_frames(); + if (_oss_progress < buffer_frames()) { + oldest = last_progress() - _oss_progress; + } + if (std::int64_t skip = buffer_advance(buffer, oldest - position)) { + // First part of the read buffer already passed, fill it up. + Log::info(SOSSO_LOC, "@%lld - %lld Read buffer late by %lld, skip %lld.", + now, end, oldest - position, skip); + position += skip; + } else if (position != _read_position) { + // Position mismatch, reread what is available. + if (std::int64_t rewind = buffer_rewind(buffer, position - oldest)) { + Log::info(SOSSO_LOC, + "@%lld - %lld Read position mismatch, reread %lld.", now, end, + rewind); + position -= rewind; + } + } + if (position >= oldest && position < last_progress() && !buffer.done()) { + // Read from offset up to current position, if read buffer can hold it. + std::int64_t offset = last_progress() - position; + std::size_t length = buffer.remaining(offset * frame_size()); + unsigned pointer = (_oss_progress - offset) % buffer_frames(); + length = read_map(buffer.position(), pointer * frame_size(), length); + buffer.advance(length); + _read_position = buffer_position(buffer, end); + } + _read_position += freewheel_finish(buffer, end, now); + return true; + } + + // Check progress when using I/O read() system call. + bool check_read_progress(std::int64_t now) { + // Check for OSS buffer overruns. + std::int64_t overdue = now - estimated_dropout(oss_available()); + if ((overdue > 0 && get_rec_overruns() > 0) || overdue > max_progress()) { + std::int64_t progress = buffer_frames() - oss_available(); + std::int64_t loss = mark_loss(progress, now); + Log::warn(SOSSO_LOC, "OSS recording buffer overrun, %lld lost.", loss); + mark_progress(progress + loss, now); + _read_position = last_progress() - buffer_frames(); + } else { + // Infer progress from OSS queue changes. + std::int64_t queued = queued_samples(); + std::int64_t progress = queued - (last_progress() - _read_position); + mark_progress(progress, now); + _read_position = last_progress() - queued; + } + return progress_done(now); + } + + // Read recorded audio data to buffer, using I/O read() syscall. + bool process_read(Buffer &buffer, std::int64_t end, std::int64_t now) { + bool ok = true; + std::int64_t position = buffer_position(buffer, end); + if (std::int64_t skip = buffer_advance(buffer, _read_position - position)) { + // Overlapping buffers, skip the overlapping part. + Log::info(SOSSO_LOC, "@%lld - %lld Read buffer overlap %lld, skip %lld.", + now, end, _read_position - position, skip); + position += skip; + } else if (std::int64_t rewind = + buffer_rewind(buffer, position - _read_position)) { + // Gap between reads, try to rewind to last read position. + Log::info(SOSSO_LOC, "@%lld - %lld Read buffer gap %lld, rewind %lld.", + now, end, position - _read_position, rewind); + position -= rewind; + } + if (oss_available() == 0) { + // OSS buffer is empty, nothing to do. + } else if (position > _read_position) { + // Read and omit data of remaining gap, drain OSS buffer. + std::int64_t gap = position - _read_position; + std::size_t read_limit = buffer.remaining(gap * frame_size()); + std::size_t bytes_read = 0; + ok = read_io(buffer.position(), read_limit, bytes_read); + Log::info(SOSSO_LOC, "@%lld - %lld Read buffer gap %lld, drain %lu.", now, + end, gap, bytes_read / frame_size()); + _read_position += bytes_read / frame_size(); + } else if (position == _read_position) { + // Read as much as currently available. + std::size_t bytes_read = 0; + ok = read_io(buffer.position(), buffer.remaining(), bytes_read); + _read_position += bytes_read / frame_size(); + buffer.advance(bytes_read); + } + freewheel_finish(buffer, end, now); + return ok; + } + +private: + // Calculate read position of the remaining buffer. + std::int64_t buffer_position(const Buffer &buffer, std::int64_t end) const { + return end - extra_latency() - (buffer.remaining() / frame_size()); + } + + // Indicate that a buffer doesn't need further processing. + bool buffer_done(const Buffer &buffer, std::int64_t end) const { + return buffer.done() && buffer_position(buffer, end) <= _read_position; + } + + // Extra latency to always finish on time, regardless of OSS progress steps. + std::int64_t extra_latency() const { return max_progress(); } + + // Avoid stalled buffers with irregular OSS progress in freewheel mode. + std::int64_t freewheel_finish(Buffer &buffer, std::int64_t end, + std::int64_t now) { + std::int64_t advance = 0; + if (freewheel() && now >= end + balance() && !buffer.done()) { + // Buffer is overdue in freewheel sync mode, finish immediately. + std::memset(buffer.position(), 0, buffer.remaining()); + advance = buffer.advance(buffer.remaining()) / frame_size(); + Log::info(SOSSO_LOC, "@%lld - %lld Read buffer overdue, fill by %lu.", + now, end, advance); + } + return advance; + } + + // Skip reading part of the buffer to match OSS read position. + std::int64_t buffer_advance(Buffer &buffer, std::int64_t frames) { + if (frames > 0) { + std::size_t skip = buffer.remaining(frames * frame_size()); + std::memset(buffer.position(), 0, skip); + return buffer.advance(skip) / frame_size(); + } + return 0; + } + + // Rewind part of the buffer to match OSS read position. + std::int64_t buffer_rewind(Buffer &buffer, std::int64_t frames) { + if (frames > 0) { + return buffer.rewind(frames * frame_size()) / frame_size(); + } + return 0; + } + + std::int64_t _oss_progress = 0; // Last memory mapped OSS progress. + std::int64_t _read_position = 0; // Current read position of channel. +}; + +} // namespace sosso + +#endif // SOSSO_READCHANNEL_HPP diff --git a/freebsd/sosso/WriteChannel.hpp b/freebsd/sosso/WriteChannel.hpp new file mode 100644 index 000000000..8e5b04663 --- /dev/null +++ b/freebsd/sosso/WriteChannel.hpp @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2023 Florian Walpen + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SOSSO_WRITECHANNEL_HPP +#define SOSSO_WRITECHANNEL_HPP + +#include "sosso/Buffer.hpp" +#include "sosso/Channel.hpp" +#include "sosso/Logging.hpp" +#include + +namespace sosso { + +/*! + * \brief Playback Channel + * + * Specializes the generic Channel class into a playback channel. It keeps track + * of the OSS playback progress, and writes audio data from an external buffer + * to the available OSS buffer. If the OSS buffer is memory mapped, the audio + * data is copied there. Otherwise I/O write() system calls are used. + */ +class WriteChannel : public Channel { +public: + /*! + * \brief Open a device for playback. + * \param device Path to the device, e.g. "/dev/dsp1". + * \param exclusive Try to get exclusive access to the device. + * \return True if the device was opened successfully. + */ + bool open(const char *device, bool exclusive = true) { + int mode = O_WRONLY | O_NONBLOCK; + if (exclusive) { + mode |= O_EXCL; + } + return Channel::open(device, mode); + } + + //! Available OSS buffer space for writing, in frames. + std::int64_t oss_available() const { + std::int64_t result = last_progress() + buffer_frames() - _write_position; + if (result < 0) { + result = 0; + } else if (result > buffer_frames()) { + result = buffer_frames(); + } + return result; + } + + /*! + * \brief Calculate next wakeup time. + * \param sync_frames Required sync event (e.g. buffer end), in frame time. + * \return Suggested and safe wakeup time for next process(), in frame time. + */ + std::int64_t wakeup_time(std::int64_t sync_frames) const { + return Channel::wakeup_time(sync_frames, oss_available()); + } + + /*! + * \brief Check OSS progress and write playback audio to the OSS buffer. + * \param buffer Buffer of playback audio data, untouched if invalid. + * \param end Buffer end position, matching channel progress. + * \param now Current time in frame time, see FrameClock. + * \return True if successful, false means there was an error. + */ + bool process(Buffer &buffer, std::int64_t end, std::int64_t now) { + if (map()) { + return (progress_done(now) || check_map_progress(now)) && + (buffer_done(buffer, end) || process_mapped(buffer, end, now)); + } else { + return (progress_done(now) || check_write_progress(now)) && + (buffer_done(buffer, end) || process_write(buffer, end, now)); + } + } + +protected: + // Indicate that OSS progress has already been checked. + bool progress_done(std::int64_t now) { return (last_processing() == now); } + + // Check OSS progress in case of memory mapped buffer. + bool check_map_progress(std::int64_t now) { + // Get OSS progress through map pointer. + if (get_play_pointer()) { + std::int64_t progress = map_progress() - _oss_progress; + if (progress > 0) { + // Sometimes OSS playback starts with a bogus extra buffer cycle. + if (progress > buffer_frames() && + now - last_processing() < buffer_frames() / 2) { + Log::warn(SOSSO_LOC, + "OSS playback bogus buffer cycle, %lld frames in %lld.", + progress, now - last_processing()); + progress = progress % buffer_frames(); + } + // Clear obsolete audio data in the buffer. + write_map(nullptr, (_oss_progress % buffer_frames()) * frame_size(), + progress * frame_size()); + _oss_progress = map_progress(); + } + std::int64_t loss = + mark_loss(last_progress() + progress - _write_position); + mark_progress(progress, now); + if (loss > 0) { + Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss); + _write_position = last_progress(); + } + } + return progress_done(now); + } + + // Write playback audio data to a memory mapped OSS buffer. + bool process_mapped(Buffer &buffer, std::int64_t end, std::int64_t now) { + // Buffer position should be between OSS progress and last write position. + std::int64_t position = buffer_position(buffer.remaining(), end); + if (std::int64_t skip = + buffer_advance(buffer, last_progress() - position)) { + // First part of the buffer already played, skip it. + Log::info(SOSSO_LOC, "@%lld - %lld Write %lld already played, skip %lld.", + now, end, last_progress() - position, skip); + position += skip; + } else if (position != _write_position) { + // Position mismatch, rewrite as much as possible. + if (std::int64_t rewind = + buffer_rewind(buffer, position - last_progress())) { + Log::info(SOSSO_LOC, + "@%lld - %lld Write position mismatch, rewrite %lld.", now, + end, rewind); + position -= rewind; + } + } + // The writable window is the whole buffer, starting from OSS progress. + if (!buffer.done() && position >= last_progress() && + position < last_progress() + buffer_frames()) { + if (_write_position < position && _write_position + 8 >= position) { + // Small remaining gap between writes, fill in a replay patch. + std::int64_t offset = _write_position - last_progress(); + unsigned pointer = (_oss_progress + offset) % buffer_frames(); + std::size_t length = (position - _write_position) * frame_size(); + length = buffer.remaining(length); + std::size_t written = + write_map(buffer.position(), pointer * frame_size(), length); + Log::info(SOSSO_LOC, "@%lld - %lld Write small gap %lld, replay %lld.", + now, end, position - _write_position, written / frame_size()); + } + // Write from buffer offset up to either OSS or write buffer end. + std::int64_t offset = position - last_progress(); + unsigned pointer = (_oss_progress + offset) % buffer_frames(); + std::size_t length = (buffer_frames() - offset) * frame_size(); + length = buffer.remaining(length); + std::size_t written = + write_map(buffer.position(), pointer * frame_size(), length); + buffer.advance(written); + _write_position = buffer_position(buffer.remaining(), end); + } + _write_position += freewheel_finish(buffer, end, now); + return true; + } + + // Check progress when using I/O write() system call. + bool check_write_progress(std::int64_t now) { + // Check for OSS buffer underruns. + std::int64_t overdue = now - estimated_dropout(oss_available()); + if ((overdue > 0 && get_play_underruns() > 0) || overdue > max_progress()) { + // OSS buffer underrun, estimate loss and progress from time. + std::int64_t progress = _write_position - last_progress(); + std::int64_t loss = mark_loss(progress, now); + Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss); + mark_progress(progress + loss, now); + _write_position = last_progress(); + } else { + // Infer progress from OSS queue changes. + std::int64_t queued = queued_samples(); + std::int64_t progress = (_write_position - last_progress()) - queued; + mark_progress(progress, now); + _write_position = last_progress() + queued; + } + return progress_done(now); + } + + // Write playback audio data to OSS buffer using I/O write() system call. + bool process_write(Buffer &buffer, std::int64_t end, std::int64_t now) { + bool ok = true; + // Adjust buffer position to OSS write position, if possible. + std::int64_t position = buffer_position(buffer.remaining(), end); + if (std::int64_t rewind = + buffer_rewind(buffer, position - _write_position)) { + // Gap between buffers, replay parts to fill it up. + Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, replay %lld.", + now, end, position - _write_position, rewind); + position -= rewind; + } else if (std::int64_t skip = + buffer_advance(buffer, _write_position - position)) { + // Overlapping buffers, skip the overlapping part. + Log::info(SOSSO_LOC, "@%lld - %lld Write buffer overlap %lld, skip %lld.", + now, end, _write_position - position, skip); + position += skip; + } + if (oss_available() == 0) { + // OSS buffer is full, nothing to do. + } else if (position > _write_position) { + // Replay to fill remaining gap, limit the write to just fill the gap. + std::int64_t gap = position - _write_position; + std::size_t write_limit = buffer.remaining(gap * frame_size()); + std::size_t bytes_written = 0; + ok = write_io(buffer.position(), write_limit, bytes_written); + Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, fill %lld.", + now, end, gap, bytes_written / frame_size()); + _write_position += bytes_written / frame_size(); + } else if (position == _write_position) { + // Write as much as currently possible. + std::size_t write_limit = buffer.remaining(); + std::size_t bytes_written = 0; + ok = write_io(buffer.position(), write_limit, bytes_written); + _write_position += bytes_written / frame_size(); + buffer.advance(bytes_written); + } + // Make sure buffers finish in time, despite irregular progress (freewheel). + freewheel_finish(buffer, end, now); + return ok; + } + +private: + // Calculate write position of the remaining buffer. + std::int64_t buffer_position(std::size_t remaining, std::int64_t end) const { + return end - (remaining / frame_size()); + } + + // Indicate that a buffer doesn't need further processing. + bool buffer_done(const Buffer &buffer, std::int64_t end) const { + return buffer.done() && end <= _write_position; + } + + // Avoid stalled buffers with irregular OSS progress in freewheel mode. + std::int64_t freewheel_finish(Buffer &buffer, std::int64_t end, + std::int64_t now) { + std::int64_t advance = 0; + // Make sure buffers finish in time, despite irregular progress (freewheel). + if (freewheel() && now >= end + balance() && !buffer.done()) { + advance = buffer.advance(buffer.remaining()) / frame_size(); + Log::info(SOSSO_LOC, + "@%lld - %lld Write freewheel finish remaining buffer %lld.", + now, end, advance); + } + return advance; + } + + // Skip writing part of the buffer to match OSS write position. + std::int64_t buffer_advance(Buffer &buffer, std::int64_t frames) { + if (frames > 0) { + return buffer.advance(frames * frame_size()) / frame_size(); + } + return 0; + } + + // Rewind part of the buffer to match OSS write postion. + std::int64_t buffer_rewind(Buffer &buffer, std::int64_t frames) { + if (frames > 0) { + return buffer.rewind(frames * frame_size()) / frame_size(); + } + return 0; + } + + std::int64_t _oss_progress = 0; // Last memory mapped OSS progress. + std::int64_t _write_position = 0; // Current write position of the channel. +}; + +} // namespace sosso + +#endif // SOSSO_WRITECHANNEL_HPP