From 5bcc9dc2921361074717dcae417ab1d5ef9ec8af Mon Sep 17 00:00:00 2001 From: Florian Walpen Date: Sat, 3 Jun 2023 18:52:39 +0200 Subject: [PATCH 1/2] 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). --- freebsd/sosso/Buffer.hpp | 153 ++++++++ freebsd/sosso/Channel.hpp | 210 ++++++++++ freebsd/sosso/Correction.hpp | 112 ++++++ freebsd/sosso/Device.hpp | 678 +++++++++++++++++++++++++++++++++ freebsd/sosso/DoubleBuffer.hpp | 218 +++++++++++ freebsd/sosso/FrameClock.hpp | 141 +++++++ freebsd/sosso/Logging.hpp | 105 +++++ freebsd/sosso/ReadChannel.hpp | 255 +++++++++++++ freebsd/sosso/WriteChannel.hpp | 280 ++++++++++++++ 9 files changed, 2152 insertions(+) create mode 100644 freebsd/sosso/Buffer.hpp create mode 100644 freebsd/sosso/Channel.hpp create mode 100644 freebsd/sosso/Correction.hpp create mode 100644 freebsd/sosso/Device.hpp create mode 100644 freebsd/sosso/DoubleBuffer.hpp create mode 100644 freebsd/sosso/FrameClock.hpp create mode 100644 freebsd/sosso/Logging.hpp create mode 100644 freebsd/sosso/ReadChannel.hpp create mode 100644 freebsd/sosso/WriteChannel.hpp 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 From 0762eac06a8c327757acfa7c770e16d827610873 Mon Sep 17 00:00:00 2001 From: Florian Walpen Date: Thu, 2 Feb 2023 18:22:11 +0100 Subject: [PATCH 2/2] FreeBSD: Rewrite the OSS driver using sosso library. Make use of the sosso library to handle the low-level operation of OSS devices on FreeBSD. Further separate this part into the JackOSSChannel class, which should make it easier to reuse the code for JackOSSAdapter (not planned yet). JackOSSChannel features an additional assist thread which helps to process the OSS device when the driver thread is busy with the JACK dependency graph. Combined with the improvements of the sosso library, these changes bring JACK into the realm of real-time audio processing on FreeBSD, for select PCI sound cards (4-6ms round trip latency). --- freebsd/oss/JackOSSChannel.cpp | 492 ++++++++++++++ freebsd/oss/JackOSSChannel.h | 132 ++++ freebsd/oss/JackOSSDriver.cpp | 1135 +++++--------------------------- freebsd/oss/JackOSSDriver.h | 52 +- wscript | 1 + 5 files changed, 780 insertions(+), 1032 deletions(-) create mode 100644 freebsd/oss/JackOSSChannel.cpp create mode 100644 freebsd/oss/JackOSSChannel.h diff --git a/freebsd/oss/JackOSSChannel.cpp b/freebsd/oss/JackOSSChannel.cpp new file mode 100644 index 000000000..416e0828a --- /dev/null +++ b/freebsd/oss/JackOSSChannel.cpp @@ -0,0 +1,492 @@ +/* +Copyright (C) 2023 Florian Walpen + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include "JackOSSChannel.h" +#include "JackError.h" +#include "JackThread.h" +#include "memops.h" + +#include +#include +#include +#include +#include +#include +#include + +typedef jack_default_audio_sample_t jack_sample_t; + +namespace +{ + +int SuggestSampleFormat(int bits) +{ + switch(bits) { + // Native-endian signed 32 bit samples. + case 32: + return AFMT_S32_NE; + // Native-endian signed 24 bit (packed) samples. + case 24: + return AFMT_S24_NE; + // Native-endian signed 16 bit samples, used by default. + case 16: + default: + return AFMT_S16_NE; + } +} + +bool SupportedSampleFormat(int format) +{ + switch(format) { + // Only signed sample formats are supported by the conversion functions. + case AFMT_S16_NE: + case AFMT_S16_OE: + case AFMT_S24_NE: + case AFMT_S24_OE: + case AFMT_S32_NE: + case AFMT_S32_OE: + return true; + } + return false; +} + +void CopyAndConvertIn(jack_sample_t *dst, char *src, size_t nframes, int channel, int chcount, int format) +{ + switch (format) { + + case AFMT_S16_NE: + src += channel * 2; + sample_move_dS_s16(dst, src, nframes, chcount * 2); + break; + case AFMT_S16_OE: + src += channel * 2; + sample_move_dS_s16s(dst, src, nframes, chcount * 2); + break; + case AFMT_S24_NE: + src += channel * 3; + sample_move_dS_s24(dst, src, nframes, chcount * 3); + break; + case AFMT_S24_OE: + src += channel * 3; + sample_move_dS_s24s(dst, src, nframes, chcount * 3); + break; + case AFMT_S32_NE: + src += channel * 4; + sample_move_dS_s32(dst, src, nframes, chcount * 4); + break; + case AFMT_S32_OE: + src += channel * 4; + sample_move_dS_s32s(dst, src, nframes, chcount * 4); + break; + } +} + +void CopyAndConvertOut(char *dst, jack_sample_t *src, size_t nframes, int channel, int chcount, int format) +{ + switch (format) { + + case AFMT_S16_NE: + dst += channel * 2; + sample_move_d16_sS(dst, src, nframes, chcount * 2, NULL); + break; + case AFMT_S16_OE: + dst += channel * 2; + sample_move_d16_sSs(dst, src, nframes, chcount * 2, NULL); + break; + case AFMT_S24_NE: + dst += channel * 3; + sample_move_d24_sS(dst, src, nframes, chcount * 3, NULL); + break; + case AFMT_S24_OE: + dst += channel * 3; + sample_move_d24_sSs(dst, src, nframes, chcount * 3, NULL); + break; + case AFMT_S32_NE: + dst += channel * 4; + sample_move_d32_sS(dst, src, nframes, chcount * 4, NULL); + break; + case AFMT_S32_OE: + dst += channel * 4; + sample_move_d32_sSs(dst, src, nframes, chcount * 4, NULL); + break; + } +} + +} + +void sosso::Log::log(sosso::SourceLocation location, const char* message) { + jack_log(message); +} + +void sosso::Log::info(sosso::SourceLocation location, const char* message) { + jack_info(message); +} + +void sosso::Log::warn(sosso::SourceLocation location, const char* message) { + jack_error(message); +} + +namespace Jack +{ + +bool JackOSSChannel::InitialSetup(unsigned int sample_rate) +{ + fFrameStamp = 0; + fCorrection.clear(); + return fFrameClock.set_sample_rate(sample_rate); +} + +bool JackOSSChannel::OpenCapture(const char *device, bool exclusive, int bits, int &channels) +{ + if (channels == 0) channels = 2; + + int sample_format = SuggestSampleFormat(bits); + + if (!fReadChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) { + jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", sample_format); + return false; + } + + if (!fReadChannel.open(device, exclusive)) { + return false; + } + + if (fReadChannel.sample_rate() != fFrameClock.sample_rate()) { + jack_error("JackOSSChannel::OpenCapture driver forced sample rate %ld", fReadChannel.sample_rate()); + fReadChannel.close(); + return false; + } + + if (!SupportedSampleFormat(fReadChannel.sample_format())) { + jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", fReadChannel.sample_format()); + fReadChannel.close(); + return false; + } + + jack_log("JackOSSChannel::OpenCapture capture file descriptor = %d", fReadChannel.file_descriptor()); + + if (fReadChannel.channels() != channels) { + channels = fReadChannel.channels(); + jack_info("JackOSSChannel::OpenCapture driver forced the number of capture channels %ld", channels); + } + + fReadChannel.memory_map(); + + return true; +} + +bool JackOSSChannel::OpenPlayback(const char *device, bool exclusive, int bits, int &channels) +{ + if (channels == 0) channels = 2; + + int sample_format = SuggestSampleFormat(bits); + + if (!fWriteChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) { + jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", sample_format); + return false; + } + + if (!fWriteChannel.open(device, exclusive)) { + return false; + } + + if (fWriteChannel.sample_rate() != fFrameClock.sample_rate()) { + jack_error("JackOSSChannel::OpenPlayback driver forced sample rate %ld", fWriteChannel.sample_rate()); + fWriteChannel.close(); + return false; + } + + if (!SupportedSampleFormat(fWriteChannel.sample_format())) { + jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", fWriteChannel.sample_format()); + fWriteChannel.close(); + return false; + } + + jack_log("JackOSSChannel::OpenPlayback playback file descriptor = %d", fWriteChannel.file_descriptor()); + + if (fWriteChannel.channels() != channels) { + channels = fWriteChannel.channels(); + jack_info("JackOSSChannel::OpenPlayback driver forced the number of playback channels %ld", channels); + } + + fWriteChannel.memory_map(); + + return true; +} + +bool JackOSSChannel::Read(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end) +{ + if (fReadChannel.recording()) { + // Get buffer from read channel. + sosso::Buffer buffer = fReadChannel.take_buffer(); + + // Get recording audio data and then clear buffer. + for (unsigned i = 0; i < fReadChannel.channels(); i++) { + if (sample_buffers[i]) { + CopyAndConvertIn(sample_buffers[i], buffer.data(), length, i, fReadChannel.channels(), fReadChannel.sample_format()); + } + } + buffer.reset(); + + // Put buffer back to capture at requested end position. + fReadChannel.set_buffer(std::move(buffer), end); + SignalWork(); + return true; + } + return false; +} + +bool JackOSSChannel::Write(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end) +{ + if (fWriteChannel.playback()) { + // Get buffer from write channel. + sosso::Buffer buffer = fWriteChannel.take_buffer(); + + // Clear buffer and write new playback audio data. + memset(buffer.data(), 0, buffer.length()); + buffer.reset(); + for (unsigned i = 0; i < fWriteChannel.channels(); i++) { + if (sample_buffers[i]) { + CopyAndConvertOut(buffer.data(), sample_buffers[i], length, i, fWriteChannel.channels(), fWriteChannel.sample_format()); + } + } + + // Put buffer back to playback at requested end position. + end += PlaybackCorrection(); + fWriteChannel.set_buffer(std::move(buffer), end); + SignalWork(); + return true; + } + return false; +} + +bool JackOSSChannel::StartChannels(unsigned int buffer_frames) +{ + int group_id = 0; + + if (fReadChannel.recording()) { + // Allocate two recording buffers for double buffering. + size_t buffer_size = buffer_frames * fReadChannel.frame_size(); + sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size); + assert(buffer.data()); + fReadChannel.set_buffer(std::move(buffer), 0); + buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size); + assert(buffer.data()); + fReadChannel.set_buffer(std::move(buffer), buffer_frames); + // Add recording channel to synced start group. + fReadChannel.add_to_sync_group(group_id); + } + + if (fWriteChannel.playback()) { + // Allocate two playback buffers for double buffering. + size_t buffer_size = buffer_frames * fWriteChannel.frame_size(); + sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size); + assert(buffer.data()); + fWriteChannel.set_buffer(std::move(buffer), 0); + buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size); + assert(buffer.data()); + fWriteChannel.set_buffer(std::move(buffer), buffer_frames); + // Add playback channel to synced start group. + fWriteChannel.add_to_sync_group(group_id); + } + + // Start both channels in sync if supported. + if (fReadChannel.recording()) { + fReadChannel.start_sync_group(group_id); + } else { + fWriteChannel.start_sync_group(group_id); + } + + // Init frame clock here to mark start time. + if (!fFrameClock.init_clock(fFrameClock.sample_rate())) { + return false; + } + + // Small drift corrections to keep latency whithin +/- 1ms. + std::int64_t limit = fFrameClock.sample_rate() / 1000; + fCorrection.set_drift_limits(-limit, limit); + // Drastic corrections when drift exceeds half a period. + limit = std::max(limit, buffer_frames / 2); + fCorrection.set_loss_limits(-limit, limit); + + SignalWork(); + + return true; +} + +bool JackOSSChannel::StopChannels() +{ + if (fReadChannel.recording()) { + free(fReadChannel.take_buffer().data()); + free(fReadChannel.take_buffer().data()); + fReadChannel.memory_unmap(); + fReadChannel.close(); + } + + if (fWriteChannel.playback()) { + free(fWriteChannel.take_buffer().data()); + free(fWriteChannel.take_buffer().data()); + fWriteChannel.memory_unmap(); + fWriteChannel.close(); + } + + return true; +} + +bool JackOSSChannel::StartAssistThread(bool realtime, int priority) +{ + if (fAssistThread.Start() >= 0) { + if (realtime && fAssistThread.AcquireRealTime(priority) != 0) { + jack_error("JackOSSChannel::StartAssistThread realtime priority failed."); + } + return true; + } + return false; +} + +bool JackOSSChannel::StopAssistThread() +{ + if (fAssistThread.GetStatus() != JackThread::kIdle) { + fAssistThread.SetStatus(JackThread::kIdle); + SignalWork(); + fAssistThread.Kill(); + } + return true; +} + +bool JackOSSChannel::CheckTimeAndRun() +{ + // Check current frame time. + if (!fFrameClock.now(fFrameStamp)) { + jack_error("JackOSSChannel::CheckTimeAndRun(): Frame clock failed."); + return false; + } + std::int64_t now = fFrameStamp; + + // Process read channel if wakeup time passed, or OSS buffer data available. + if (fReadChannel.recording() && !fReadChannel.total_finished(now)) { + if (now >= fReadChannel.wakeup_time(now)) { + if (!fReadChannel.process(now)) { + jack_error("JackOSSChannel::CheckTimeAndRun(): Read process failed."); + return false; + } + } + } + // Process write channel if wakeup time passed, or OSS buffer space available. + if (fWriteChannel.playback() && !fWriteChannel.total_finished(now)) { + if (now >= fWriteChannel.wakeup_time(now)) { + if (!fWriteChannel.process(now)) { + jack_error("JackOSSChannel::CheckTimeAndRun(): Write process failed."); + return false; + } + } + } + + return true; +} + +bool JackOSSChannel::Sleep() const +{ + std::int64_t wakeup = NextWakeup(); + if (wakeup > fFrameStamp) { + return fFrameClock.sleep(wakeup); + } + return true; +} + +bool JackOSSChannel::CaptureFinished() const +{ + return fReadChannel.finished(fFrameStamp); +} + +bool JackOSSChannel::PlaybackFinished() const +{ + return fWriteChannel.finished(fFrameStamp); +} + +std::int64_t JackOSSChannel::PlaybackCorrection() +{ + std::int64_t correction = 0; + // If both channels are used, correct drift relative to recording balance. + if (fReadChannel.recording() && fWriteChannel.playback()) { + std::int64_t previous = fCorrection.correction(); + correction = fCorrection.correct(fWriteChannel.balance(), fReadChannel.balance()); + if (correction != previous) { + jack_info("Playback correction changed from %lld to %lld.", previous, correction); + jack_info("Read balance %lld vs write balance %lld.", fReadChannel.balance(), fWriteChannel.balance()); + } + } + return correction; +} + +bool JackOSSChannel::Init() +{ + return true; +} + +bool JackOSSChannel::Execute() +{ + if (Lock()) { + if (fAssistThread.GetStatus() != JackThread::kIdle) { + if (!CheckTimeAndRun()) { + return Unlock() && false; + } + std::int64_t wakeup = NextWakeup(); + if (fReadChannel.total_finished(fFrameStamp) && fWriteChannel.total_finished(fFrameStamp)) { + // Nothing to do, wait on the mutex for work. + jack_info("JackOSSChannel::Execute waiting for work."); + fMutex.TimedWait(1000000); + jack_info("JackOSSChannel::Execute resuming work."); + } else if (fFrameStamp < wakeup) { + // Unlock mutex before going to sleep, let others process. + return Unlock() && fFrameClock.sleep(wakeup); + } + } + return Unlock(); + } + return false; +} + +std::int64_t JackOSSChannel::XRunGap() const +{ + // Compute processing gap in case we are late. + std::int64_t max_end = std::max(fReadChannel.total_end(), fWriteChannel.total_end()); + if (max_end < fFrameStamp) { + return fFrameStamp - max_end; + } + return 0; +} + +void JackOSSChannel::ResetBuffers(std::int64_t offset) +{ + // Clear buffers and offset their positions, after processing gaps. + if (fReadChannel.recording()) { + fReadChannel.reset_buffers(fReadChannel.end_frames() + offset); + } + if (fWriteChannel.playback()) { + fWriteChannel.reset_buffers(fWriteChannel.end_frames() + offset); + } +} + +std::int64_t JackOSSChannel::NextWakeup() const +{ + return std::min(fReadChannel.wakeup_time(fFrameStamp), fWriteChannel.wakeup_time(fFrameStamp)); +} + +} // end of namespace diff --git a/freebsd/oss/JackOSSChannel.h b/freebsd/oss/JackOSSChannel.h new file mode 100644 index 000000000..d1a4a969e --- /dev/null +++ b/freebsd/oss/JackOSSChannel.h @@ -0,0 +1,132 @@ +/* +Copyright (C) 2003-2007 Jussi Laako +Copyright (C) 2008 Grame & RTL 2008 + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef __JackOSSChannel__ +#define __JackOSSChannel__ + +#include "JackMutex.h" +#include "JackThread.h" +#include "sosso/Correction.hpp" +#include "sosso/DoubleBuffer.hpp" +#include "sosso/FrameClock.hpp" +#include "sosso/ReadChannel.hpp" +#include "sosso/WriteChannel.hpp" + +namespace Jack +{ + +typedef jack_default_audio_sample_t jack_sample_t; + +/*! +\brief The OSS driver. +*/ + +class JackOSSChannel : public JackRunnableInterface +{ + + private: + JackThread fAssistThread; + JackProcessSync fMutex; + sosso::FrameClock fFrameClock; + sosso::DoubleBuffer fReadChannel; + sosso::DoubleBuffer fWriteChannel; + sosso::Correction fCorrection; + + std::int64_t fFrameStamp = 0; + + public: + + JackOSSChannel() : fAssistThread(this) + {} + virtual ~JackOSSChannel() + {} + + sosso::DoubleBuffer &Capture() + { + return fReadChannel; + } + + sosso::DoubleBuffer &Playback() + { + return fWriteChannel; + } + + sosso::FrameClock &FrameClock() + { + return fFrameClock; + } + + bool Lock() + { + return fMutex.Lock(); + } + + bool Unlock() + { + return fMutex.Unlock(); + } + + void SignalWork() + { + fMutex.SignalAll(); + } + + bool InitialSetup(unsigned sample_rate); + + bool OpenCapture(const char* device, bool exclusive, int bits, int &channels); + bool OpenPlayback(const char* device, bool exclusive, int bits, int &channels); + + bool Read(jack_sample_t** sample_buffers, jack_nframes_t length, std::int64_t end); + bool Write(jack_sample_t** sample_buffers, jack_nframes_t length, std::int64_t end); + + bool StartChannels(unsigned buffer_frames); + bool StopChannels(); + + bool StartAssistThread(bool realtime, int priority); + bool StopAssistThread(); + + bool CheckTimeAndRun(); + + bool Sleep() const; + + bool CaptureFinished() const; + bool PlaybackFinished() const; + + std::int64_t PlaybackCorrection(); + + virtual bool Init(); + + virtual bool Execute(); + + std::int64_t XRunGap() const; + + void ResetBuffers(std::int64_t offset); + + std::int64_t FrameStamp() const + { + return fFrameStamp; + } + + std::int64_t NextWakeup() const; +}; + +} // end of namespace + +#endif diff --git a/freebsd/oss/JackOSSDriver.cpp b/freebsd/oss/JackOSSDriver.cpp index d6f3e86cf..b2b90d5d1 100644 --- a/freebsd/oss/JackOSSDriver.cpp +++ b/freebsd/oss/JackOSSDriver.cpp @@ -20,103 +20,17 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #include "driver_interface.h" #include "JackThreadedDriver.h" -#include "JackDriverLoader.h" #include "JackOSSDriver.h" #include "JackEngineControl.h" #include "JackGraphManager.h" #include "JackError.h" #include "JackTime.h" -#include "JackShmMem.h" -#include "memops.h" - -#include -#include -#include -#include -#include + +#include #include using namespace std; -namespace -{ - -inline jack_nframes_t TimeToFrames(jack_time_t time, jack_nframes_t sample_rate) { - return ((time * sample_rate) + 500000ULL) / 1000000ULL; -} - -inline long long TimeToOffset(jack_time_t time1, jack_time_t time2, jack_nframes_t sample_rate) -{ - if (time2 > time1) { - return TimeToFrames(time2 - time1, sample_rate); - } else { - return 0LL - TimeToFrames(time1 - time2, sample_rate); - } -} - -inline jack_time_t FramesToTime(jack_nframes_t frames, jack_nframes_t sample_rate) { - return ((frames * 1000000ULL) + (sample_rate / 2ULL)) / sample_rate; -} - -inline jack_nframes_t RoundUp(jack_nframes_t frames, jack_nframes_t block) { - if (block > 0) { - frames += (block - 1); - frames -= (frames % block); - } - return frames; -} - -inline jack_time_t RoundDown(jack_time_t time, jack_time_t interval) { - if (interval > 0) { - time -= (time % interval); - } - return time; -} - -int GetSampleFormat(int bits) -{ - switch(bits) { - // Native-endian signed 32 bit samples. - case 32: - return AFMT_S32_NE; - // Native-endian signed 24 bit (packed) samples. - case 24: - return AFMT_S24_NE; - // Native-endian signed 16 bit samples, used by default. - case 16: - default: - return AFMT_S16_NE; - } -} - -unsigned int GetSampleSize(int format) -{ - switch(format) { - // Native-endian signed 32 bit samples. - case AFMT_S32_NE: - return 4; - // Native-endian signed 24 bit (packed) samples. - case AFMT_S24_NE: - return 3; - // Native-endian signed 16 bit samples. - case AFMT_S16_NE: - return 2; - // Unsupported sample format. - default: - return 0; - } -} - -inline int UpToPower2(int x) -{ - int r = 0; - while ((1 << r) < x) - r++; - return r; -} - -} - namespace Jack { @@ -144,707 +58,6 @@ int gCycleCount = 0; #endif -static inline void CopyAndConvertIn(jack_sample_t *dst, void *src, size_t nframes, int channel, int chcount, int bits) -{ - switch (bits) { - - case 16: { - signed short *s16src = (signed short*)src; - s16src += channel; - sample_move_dS_s16(dst, (char*)s16src, nframes, chcount<<1); - break; - } - case 24: { - char *s24src = (char*)src; - s24src += channel * 3; - sample_move_dS_s24(dst, s24src, nframes, chcount*3); - break; - } - case 32: { - signed int *s32src = (signed int*)src; - s32src += channel; - sample_move_dS_s32u24(dst, (char*)s32src, nframes, chcount<<2); - break; - } - } -} - -static inline void CopyAndConvertOut(void *dst, jack_sample_t *src, size_t nframes, int channel, int chcount, int bits) -{ - switch (bits) { - - case 16: { - signed short *s16dst = (signed short*)dst; - s16dst += channel; - sample_move_d16_sS((char*)s16dst, src, nframes, chcount<<1, NULL); // No dithering for now... - break; - } - case 24: { - char *s24dst = (char*)dst; - s24dst += channel * 3; - sample_move_d24_sS(s24dst, src, nframes, chcount*3, NULL); - break; - } - case 32: { - signed int *s32dst = (signed int*)dst; - s32dst += channel; - sample_move_d32u24_sS((char*)s32dst, src, nframes, chcount<<2, NULL); - break; - } - } -} - -void JackOSSDriver::DisplayDeviceInfo() -{ - audio_buf_info info; - memset(&info, 0, sizeof(audio_buf_info)); - int cap = 0; - - // Duplex cards : http://manuals.opensound.com/developer/full_duplex.html - jack_info("Audio Interface Description :"); - jack_info("Sampling Frequency : %d, Sample Size : %d", fEngineControl->fSampleRate, fInSampleSize * 8); - - if (fPlayback) { - - oss_sysinfo si; - if (ioctl(fOutFD, OSS_SYSINFO, &si) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo OSS_SYSINFO failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("OSS product %s", si.product); - jack_info("OSS version %s", si.version); - jack_info("OSS version num %d", si.versionnum); - jack_info("OSS numaudios %d", si.numaudios); - jack_info("OSS numaudioengines %d", si.numaudioengines); - jack_info("OSS numcards %d", si.numcards); - } - - jack_info("Output capabilities - %d channels : ", fPlaybackChannels); - jack_info("Output block size = %d", fOutputBufferSize); - - if (ioctl(fOutFD, SNDCTL_DSP_GETOSPACE, &info) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETOSPACE failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("output space info: fragments = %d, fragstotal = %d, fragsize = %d, bytes = %d", - info.fragments, info.fragstotal, info.fragsize, info.bytes); - } - - if (ioctl(fOutFD, SNDCTL_DSP_GETCAPS, &cap) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETCAPS failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - if (cap & DSP_CAP_DUPLEX) jack_info(" DSP_CAP_DUPLEX"); - if (cap & DSP_CAP_REALTIME) jack_info(" DSP_CAP_REALTIME"); - if (cap & DSP_CAP_BATCH) jack_info(" DSP_CAP_BATCH"); - if (cap & DSP_CAP_COPROC) jack_info(" DSP_CAP_COPROC"); - if (cap & DSP_CAP_TRIGGER) jack_info(" DSP_CAP_TRIGGER"); - if (cap & DSP_CAP_MMAP) jack_info(" DSP_CAP_MMAP"); - if (cap & DSP_CAP_MULTI) jack_info(" DSP_CAP_MULTI"); - if (cap & DSP_CAP_BIND) jack_info(" DSP_CAP_BIND"); - } - } - - if (fCapture) { - - oss_sysinfo si; - if (ioctl(fInFD, OSS_SYSINFO, &si) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo OSS_SYSINFO failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("OSS product %s", si.product); - jack_info("OSS version %s", si.version); - jack_info("OSS version num %d", si.versionnum); - jack_info("OSS numaudios %d", si.numaudios); - jack_info("OSS numaudioengines %d", si.numaudioengines); - jack_info("OSS numcards %d", si.numcards); - } - - jack_info("Input capabilities - %d channels : ", fCaptureChannels); - jack_info("Input block size = %d", fInputBufferSize); - - if (ioctl(fInFD, SNDCTL_DSP_GETISPACE, &info) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETOSPACE failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - jack_info("input space info: fragments = %d, fragstotal = %d, fragsize = %d, bytes = %d", - info.fragments, info.fragstotal, info.fragsize, info.bytes); - } - - if (ioctl(fInFD, SNDCTL_DSP_GETCAPS, &cap) == -1) { - jack_error("JackOSSDriver::DisplayDeviceInfo SNDCTL_DSP_GETCAPS failed : %s@%i, errno = %d", __FILE__, __LINE__, errno); - } else { - if (cap & DSP_CAP_DUPLEX) jack_info(" DSP_CAP_DUPLEX"); - if (cap & DSP_CAP_REALTIME) jack_info(" DSP_CAP_REALTIME"); - if (cap & DSP_CAP_BATCH) jack_info(" DSP_CAP_BATCH"); - if (cap & DSP_CAP_COPROC) jack_info(" DSP_CAP_COPROC"); - if (cap & DSP_CAP_TRIGGER) jack_info(" DSP_CAP_TRIGGER"); - if (cap & DSP_CAP_MMAP) jack_info(" DSP_CAP_MMAP"); - if (cap & DSP_CAP_MULTI) jack_info(" DSP_CAP_MULTI"); - if (cap & DSP_CAP_BIND) jack_info(" DSP_CAP_BIND"); - } - } -} - -int JackOSSDriver::ProbeInBlockSize() -{ - jack_nframes_t blocks[8] = {0, 0, 0, 0, 0, 0, 0, 0}; - int probes = 0; - int ret = 0; - // Default values in case of an error. - fInMeanStep = fEngineControl->fBufferSize; - fInBlockSize = 1; - - if (fInFD > 0) { - // Read one frame into a new hardware block so we can check its size. - // Repeat that for multiple probes, sometimes the first reads differ. - jack_nframes_t frames = 1; - for (int p = 0; p < 8 && frames > 0; ++p) { - ret = Discard(frames); - frames = 0; - if (ret == 0) { - oss_count_t ptr; - if (ioctl(fInFD, SNDCTL_DSP_CURRENT_IPTR, &ptr) == 0 && ptr.fifo_samples > 0) { - // Success, store probed hardware block size for later. - blocks[p] = 1U + ptr.fifo_samples; - ++probes; - // Proceed by reading one frame into the next hardware block. - frames = blocks[p]; - } - } else { - // Read error - abort. - jack_error("JackOSSDriver::ProbeInBlockSize read failed with %d", ret); - } - } - - // Stop recording. - ioctl(fInFD, SNDCTL_DSP_HALT_INPUT, NULL); - } - - if (probes == 8) { - // Compute mean block size of the last six probes. - jack_nframes_t sum = 0; - for (int p = 2; p < 8; ++p) { - jack_log("JackOSSDriver::ProbeInBlockSize read block of %d frames", blocks[p]); - sum += blocks[p]; - } - fInMeanStep = sum / 6; - - // Check that none of the probed block sizes deviates too much. - jack_nframes_t slack = fInMeanStep / 16; - bool strict = true; - for (int p = 2; p < 8; ++p) { - strict = strict && (blocks[p] > fInMeanStep - slack) && (blocks[p] < fInMeanStep + slack); - } - - if (strict && fInMeanStep <= fEngineControl->fBufferSize) { - // Regular hardware block size, use it for rounding. - jack_info("JackOSSDriver::ProbeInBlockSize read blocks are %d frames", fInMeanStep); - fInBlockSize = fInMeanStep; - } else { - jack_info("JackOSSDriver::ProbeInBlockSize irregular read block sizes"); - jack_info("JackOSSDriver::ProbeInBlockSize mean read block was %d frames", fInMeanStep); - } - - if (fInBlockSize > fEngineControl->fBufferSize / 2) { - jack_info("JackOSSDriver::ProbeInBlockSize less than two read blocks per cycle"); - jack_info("JackOSSDriver::ProbeInBlockSize for best results make period a multiple of %d", fInBlockSize); - } - - if (fInMeanStep > fEngineControl->fBufferSize) { - jack_error("JackOSSDriver::ProbeInBlockSize period is too small, minimum is %d frames", fInMeanStep); - return -1; - } - } - - return ret; -} - -int JackOSSDriver::ProbeOutBlockSize() -{ - jack_nframes_t blocks[8] = {0, 0, 0, 0, 0, 0, 0, 0}; - int probes = 0; - int ret = 0; - // Default values in case of an error. - fOutMeanStep = fEngineControl->fBufferSize; - fOutBlockSize = 1; - - if (fOutFD) { - // Write one frame over the low water mark, then check the consumed block size. - // Repeat that for multiple probes, sometimes the initial ones differ. - jack_nframes_t mark = fNperiods * fEngineControl->fBufferSize; - WriteSilence(mark + 1); - for (int p = 0; p < 8 && ret >= 0; ++p) { - pollfd poll_fd; - poll_fd.fd = fOutFD; - poll_fd.events = POLLOUT; - ret = poll(&poll_fd, 1, 500); - if (ret < 0) { - jack_error("JackOSSDriver::ProbeOutBlockSize poll failed with %d", ret); - break; - } - if (poll_fd.revents & POLLOUT) { - oss_count_t ptr; - if (ioctl(fOutFD, SNDCTL_DSP_CURRENT_OPTR, &ptr) != -1 && ptr.fifo_samples >= 0) { - // Success, store probed hardware block size for later. - blocks[p] = mark + 1 - ptr.fifo_samples; - ++probes; - // Proceed by writing one frame over the low water mark. - WriteSilence(blocks[p]); - } - poll_fd.revents = 0; - } - } - - // Stop playback. - ioctl(fOutFD, SNDCTL_DSP_HALT_INPUT, NULL); - } - - if (probes == 8) { - // Compute mean and maximum block size of the last six probes. - jack_nframes_t sum = 0; - for (int p = 2; p < 8; ++p) { - jack_log("JackOSSDriver::ProbeOutBlockSize write block of %d frames", blocks[p]); - sum += blocks[p]; - } - fOutMeanStep = sum / 6; - - // Check that none of the probed block sizes deviates too much. - jack_nframes_t slack = fOutMeanStep / 16; - bool strict = true; - for (int p = 2; p < 8; ++p) { - strict = strict && (blocks[p] > fOutMeanStep - slack) && (blocks[p] < fOutMeanStep + slack); - } - - if (strict && fOutMeanStep <= fEngineControl->fBufferSize) { - // Regular hardware block size, use it for rounding. - jack_info("JackOSSDriver::ProbeOutBlockSize write blocks are %d frames", fOutMeanStep); - fOutBlockSize = fOutMeanStep; - } else { - jack_info("JackOSSDriver::ProbeOutBlockSize irregular write block sizes"); - jack_info("JackOSSDriver::ProbeOutBlockSize mean write block was %d frames", fOutMeanStep); - } - - if (fOutBlockSize > fEngineControl->fBufferSize / 2) { - jack_info("JackOSSDriver::ProbeOutBlockSize less than two write blocks per cycle"); - jack_info("JackOSSDriver::ProbeOutBlockSize for best results make period a multiple of %d", fOutBlockSize); - } - - if (fOutMeanStep > fEngineControl->fBufferSize) { - jack_error("JackOSSDriver::ProbeOutBlockSize period is too small, minimum is %d frames", fOutMeanStep); - return -1; - } - } - - return ret; -} - -int JackOSSDriver::Discard(jack_nframes_t frames) -{ - if (fInFD < 0) { - return -1; - } - - // Read frames from OSS capture buffer to be discarded. - ssize_t size = frames * fInSampleSize * fCaptureChannels; - while (size > 0) { - ssize_t chunk = (size > fInputBufferSize) ? fInputBufferSize : size; - ssize_t count = ::read(fInFD, fInputBuffer, chunk); - if (count <= 0) { - jack_error("JackOSSDriver::Discard error bytes read = %ld", count); - return -1; - } - fOSSReadOffset += count / (fInSampleSize * fCaptureChannels); - size -= count; - } - return 0; -} - -int JackOSSDriver::WriteSilence(jack_nframes_t frames) -{ - if (fOutFD < 0) { - return -1; - } - - // Fill OSS playback buffer, write some periods of silence. - memset(fOutputBuffer, 0, fOutputBufferSize); - ssize_t size = frames * fOutSampleSize * fPlaybackChannels; - while (size > 0) { - ssize_t chunk = (size > fOutputBufferSize) ? fOutputBufferSize : size; - ssize_t count = ::write(fOutFD, fOutputBuffer, chunk); - if (count <= 0) { - jack_error("JackOSSDriver::WriteSilence error bytes written = %ld", count); - return -1; - } - fOSSWriteOffset += (count / (fOutSampleSize * fPlaybackChannels)); - size -= count; - } - return 0; -} - -int JackOSSDriver::WaitAndSync() -{ - oss_count_t ptr = {0, 0, {0}}; - if (fInFD > 0 && fOSSReadSync != 0) { - // Predict time of next capture sync (poll() return). - if (fOSSReadOffset + fEngineControl->fBufferSize > 0) { - jack_nframes_t frames = fOSSReadOffset + fEngineControl->fBufferSize; - jack_nframes_t rounded = RoundUp(frames, fInBlockSize); - fOSSReadSync += FramesToTime(rounded, fEngineControl->fSampleRate); - fOSSReadOffset -= rounded; - } - } - if (fOutFD > 0 && fOSSWriteSync != 0) { - // Predict time of next playback sync (poll() return). - if (fOSSWriteOffset > fNperiods * fEngineControl->fBufferSize) { - jack_nframes_t frames = fOSSWriteOffset - fNperiods * fEngineControl->fBufferSize; - jack_nframes_t rounded = RoundUp(frames, fOutBlockSize); - fOSSWriteSync += FramesToTime(rounded, fEngineControl->fSampleRate); - fOSSWriteOffset -= rounded; - } - } - jack_time_t poll_start = GetMicroSeconds(); - // Poll until recording and playback buffer are ready for this cycle. - pollfd poll_fd[2]; - poll_fd[0].fd = fInFD; - if (fInFD > 0 && (fForceSync || poll_start < fOSSReadSync)) { - poll_fd[0].events = POLLIN; - } else { - poll_fd[0].events = 0; - } - poll_fd[1].fd = fOutFD; - if (fOutFD > 0 && (fForceSync || poll_start < fOSSWriteSync)) { - poll_fd[1].events = POLLOUT; - } else { - poll_fd[1].events = 0; - } - while (poll_fd[0].events != 0 || poll_fd[1].events != 0) { - poll_fd[0].revents = 0; - poll_fd[1].revents = 0; - int ret = poll(poll_fd, 2, 500); - jack_time_t now = GetMicroSeconds(); - if (ret <= 0) { - jack_error("JackOSSDriver::WaitAndSync poll failed with %d after %ld us", ret, now - poll_start); - return ret; - } - if (poll_fd[0].revents & POLLIN) { - // Check the excess recording frames. - if (ioctl(fInFD, SNDCTL_DSP_CURRENT_IPTR, &ptr) != -1 && ptr.fifo_samples >= 0) { - if (fInBlockSize <= 1) { - // Irregular block size, let sync time converge slowly when late. - fOSSReadSync = min(fOSSReadSync, now) / 2 + now / 2; - fOSSReadOffset = -ptr.fifo_samples; - } else if (ptr.fifo_samples - fEngineControl->fBufferSize >= fInBlockSize) { - // Too late for a reliable sync, make sure sync time is not in the future. - if (now < fOSSReadSync) { - fOSSReadOffset = -ptr.fifo_samples; - jack_info("JackOSSDriver::WaitAndSync capture sync %ld us early, %ld frames", fOSSReadSync - now, fOSSReadOffset); - fOSSReadSync = now; - } - } else if (fForceSync) { - // Uncertain previous sync, just use sync time directly. - fOSSReadSync = now; - fOSSReadOffset = -ptr.fifo_samples; - } else { - // Adapt expected sync time when early or late - in whole block intervals. - // Account for some speed drift, but otherwise round down to earlier interval. - jack_time_t interval = FramesToTime(fInBlockSize, fEngineControl->fSampleRate); - jack_time_t remainder = fOSSReadSync % interval; - jack_time_t max_drift = interval / 4; - jack_time_t rounded = RoundDown((now - remainder) + max_drift, interval) + remainder; - // Let sync time converge slowly when late, prefer earlier sync times. - fOSSReadSync = min(rounded, now) / 2 + now / 2; - fOSSReadOffset = -ptr.fifo_samples; - } - } - poll_fd[0].events = 0; - } - if (poll_fd[1].revents & POLLOUT) { - // Check the remaining playback frames. - if (ioctl(fOutFD, SNDCTL_DSP_CURRENT_OPTR, &ptr) != -1 && ptr.fifo_samples >= 0) { - if (fOutBlockSize <= 1) { - // Irregular block size, let sync time converge slowly when late. - fOSSWriteSync = min(fOSSWriteSync, now) / 2 + now / 2; - fOSSWriteOffset = ptr.fifo_samples; - } else if (ptr.fifo_samples + fOutBlockSize <= fNperiods * fEngineControl->fBufferSize) { - // Too late for a reliable sync, make sure sync time is not in the future. - if (now < fOSSWriteSync) { - fOSSWriteOffset = ptr.fifo_samples; - jack_info("JackOSSDriver::WaitAndSync playback sync %ld us early, %ld frames", fOSSWriteSync - now, fOSSWriteOffset); - fOSSWriteSync = now; - } - } else if (fForceSync) { - // Uncertain previous sync, just use sync time directly. - fOSSWriteSync = now; - fOSSWriteOffset = ptr.fifo_samples; - } else { - // Adapt expected sync time when early or late - in whole block intervals. - // Account for some speed drift, but otherwise round down to earlier interval. - jack_time_t interval = FramesToTime(fOutBlockSize, fEngineControl->fSampleRate); - jack_time_t remainder = fOSSWriteSync % interval; - jack_time_t max_drift = interval / 4; - jack_time_t rounded = RoundDown((now - remainder) + max_drift, interval) + remainder; - // Let sync time converge slowly when late, prefer earlier sync times. - fOSSWriteSync = min(rounded, now) / 2 + now / 2; - fOSSWriteOffset = ptr.fifo_samples; - } - } - poll_fd[1].events = 0; - } - } - - fForceSync = false; - - // Compute balance of read and write buffers combined. - fBufferBalance = 0; - if (fInFD > 0 && fOutFD > 0) { - // Compare actual buffer content with target of (1 + n) * period. - fBufferBalance += ((1 + fNperiods) * fEngineControl->fBufferSize); - fBufferBalance -= (fOSSWriteOffset - fOSSReadOffset); - fBufferBalance += TimeToOffset(fOSSWriteSync, fOSSReadSync, fEngineControl->fSampleRate); - - // Force balancing if sync times deviate too much. - jack_time_t slack = FramesToTime((fEngineControl->fBufferSize * 2) / 3, fEngineControl->fSampleRate); - fForceBalancing = fForceBalancing || (fOSSReadSync > fOSSWriteSync + slack); - fForceBalancing = fForceBalancing || (fOSSWriteSync > fOSSReadSync + slack); - // Force balancing if buffer is badly balanced. - fForceBalancing = fForceBalancing || (abs(fBufferBalance) > max(fInMeanStep, fOutMeanStep)); - } - - // Print debug info every 10 seconds. - if (ptr.samples > 0 && (ptr.samples % (10 * fEngineControl->fSampleRate)) < fEngineControl->fBufferSize) { - jack_log("JackOSSDriver::Read buffer balance is %ld frames", fBufferBalance); - jack_time_t now = GetMicroSeconds(); - jack_log("JackOSSDriver::Read recording sync %ld frames %ld us ago", fOSSReadOffset, now - fOSSReadSync); - jack_log("JackOSSDriver::Read playback sync %ld frames %ld us ago", fOSSWriteOffset, now - fOSSWriteSync); - } - - return 0; -} - -int JackOSSDriver::OpenInput() -{ - int flags = 0; - int gFragFormat; - int cur_capture_channels; - int cur_sample_format; - jack_nframes_t cur_sample_rate; - audio_buf_info info; - - if (fCaptureChannels == 0) fCaptureChannels = 2; - - if ((fInFD = open(fCaptureDriverName, O_RDONLY | ((fExcl) ? O_EXCL : 0))) < 0) { - jack_error("JackOSSDriver::OpenInput failed to open device : %s@%i, errno = %d", __FILE__, __LINE__, errno); - return -1; - } - - jack_log("JackOSSDriver::OpenInput input fInFD = %d", fInFD); - - if (fExcl) { - if (ioctl(fInFD, SNDCTL_DSP_COOKEDMODE, &flags) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set cooked mode : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - } - - cur_sample_format = GetSampleFormat(fBits); - if (ioctl(fInFD, SNDCTL_DSP_SETFMT, &cur_sample_format) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set format : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fInSampleSize = GetSampleSize(cur_sample_format); - if (cur_sample_format != GetSampleFormat(fBits)) { - if (fInSampleSize > 0) { - jack_info("JackOSSDriver::OpenInput driver forced %d bit sample format", fInSampleSize * 8); - } else { - jack_error("JackOSSDriver::OpenInput unsupported sample format %#x", cur_sample_format); - goto error; - } - } - - cur_capture_channels = fCaptureChannels; - if (ioctl(fInFD, SNDCTL_DSP_CHANNELS, &fCaptureChannels) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set channels : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_capture_channels != fCaptureChannels) { - jack_info("JackOSSDriver::OpenInput driver forced the number of capture channels %ld", fCaptureChannels); - } - - cur_sample_rate = fEngineControl->fSampleRate; - if (ioctl(fInFD, SNDCTL_DSP_SPEED, &fEngineControl->fSampleRate) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set sample rate : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_sample_rate != fEngineControl->fSampleRate) { - jack_info("JackOSSDriver::OpenInput driver forced the sample rate %ld", fEngineControl->fSampleRate); - } - - // Internal buffer size required for one period. - fInputBufferSize = fEngineControl->fBufferSize * fInSampleSize * fCaptureChannels; - - // Get the total size of the OSS recording buffer, in sample frames. - info = {0, 0, 0, 0}; - if (ioctl(fInFD, SNDCTL_DSP_GETISPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenInput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSInBuffer = info.fragstotal * info.fragsize / (fInSampleSize * fCaptureChannels); - - if (fOSSInBuffer < fEngineControl->fBufferSize * (1 + fNperiods)) { - // Total size of the OSS recording buffer is too small, resize it. - unsigned int buf_size = fInputBufferSize * (1 + fNperiods); - // Keep current fragment size if possible - respect OSS latency settings. - gFragFormat = UpToPower2(info.fragsize); - unsigned int frag_size = 1U << gFragFormat; - gFragFormat |= ((buf_size + frag_size - 1) / frag_size) << 16; - jack_info("JackOSSDriver::OpenInput request %d fragments of %d", (gFragFormat >> 16), frag_size); - if (ioctl(fInFD, SNDCTL_DSP_SETFRAGMENT, &gFragFormat) == -1) { - jack_error("JackOSSDriver::OpenInput failed to set fragments : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - // Check the new OSS recording buffer size. - info = {0, 0, 0, 0}; - if (ioctl(fInFD, SNDCTL_DSP_GETISPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenInput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSInBuffer = info.fragstotal * info.fragsize / (fInSampleSize * fCaptureChannels); - } - - if (fOSSInBuffer > fEngineControl->fBufferSize) { - int mark = fInputBufferSize; - if (ioctl(fInFD, SNDCTL_DSP_LOW_WATER, &mark) != 0) { - jack_error("JackOSSDriver::OpenInput failed to set low water mark : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - jack_info("JackOSSDriver::OpenInput set low water mark to %d", mark); - } - - fInputBuffer = (void*)calloc(fInputBufferSize, 1); - assert(fInputBuffer); - - if (ProbeInBlockSize() < 0) { - goto error; - } - - return 0; - -error: - ::close(fInFD); - return -1; -} - -int JackOSSDriver::OpenOutput() -{ - int flags = 0; - int gFragFormat; - int cur_sample_format; - int cur_playback_channels; - jack_nframes_t cur_sample_rate; - audio_buf_info info; - - if (fPlaybackChannels == 0) fPlaybackChannels = 2; - - if ((fOutFD = open(fPlaybackDriverName, O_WRONLY | ((fExcl) ? O_EXCL : 0))) < 0) { - jack_error("JackOSSDriver::OpenOutput failed to open device : %s@%i, errno = %d", __FILE__, __LINE__, errno); - return -1; - } - - jack_log("JackOSSDriver::OpenOutput output fOutFD = %d", fOutFD); - - if (fExcl) { - if (ioctl(fOutFD, SNDCTL_DSP_COOKEDMODE, &flags) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set cooked mode : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - } - - cur_sample_format = GetSampleFormat(fBits); - if (ioctl(fOutFD, SNDCTL_DSP_SETFMT, &cur_sample_format) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set format : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOutSampleSize = GetSampleSize(cur_sample_format); - if (cur_sample_format != GetSampleFormat(fBits)) { - if (fOutSampleSize > 0) { - jack_info("JackOSSDriver::OpenOutput driver forced %d bit sample format", fOutSampleSize * 8); - } else { - jack_error("JackOSSDriver::OpenOutput unsupported sample format %#x", cur_sample_format); - goto error; - } - } - - cur_playback_channels = fPlaybackChannels; - if (ioctl(fOutFD, SNDCTL_DSP_CHANNELS, &fPlaybackChannels) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set channels : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_playback_channels != fPlaybackChannels) { - jack_info("JackOSSDriver::OpenOutput driver forced the number of playback channels %ld", fPlaybackChannels); - } - - cur_sample_rate = fEngineControl->fSampleRate; - if (ioctl(fOutFD, SNDCTL_DSP_SPEED, &fEngineControl->fSampleRate) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set sample rate : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - if (cur_sample_rate != fEngineControl->fSampleRate) { - jack_info("JackOSSDriver::OpenInput driver forced the sample rate %ld", fEngineControl->fSampleRate); - } - - // Internal buffer size required for one period. - fOutputBufferSize = fEngineControl->fBufferSize * fOutSampleSize * fPlaybackChannels; - - // Get the total size of the OSS playback buffer, in sample frames. - info = {0, 0, 0, 0}; - if (ioctl(fOutFD, SNDCTL_DSP_GETOSPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenOutput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSOutBuffer = info.fragstotal * info.fragsize / (fOutSampleSize * fPlaybackChannels); - - if (fOSSOutBuffer < fEngineControl->fBufferSize * (1 + fNperiods)) { - // Total size of the OSS playback buffer is too small, resize it. - unsigned int buf_size = fOutputBufferSize * (1 + fNperiods); - // Keep current fragment size if possible - respect OSS latency settings. - // Some sound cards like Intel HDA may stutter when changing the fragment size. - gFragFormat = UpToPower2(info.fragsize); - unsigned int frag_size = 1U << gFragFormat; - gFragFormat |= ((buf_size + frag_size - 1) / frag_size) << 16; - jack_info("JackOSSDriver::OpenOutput request %d fragments of %d", (gFragFormat >> 16), frag_size); - if (ioctl(fOutFD, SNDCTL_DSP_SETFRAGMENT, &gFragFormat) == -1) { - jack_error("JackOSSDriver::OpenOutput failed to set fragments : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - // Check the new OSS playback buffer size. - info = {0, 0, 0, 0}; - if (ioctl(fOutFD, SNDCTL_DSP_GETOSPACE, &info) == -1 || info.fragsize <= 0 || info.fragstotal <= 0) { - jack_error("JackOSSDriver::OpenOutput failed to get buffer info : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - fOSSOutBuffer = info.fragstotal * info.fragsize / (fOutSampleSize * fPlaybackChannels); - } - - if (fOSSOutBuffer > fEngineControl->fBufferSize * fNperiods) { - jack_nframes_t low = fOSSOutBuffer - (fNperiods * fEngineControl->fBufferSize); - int mark = low * fOutSampleSize * fPlaybackChannels; - if (ioctl(fOutFD, SNDCTL_DSP_LOW_WATER, &mark) != 0) { - jack_error("JackOSSDriver::OpenOutput failed to set low water mark : %s@%i, errno = %d", __FILE__, __LINE__, errno); - goto error; - } - jack_info("JackOSSDriver::OpenOutput set low water mark to %d", mark); - } - - fOutputBuffer = (void*)calloc(fOutputBufferSize, 1); - assert(fOutputBuffer); - - if (ProbeOutBlockSize() < 0) { - goto error; - } - - return 0; - -error: - ::close(fOutFD); - return -1; -} - int JackOSSDriver::Open(jack_nframes_t nframes, int user_nperiods, jack_nframes_t samplerate, @@ -884,6 +97,7 @@ int JackOSSDriver::Open(jack_nframes_t nframes, Close(); return -1; } else { + fChannel.StartAssistThread(fEngineControl->fRealTime, fEngineControl->fServerPriority); return 0; } } @@ -938,6 +152,11 @@ int JackOSSDriver::Close() fclose(file); } #endif + + fChannel.Lock(); + fChannel.StopAssistThread(); + fChannel.Unlock(); + int res = JackAudioDriver::Close(); CloseAux(); return res; @@ -946,286 +165,225 @@ int JackOSSDriver::Close() int JackOSSDriver::OpenAux() { - // (Re-)Initialize runtime variables. - fInSampleSize = fOutSampleSize = 0; - fInputBufferSize = fOutputBufferSize = 0; - fInBlockSize = fOutBlockSize = 1; - fInMeanStep = fOutMeanStep = 0; - fOSSInBuffer = fOSSOutBuffer = 0; - fOSSReadSync = fOSSWriteSync = 0; - fOSSReadOffset = fOSSWriteOffset = 0; - fBufferBalance = 0; - fForceBalancing = false; - fForceSync = false; - - if (fCapture && (OpenInput() < 0)) { + if (!fChannel.Lock()) { return -1; } - if (fPlayback && (OpenOutput() < 0)) { + // (Re-)Initialize runtime variables. + fCycleEnd = 0; + fLastRun = 0; + fMaxRunGap = 0; + + if (!fChannel.InitialSetup(fEngineControl->fSampleRate)) { + fChannel.Unlock(); return -1; } - DisplayDeviceInfo(); - return 0; -} + if (fCapture) { + if (!fChannel.OpenCapture(fCaptureDriverName, fExcl, fBits, fCaptureChannels)) { + fChannel.Unlock(); + return -1; + } + } -void JackOSSDriver::CloseAux() -{ - if (fCapture && fInFD > 0) { - close(fInFD); - fInFD = -1; + if (fPlayback) { + if (!fChannel.OpenPlayback(fPlaybackDriverName, fExcl, fBits, fPlaybackChannels)) { + fChannel.Unlock(); + return -1; + } } - if (fPlayback && fOutFD > 0) { - close(fOutFD); - fOutFD = -1; + if (!fChannel.StartChannels(fEngineControl->fBufferSize)) { + fChannel.Unlock(); + return -1; } - if (fInputBuffer) - free(fInputBuffer); - fInputBuffer = NULL; + if (fCapture) { + fChannel.Capture().log_device_info(); + } + if (fPlayback) { + fChannel.Playback().log_device_info(); + } + + if (size_t max_channels = std::max(fCaptureChannels, fPlaybackChannels)) { + fSampleBuffers = new jack_sample_t * [max_channels]; + } + + if (!fChannel.Unlock()) { + return -1; + } - if (fOutputBuffer) - free(fOutputBuffer); - fOutputBuffer = NULL; + return 0; } -int JackOSSDriver::Read() +void JackOSSDriver::CloseAux() { - if (fInFD > 0 && fOSSReadSync == 0) { - // First cycle, account for leftover samples from previous reads. - fOSSReadOffset = 0; - oss_count_t ptr; - if (ioctl(fInFD, SNDCTL_DSP_CURRENT_IPTR, &ptr) == 0 && ptr.fifo_samples > 0) { - jack_log("JackOSSDriver::Read pre recording samples = %ld, fifo_samples = %d", ptr.samples, ptr.fifo_samples); - fOSSReadOffset = -ptr.fifo_samples; - } + fChannel.Lock(); - // Start capture by reading a new hardware block., - jack_nframes_t discard = fInMeanStep - fOSSReadOffset; - // Let half a block or at most 1ms remain in buffer, avoid drift issues at start. - discard -= min(TimeToFrames(1000, fEngineControl->fSampleRate), (fInMeanStep / 2)); - jack_log("JackOSSDriver::Read start recording discard %ld frames", discard); - fOSSReadSync = GetMicroSeconds(); - Discard(discard); + fChannel.StopChannels(); - fForceSync = true; - fForceBalancing = true; + if (fSampleBuffers) { + delete[] fSampleBuffers; + fSampleBuffers = nullptr; } - if (fOutFD > 0 && fOSSWriteSync == 0) { - // First cycle, account for leftover samples from previous writes. - fOSSWriteOffset = 0; - oss_count_t ptr; - if (ioctl(fOutFD, SNDCTL_DSP_CURRENT_OPTR, &ptr) == 0 && ptr.fifo_samples > 0) { - jack_log("JackOSSDriver::Read pre playback samples = %ld, fifo_samples = %d", ptr.samples, ptr.fifo_samples); - fOSSWriteOffset = ptr.fifo_samples; - } - - // Start playback with silence, target latency as given by the user. - jack_nframes_t silence = (fNperiods + 1) * fEngineControl->fBufferSize; - // Minus half a block or at most 1ms of frames, avoid drift issues at start. - silence -= min(TimeToFrames(1000, fEngineControl->fSampleRate), (fOutMeanStep / 2)); - silence = max(silence - fOSSWriteOffset, 1LL); - jack_log("JackOSSDriver::Read start playback with %ld frames of silence", silence); - fOSSWriteSync = GetMicroSeconds(); - WriteSilence(silence); - - fForceSync = true; - fForceBalancing = true; - } + fChannel.Unlock(); +} +int JackOSSDriver::Read() +{ #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fBeforeRead = GetMicroSeconds(); #endif - if (WaitAndSync() < 0) { + if (!fChannel.Lock()) { return -1; } - // Keep begin cycle time - JackDriver::CycleTakeBeginTime(); + // Mark the end time of this cycle, in frames. + fCycleEnd += fEngineControl->fBufferSize; - if (fInFD < 0) { - return 0; + // Process read and write channels at least once. + std::int64_t channel_stamp = fChannel.FrameStamp(); + if (!fChannel.CheckTimeAndRun()) { + fChannel.Unlock(); + return -1; + } + if (fChannel.FrameStamp() - fLastRun > fMaxRunGap) { + fMaxRunGap = fChannel.FrameStamp() - fLastRun; + std::int64_t channel_gap = fChannel.FrameStamp() - channel_stamp; + jack_log("JackOSSDriver::Read max run gap %lld frames vs channel %lld.", fMaxRunGap, channel_gap); } - // Try to read multiple times in case of short reads. - size_t count = 0; - for (int i = 0; i < 3 && count < fInputBufferSize; ++i) { - ssize_t ret = ::read(fInFD, ((char*)fInputBuffer) + count, fInputBufferSize - count); - if (ret < 0) { - jack_error("JackOSSDriver::Read error = %s", strerror(errno)); - return -1; - } - count += ret; + // Check for over- and underruns. + if (fChannel.XRunGap() > 0) { + std::int64_t skip = fChannel.XRunGap() + fEngineControl->fBufferSize; + NotifyXRun(GetMicroSeconds(), (float) (fChannel.FrameClock().frames_to_time(skip) / 1000)); + fCycleEnd += skip; + jack_error("JackOSSDriver::Read(): XRun, late by %lld frames.", skip); + fChannel.ResetBuffers(skip); } - // Read offset accounting and overrun detection. - if (count > 0) { - jack_time_t now = GetMicroSeconds(); - jack_time_t sync = max(fOSSReadSync, fOSSWriteSync); - if (now - sync > 1000) { - // Blocking read() may indicate sample loss in OSS - force resync. - jack_log("JackOSSDriver::Read long read duration of %ld us", now - sync); - fForceSync = true; - } - long long passed = TimeToFrames(now - fOSSReadSync, fEngineControl->fSampleRate); - passed -= (passed % fInBlockSize); - if (passed > fOSSReadOffset + fOSSInBuffer) { - // Overrun, adjust read and write position. - long long missed = passed - (fOSSReadOffset + fOSSInBuffer); - jack_error("JackOSSDriver::Read missed %ld frames by overrun, passed=%ld, sync=%ld, now=%ld", missed, passed, fOSSReadSync, now); - fOSSReadOffset += missed; - fOSSWriteOffset += missed; - NotifyXRun(now, float(FramesToTime(missed, fEngineControl->fSampleRate))); + // Wait and process channels until read, or else write, buffer is finished. + while ((fCapture && !fChannel.CaptureFinished()) || + (!fCapture && !fChannel.PlaybackFinished())) { + if (!(fChannel.Sleep() && fChannel.CheckTimeAndRun())) { + fChannel.Unlock(); + return -1; } - fOSSReadOffset += count / (fInSampleSize * fCaptureChannels); } -#ifdef JACK_MONITOR - if (count > 0 && count != (int)fInputBufferSize) - jack_log("JackOSSDriver::Read count = %ld", count / (fInSampleSize * fCaptureChannels)); - gCycleTable.fTable[gCycleCount].fAfterRead = GetMicroSeconds(); -#endif - - // Check and clear OSS errors. - audio_errinfo ei_in; - if (ioctl(fInFD, SNDCTL_DSP_GETERROR, &ei_in) == 0) { - - // Not reliable for overrun detection, virtual_oss doesn't implement it. - if (ei_in.rec_overruns > 0 ) { - jack_error("JackOSSDriver::Read %d overrun events", ei_in.rec_overruns); - } + // Keep begin cycle time + JackDriver::CycleTakeBeginTime(); - if (ei_in.rec_errorcount > 0 && ei_in.rec_lasterror != 0) { - jack_error("%d OSS rec event(s), last=%05d:%d", ei_in.rec_errorcount, ei_in.rec_lasterror, ei_in.rec_errorparm); + if (!fCapture) { + if (!fChannel.Unlock()) { + return -1; } + return 0; } - if (count < fInputBufferSize) { - jack_error("JackOSSDriver::Read incomplete read of %ld bytes", count); - return -1; + if ((fChannel.FrameStamp() / fEngineControl->fBufferSize) % ((5 * fEngineControl->fSampleRate) / fEngineControl->fBufferSize) == 0) { + fChannel.Capture().log_state(fChannel.FrameStamp()); + fMaxRunGap = 0; } +#ifdef JACK_MONITOR + gCycleTable.fTable[gCycleCount].fAfterRead = GetMicroSeconds(); +#endif + for (int i = 0; i < fCaptureChannels; i++) { + fSampleBuffers[i] = nullptr; if (fGraphManager->GetConnectionsNum(fCapturePortList[i]) > 0) { - CopyAndConvertIn(GetInputBuffer(i), fInputBuffer, fEngineControl->fBufferSize, i, fCaptureChannels, fInSampleSize * 8); + fSampleBuffers[i] = GetInputBuffer(i); } } + std::int64_t buffer_end = fCycleEnd + fEngineControl->fBufferSize; + if (!fChannel.Read(fSampleBuffers, fEngineControl->fBufferSize, buffer_end)) { + fChannel.Unlock(); + return -1; + } #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fAfterReadConvert = GetMicroSeconds(); #endif + if (!fChannel.CheckTimeAndRun() || !fChannel.Unlock()) { + return -1; + } + fLastRun = fChannel.FrameStamp(); + return 0; } int JackOSSDriver::Write() { - if (fOutFD < 0) { + if (!fPlayback) { return 0; } - unsigned int skip = 0; - jack_time_t start = GetMicroSeconds(); - - if (fOSSWriteSync > 0) { - // Check for underruns, rounded to hardware block size if available. - long long passed = TimeToFrames(start - fOSSWriteSync, fEngineControl->fSampleRate); - long long consumed = passed - (passed % fOutBlockSize); - long long tolerance = (fOutBlockSize > 1) ? 0 : fOutMeanStep; - long long overdue = 0; - if (consumed > fOSSWriteOffset + tolerance) { - // Skip playback data that already passed. - overdue = consumed - fOSSWriteOffset - tolerance; - jack_error("JackOSSDriver::Write underrun, late by %ld, skip %ld frames", passed - fOSSWriteOffset, overdue); - jack_log("JackOSSDriver::Write playback offset %ld frames synced %ld us ago", fOSSWriteOffset, start - fOSSWriteSync); - // Also consider buffer balance, there was a gap in playback anyway. - fForceBalancing = true; - } - // Account for buffer balance if needed. - long long progress = fEngineControl->fBufferSize; - if (fForceBalancing) { - fForceBalancing = false; - progress = max(progress + fBufferBalance, 0LL); - jack_info("JackOSSDriver::Write buffer balancing %ld frames", fBufferBalance); - jack_log("JackOSSDriver::Write recording sync %ld frames %ld us ago", fOSSReadOffset, start - fOSSReadSync); - jack_log("JackOSSDriver::Write playback sync %ld frames %ld us ago", fOSSWriteOffset, start - fOSSWriteSync); - } - // How many samples to skip or prepend due to underrun and balancing. - long long write_length = progress - overdue; - if (write_length <= 0) { - skip += fOutputBufferSize; - fOSSWriteOffset += progress; - } else if (write_length < fEngineControl->fBufferSize) { - skip += (fEngineControl->fBufferSize - write_length) * fOutSampleSize * fPlaybackChannels; - fOSSWriteOffset += overdue; - } else if (write_length > fEngineControl->fBufferSize) { - jack_nframes_t fill = write_length - fEngineControl->fBufferSize; - WriteSilence(fill); + if (!fChannel.Lock()) { + return -1; + } + + // Process read and write channels at least once. + std::int64_t channel_stamp = fChannel.FrameStamp(); + if (!fChannel.CheckTimeAndRun()) { + fChannel.Unlock(); + return -1; + } + if (fChannel.FrameStamp() - fLastRun > fMaxRunGap) { + fMaxRunGap = fChannel.FrameStamp() - fLastRun; + std::int64_t channel_gap = fChannel.FrameStamp() - channel_stamp; + jack_log("JackOSSDriver::Write max run gap %lld frames vs channel %lld.", fMaxRunGap, channel_gap); + } + + // Wait and process channels until write buffer is finished. + while (!fChannel.PlaybackFinished()) { + if (!(fChannel.Sleep() && fChannel.CheckTimeAndRun())) { + fChannel.Unlock(); + return -1; } } + if ((fChannel.FrameStamp() / fEngineControl->fBufferSize) % ((5 * fEngineControl->fSampleRate) / fEngineControl->fBufferSize) == 0) { + fChannel.Playback().log_state(fChannel.FrameStamp()); + fMaxRunGap = 0; + } + #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fBeforeWriteConvert = GetMicroSeconds(); #endif - memset(fOutputBuffer, 0, fOutputBufferSize); for (int i = 0; i < fPlaybackChannels; i++) { + fSampleBuffers[i] = nullptr; if (fGraphManager->GetConnectionsNum(fPlaybackPortList[i]) > 0) { - CopyAndConvertOut(fOutputBuffer, GetOutputBuffer(i), fEngineControl->fBufferSize, i, fPlaybackChannels, fOutSampleSize * 8); + fSampleBuffers[i] = GetOutputBuffer(i); } } + std::int64_t buffer_end = fCycleEnd + fEngineControl->fBufferSize; + if (!fChannel.Write(fSampleBuffers, fEngineControl->fBufferSize, buffer_end)) { + fChannel.Unlock(); + return -1; + } #ifdef JACK_MONITOR gCycleTable.fTable[gCycleCount].fBeforeWrite = GetMicroSeconds(); #endif - // Try multiple times in case of short writes. - ssize_t count = skip; - for (int i = 0; i < 3 && count < fOutputBufferSize; ++i) { - ssize_t ret = ::write(fOutFD, ((char*)fOutputBuffer) + count, fOutputBufferSize - count); - if (ret < 0) { - jack_error("JackOSSDriver::Write error = %s", strerror(errno)); - return -1; - } - count += ret; - } - - fOSSWriteOffset += ((count - skip) / (fOutSampleSize * fPlaybackChannels)); - - jack_time_t duration = GetMicroSeconds() - start; - if (duration > 1000) { - // Blocking write() may indicate sample loss in OSS - force resync. - jack_log("JackOSSDriver::Write long write duration of %ld us", duration); - fForceSync = true; + // Do a processing step here. + if (!fChannel.CheckTimeAndRun()) { + fChannel.Unlock(); + return -1; } + fLastRun = fChannel.FrameStamp(); #ifdef JACK_MONITOR - if (count > 0 && count != (int)fOutputBufferSize) - jack_log("JackOSSDriver::Write count = %ld", (count - skip) / (fOutSampleSize * fPlaybackChannels)); gCycleTable.fTable[gCycleCount].fAfterWrite = GetMicroSeconds(); gCycleCount = (gCycleCount == CYCLE_POINTS - 1) ? gCycleCount: gCycleCount + 1; #endif - // Check and clear OSS errors. - audio_errinfo ei_out; - if (ioctl(fOutFD, SNDCTL_DSP_GETERROR, &ei_out) == 0) { - - // Not reliable for underrun detection, virtual_oss does not implement it. - if (ei_out.play_underruns > 0) { - jack_error("JackOSSDriver::Write %d underrun events", ei_out.play_underruns); - } - - if (ei_out.play_errorcount > 0 && ei_out.play_lasterror != 0) { - jack_error("%d OSS play event(s), last=%05d:%d",ei_out.play_errorcount, ei_out.play_lasterror, ei_out.play_errorparm); - } - } - - if (count < (int)fOutputBufferSize) { - jack_error("JackOSSDriver::Write incomplete write of %ld bytes", count - skip); + if (!fChannel.Unlock()) { return -1; } @@ -1245,6 +403,7 @@ void JackOSSDriver::UpdateLatencies() } for (int i = 0; i < fPlaybackChannels; i++) { + // TODO: Move this half period to capture latency. output_range.max = (fEngineControl->fBufferSize / 2) + fPlaybackLatency; // Additional latency introduced by the OSS buffer. output_range.max += fNperiods * fEngineControl->fBufferSize; diff --git a/freebsd/oss/JackOSSDriver.h b/freebsd/oss/JackOSSDriver.h index c5449b376..4cdb160fe 100644 --- a/freebsd/oss/JackOSSDriver.h +++ b/freebsd/oss/JackOSSDriver.h @@ -22,6 +22,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. #define __JackOSSDriver__ #include "JackAudioDriver.h" +#include "JackOSSChannel.h" namespace Jack { @@ -44,9 +45,6 @@ class JackOSSDriver : public JackAudioDriver { private: - int fInFD; - int fOutFD; - int fBits; int fNperiods; bool fCapture; @@ -54,42 +52,15 @@ class JackOSSDriver : public JackAudioDriver bool fExcl; bool fIgnoreHW; - unsigned int fInSampleSize; - unsigned int fOutSampleSize; - - unsigned int fInputBufferSize; - unsigned int fOutputBufferSize; - - void* fInputBuffer; - void* fOutputBuffer; - - jack_nframes_t fInBlockSize; - jack_nframes_t fOutBlockSize; - jack_nframes_t fInMeanStep; - jack_nframes_t fOutMeanStep; - jack_nframes_t fOSSInBuffer; - jack_nframes_t fOSSOutBuffer; - - jack_time_t fOSSReadSync; - long long fOSSReadOffset; - jack_time_t fOSSWriteSync; - long long fOSSWriteOffset; + std::int64_t fCycleEnd; + std::int64_t fLastRun; + std::int64_t fMaxRunGap; + jack_sample_t** fSampleBuffers; - // Buffer balance and sync correction - long long fBufferBalance; - bool fForceBalancing; - bool fForceSync; + JackOSSChannel fChannel; - int OpenInput(); - int OpenOutput(); int OpenAux(); void CloseAux(); - void DisplayDeviceInfo(); - int ProbeInBlockSize(); - int ProbeOutBlockSize(); - int Discard(jack_nframes_t frames); - int WriteSilence(jack_nframes_t frames); - int WaitAndSync(); protected: virtual void UpdateLatencies(); @@ -98,16 +69,9 @@ class JackOSSDriver : public JackAudioDriver JackOSSDriver(const char* name, const char* alias, JackLockedEngine* engine, JackSynchro* table) : JackAudioDriver(name, alias, engine, table), - fInFD(-1), fOutFD(-1), fBits(0), + fBits(0), fNperiods(0), fCapture(false), fPlayback(false), fExcl(false), fIgnoreHW(true), - fInSampleSize(0), fOutSampleSize(0), - fInputBufferSize(0), fOutputBufferSize(0), - fInputBuffer(NULL), fOutputBuffer(NULL), - fInBlockSize(1), fOutBlockSize(1), - fInMeanStep(0), fOutMeanStep(0), - fOSSInBuffer(0), fOSSOutBuffer(0), - fOSSReadSync(0), fOSSReadOffset(0), fOSSWriteSync(0), fOSSWriteOffset(0), - fBufferBalance(0), fForceBalancing(false), fForceSync(false) + fCycleEnd(0), fLastRun(0), fMaxRunGap(0), fSampleBuffers(nullptr) {} virtual ~JackOSSDriver() diff --git a/wscript b/wscript index f0a09c876..8a081cbec 100644 --- a/wscript +++ b/wscript @@ -695,6 +695,7 @@ def build_drivers(bld): freebsd_oss_src = [ 'common/memops.c', + 'freebsd/oss/JackOSSChannel.cpp', 'freebsd/oss/JackOSSDriver.cpp' ]