|
|
@@ -0,0 +1,492 @@ |
|
|
|
/* |
|
|
|
Copyright (C) 2023 Florian Walpen <dev@submerge.ch> |
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify |
|
|
|
it under the terms of the GNU General Public License as published by |
|
|
|
the Free Software Foundation; either version 2 of the License, or |
|
|
|
(at your option) any later version. |
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful, |
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
|
GNU General Public License for more details. |
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License |
|
|
|
along with this program; if not, write to the Free Software |
|
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
#include "JackOSSChannel.h" |
|
|
|
#include "JackError.h" |
|
|
|
#include "JackThread.h" |
|
|
|
#include "memops.h" |
|
|
|
|
|
|
|
#include <cstdint> |
|
|
|
#include <sys/ioctl.h> |
|
|
|
#include <sys/soundcard.h> |
|
|
|
#include <fcntl.h> |
|
|
|
#include <iostream> |
|
|
|
#include <assert.h> |
|
|
|
#include <stdio.h> |
|
|
|
|
|
|
|
typedef jack_default_audio_sample_t jack_sample_t; |
|
|
|
|
|
|
|
namespace |
|
|
|
{ |
|
|
|
|
|
|
|
int SuggestSampleFormat(int bits) |
|
|
|
{ |
|
|
|
switch(bits) { |
|
|
|
// Native-endian signed 32 bit samples. |
|
|
|
case 32: |
|
|
|
return AFMT_S32_NE; |
|
|
|
// Native-endian signed 24 bit (packed) samples. |
|
|
|
case 24: |
|
|
|
return AFMT_S24_NE; |
|
|
|
// Native-endian signed 16 bit samples, used by default. |
|
|
|
case 16: |
|
|
|
default: |
|
|
|
return AFMT_S16_NE; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
bool SupportedSampleFormat(int format) |
|
|
|
{ |
|
|
|
switch(format) { |
|
|
|
// Only signed sample formats are supported by the conversion functions. |
|
|
|
case AFMT_S16_NE: |
|
|
|
case AFMT_S16_OE: |
|
|
|
case AFMT_S24_NE: |
|
|
|
case AFMT_S24_OE: |
|
|
|
case AFMT_S32_NE: |
|
|
|
case AFMT_S32_OE: |
|
|
|
return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
void CopyAndConvertIn(jack_sample_t *dst, char *src, size_t nframes, int channel, int chcount, int format) |
|
|
|
{ |
|
|
|
switch (format) { |
|
|
|
|
|
|
|
case AFMT_S16_NE: |
|
|
|
src += channel * 2; |
|
|
|
sample_move_dS_s16(dst, src, nframes, chcount * 2); |
|
|
|
break; |
|
|
|
case AFMT_S16_OE: |
|
|
|
src += channel * 2; |
|
|
|
sample_move_dS_s16s(dst, src, nframes, chcount * 2); |
|
|
|
break; |
|
|
|
case AFMT_S24_NE: |
|
|
|
src += channel * 3; |
|
|
|
sample_move_dS_s24(dst, src, nframes, chcount * 3); |
|
|
|
break; |
|
|
|
case AFMT_S24_OE: |
|
|
|
src += channel * 3; |
|
|
|
sample_move_dS_s24s(dst, src, nframes, chcount * 3); |
|
|
|
break; |
|
|
|
case AFMT_S32_NE: |
|
|
|
src += channel * 4; |
|
|
|
sample_move_dS_s32(dst, src, nframes, chcount * 4); |
|
|
|
break; |
|
|
|
case AFMT_S32_OE: |
|
|
|
src += channel * 4; |
|
|
|
sample_move_dS_s32s(dst, src, nframes, chcount * 4); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void CopyAndConvertOut(char *dst, jack_sample_t *src, size_t nframes, int channel, int chcount, int format) |
|
|
|
{ |
|
|
|
switch (format) { |
|
|
|
|
|
|
|
case AFMT_S16_NE: |
|
|
|
dst += channel * 2; |
|
|
|
sample_move_d16_sS(dst, src, nframes, chcount * 2, NULL); |
|
|
|
break; |
|
|
|
case AFMT_S16_OE: |
|
|
|
dst += channel * 2; |
|
|
|
sample_move_d16_sSs(dst, src, nframes, chcount * 2, NULL); |
|
|
|
break; |
|
|
|
case AFMT_S24_NE: |
|
|
|
dst += channel * 3; |
|
|
|
sample_move_d24_sS(dst, src, nframes, chcount * 3, NULL); |
|
|
|
break; |
|
|
|
case AFMT_S24_OE: |
|
|
|
dst += channel * 3; |
|
|
|
sample_move_d24_sSs(dst, src, nframes, chcount * 3, NULL); |
|
|
|
break; |
|
|
|
case AFMT_S32_NE: |
|
|
|
dst += channel * 4; |
|
|
|
sample_move_d32_sS(dst, src, nframes, chcount * 4, NULL); |
|
|
|
break; |
|
|
|
case AFMT_S32_OE: |
|
|
|
dst += channel * 4; |
|
|
|
sample_move_d32_sSs(dst, src, nframes, chcount * 4, NULL); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
void sosso::Log::log(sosso::SourceLocation location, const char* message) { |
|
|
|
jack_log(message); |
|
|
|
} |
|
|
|
|
|
|
|
void sosso::Log::info(sosso::SourceLocation location, const char* message) { |
|
|
|
jack_info(message); |
|
|
|
} |
|
|
|
|
|
|
|
void sosso::Log::warn(sosso::SourceLocation location, const char* message) { |
|
|
|
jack_error(message); |
|
|
|
} |
|
|
|
|
|
|
|
namespace Jack |
|
|
|
{ |
|
|
|
|
|
|
|
bool JackOSSChannel::InitialSetup(unsigned int sample_rate) |
|
|
|
{ |
|
|
|
fFrameStamp = 0; |
|
|
|
fCorrection.clear(); |
|
|
|
return fFrameClock.set_sample_rate(sample_rate); |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::OpenCapture(const char *device, bool exclusive, int bits, int &channels) |
|
|
|
{ |
|
|
|
if (channels == 0) channels = 2; |
|
|
|
|
|
|
|
int sample_format = SuggestSampleFormat(bits); |
|
|
|
|
|
|
|
if (!fReadChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) { |
|
|
|
jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", sample_format); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (!fReadChannel.open(device, exclusive)) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (fReadChannel.sample_rate() != fFrameClock.sample_rate()) { |
|
|
|
jack_error("JackOSSChannel::OpenCapture driver forced sample rate %ld", fReadChannel.sample_rate()); |
|
|
|
fReadChannel.close(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (!SupportedSampleFormat(fReadChannel.sample_format())) { |
|
|
|
jack_error("JackOSSChannel::OpenCapture unsupported sample format %#x", fReadChannel.sample_format()); |
|
|
|
fReadChannel.close(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
jack_log("JackOSSChannel::OpenCapture capture file descriptor = %d", fReadChannel.file_descriptor()); |
|
|
|
|
|
|
|
if (fReadChannel.channels() != channels) { |
|
|
|
channels = fReadChannel.channels(); |
|
|
|
jack_info("JackOSSChannel::OpenCapture driver forced the number of capture channels %ld", channels); |
|
|
|
} |
|
|
|
|
|
|
|
fReadChannel.memory_map(); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::OpenPlayback(const char *device, bool exclusive, int bits, int &channels) |
|
|
|
{ |
|
|
|
if (channels == 0) channels = 2; |
|
|
|
|
|
|
|
int sample_format = SuggestSampleFormat(bits); |
|
|
|
|
|
|
|
if (!fWriteChannel.set_parameters(sample_format, fFrameClock.sample_rate(), channels)) { |
|
|
|
jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", sample_format); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (!fWriteChannel.open(device, exclusive)) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (fWriteChannel.sample_rate() != fFrameClock.sample_rate()) { |
|
|
|
jack_error("JackOSSChannel::OpenPlayback driver forced sample rate %ld", fWriteChannel.sample_rate()); |
|
|
|
fWriteChannel.close(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (!SupportedSampleFormat(fWriteChannel.sample_format())) { |
|
|
|
jack_error("JackOSSChannel::OpenPlayback unsupported sample format %#x", fWriteChannel.sample_format()); |
|
|
|
fWriteChannel.close(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
jack_log("JackOSSChannel::OpenPlayback playback file descriptor = %d", fWriteChannel.file_descriptor()); |
|
|
|
|
|
|
|
if (fWriteChannel.channels() != channels) { |
|
|
|
channels = fWriteChannel.channels(); |
|
|
|
jack_info("JackOSSChannel::OpenPlayback driver forced the number of playback channels %ld", channels); |
|
|
|
} |
|
|
|
|
|
|
|
fWriteChannel.memory_map(); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::Read(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end) |
|
|
|
{ |
|
|
|
if (fReadChannel.recording()) { |
|
|
|
// Get buffer from read channel. |
|
|
|
sosso::Buffer buffer = fReadChannel.take_buffer(); |
|
|
|
|
|
|
|
// Get recording audio data and then clear buffer. |
|
|
|
for (unsigned i = 0; i < fReadChannel.channels(); i++) { |
|
|
|
if (sample_buffers[i]) { |
|
|
|
CopyAndConvertIn(sample_buffers[i], buffer.data(), length, i, fReadChannel.channels(), fReadChannel.sample_format()); |
|
|
|
} |
|
|
|
} |
|
|
|
buffer.reset(); |
|
|
|
|
|
|
|
// Put buffer back to capture at requested end position. |
|
|
|
fReadChannel.set_buffer(std::move(buffer), end); |
|
|
|
SignalWork(); |
|
|
|
return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::Write(jack_sample_t **sample_buffers, jack_nframes_t length, std::int64_t end) |
|
|
|
{ |
|
|
|
if (fWriteChannel.playback()) { |
|
|
|
// Get buffer from write channel. |
|
|
|
sosso::Buffer buffer = fWriteChannel.take_buffer(); |
|
|
|
|
|
|
|
// Clear buffer and write new playback audio data. |
|
|
|
memset(buffer.data(), 0, buffer.length()); |
|
|
|
buffer.reset(); |
|
|
|
for (unsigned i = 0; i < fWriteChannel.channels(); i++) { |
|
|
|
if (sample_buffers[i]) { |
|
|
|
CopyAndConvertOut(buffer.data(), sample_buffers[i], length, i, fWriteChannel.channels(), fWriteChannel.sample_format()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Put buffer back to playback at requested end position. |
|
|
|
end += PlaybackCorrection(); |
|
|
|
fWriteChannel.set_buffer(std::move(buffer), end); |
|
|
|
SignalWork(); |
|
|
|
return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::StartChannels(unsigned int buffer_frames) |
|
|
|
{ |
|
|
|
int group_id = 0; |
|
|
|
|
|
|
|
if (fReadChannel.recording()) { |
|
|
|
// Allocate two recording buffers for double buffering. |
|
|
|
size_t buffer_size = buffer_frames * fReadChannel.frame_size(); |
|
|
|
sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size); |
|
|
|
assert(buffer.data()); |
|
|
|
fReadChannel.set_buffer(std::move(buffer), 0); |
|
|
|
buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size); |
|
|
|
assert(buffer.data()); |
|
|
|
fReadChannel.set_buffer(std::move(buffer), buffer_frames); |
|
|
|
// Add recording channel to synced start group. |
|
|
|
fReadChannel.add_to_sync_group(group_id); |
|
|
|
} |
|
|
|
|
|
|
|
if (fWriteChannel.playback()) { |
|
|
|
// Allocate two playback buffers for double buffering. |
|
|
|
size_t buffer_size = buffer_frames * fWriteChannel.frame_size(); |
|
|
|
sosso::Buffer buffer((char*) calloc(buffer_size, 1), buffer_size); |
|
|
|
assert(buffer.data()); |
|
|
|
fWriteChannel.set_buffer(std::move(buffer), 0); |
|
|
|
buffer = sosso::Buffer((char*) calloc(buffer_size, 1), buffer_size); |
|
|
|
assert(buffer.data()); |
|
|
|
fWriteChannel.set_buffer(std::move(buffer), buffer_frames); |
|
|
|
// Add playback channel to synced start group. |
|
|
|
fWriteChannel.add_to_sync_group(group_id); |
|
|
|
} |
|
|
|
|
|
|
|
// Start both channels in sync if supported. |
|
|
|
if (fReadChannel.recording()) { |
|
|
|
fReadChannel.start_sync_group(group_id); |
|
|
|
} else { |
|
|
|
fWriteChannel.start_sync_group(group_id); |
|
|
|
} |
|
|
|
|
|
|
|
// Init frame clock here to mark start time. |
|
|
|
if (!fFrameClock.init_clock(fFrameClock.sample_rate())) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// Small drift corrections to keep latency whithin +/- 1ms. |
|
|
|
std::int64_t limit = fFrameClock.sample_rate() / 1000; |
|
|
|
fCorrection.set_drift_limits(-limit, limit); |
|
|
|
// Drastic corrections when drift exceeds half a period. |
|
|
|
limit = std::max<std::int64_t>(limit, buffer_frames / 2); |
|
|
|
fCorrection.set_loss_limits(-limit, limit); |
|
|
|
|
|
|
|
SignalWork(); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::StopChannels() |
|
|
|
{ |
|
|
|
if (fReadChannel.recording()) { |
|
|
|
free(fReadChannel.take_buffer().data()); |
|
|
|
free(fReadChannel.take_buffer().data()); |
|
|
|
fReadChannel.memory_unmap(); |
|
|
|
fReadChannel.close(); |
|
|
|
} |
|
|
|
|
|
|
|
if (fWriteChannel.playback()) { |
|
|
|
free(fWriteChannel.take_buffer().data()); |
|
|
|
free(fWriteChannel.take_buffer().data()); |
|
|
|
fWriteChannel.memory_unmap(); |
|
|
|
fWriteChannel.close(); |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::StartAssistThread(bool realtime, int priority) |
|
|
|
{ |
|
|
|
if (fAssistThread.Start() >= 0) { |
|
|
|
if (realtime && fAssistThread.AcquireRealTime(priority) != 0) { |
|
|
|
jack_error("JackOSSChannel::StartAssistThread realtime priority failed."); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::StopAssistThread() |
|
|
|
{ |
|
|
|
if (fAssistThread.GetStatus() != JackThread::kIdle) { |
|
|
|
fAssistThread.SetStatus(JackThread::kIdle); |
|
|
|
SignalWork(); |
|
|
|
fAssistThread.Kill(); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::CheckTimeAndRun() |
|
|
|
{ |
|
|
|
// Check current frame time. |
|
|
|
if (!fFrameClock.now(fFrameStamp)) { |
|
|
|
jack_error("JackOSSChannel::CheckTimeAndRun(): Frame clock failed."); |
|
|
|
return false; |
|
|
|
} |
|
|
|
std::int64_t now = fFrameStamp; |
|
|
|
|
|
|
|
// Process read channel if wakeup time passed, or OSS buffer data available. |
|
|
|
if (fReadChannel.recording() && !fReadChannel.total_finished(now)) { |
|
|
|
if (now >= fReadChannel.wakeup_time(now)) { |
|
|
|
if (!fReadChannel.process(now)) { |
|
|
|
jack_error("JackOSSChannel::CheckTimeAndRun(): Read process failed."); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// Process write channel if wakeup time passed, or OSS buffer space available. |
|
|
|
if (fWriteChannel.playback() && !fWriteChannel.total_finished(now)) { |
|
|
|
if (now >= fWriteChannel.wakeup_time(now)) { |
|
|
|
if (!fWriteChannel.process(now)) { |
|
|
|
jack_error("JackOSSChannel::CheckTimeAndRun(): Write process failed."); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::Sleep() const |
|
|
|
{ |
|
|
|
std::int64_t wakeup = NextWakeup(); |
|
|
|
if (wakeup > fFrameStamp) { |
|
|
|
return fFrameClock.sleep(wakeup); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::CaptureFinished() const |
|
|
|
{ |
|
|
|
return fReadChannel.finished(fFrameStamp); |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::PlaybackFinished() const |
|
|
|
{ |
|
|
|
return fWriteChannel.finished(fFrameStamp); |
|
|
|
} |
|
|
|
|
|
|
|
std::int64_t JackOSSChannel::PlaybackCorrection() |
|
|
|
{ |
|
|
|
std::int64_t correction = 0; |
|
|
|
// If both channels are used, correct drift relative to recording balance. |
|
|
|
if (fReadChannel.recording() && fWriteChannel.playback()) { |
|
|
|
std::int64_t previous = fCorrection.correction(); |
|
|
|
correction = fCorrection.correct(fWriteChannel.balance(), fReadChannel.balance()); |
|
|
|
if (correction != previous) { |
|
|
|
jack_info("Playback correction changed from %lld to %lld.", previous, correction); |
|
|
|
jack_info("Read balance %lld vs write balance %lld.", fReadChannel.balance(), fWriteChannel.balance()); |
|
|
|
} |
|
|
|
} |
|
|
|
return correction; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::Init() |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
bool JackOSSChannel::Execute() |
|
|
|
{ |
|
|
|
if (Lock()) { |
|
|
|
if (fAssistThread.GetStatus() != JackThread::kIdle) { |
|
|
|
if (!CheckTimeAndRun()) { |
|
|
|
return Unlock() && false; |
|
|
|
} |
|
|
|
std::int64_t wakeup = NextWakeup(); |
|
|
|
if (fReadChannel.total_finished(fFrameStamp) && fWriteChannel.total_finished(fFrameStamp)) { |
|
|
|
// Nothing to do, wait on the mutex for work. |
|
|
|
jack_info("JackOSSChannel::Execute waiting for work."); |
|
|
|
fMutex.TimedWait(1000000); |
|
|
|
jack_info("JackOSSChannel::Execute resuming work."); |
|
|
|
} else if (fFrameStamp < wakeup) { |
|
|
|
// Unlock mutex before going to sleep, let others process. |
|
|
|
return Unlock() && fFrameClock.sleep(wakeup); |
|
|
|
} |
|
|
|
} |
|
|
|
return Unlock(); |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
std::int64_t JackOSSChannel::XRunGap() const |
|
|
|
{ |
|
|
|
// Compute processing gap in case we are late. |
|
|
|
std::int64_t max_end = std::max(fReadChannel.total_end(), fWriteChannel.total_end()); |
|
|
|
if (max_end < fFrameStamp) { |
|
|
|
return fFrameStamp - max_end; |
|
|
|
} |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
void JackOSSChannel::ResetBuffers(std::int64_t offset) |
|
|
|
{ |
|
|
|
// Clear buffers and offset their positions, after processing gaps. |
|
|
|
if (fReadChannel.recording()) { |
|
|
|
fReadChannel.reset_buffers(fReadChannel.end_frames() + offset); |
|
|
|
} |
|
|
|
if (fWriteChannel.playback()) { |
|
|
|
fWriteChannel.reset_buffers(fWriteChannel.end_frames() + offset); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
std::int64_t JackOSSChannel::NextWakeup() const |
|
|
|
{ |
|
|
|
return std::min(fReadChannel.wakeup_time(fFrameStamp), fWriteChannel.wakeup_time(fFrameStamp)); |
|
|
|
} |
|
|
|
|
|
|
|
} // end of namespace |