/* * 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