From a64106783f0e8cbede17f3ef0de68d5ee05e0113 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Sat, 23 Jan 2021 11:45:20 +0200 Subject: [PATCH] Support for various sample formats (#11) * wip : support for various sample formats * finalize support for various sample formats * adding more tests * update python bindings * add "string" header --- README.md | 6 - bindings/python/cggwave.pxd | 10 +- bindings/python/ggwave.pyx | 8 +- examples/ggwave-common-sdl2.cpp | 49 +++--- examples/ggwave-to-file/main.cpp | 2 +- examples/waver/common.cpp | 6 +- include/ggwave/ggwave.h | 56 ++++--- src/ggwave.cpp | 264 +++++++++++++++++++++++++------ tests/test-ggwave.cpp | 208 ++++++++++++++++++++++-- 9 files changed, 473 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index fc5cd26..0ad6cfd 100644 --- a/README.md +++ b/README.md @@ -164,12 +164,6 @@ sudo snap connect waver:audio-record :audio-record brew install ggerganov/ggerganov/waver ``` -## Todo - - - [ ] Improve library interface - - [ ] Support for non-float32 input and non-int16 output - - [x] Mobile app examples - [changelog]: ./CHANGELOG.md [changelog-badge]: https://img.shields.io/badge/changelog-ggwave%20v0.1-dummy [license]: ./LICENSE diff --git a/bindings/python/cggwave.pxd b/bindings/python/cggwave.pxd index 6a8bec7..e8d325d 100644 --- a/bindings/python/cggwave.pxd +++ b/bindings/python/cggwave.pxd @@ -1,6 +1,10 @@ cdef extern from "ggwave.h" nogil: ctypedef enum ggwave_SampleFormat: + GGWAVE_SAMPLE_FORMAT_UNDEFINED, + GGWAVE_SAMPLE_FORMAT_U8, + GGWAVE_SAMPLE_FORMAT_I8, + GGWAVE_SAMPLE_FORMAT_U16, GGWAVE_SAMPLE_FORMAT_I16, GGWAVE_SAMPLE_FORMAT_F32 @@ -16,12 +20,12 @@ cdef extern from "ggwave.h" nogil: int sampleRateIn int sampleRateOut int samplesPerFrame - ggwave_SampleFormat formatIn - ggwave_SampleFormat formatOut + ggwave_SampleFormat sampleFormatIn + ggwave_SampleFormat sampleFormatOut ctypedef int ggwave_Instance - ggwave_Parameters ggwave_defaultParameters(); + ggwave_Parameters ggwave_getDefaultParameters(); ggwave_Instance ggwave_init(const ggwave_Parameters parameters); diff --git a/bindings/python/ggwave.pyx b/bindings/python/ggwave.pyx index 192769d..2a05d55 100644 --- a/bindings/python/ggwave.pyx +++ b/bindings/python/ggwave.pyx @@ -6,12 +6,12 @@ import struct cimport cggwave -def defaultParameters(): - return cggwave.ggwave_defaultParameters() +def getDefaultParameters(): + return cggwave.ggwave_getDefaultParameters() def init(parameters = None): if (parameters is None): - parameters = defaultParameters() + parameters = getDefaultParameters() return cggwave.ggwave_init(parameters) @@ -33,7 +33,7 @@ def encode(payload, txProtocolId = 1, volume = 10, instance = None): own = False if (instance is None): own = True - instance = init(defaultParameters()) + instance = init(getDefaultParameters()) n = cggwave.ggwave_encode(instance, cdata, len(data_bytes), txProtocolId, volume, coutput) diff --git a/examples/ggwave-common-sdl2.cpp b/examples/ggwave-common-sdl2.cpp index 90d9f3b..0e0a202 100644 --- a/examples/ggwave-common-sdl2.cpp +++ b/examples/ggwave-common-sdl2.cpp @@ -188,48 +188,37 @@ bool GGWave_init( } } - int sampleSizeBytesIn = 4; - int sampleSizeBytesOut = 2; + GGWave::SampleFormat sampleFormatIn = GGWAVE_SAMPLE_FORMAT_UNDEFINED; + GGWave::SampleFormat sampleFormatOut = GGWAVE_SAMPLE_FORMAT_UNDEFINED; switch (g_obtainedSpecIn.format) { - case AUDIO_U8: - case AUDIO_S8: - sampleSizeBytesIn = 1; - break; - case AUDIO_U16SYS: - case AUDIO_S16SYS: - sampleSizeBytesIn = 2; - break; - case AUDIO_S32SYS: - case AUDIO_F32SYS: - sampleSizeBytesIn = 4; - break; + case AUDIO_U8: sampleFormatIn = GGWAVE_SAMPLE_FORMAT_U8; break; + case AUDIO_S8: sampleFormatIn = GGWAVE_SAMPLE_FORMAT_I8; break; + case AUDIO_U16SYS: sampleFormatIn = GGWAVE_SAMPLE_FORMAT_U16; break; + case AUDIO_S16SYS: sampleFormatIn = GGWAVE_SAMPLE_FORMAT_I16; break; + case AUDIO_S32SYS: sampleFormatIn = GGWAVE_SAMPLE_FORMAT_F32; break; + case AUDIO_F32SYS: sampleFormatIn = GGWAVE_SAMPLE_FORMAT_F32; break; } switch (g_obtainedSpecOut.format) { - case AUDIO_U8: - case AUDIO_S8: - sampleSizeBytesOut = 1; - break; - case AUDIO_U16SYS: - case AUDIO_S16SYS: - sampleSizeBytesOut = 2; - break; - case AUDIO_S32SYS: - case AUDIO_F32SYS: - sampleSizeBytesOut = 4; + case AUDIO_U8: sampleFormatOut = GGWAVE_SAMPLE_FORMAT_U8; break; + case AUDIO_S8: sampleFormatOut = GGWAVE_SAMPLE_FORMAT_I8; break; + case AUDIO_U16SYS: sampleFormatOut = GGWAVE_SAMPLE_FORMAT_U16; break; + case AUDIO_S16SYS: sampleFormatOut = GGWAVE_SAMPLE_FORMAT_I16; break; + case AUDIO_S32SYS: sampleFormatOut = GGWAVE_SAMPLE_FORMAT_F32; break; + case AUDIO_F32SYS: sampleFormatOut = GGWAVE_SAMPLE_FORMAT_F32; break; break; } if (reinit) { if (g_ggWave) delete g_ggWave; - g_ggWave = new GGWave( + g_ggWave = new GGWave({ g_obtainedSpecIn.freq, g_obtainedSpecOut.freq, GGWave::kDefaultSamplesPerFrame, - sampleSizeBytesIn, - sampleSizeBytesOut); + sampleFormatIn, + sampleFormatOut}); } return true; @@ -246,7 +235,7 @@ bool GGWave_mainLoop() { SDL_QueueAudio(g_devIdOut, data, nBytes); }; - static GGWave::CBDequeueAudio CBDequeueAudio = [&](void * data, uint32_t nMaxBytes) { + static GGWave::CBDequeueAudio cbDequeueAudio = [&](void * data, uint32_t nMaxBytes) { return SDL_DequeueAudio(g_devIdIn, data, nMaxBytes); }; @@ -259,7 +248,7 @@ bool GGWave_mainLoop() { if ((int) SDL_GetQueuedAudioSize(g_devIdOut) < g_ggWave->getSamplesPerFrame()*g_ggWave->getSampleSizeBytesOut()) { SDL_PauseAudioDevice(g_devIdIn, SDL_FALSE); if (::getTime_ms(tLastNoData, tNow) > 500.0f) { - g_ggWave->decode(CBDequeueAudio); + g_ggWave->decode(cbDequeueAudio); if ((int) SDL_GetQueuedAudioSize(g_devIdIn) > 32*g_ggWave->getSamplesPerFrame()*g_ggWave->getSampleSizeBytesIn()) { SDL_ClearQueuedAudio(g_devIdIn); } diff --git a/examples/ggwave-to-file/main.cpp b/examples/ggwave-to-file/main.cpp index beba159..c87937d 100644 --- a/examples/ggwave-to-file/main.cpp +++ b/examples/ggwave-to-file/main.cpp @@ -70,7 +70,7 @@ int main(int argc, char** argv) { fprintf(stderr, "Generating waveform for message '%s' ...\n", message.c_str()); - GGWave ggWave(GGWave::kBaseSampleRate, sampleRateOut, 1024, 4, 2); + GGWave ggWave({ GGWave::kBaseSampleRate, sampleRateOut, 1024, GGWAVE_SAMPLE_FORMAT_F32, GGWAVE_SAMPLE_FORMAT_I16 }); ggWave.init(message.size(), message.data(), ggWave.getTxProtocol(protocolId), volume); std::vector bufferPCM; diff --git a/examples/waver/common.cpp b/examples/waver/common.cpp index 191da83..798af50 100644 --- a/examples/waver/common.cpp +++ b/examples/waver/common.cpp @@ -181,7 +181,7 @@ struct State { Message message; GGWave::SpectrumData spectrum; - GGWave::AmplitudeData16 txAmplitudeData; + GGWave::AmplitudeDataI16 txAmplitudeData; GGWaveStats stats; }; @@ -563,7 +563,7 @@ void updateCore() { g_buffer.stateCore.flags.newSpectrum = true; } - if (g_ggWave->takeTxAmplitudeData16(g_buffer.stateCore.txAmplitudeData)) { + if (g_ggWave->takeTxAmplitudeDataI16(g_buffer.stateCore.txAmplitudeData)) { g_buffer.stateCore.update = true; g_buffer.stateCore.flags.newTxAmplitudeData = true; } @@ -696,7 +696,7 @@ void renderMain() { static GGWaveStats statsCurrent; static GGWave::SpectrumData spectrumCurrent; - static GGWave::AmplitudeData16 txAmplitudeDataCurrent; + static GGWave::AmplitudeDataI16 txAmplitudeDataCurrent; static std::vector messageHistory; if (stateCurrent.update) { diff --git a/include/ggwave/ggwave.h b/include/ggwave/ggwave.h index d4d1d74..daba36d 100644 --- a/include/ggwave/ggwave.h +++ b/include/ggwave/ggwave.h @@ -25,6 +25,10 @@ extern "C" { // Data format of the audio samples typedef enum { + GGWAVE_SAMPLE_FORMAT_UNDEFINED, + GGWAVE_SAMPLE_FORMAT_U8, + GGWAVE_SAMPLE_FORMAT_I8, + GGWAVE_SAMPLE_FORMAT_U16, GGWAVE_SAMPLE_FORMAT_I16, GGWAVE_SAMPLE_FORMAT_F32, } ggwave_SampleFormat; @@ -41,11 +45,11 @@ extern "C" { // GGWave instance parameters typedef struct { - int sampleRateIn; // capture sample rate - int sampleRateOut; // playback sample rate - int samplesPerFrame; // number of samples per audio frame - ggwave_SampleFormat formatIn; // format of the captured audio samples - ggwave_SampleFormat formatOut; // format of the playback audio samples + int sampleRateIn; // capture sample rate + int sampleRateOut; // playback sample rate + int samplesPerFrame; // number of samples per audio frame + ggwave_SampleFormat sampleFormatIn; // format of the captured audio samples + ggwave_SampleFormat sampleFormatOut; // format of the playback audio samples } ggwave_Parameters; // GGWave instances are identified with an integer and are stored @@ -54,7 +58,7 @@ extern "C" { typedef int ggwave_Instance; // Helper method to get default instance parameters - GGWAVE_API ggwave_Parameters ggwave_defaultParameters(void); + GGWAVE_API ggwave_Parameters ggwave_getDefaultParameters(void); // Create a new GGWave instance with the specified parameters GGWAVE_API ggwave_Instance ggwave_init(const ggwave_Parameters parameters); @@ -120,6 +124,7 @@ extern "C" { #include #include #include +#include class GGWave { public: @@ -133,7 +138,9 @@ public: static constexpr auto kMaxSpectrumHistory = 4; static constexpr auto kMaxRecordedFrames = 1024; - using TxProtocolId = ggwave_TxProtocolId; + using Parameters = ggwave_Parameters; + using SampleFormat = ggwave_SampleFormat; + using TxProtocolId = ggwave_TxProtocolId; struct TxProtocol { const char * name; // string identifier of the protocol @@ -160,26 +167,24 @@ public: return kTxProtocols; } - using AmplitudeData = std::vector; - using AmplitudeData16 = std::vector; - using SpectrumData = std::vector; - using RecordedData = std::vector; - using TxRxData = std::vector; + using AmplitudeData = std::vector; + using AmplitudeDataI16 = std::vector; + using SpectrumData = std::vector; + using RecordedData = std::vector; + using TxRxData = std::vector; using CBEnqueueAudio = std::function; using CBDequeueAudio = std::function; - GGWave( - int sampleRateIn, - int sampleRateOut, - int samplesPerFrame, - int sampleSizeBytesIn, - int sampleSizeBytesOut); - + GGWave(const Parameters & parameters); ~GGWave(); + static const Parameters & getDefaultParameters(); + + bool init(const std::string & text, const int volume = kDefaultVolume); + bool init(const std::string & text, const TxProtocol & txProtocol, const int volume = kDefaultVolume); bool init(int dataSize, const char * dataBuffer, const int volume = kDefaultVolume); - bool init(int dataSize, const char * dataBuffer, const TxProtocol & aProtocol, const int volume = kDefaultVolume); + bool init(int dataSize, const char * dataBuffer, const TxProtocol & txProtocol, const int volume = kDefaultVolume); bool encode(const CBEnqueueAudio & cbEnqueueAudio); void decode(const CBDequeueAudio & cbDequeueAudio); @@ -209,7 +214,7 @@ public: const TxProtocolId & getRxProtocolId() const { return m_rxProtocolId; } int takeRxData(TxRxData & dst); - int takeTxAmplitudeData16(AmplitudeData16 & dst); + int takeTxAmplitudeDataI16(AmplitudeDataI16 & dst); bool takeSpectrum(SpectrumData & dst); private: @@ -226,6 +231,8 @@ private: const float m_isamplesPerFrame; const int m_sampleSizeBytesIn; const int m_sampleSizeBytesOut; + const SampleFormat m_sampleFormatIn; + const SampleFormat m_sampleFormatOut; const float m_hzPerSample; const float m_ihzPerSample; @@ -249,6 +256,7 @@ private: int m_framesLeftToRecord; int m_framesToAnalyze; int m_framesToRecord; + int m_samplesNeeded; std::vector m_fftIn; // real std::vector m_fftOut; // complex @@ -256,6 +264,7 @@ private: bool m_hasNewSpectrum; SpectrumData m_sampleSpectrum; AmplitudeData m_sampleAmplitude; + TxRxData m_sampleAmplitudeTmp; bool m_hasNewRxData; int m_lastRxDataLength; @@ -282,8 +291,9 @@ private: TxProtocol m_txProtocol; AmplitudeData m_outputBlock; - AmplitudeData16 m_outputBlock16; - AmplitudeData16 m_txAmplitudeData16; + TxRxData m_outputBlockTmp; + AmplitudeDataI16 m_outputBlockI16; + AmplitudeDataI16 m_txAmplitudeDataI16; }; #endif diff --git a/src/ggwave.cpp b/src/ggwave.cpp index cb1e6c6..d9f55a2 100644 --- a/src/ggwave.cpp +++ b/src/ggwave.cpp @@ -3,8 +3,9 @@ #include "reed-solomon/rs.hpp" #include +#include #include -#include +//#include #include #include @@ -17,27 +18,20 @@ std::map g_instances; } extern "C" -ggwave_Parameters ggwave_defaultParameters(void) { - ggwave_Parameters result { - GGWave::kBaseSampleRate, - GGWave::kBaseSampleRate, - GGWave::kDefaultSamplesPerFrame, - GGWAVE_SAMPLE_FORMAT_F32, - GGWAVE_SAMPLE_FORMAT_I16 - }; - return result; +ggwave_Parameters ggwave_getDefaultParameters(void) { + return GGWave::getDefaultParameters(); } extern "C" ggwave_Instance ggwave_init(const ggwave_Parameters parameters) { static ggwave_Instance curId = 0; - g_instances[curId] = new GGWave( + g_instances[curId] = new GGWave({ parameters.sampleRateIn, parameters.sampleRateOut, parameters.samplesPerFrame, - 4, // todo : hardcoded sample sizes - 2); + GGWAVE_SAMPLE_FORMAT_F32, + GGWAVE_SAMPLE_FORMAT_I16}); return curId++; } @@ -74,8 +68,7 @@ int ggwave_encode( char * p = (char *) data; std::copy(p, p + nBytes, outputBuffer); - // todo : tmp assume int16 - nSamples = nBytes/2; + nSamples = nBytes/ggWave->getSampleSizeBytesOut(); }; if (ggWave->encode(cbEnqueueAudio) == false) { @@ -242,21 +235,45 @@ int getECCBytesForLength(int len) { return std::max(4, 2*(len/5)); } +int bytesForSampleFormat(GGWave::SampleFormat sampleFormat) { + switch (sampleFormat) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: return 0; break; + case GGWAVE_SAMPLE_FORMAT_U8: return sizeof(uint8_t); break; + case GGWAVE_SAMPLE_FORMAT_I8: return sizeof(int8_t); break; + case GGWAVE_SAMPLE_FORMAT_U16: return sizeof(uint16_t); break; + case GGWAVE_SAMPLE_FORMAT_I16: return sizeof(int16_t); break; + case GGWAVE_SAMPLE_FORMAT_F32: return sizeof(float); break; + }; + + fprintf(stderr, "Invalid sample format: %d\n", (int) sampleFormat); + + return 0; } -GGWave::GGWave( - int sampleRateIn, - int sampleRateOut, - int samplesPerFrame, - int sampleSizeBytesIn, - int sampleSizeBytesOut) : - m_sampleRateIn(sampleRateIn), - m_sampleRateOut(sampleRateOut), - m_samplesPerFrame(samplesPerFrame), +} + +const GGWave::Parameters & GGWave::getDefaultParameters() { + static ggwave_Parameters result { + GGWave::kBaseSampleRate, + GGWave::kBaseSampleRate, + GGWave::kDefaultSamplesPerFrame, + GGWAVE_SAMPLE_FORMAT_F32, + GGWAVE_SAMPLE_FORMAT_I16 + }; + + return result; +} + +GGWave::GGWave(const Parameters & parameters) : + m_sampleRateIn(parameters.sampleRateIn), + m_sampleRateOut(parameters.sampleRateOut), + m_samplesPerFrame(parameters.samplesPerFrame), m_isamplesPerFrame(1.0f/m_samplesPerFrame), - m_sampleSizeBytesIn(sampleSizeBytesIn), - m_sampleSizeBytesOut(sampleSizeBytesOut), - m_hzPerSample(m_sampleRateIn/samplesPerFrame), + m_sampleSizeBytesIn(bytesForSampleFormat(parameters.sampleFormatIn)), + m_sampleSizeBytesOut(bytesForSampleFormat(parameters.sampleFormatOut)), + m_sampleFormatIn(parameters.sampleFormatIn), + m_sampleFormatOut(parameters.sampleFormatOut), + m_hzPerSample(m_sampleRateIn/parameters.samplesPerFrame), m_ihzPerSample(1.0f/m_hzPerSample), m_freqDelta_bin(1), m_freqDelta_hz(2*m_hzPerSample), @@ -264,11 +281,13 @@ GGWave::GGWave( m_nMarkerFrames(16), m_nPostMarkerFrames(0), m_encodedDataOffset(3), + m_samplesNeeded(m_samplesPerFrame), m_fftIn(kMaxSamplesPerFrame), m_fftOut(2*kMaxSamplesPerFrame), m_hasNewSpectrum(false), m_sampleSpectrum(kMaxSamplesPerFrame), m_sampleAmplitude(kMaxSamplesPerFrame), + m_sampleAmplitudeTmp(kMaxSamplesPerFrame*m_sampleSizeBytesIn), m_hasNewRxData(false), m_lastRxDataLength(0), m_rxData(kMaxDataSize), @@ -278,23 +297,40 @@ GGWave::GGWave( m_txData(kMaxDataSize), m_txDataEncoded(kMaxDataSize), m_outputBlock(kMaxSamplesPerFrame), - m_outputBlock16(kMaxRecordedFrames*kMaxSamplesPerFrame) -{ - if (samplesPerFrame > kMaxSamplesPerFrame) { + m_outputBlockTmp(kMaxRecordedFrames*kMaxSamplesPerFrame*m_sampleSizeBytesOut), + m_outputBlockI16(kMaxRecordedFrames*kMaxSamplesPerFrame) { + + if (m_sampleSizeBytesIn == 0) { + throw std::runtime_error("Invalid or unsupported capture sample format"); + } + + if (m_sampleSizeBytesOut == 0) { + throw std::runtime_error("Invalid or unsupported playback sample format"); + } + + if (parameters.samplesPerFrame > kMaxSamplesPerFrame) { throw std::runtime_error("Invalid samples per frame"); } - init(0, "", getDefaultTxProtocol(), 0); + init("", getDefaultTxProtocol(), 0); } GGWave::~GGWave() { } +bool GGWave::init(const std::string & text, const int volume) { + return init(text.size(), text.data(), getDefaultTxProtocol(), volume); +} + +bool GGWave::init(const std::string & text, const TxProtocol & txProtocol, const int volume) { + return init(text.size(), text.data(), txProtocol, volume); +} + bool GGWave::init(int dataSize, const char * dataBuffer, const int volume) { return init(dataSize, dataBuffer, getDefaultTxProtocol(), volume); } -bool GGWave::init(int dataSize, const char * dataBuffer, const TxProtocol & aProtocol, const int volume) { +bool GGWave::init(int dataSize, const char * dataBuffer, const TxProtocol & txProtocol, const int volume) { if (dataSize < 0) { fprintf(stderr, "Negative data size: %d\n", dataSize); return false; @@ -310,7 +346,7 @@ bool GGWave::init(int dataSize, const char * dataBuffer, const TxProtocol & aPro return false; } - m_txProtocol = aProtocol; + m_txProtocol = txProtocol; m_txDataLength = dataSize; m_sendVolume = ((double)(volume))/100.0f; @@ -372,10 +408,10 @@ bool GGWave::encode(const CBEnqueueAudio & cbEnqueueAudio) { } // note : what is the purpose of this shuffle ? I forgot .. :( - std::random_device rd; - std::mt19937 g(rd()); + //std::random_device rd; + //std::mt19937 g(rd()); - std::shuffle(phaseOffsets.begin(), phaseOffsets.end(), g); + //std::shuffle(phaseOffsets.begin(), phaseOffsets.end(), g); std::vector dataBits(kMaxDataBits); @@ -507,19 +543,75 @@ bool GGWave::encode(const CBEnqueueAudio & cbEnqueueAudio) { m_outputBlock[i] *= scale; } - // todo : support for non-int16 output + uint32_t offset = frameId*samplesPerFrameOut; + + // default output is in 16-bit signed int so we always compute it for (int i = 0; i < samplesPerFrameOut; ++i) { - m_outputBlock16[frameId*samplesPerFrameOut + i] = std::round(32000.0*m_outputBlock[i]); + m_outputBlockI16[offset + i] = 32768*m_outputBlock[i]; + } + + // convert from 32-bit float + switch (m_sampleFormatOut) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: break; + case GGWAVE_SAMPLE_FORMAT_U8: + { + auto p = reinterpret_cast(m_outputBlockTmp.data()); + for (int i = 0; i < samplesPerFrameOut; ++i) { + p[offset + i] = 128*(m_outputBlock[i] + 1.0f); + } + } break; + case GGWAVE_SAMPLE_FORMAT_I8: + { + auto p = reinterpret_cast(m_outputBlockTmp.data()); + for (int i = 0; i < samplesPerFrameOut; ++i) { + p[offset + i] = 128*m_outputBlock[i]; + } + } break; + case GGWAVE_SAMPLE_FORMAT_U16: + { + auto p = reinterpret_cast(m_outputBlockTmp.data()); + for (int i = 0; i < samplesPerFrameOut; ++i) { + p[offset + i] = 32768*(m_outputBlock[i] + 1.0f); + } + } break; + case GGWAVE_SAMPLE_FORMAT_I16: + { + // skip because we already have the data in m_outputBlockI16 + //auto p = reinterpret_cast(m_outputBlockTmp.data()); + //for (int i = 0; i < samplesPerFrameOut; ++i) { + // p[offset + i] = 32768*m_outputBlock[i]; + //} + } break; + case GGWAVE_SAMPLE_FORMAT_F32: + { + auto p = reinterpret_cast(m_outputBlockTmp.data()); + for (int i = 0; i < samplesPerFrameOut; ++i) { + p[offset + i] = m_outputBlock[i]; + } + } break; } ++frameId; } - cbEnqueueAudio(m_outputBlock16.data(), frameId*samplesPerFrameOut*m_sampleSizeBytesOut); + switch (m_sampleFormatOut) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: break; + case GGWAVE_SAMPLE_FORMAT_I16: + { + cbEnqueueAudio(m_outputBlockI16.data(), frameId*samplesPerFrameOut*m_sampleSizeBytesOut); + } break; + case GGWAVE_SAMPLE_FORMAT_U8: + case GGWAVE_SAMPLE_FORMAT_I8: + case GGWAVE_SAMPLE_FORMAT_U16: + case GGWAVE_SAMPLE_FORMAT_F32: + { + cbEnqueueAudio(m_outputBlockTmp.data(), frameId*samplesPerFrameOut*m_sampleSizeBytesOut); + } break; + } - m_txAmplitudeData16.resize(frameId*samplesPerFrameOut); + m_txAmplitudeDataI16.resize(frameId*samplesPerFrameOut); for (int i = 0; i < frameId*samplesPerFrameOut; ++i) { - m_txAmplitudeData16[i] = m_outputBlock16[i]; + m_txAmplitudeDataI16[i] = m_outputBlockI16[i]; } return true; @@ -528,11 +620,84 @@ bool GGWave::encode(const CBEnqueueAudio & cbEnqueueAudio) { void GGWave::decode(const CBDequeueAudio & cbDequeueAudio) { while (m_hasNewTxData == false) { // read capture data - // - // todo : support for non-float input - auto nBytesRecorded = cbDequeueAudio(m_sampleAmplitude.data(), m_samplesPerFrame*m_sampleSizeBytesIn); + uint32_t nBytesNeeded = m_samplesNeeded*m_sampleSizeBytesIn; + uint32_t nBytesRecorded = 0; + uint32_t offset = m_samplesPerFrame - m_samplesNeeded; - if (nBytesRecorded != 0) { + switch (m_sampleFormatIn) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: break; + case GGWAVE_SAMPLE_FORMAT_U8: + case GGWAVE_SAMPLE_FORMAT_I8: + case GGWAVE_SAMPLE_FORMAT_U16: + case GGWAVE_SAMPLE_FORMAT_I16: + { + nBytesRecorded = cbDequeueAudio(m_sampleAmplitudeTmp.data() + offset, nBytesNeeded); + } break; + case GGWAVE_SAMPLE_FORMAT_F32: + { + nBytesRecorded = cbDequeueAudio(m_sampleAmplitude.data() + offset, nBytesNeeded); + } break; + } + + if (nBytesRecorded % m_sampleSizeBytesIn != 0) { + fprintf(stderr, "Failure during capture - provided bytes (%d) are not multiple of sample size (%d)\n", + nBytesRecorded, m_sampleSizeBytesIn); + m_samplesNeeded = m_samplesPerFrame; + break; + } + + if (nBytesRecorded > nBytesNeeded) { + fprintf(stderr, "Failure during capture - more samples were provided (%d) than requested (%d)\n", + nBytesRecorded/m_sampleSizeBytesIn, nBytesNeeded/m_sampleSizeBytesIn); + m_samplesNeeded = m_samplesPerFrame; + break; + } + + // convert to 32-bit float + switch (m_sampleFormatIn) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: break; + case GGWAVE_SAMPLE_FORMAT_U8: + { + constexpr float scale = 1.0f/128; + int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesIn; + auto p = reinterpret_cast(m_sampleAmplitudeTmp.data()); + for (int i = 0; i < nSamplesRecorded; ++i) { + m_sampleAmplitude[offset + i] = float(int16_t(*(p + offset + i)) - 128)*scale; + } + } break; + case GGWAVE_SAMPLE_FORMAT_I8: + { + constexpr float scale = 1.0f/128; + int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesIn; + auto p = reinterpret_cast(m_sampleAmplitudeTmp.data()); + for (int i = 0; i < nSamplesRecorded; ++i) { + m_sampleAmplitude[offset + i] = float(*(p + offset + i))*scale; + } + } break; + case GGWAVE_SAMPLE_FORMAT_U16: + { + constexpr float scale = 1.0f/32768; + int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesIn; + auto p = reinterpret_cast(m_sampleAmplitudeTmp.data()); + for (int i = 0; i < nSamplesRecorded; ++i) { + m_sampleAmplitude[offset + i] = float(int32_t(*(p + offset + i)) - 32768)*scale; + } + } break; + case GGWAVE_SAMPLE_FORMAT_I16: + { + constexpr float scale = 1.0f/32768; + int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesIn; + auto p = reinterpret_cast(m_sampleAmplitudeTmp.data()); + for (int i = 0; i < nSamplesRecorded; ++i) { + m_sampleAmplitude[offset + i] = float(*(p + offset + i))*scale; + } + } break; + case GGWAVE_SAMPLE_FORMAT_F32: break; + } + + // we have enough bytes to do analysis + if (nBytesRecorded == nBytesNeeded) { + m_samplesNeeded = m_samplesPerFrame; m_sampleAmplitudeHistory[m_historyId] = m_sampleAmplitude; if (++m_historyId >= kMaxSpectrumHistory) { @@ -784,6 +949,7 @@ void GGWave::decode(const CBDequeueAudio & cbDequeueAudio) { } } } else { + m_samplesNeeded -= nBytesRecorded/m_sampleSizeBytesIn; break; } } @@ -802,11 +968,11 @@ int GGWave::takeRxData(TxRxData & dst) { return res; } -int GGWave::takeTxAmplitudeData16(AmplitudeData16 & dst) { - if (m_txAmplitudeData16.size() == 0) return 0; +int GGWave::takeTxAmplitudeDataI16(AmplitudeDataI16 & dst) { + if (m_txAmplitudeDataI16.size() == 0) return 0; - int res = (int) m_txAmplitudeData16.size(); - dst = std::move(m_txAmplitudeData16); + int res = (int) m_txAmplitudeDataI16.size(); + dst = std::move(m_txAmplitudeDataI16); return res; } diff --git a/tests/test-ggwave.cpp b/tests/test-ggwave.cpp index 24968cd..93be817 100644 --- a/tests/test-ggwave.cpp +++ b/tests/test-ggwave.cpp @@ -1,6 +1,11 @@ #include "ggwave/ggwave.h" +#include #include +#include +#include +#include +#include #define CHECK(cond) \ if (!(cond)) { \ @@ -11,29 +16,198 @@ #define CHECK_T(cond) CHECK(cond) #define CHECK_F(cond) CHECK(!(cond)) +const std::map kSampleScale = { + { typeid(uint8_t), std::numeric_limits::max() }, + { typeid(int8_t), std::numeric_limits::max() }, + { typeid(uint16_t), std::numeric_limits::max() }, + { typeid(int16_t), std::numeric_limits::max() }, + { typeid(float), 1.0f }, +}; + +const std::map kSampleOffset = { + { typeid(uint8_t), 0.5f*std::numeric_limits::max() }, + { typeid(int8_t), 0.0f }, + { typeid(uint16_t), 0.5f*std::numeric_limits::max() }, + { typeid(int16_t), 0.0f }, + { typeid(float), 0.0f }, +}; + +const std::set kFormats = { + GGWAVE_SAMPLE_FORMAT_U8, + GGWAVE_SAMPLE_FORMAT_I8, + GGWAVE_SAMPLE_FORMAT_U16, + GGWAVE_SAMPLE_FORMAT_I16, + GGWAVE_SAMPLE_FORMAT_F32, +}; + +template +GGWave::CBEnqueueAudio getCBEnqueueAudio(uint32_t & nSamples, std::vector & buffer) { + return [&nSamples, &buffer](const void * data, uint32_t nBytes) { + nSamples = nBytes/sizeof(T); + CHECK(nSamples*sizeof(T) == nBytes); + buffer.resize(nSamples); + std::copy((char *) data, (char *) data + nBytes, (char *) buffer.data()); + }; +} + +template +GGWave::CBDequeueAudio getCBDequeueAudio(uint32_t & nSamples, std::vector & buffer) { + return [&nSamples, &buffer](void * data, uint32_t nMaxBytes) { + uint32_t nCopied = std::min((uint32_t) (nSamples*sizeof(T)), nMaxBytes); + const char * p = (char *) (buffer.data() + buffer.size() - nSamples); + std::copy(p, p + nCopied, (char *) data); + nSamples -= nCopied/sizeof(T); + + return nCopied; + }; +} + +template +void convert(const std::vector & src, std::vector & dst) { + int n = src.size(); + dst.resize(n); + for (int i = 0; i < n; ++i) { + dst[i] = ((float(src[i]) - kSampleOffset.at(typeid(S)))/kSampleScale.at(typeid(S)))*kSampleScale.at(typeid(D)) + kSampleOffset.at(typeid(D)); + } +} + int main() { - GGWave instance(48000, 48000, 1024, 4, 2); + std::vector bufferU8; + std::vector bufferI8; + std::vector bufferU16; + std::vector bufferI16; + std::vector bufferF32; - std::string payload = "hello"; + auto convertHelper = [&](GGWave::SampleFormat formatOut, GGWave::SampleFormat formatIn) { + switch (formatOut) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: CHECK(false); break; + case GGWAVE_SAMPLE_FORMAT_U8: + { + switch (formatIn) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: CHECK(false); break; + case GGWAVE_SAMPLE_FORMAT_U8: break; + case GGWAVE_SAMPLE_FORMAT_I8: convert(bufferU8, bufferI8); break; + case GGWAVE_SAMPLE_FORMAT_U16: convert(bufferU8, bufferU16); break; + case GGWAVE_SAMPLE_FORMAT_I16: convert(bufferU8, bufferI16); break; + case GGWAVE_SAMPLE_FORMAT_F32: convert(bufferU8, bufferF32); break; + }; + } break; + case GGWAVE_SAMPLE_FORMAT_I8: + { + switch (formatIn) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: CHECK(false); break; + case GGWAVE_SAMPLE_FORMAT_U8: convert(bufferI8, bufferU8); break; + case GGWAVE_SAMPLE_FORMAT_I8: break; + case GGWAVE_SAMPLE_FORMAT_U16: convert(bufferI8, bufferU16); break; + case GGWAVE_SAMPLE_FORMAT_I16: convert(bufferI8, bufferI16); break; + case GGWAVE_SAMPLE_FORMAT_F32: convert(bufferI8, bufferF32); break; + }; + } break; + case GGWAVE_SAMPLE_FORMAT_U16: + { + switch (formatIn) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: CHECK(false); break; + case GGWAVE_SAMPLE_FORMAT_U8: convert(bufferU16, bufferU8); break; + case GGWAVE_SAMPLE_FORMAT_I8: convert(bufferU16, bufferI8); break; + case GGWAVE_SAMPLE_FORMAT_U16: break; + case GGWAVE_SAMPLE_FORMAT_I16: convert(bufferU16, bufferI16); break; + case GGWAVE_SAMPLE_FORMAT_F32: convert(bufferU16, bufferF32); break; + }; + } break; + case GGWAVE_SAMPLE_FORMAT_I16: + { + switch (formatIn) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: CHECK(false); break; + case GGWAVE_SAMPLE_FORMAT_U8: convert(bufferI16, bufferU8); break; + case GGWAVE_SAMPLE_FORMAT_I8: convert(bufferI16, bufferI8); break; + case GGWAVE_SAMPLE_FORMAT_U16: convert(bufferI16, bufferU16); break; + case GGWAVE_SAMPLE_FORMAT_I16: break; + case GGWAVE_SAMPLE_FORMAT_F32: convert(bufferI16, bufferF32); break; + }; + } break; + case GGWAVE_SAMPLE_FORMAT_F32: + { + switch (formatIn) { + case GGWAVE_SAMPLE_FORMAT_UNDEFINED: CHECK(false); break; + case GGWAVE_SAMPLE_FORMAT_U8: convert(bufferF32, bufferU8); break; + case GGWAVE_SAMPLE_FORMAT_I8: convert(bufferF32, bufferI8); break; + case GGWAVE_SAMPLE_FORMAT_U16: convert(bufferF32, bufferU16); break; + case GGWAVE_SAMPLE_FORMAT_I16: convert(bufferF32, bufferI16); break; + case GGWAVE_SAMPLE_FORMAT_F32: break; + }; + } break; + }; - CHECK(instance.init(payload.size(), payload.c_str())); + }; - // data - CHECK_F(instance.init(-1, "asd")); - CHECK_T(instance.init(0, nullptr)); - CHECK_T(instance.init(0, "asd")); - CHECK_T(instance.init(1, "asd")); - CHECK_T(instance.init(2, "asd")); - CHECK_T(instance.init(3, "asd")); + uint32_t nSamples = 0; - // volume - CHECK_F(instance.init(payload.size(), payload.c_str(), -1)); - CHECK_T(instance.init(payload.size(), payload.c_str(), 0)); - CHECK_T(instance.init(payload.size(), payload.c_str(), 50)); - CHECK_T(instance.init(payload.size(), payload.c_str(), 100)); - CHECK_F(instance.init(payload.size(), payload.c_str(), 101)); + const std::map kCBEnqueueAudio = { + { GGWAVE_SAMPLE_FORMAT_U8, getCBEnqueueAudio(nSamples, bufferU8) }, + { GGWAVE_SAMPLE_FORMAT_I8, getCBEnqueueAudio(nSamples, bufferI8) }, + { GGWAVE_SAMPLE_FORMAT_U16, getCBEnqueueAudio(nSamples, bufferU16) }, + { GGWAVE_SAMPLE_FORMAT_I16, getCBEnqueueAudio(nSamples, bufferI16) }, + { GGWAVE_SAMPLE_FORMAT_F32, getCBEnqueueAudio(nSamples, bufferF32) }, + }; - // todo .. + const std::map kCBDequeueAudio = { + { GGWAVE_SAMPLE_FORMAT_U8, getCBDequeueAudio(nSamples, bufferU8) }, + { GGWAVE_SAMPLE_FORMAT_I8, getCBDequeueAudio(nSamples, bufferI8) }, + { GGWAVE_SAMPLE_FORMAT_U16, getCBDequeueAudio(nSamples, bufferU16) }, + { GGWAVE_SAMPLE_FORMAT_I16, getCBDequeueAudio(nSamples, bufferI16) }, + { GGWAVE_SAMPLE_FORMAT_F32, getCBDequeueAudio(nSamples, bufferF32) }, + }; + + { + GGWave instance(GGWave::getDefaultParameters()); + + std::string payload = "hello"; + + CHECK(instance.init(payload)); + + // data + CHECK_F(instance.init(-1, "asd")); + CHECK_T(instance.init(0, nullptr)); + CHECK_T(instance.init(0, "asd")); + CHECK_T(instance.init(1, "asd")); + CHECK_T(instance.init(2, "asd")); + CHECK_T(instance.init(3, "asd")); + + // volume + CHECK_F(instance.init(payload.size(), payload.c_str(), -1)); + CHECK_T(instance.init(payload.size(), payload.c_str(), 0)); + CHECK_T(instance.init(payload.size(), payload.c_str(), 50)); + CHECK_T(instance.init(payload.size(), payload.c_str(), 100)); + CHECK_F(instance.init(payload.size(), payload.c_str(), 101)); + } + + for (const auto & txProtocol : GGWave::getTxProtocols()) { + for (const auto & formatOut : kFormats) { + for (const auto & formatIn : kFormats) { + printf("Testing: protocol = %s, in = %d, out = %d\n", txProtocol.second.name, formatIn, formatOut); + + auto parameters = GGWave::getDefaultParameters(); + parameters.sampleFormatIn = formatIn; + parameters.sampleFormatOut = formatOut; + GGWave instance(parameters); + + std::string payload = "test message xxxxxxxxxxxx"; + + instance.init(payload, txProtocol.second, 25); + instance.encode(kCBEnqueueAudio.at(formatOut)); + convertHelper(formatOut, formatIn); + instance.decode(kCBDequeueAudio.at(formatIn)); + + { + GGWave::TxRxData result; + CHECK(instance.takeRxData(result) == (int) payload.size()); + for (int i = 0; i < (int) payload.size(); ++i) { + CHECK(payload[i] == result[i]); + } + } + } + } + } return 0; }