/* * Copyright (c) 2023 Florian Walpen * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #ifndef SOSSO_WRITECHANNEL_HPP #define SOSSO_WRITECHANNEL_HPP #include "sosso/Buffer.hpp" #include "sosso/Channel.hpp" #include "sosso/Logging.hpp" #include namespace sosso { /*! * \brief Playback Channel * * Specializes the generic Channel class into a playback channel. It keeps track * of the OSS playback progress, and writes audio data from an external buffer * to the available OSS buffer. If the OSS buffer is memory mapped, the audio * data is copied there. Otherwise I/O write() system calls are used. */ class WriteChannel : public Channel { public: /*! * \brief Open a device for playback. * \param device Path to the device, e.g. "/dev/dsp1". * \param exclusive Try to get exclusive access to the device. * \return True if the device was opened successfully. */ bool open(const char *device, bool exclusive = true) { int mode = O_WRONLY | O_NONBLOCK; if (exclusive) { mode |= O_EXCL; } return Channel::open(device, mode); } //! Available OSS buffer space for writing, in frames. std::int64_t oss_available() const { std::int64_t result = last_progress() + buffer_frames() - _write_position; if (result < 0) { result = 0; } else if (result > buffer_frames()) { result = buffer_frames(); } return result; } /*! * \brief Calculate next wakeup time. * \param sync_frames Required sync event (e.g. buffer end), in frame time. * \return Suggested and safe wakeup time for next process(), in frame time. */ std::int64_t wakeup_time(std::int64_t sync_frames) const { return Channel::wakeup_time(sync_frames, oss_available()); } /*! * \brief Check OSS progress and write playback audio to the OSS buffer. * \param buffer Buffer of playback audio data, untouched if invalid. * \param end Buffer end position, matching channel progress. * \param now Current time in frame time, see FrameClock. * \return True if successful, false means there was an error. */ bool process(Buffer &buffer, std::int64_t end, std::int64_t now) { if (map()) { return (progress_done(now) || check_map_progress(now)) && (buffer_done(buffer, end) || process_mapped(buffer, end, now)); } else { return (progress_done(now) || check_write_progress(now)) && (buffer_done(buffer, end) || process_write(buffer, end, now)); } } protected: // Indicate that OSS progress has already been checked. bool progress_done(std::int64_t now) { return (last_processing() == now); } // Check OSS progress in case of memory mapped buffer. bool check_map_progress(std::int64_t now) { // Get OSS progress through map pointer. if (get_play_pointer()) { std::int64_t progress = map_progress() - _oss_progress; if (progress > 0) { // Sometimes OSS playback starts with a bogus extra buffer cycle. if (progress > buffer_frames() && now - last_processing() < buffer_frames() / 2) { Log::warn(SOSSO_LOC, "OSS playback bogus buffer cycle, %lld frames in %lld.", progress, now - last_processing()); progress = progress % buffer_frames(); } // Clear obsolete audio data in the buffer. write_map(nullptr, (_oss_progress % buffer_frames()) * frame_size(), progress * frame_size()); _oss_progress = map_progress(); } std::int64_t loss = mark_loss(last_progress() + progress - _write_position); mark_progress(progress, now); if (loss > 0) { Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss); _write_position = last_progress(); } } return progress_done(now); } // Write playback audio data to a memory mapped OSS buffer. bool process_mapped(Buffer &buffer, std::int64_t end, std::int64_t now) { // Buffer position should be between OSS progress and last write position. std::int64_t position = buffer_position(buffer.remaining(), end); if (std::int64_t skip = buffer_advance(buffer, last_progress() - position)) { // First part of the buffer already played, skip it. Log::info(SOSSO_LOC, "@%lld - %lld Write %lld already played, skip %lld.", now, end, last_progress() - position, skip); position += skip; } else if (position != _write_position) { // Position mismatch, rewrite as much as possible. if (std::int64_t rewind = buffer_rewind(buffer, position - last_progress())) { Log::info(SOSSO_LOC, "@%lld - %lld Write position mismatch, rewrite %lld.", now, end, rewind); position -= rewind; } } // The writable window is the whole buffer, starting from OSS progress. if (!buffer.done() && position >= last_progress() && position < last_progress() + buffer_frames()) { if (_write_position < position && _write_position + 8 >= position) { // Small remaining gap between writes, fill in a replay patch. std::int64_t offset = _write_position - last_progress(); unsigned pointer = (_oss_progress + offset) % buffer_frames(); std::size_t length = (position - _write_position) * frame_size(); length = buffer.remaining(length); std::size_t written = write_map(buffer.position(), pointer * frame_size(), length); Log::info(SOSSO_LOC, "@%lld - %lld Write small gap %lld, replay %lld.", now, end, position - _write_position, written / frame_size()); } // Write from buffer offset up to either OSS or write buffer end. std::int64_t offset = position - last_progress(); unsigned pointer = (_oss_progress + offset) % buffer_frames(); std::size_t length = (buffer_frames() - offset) * frame_size(); length = buffer.remaining(length); std::size_t written = write_map(buffer.position(), pointer * frame_size(), length); buffer.advance(written); _write_position = buffer_position(buffer.remaining(), end); } _write_position += freewheel_finish(buffer, end, now); return true; } // Check progress when using I/O write() system call. bool check_write_progress(std::int64_t now) { // Check for OSS buffer underruns. std::int64_t overdue = now - estimated_dropout(oss_available()); if ((overdue > 0 && get_play_underruns() > 0) || overdue > max_progress()) { // OSS buffer underrun, estimate loss and progress from time. std::int64_t progress = _write_position - last_progress(); std::int64_t loss = mark_loss(progress, now); Log::warn(SOSSO_LOC, "OSS playback buffer underrun, %lld lost.", loss); mark_progress(progress + loss, now); _write_position = last_progress(); } else { // Infer progress from OSS queue changes. std::int64_t queued = queued_samples(); std::int64_t progress = (_write_position - last_progress()) - queued; mark_progress(progress, now); _write_position = last_progress() + queued; } return progress_done(now); } // Write playback audio data to OSS buffer using I/O write() system call. bool process_write(Buffer &buffer, std::int64_t end, std::int64_t now) { bool ok = true; // Adjust buffer position to OSS write position, if possible. std::int64_t position = buffer_position(buffer.remaining(), end); if (std::int64_t rewind = buffer_rewind(buffer, position - _write_position)) { // Gap between buffers, replay parts to fill it up. Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, replay %lld.", now, end, position - _write_position, rewind); position -= rewind; } else if (std::int64_t skip = buffer_advance(buffer, _write_position - position)) { // Overlapping buffers, skip the overlapping part. Log::info(SOSSO_LOC, "@%lld - %lld Write buffer overlap %lld, skip %lld.", now, end, _write_position - position, skip); position += skip; } if (oss_available() == 0) { // OSS buffer is full, nothing to do. } else if (position > _write_position) { // Replay to fill remaining gap, limit the write to just fill the gap. std::int64_t gap = position - _write_position; std::size_t write_limit = buffer.remaining(gap * frame_size()); std::size_t bytes_written = 0; ok = write_io(buffer.position(), write_limit, bytes_written); Log::info(SOSSO_LOC, "@%lld - %lld Write buffer gap %lld, fill %lld.", now, end, gap, bytes_written / frame_size()); _write_position += bytes_written / frame_size(); } else if (position == _write_position) { // Write as much as currently possible. std::size_t write_limit = buffer.remaining(); std::size_t bytes_written = 0; ok = write_io(buffer.position(), write_limit, bytes_written); _write_position += bytes_written / frame_size(); buffer.advance(bytes_written); } // Make sure buffers finish in time, despite irregular progress (freewheel). freewheel_finish(buffer, end, now); return ok; } private: // Calculate write position of the remaining buffer. std::int64_t buffer_position(std::size_t remaining, std::int64_t end) const { return end - (remaining / frame_size()); } // Indicate that a buffer doesn't need further processing. bool buffer_done(const Buffer &buffer, std::int64_t end) const { return buffer.done() && end <= _write_position; } // Avoid stalled buffers with irregular OSS progress in freewheel mode. std::int64_t freewheel_finish(Buffer &buffer, std::int64_t end, std::int64_t now) { std::int64_t advance = 0; // Make sure buffers finish in time, despite irregular progress (freewheel). if (freewheel() && now >= end + balance() && !buffer.done()) { advance = buffer.advance(buffer.remaining()) / frame_size(); Log::info(SOSSO_LOC, "@%lld - %lld Write freewheel finish remaining buffer %lld.", now, end, advance); } return advance; } // Skip writing part of the buffer to match OSS write position. std::int64_t buffer_advance(Buffer &buffer, std::int64_t frames) { if (frames > 0) { return buffer.advance(frames * frame_size()) / frame_size(); } return 0; } // Rewind part of the buffer to match OSS write postion. std::int64_t buffer_rewind(Buffer &buffer, std::int64_t frames) { if (frames > 0) { return buffer.rewind(frames * frame_size()) / frame_size(); } return 0; } std::int64_t _oss_progress = 0; // Last memory mapped OSS progress. std::int64_t _write_position = 0; // Current write position of the channel. }; } // namespace sosso #endif // SOSSO_WRITECHANNEL_HPP