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