From 1d48e5e3e049f8e151fa6c2f6d8c537f49e178e6 Mon Sep 17 00:00:00 2001 From: Alain Carlucci Date: Fri, 3 Jun 2022 01:36:32 +0200 Subject: [PATCH] Implementation of linux input stream driver --- .clang-format | 1 + meson.build | 9 +- .../audio/{audio_linux.c => audio_linux.cpp} | 0 platform/drivers/audio/inputStream_linux.c | 93 ----- platform/drivers/audio/inputStream_linux.cpp | 339 ++++++++++++++++++ tests/unit/linux_inputStream_test.cpp | 50 +++ 6 files changed, 397 insertions(+), 95 deletions(-) rename platform/drivers/audio/{audio_linux.c => audio_linux.cpp} (100%) delete mode 100644 platform/drivers/audio/inputStream_linux.c create mode 100644 platform/drivers/audio/inputStream_linux.cpp create mode 100644 tests/unit/linux_inputStream_test.cpp diff --git a/.clang-format b/.clang-format index 62b427f8..8bae651a 100644 --- a/.clang-format +++ b/.clang-format @@ -10,6 +10,7 @@ IndentExternBlock: NoIndent IndentWidth: '4' NamespaceIndentation: Inner +BinPackParameters: 'false' BreakBeforeBraces: Custom BraceWrapping: AfterCaseLabel: 'true' diff --git a/meson.build b/meson.build index c1a6fa3a..2b7f4663 100644 --- a/meson.build +++ b/meson.build @@ -250,8 +250,8 @@ linux_platform_src = ['platform/targets/linux/emulator/emulator.c', 'platform/mcu/x86_64/drivers/delays.c', 'platform/mcu/x86_64/drivers/rtc.c', 'platform/drivers/baseband/radio_linux.cpp', - 'platform/drivers/audio/audio_linux.c', - 'platform/drivers/audio/inputStream_linux.c', + 'platform/drivers/audio/audio_linux.cpp', + 'platform/drivers/audio/inputStream_linux.cpp', 'platform/drivers/audio/outputStream_linux.c', 'platform/targets/linux/platform.c', 'platform/drivers/CPS/cps_io_libc.c'] @@ -670,8 +670,13 @@ cps_test = executable('cps_test', sources : unit_test_src + ['tests/unit/cps.c'], kwargs : unit_test_opts) +linux_inputStream_test = executable('linux_inputStream_test', + sources : unit_test_src + ['tests/unit/linux_inputStream_test.cpp'], + kwargs : unit_test_opts) + test('M17 Golay Unit Test', m17_golay_test) test('M17 Viterbi Unit Test', m17_viterbi_test) test('M17 Demodulator Test', m17_demodulator_test) test('M17 RRC Test', m17_rrc_test) test('Codeplug Test', cps_test) +test('Linux InputStream Test', linux_inputStream_test) diff --git a/platform/drivers/audio/audio_linux.c b/platform/drivers/audio/audio_linux.cpp similarity index 100% rename from platform/drivers/audio/audio_linux.c rename to platform/drivers/audio/audio_linux.cpp diff --git a/platform/drivers/audio/inputStream_linux.c b/platform/drivers/audio/inputStream_linux.c deleted file mode 100644 index c8f356c3..00000000 --- a/platform/drivers/audio/inputStream_linux.c +++ /dev/null @@ -1,93 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2021 - 2022 by Federico Amedeo Izzo IU2NUO, * - * Niccolò Izzo IU2KIN * - * Frederik Saraci IU2NRO * - * Silvano Seva IU2KWO * - * * - * 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 3 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, see * - ***************************************************************************/ - -#include -#include -#include -#include -#include -#include - -static stream_sample_t *buffer = NULL; -static size_t bufferLength = 0; -static bool first_half = true; -static FILE *baseband_file = NULL; - -streamId inputStream_start(const enum AudioSource source, - const enum AudioPriority prio, - stream_sample_t * const buf, - const size_t bufLength, - const enum BufMode mode, - const uint32_t sampleRate) -{ - (void) source; - (void) prio; - (void) mode; - (void) sampleRate; - - buffer = buf; - bufferLength = bufLength; - - baseband_file = fopen("./tests/unit/assets/M17_test_baseband_dc.raw", "rb"); - if (!baseband_file) - { - perror("Error while reading file\n"); - } - - return -1; -} - -dataBlock_t inputStream_getData(streamId id) -{ - (void) id; - - dataBlock_t block; - - block.len = bufferLength / 2; - - if (first_half) - { - first_half = false; - block.data = buffer; - } - else - { - first_half = true; - block.data = buffer + (bufferLength / 2); - } - - size_t read_items = fread(block.data, 2, block.len, baseband_file); - - if (read_items != block.len) - { - block.data = NULL; - return block; - } - - return block; -} - -void inputStream_stop(streamId id) -{ - (void) id; - if(baseband_file == NULL) return; - fclose(baseband_file); - baseband_file = NULL; -} diff --git a/platform/drivers/audio/inputStream_linux.cpp b/platform/drivers/audio/inputStream_linux.cpp new file mode 100644 index 00000000..96821db0 --- /dev/null +++ b/platform/drivers/audio/inputStream_linux.cpp @@ -0,0 +1,339 @@ +/*************************************************************************** + * Copyright (C) 2022 by Alain Carlucci * + * * + * 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 3 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, see * + ***************************************************************************/ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +streamId gNextAvailableStreamId = 0; + +class InputStream +{ + public: + InputStream(enum AudioSource source, + enum AudioPriority priority, + stream_sample_t* buf, + size_t bufLength, + enum BufMode mode, + uint32_t sampleRate) + : m_run_thread(true), m_func_running(false) + { + m_db_ready[0] = m_db_ready[1] = false; + + changeId(); + + std::string sourceString; + switch (source) + { + case SOURCE_MIC: + sourceString = "MIC"; + break; + case SOURCE_MCU: + sourceString = "MCU"; + break; + case SOURCE_RTX: + sourceString = "RTX"; + break; + default: + break; + } + m_fp = fopen((sourceString + ".raw").c_str(), "rb"); + if (!m_fp) + throw std::runtime_error("Cannot open: " + sourceString + ".raw"); + + fseek(m_fp, 0, SEEK_END); + m_size = ftell(m_fp); + fseek(m_fp, 0, SEEK_SET); + if (m_size % 2 || m_size == 0) + throw std::runtime_error("Invalid file: " + sourceString + ".raw"); + + setStreamData(priority, buf, bufLength, mode, sampleRate); + } + + ~InputStream() + { + stopThread(); + + if (m_fp) fclose(m_fp); + } + + dataBlock_t getDataBlock() + { + switch (m_mode) + { + case BufMode::BUF_LINEAR: + { + // With this mode, just sleep for the right amount of time + // and return the buffer content + if (!fillBuffer(m_buf, m_bufLength)) return {NULL, 0}; + + return {m_buf, m_bufLength}; + } + case BufMode::BUF_CIRC_DOUBLE: + { + // If this mode is selected, wait for the readiness of the + // current slice and return it + + int id = m_db_curread; + + // Wait for `m_buf` to be ready + while (!m_db_ready[id] && m_run_thread) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (!m_run_thread) return {NULL, 0}; + + // Return the buffer contents + auto* pos = m_buf; + m_buf += m_bufLength / 2 * id; + m_db_curread = (id + 1) % 2; + return {pos, m_bufLength / 2}; + } + default: + return {NULL, 0}; + } + } + + AudioPriority priority() const + { + return m_prio; + } + + streamId id() const + { + return m_id; + } + + void changeId() + { + m_id = gNextAvailableStreamId; + gNextAvailableStreamId += 1; + } + + void setStreamData(AudioPriority priority, + stream_sample_t* buf, + size_t bufLength, + BufMode mode, + uint32_t sampleRate) + { + stopThread(); + m_run_thread = true; // set it as runnable again + + // HERE stop thread + m_prio = priority; + m_buf = buf; + m_bufLength = bufLength; + m_mode = mode; + m_sampleRate = sampleRate; + + switch (m_mode) + { + case BufMode::BUF_LINEAR: + // TODO: stop a running thread + break; + case BufMode::BUF_CIRC_DOUBLE: + m_thread = + std::thread(std::bind(&InputStream::threadFunc, this)); + // TODO: start thread + break; + } + } + + private: + FILE* m_fp; + uint64_t m_size; + streamId m_id; + AudioPriority m_prio; + BufMode m_mode; + uint32_t m_sampleRate; + + stream_sample_t* m_buf; + size_t m_bufLength; + + size_t m_db_curwrite = 0; + size_t m_db_curread = 0; + std::atomic m_db_ready[2]; + std::atomic m_run_thread; + std::atomic m_func_running; + std::thread m_thread; + + // Emulate an ADC that reads to the circular buffer + void threadFunc() + { + m_db_ready[0] = m_db_ready[1] = false; + while (m_run_thread) + { + m_db_ready[0] = false; + m_db_curwrite = 0; + fillBuffer(m_buf, m_bufLength / 2); + m_db_ready[0] = true; + if (!m_run_thread) break; + + m_db_curwrite = 1; + m_db_ready[1] = false; + fillBuffer(m_buf + m_bufLength / 2, m_bufLength / 2); + m_db_ready[1] = true; + } + } + + // This is a blocking function that emulates an ADC writing to the + // specified memory region. It takes the same time that an ADC would take + // to sample the same quantity of data. + bool fillBuffer(stream_sample_t* dest, size_t sz) + { + size_t i = 0; + if (!m_run_thread) return false; + + assert(m_func_running == false); + m_func_running = true; + + auto reset_func_running = [&]() + { + assert(m_func_running == true); + m_func_running = false; + }; + + using std::chrono::milliseconds; + + if (m_sampleRate > 0) + { + // Do a piecewise-sleep so that it's easily interruptible + int msec = sz * 1000 / m_sampleRate; + while (msec > 10) + { + printf("Wait 10ms: %d\n", msec); + if (!m_run_thread) + { + // Early exit if the class is being deallocated + reset_func_running(); + return false; + } + + std::this_thread::sleep_for(milliseconds(10)); + msec -= 10; + } + std::this_thread::sleep_for(milliseconds(msec)); + } + + if (!m_run_thread) + { + // Early exit if the class is being deallocated + reset_func_running(); + return false; + } + + // Fill the buffer + while (i < sz) + { + printf("Read: %lu/%lu\n", i, sz); + auto n = fread(dest + i, 2, sz - i, m_fp); + printf("Ret: %lu\n", n); + i += n; + fseek(m_fp, 0, SEEK_SET); + } + + assert(i == sz); + reset_func_running(); + return true; + } + + void stopThread() + { + m_run_thread = false; + + while (m_func_running) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + if (m_thread.joinable()) m_thread.join(); + } +}; + +std::map gOpenStreams; + +streamId inputStream_start(const enum AudioSource source, + const enum AudioPriority priority, + stream_sample_t* const buf, + const size_t bufLength, + const enum BufMode mode, + const uint32_t sampleRate) +{ + auto it = gOpenStreams.find(source); + if (it != gOpenStreams.end()) + { + auto& inputStream = it->second; + if (inputStream.priority() >= priority) return -1; + + inputStream.changeId(); + inputStream.setStreamData(priority, buf, bufLength, mode, sampleRate); + + return inputStream.id(); + } + + // New stream: allocate directly in the std::map + auto res = gOpenStreams.emplace( + std::piecewise_construct, std::forward_as_tuple(source), + std::forward_as_tuple(source, priority, buf, bufLength, mode, + sampleRate)); + + if (!res.second) return -1; + + return res.first->second.id(); +} + +dataBlock_t inputStream_getData(streamId id) +{ + InputStream* stream = nullptr; + for (auto& i : gOpenStreams) + if (i.second.id() == id) + { + stream = &i.second; + break; + } + + if (stream == nullptr) return dataBlock_t{NULL, 0}; + + return stream->getDataBlock(); +} + +void inputStream_stop(streamId id) +{ + AudioSource src; + bool found = false; + for (auto& i : gOpenStreams) + if (i.second.id() == id) + { + found = true; + src = i.first; + break; + } + + if (!found) return; + + gOpenStreams.erase(src); +} diff --git a/tests/unit/linux_inputStream_test.cpp b/tests/unit/linux_inputStream_test.cpp new file mode 100644 index 00000000..0724b332 --- /dev/null +++ b/tests/unit/linux_inputStream_test.cpp @@ -0,0 +1,50 @@ +/*************************************************************************** + * Copyright (C) 2022 by Alain Carlucci * + * * + * 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 3 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, see * + ***************************************************************************/ + +#include +#include + +#include "interfaces/audio_stream.h" + +#define CHECK(x) \ + do \ + { \ + if (!(x)) \ + { \ + puts("Failed assertion: " #x "\n"); \ + abort(); \ + } \ + } while (0) + +int main() +{ + FILE* fp = fopen("MIC.raw", "wb"); + for (int i = 0; i < 13; i++) + { + fputc(i, fp); + fputc(0, fp); + } + fclose(fp); + + stream_sample_t tmp[128]; + auto id = inputStream_start( + AudioSource::SOURCE_MIC, AudioPriority::PRIO_BEEP, tmp, + sizeof(tmp) / sizeof(tmp[0]), BufMode::BUF_LINEAR, 44100); + inputStream_getData(id); + for (int i = 0; i < 128; i++) CHECK(tmp[i] == (i % 13)); + return 0; +}