diff --git a/bindings/ios b/bindings/ios index 5c56a02..c96067a 160000 --- a/bindings/ios +++ b/bindings/ios @@ -1 +1 @@ -Subproject commit 5c56a02ca59f8b86d5df32ef75190c040550ac2c +Subproject commit c96067aab0d75002c92c8be2604c087f1b5e303b diff --git a/bindings/python/setup-tmpl.py b/bindings/python/setup-tmpl.py index 1ee64cb..f5b3849 100644 --- a/bindings/python/setup-tmpl.py +++ b/bindings/python/setup-tmpl.py @@ -38,7 +38,7 @@ setup( keywords = "data-over-sound fsk ecc serverless pairing qrcode ultrasound", # Build instructions ext_modules = [Extension("ggwave", - [ggwave_module_src, "ggwave/src/ggwave.cpp"], + [ggwave_module_src, "ggwave/src/ggwave.cpp", "ggwave/src/resampler.cpp"], include_dirs=["ggwave/include", "ggwave/include/ggwave"], depends=["ggwave/include/ggwave/ggwave.h"], language="c++", diff --git a/bindings/python/setup.py b/bindings/python/setup.py index 0cf0c25..34cd488 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -38,7 +38,7 @@ setup( keywords = "data-over-sound fsk ecc serverless pairing qrcode ultrasound", # Build instructions ext_modules = [Extension("ggwave", - [ggwave_module_src, "ggwave/src/ggwave.cpp"], + [ggwave_module_src, "ggwave/src/ggwave.cpp", "ggwave/src/resampler.cpp"], include_dirs=["ggwave/include", "ggwave/include/ggwave"], depends=["ggwave/include/ggwave/ggwave.h"], language="c++", diff --git a/examples/ggwave-cli/main.cpp b/examples/ggwave-cli/main.cpp index 6f8c7ad..fff813d 100644 --- a/examples/ggwave-cli/main.cpp +++ b/examples/ggwave-cli/main.cpp @@ -3,6 +3,8 @@ #include "ggwave-common.h" #include "ggwave-common-sdl2.h" +#include + #include #include @@ -11,18 +13,20 @@ #include int main(int argc, char** argv) { - printf("Usage: %s [-cN] [-pN] [-tN]\n", argv[0]); + printf("Usage: %s [-cN] [-pN] [-tN] [-lN]\n", argv[0]); printf(" -cN - select capture device N\n"); printf(" -pN - select playback device N\n"); printf(" -tN - transmission protocol\n"); + printf(" -lN - fixed payload length of size N, N in [1, %d]\n", GGWave::kMaxLengthFixed); printf("\n"); auto argm = parseCmdArguments(argc, argv); int captureId = argm["c"].empty() ? 0 : std::stoi(argm["c"]); int playbackId = argm["p"].empty() ? 0 : std::stoi(argm["p"]); int txProtocol = argm["t"].empty() ? 1 : std::stoi(argm["t"]); + int payloadLength = argm["l"].empty() ? -1 : std::stoi(argm["l"]); - if (GGWave_init(playbackId, captureId) == false) { + if (GGWave_init(playbackId, captureId, payloadLength) == false) { fprintf(stderr, "Failed to initialize GGWave\n"); return -1; } @@ -75,5 +79,8 @@ int main(int argc, char** argv) { GGWave_deinit(); + SDL_CloseAudio(); + SDL_Quit(); + return 0; } diff --git a/examples/ggwave-common-sdl2.cpp b/examples/ggwave-common-sdl2.cpp index ac0c257..c5d56ca 100644 --- a/examples/ggwave-common-sdl2.cpp +++ b/examples/ggwave-common-sdl2.cpp @@ -77,7 +77,9 @@ void GGWave_setDefaultCaptureDeviceName(std::string name) { bool GGWave_init( const int playbackId, - const int captureId) { + const int captureId, + const int payloadLength, + const int sampleRateOffset) { if (g_devIdInp && g_devIdOut) { return false; @@ -117,7 +119,7 @@ bool GGWave_init( SDL_AudioSpec playbackSpec; SDL_zero(playbackSpec); - playbackSpec.freq = GGWave::kBaseSampleRate; + playbackSpec.freq = GGWave::kBaseSampleRate + sampleRateOffset; playbackSpec.format = AUDIO_S16SYS; playbackSpec.channels = 1; playbackSpec.samples = 16*1024; @@ -160,7 +162,7 @@ bool GGWave_init( if (g_devIdInp == 0) { SDL_AudioSpec captureSpec; captureSpec = g_obtainedSpecOut; - captureSpec.freq = GGWave::kBaseSampleRate; + captureSpec.freq = GGWave::kBaseSampleRate + sampleRateOffset; captureSpec.format = AUDIO_F32SYS; captureSpec.samples = 4096; @@ -214,11 +216,12 @@ bool GGWave_init( if (g_ggWave) delete g_ggWave; g_ggWave = new GGWave({ - g_obtainedSpecInp.freq, - g_obtainedSpecOut.freq, - GGWave::kDefaultSamplesPerFrame, - sampleFormatInp, - sampleFormatOut}); + payloadLength, + g_obtainedSpecInp.freq, + g_obtainedSpecOut.freq, + GGWave::kDefaultSamplesPerFrame, + sampleFormatInp, + sampleFormatOut}); } return true; @@ -280,8 +283,9 @@ bool GGWave_deinit() { SDL_CloseAudioDevice(g_devIdInp); SDL_PauseAudioDevice(g_devIdOut, 1); SDL_CloseAudioDevice(g_devIdOut); - SDL_CloseAudio(); - SDL_Quit(); + + g_devIdInp = 0; + g_devIdOut = 0; return true; } diff --git a/examples/ggwave-common-sdl2.h b/examples/ggwave-common-sdl2.h index a411bd2..0b09597 100644 --- a/examples/ggwave-common-sdl2.h +++ b/examples/ggwave-common-sdl2.h @@ -7,7 +7,7 @@ class GGWave; // GGWave helpers void GGWave_setDefaultCaptureDeviceName(std::string name); -bool GGWave_init(const int playbackId, const int captureId); +bool GGWave_init(const int playbackId, const int captureId, const int payloadLength = -1, const int sampleRateOffset = 0); GGWave * GGWave_instance(); bool GGWave_mainLoop(); bool GGWave_deinit(); diff --git a/examples/ggwave-rx/main.cpp b/examples/ggwave-rx/main.cpp index c73623b..f3242e9 100644 --- a/examples/ggwave-rx/main.cpp +++ b/examples/ggwave-rx/main.cpp @@ -3,6 +3,8 @@ #include "ggwave-common.h" #include "ggwave-common-sdl2.h" +#include + #include #include @@ -26,5 +28,8 @@ int main(int argc, char** argv) { GGWave_deinit(); + SDL_CloseAudio(); + SDL_Quit(); + return 0; } diff --git a/examples/ggwave-to-file/main.cpp b/examples/ggwave-to-file/main.cpp index 7df49b6..10c90a7 100644 --- a/examples/ggwave-to-file/main.cpp +++ b/examples/ggwave-to-file/main.cpp @@ -69,7 +69,7 @@ int main(int argc, char** argv) { fprintf(stderr, "Generating waveform for message '%s' ...\n", message.c_str()); - GGWave ggWave({ GGWave::kBaseSampleRate, sampleRateOut, 1024, GGWAVE_SAMPLE_FORMAT_F32, GGWAVE_SAMPLE_FORMAT_I16 }); + GGWave ggWave({ -1, 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/spectrogram/main.cpp b/examples/spectrogram/main.cpp index b0e2eed..d1f8ac6 100644 --- a/examples/spectrogram/main.cpp +++ b/examples/spectrogram/main.cpp @@ -31,10 +31,10 @@ struct FreqData { bool g_isCapturing = true; constexpr int g_nSamplesPerFrame = 1024; -int g_binMin = 40; -int g_binMax = 40 + 96; +int g_binMin = 20; +int g_binMax = 60; -float g_scale = 5.0; +float g_scale = 30.0; bool g_showControls = true; @@ -42,6 +42,8 @@ int g_freqDataHead = 0; int g_freqDataSize = 0; std::vector g_freqData; +int g_sampleRateOffset = 0; + } void GGWave_setDefaultCaptureDeviceName(std::string name) { @@ -90,7 +92,7 @@ bool GGWave_init( SDL_AudioSpec playbackSpec; SDL_zero(playbackSpec); - playbackSpec.freq = GGWave::kBaseSampleRate; + playbackSpec.freq = GGWave::kBaseSampleRate + g_sampleRateOffset; playbackSpec.format = AUDIO_S16SYS; playbackSpec.channels = 1; playbackSpec.samples = 16*1024; @@ -133,7 +135,7 @@ bool GGWave_init( if (g_devIdInp == 0) { SDL_AudioSpec captureSpec; captureSpec = g_obtainedSpecOut; - captureSpec.freq = GGWave::kBaseSampleRate; + captureSpec.freq = GGWave::kBaseSampleRate + g_sampleRateOffset; captureSpec.format = AUDIO_F32SYS; captureSpec.samples = g_nSamplesPerFrame; @@ -240,8 +242,9 @@ bool GGWave_deinit() { SDL_CloseAudioDevice(g_devIdInp); SDL_PauseAudioDevice(g_devIdOut, 1); SDL_CloseAudioDevice(g_devIdOut); - SDL_CloseAudio(); - SDL_Quit(); + + g_devIdInp = 0; + g_devIdOut = 0; return true; } @@ -474,6 +477,10 @@ int main(int argc, char** argv) { ImGui::DragInt("Min", &g_binMin, 1, 0, g_binMax - 1); ImGui::DragInt("Max", &g_binMax, 1, g_binMin + 1, g_nSamplesPerFrame/2); ImGui::DragFloat("Scale", &g_scale, 1.0f, 1.0f, 1000.0f); + if (ImGui::SliderInt("Offset", &g_sampleRateOffset, -2048, 2048)) { + GGWave_deinit(); + GGWave_init(0, 0); + } if (ImGui::Button("Pause")) { togglePause = true; } @@ -515,6 +522,7 @@ int main(int argc, char** argv) { SDL_GL_DeleteContext(gl_context); SDL_DestroyWindow(window); + SDL_CloseAudio(); SDL_Quit(); return 0; diff --git a/examples/waver/common.cpp b/examples/waver/common.cpp index 1911e49..89195ce 100644 --- a/examples/waver/common.cpp +++ b/examples/waver/common.cpp @@ -188,6 +188,10 @@ struct State { struct Input { bool update = false; Message message; + + bool reinit = false; + bool isSampleRateOffset = false; + int payloadLength = -1; }; struct Buffer { @@ -509,6 +513,7 @@ void updateCore() { static Input inputCurrent; static int lastRxDataLength = 0; + static float lastRxTimestamp = 0.0f; static GGWave::TxRxData lastRxData; { @@ -519,6 +524,15 @@ void updateCore() { } } + if (inputCurrent.reinit) { + GGWave_deinit(); + // todo : use the provided cli arguments for playback and capture device + GGWave_init(0, 0, inputCurrent.payloadLength, inputCurrent.isSampleRateOffset ? -512 : 0); + g_ggWave = GGWave_instance(); + + inputCurrent.reinit = false; + } + if (inputCurrent.update) { g_ggWave->init( (int) inputCurrent.message.data.size(), @@ -543,7 +557,7 @@ void updateCore() { 0, Message::Error, }; - } else if (lastRxDataLength > 0) { + } else if (lastRxDataLength > 0 && ImGui::GetTime() - lastRxTimestamp > 0.5f) { auto message = std::string((char *) lastRxData.data(), lastRxDataLength); const Message::Type type = isFileBroadcastMessage(message) ? Message::FileBroadcast : Message::Text; g_buffer.stateCore.update = true; @@ -556,6 +570,7 @@ void updateCore() { 0, type, }; + lastRxTimestamp = ImGui::GetTime(); } if (g_ggWave->takeSpectrum(g_buffer.stateCore.spectrum)) { @@ -663,6 +678,9 @@ void renderMain() { struct Settings { int protocolId = 1; + bool isFixedLength = false; + int payloadLength = 1; + bool isSampleRateOffset = false; float volume = 0.10f; }; @@ -817,14 +835,14 @@ void renderMain() { ImGui::BeginChild("Settings:main", ImGui::GetContentRegionAvail(), true); ImGui::Text("%s", ""); ImGui::Text("%s", ""); - ImGui::Text("Waver v1.3.2"); + ImGui::Text("Waver v1.4.0"); ImGui::Separator(); ImGui::Text("%s", ""); ImGui::Text("Sample rate (capture): %g, %d B/sample", g_ggWave->getSampleRateInp(), g_ggWave->getSampleSizeBytesInp()); ImGui::Text("Sample rate (playback): %g, %d B/sample", g_ggWave->getSampleRateOut(), g_ggWave->getSampleSizeBytesOut()); - const float kLabelWidth = ImGui::CalcTextSize("Tx Protocol: ").x; + const float kLabelWidth = ImGui::CalcTextSize("Fixed-length: ").x; // volume ImGui::Text("%s", ""); @@ -879,6 +897,37 @@ void renderMain() { ImGui::SetCursorScreenPos(posSave); } + // fixed-length + ImGui::Text("%s", ""); + { + auto posSave = ImGui::GetCursorScreenPos(); + ImGui::Text("Fixed-length: "); + ImGui::SetCursorScreenPos({ posSave.x + kLabelWidth, posSave.y }); + } + if (ImGui::Checkbox("##fixed-length", &settings.isFixedLength)) { + g_buffer.inputUI.update = true; + g_buffer.inputUI.reinit = true; + g_buffer.inputUI.isSampleRateOffset = settings.isSampleRateOffset; + g_buffer.inputUI.payloadLength = settings.isFixedLength ? settings.payloadLength : -1; + } else { + g_buffer.inputUI.reinit = false; + g_buffer.inputUI.isSampleRateOffset = false; + } + + if (settings.isFixedLength) { + ImGui::SameLine(); + ImGui::PushItemWidth(0.5*ImGui::GetContentRegionAvailWidth()); + ImGui::SliderInt("Bytes", &settings.payloadLength, 1, 16); + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Checkbox("Offset", &settings.isSampleRateOffset)) { + g_buffer.inputUI.update = true; + g_buffer.inputUI.reinit = true; + g_buffer.inputUI.isSampleRateOffset = settings.isSampleRateOffset; + g_buffer.inputUI.payloadLength = settings.isFixedLength ? settings.payloadLength : -1; + } + } + // protocol ImGui::Text("%s", ""); { @@ -887,6 +936,12 @@ void renderMain() { ImGui::SetCursorScreenPos({ posSave.x + kLabelWidth, posSave.y }); ImGui::TextDisabled("[U] = ultrasound"); } + { + auto posSave = ImGui::GetCursorScreenPos(); + ImGui::Text("%s", ""); + ImGui::SetCursorScreenPos({ posSave.x + kLabelWidth, posSave.y }); + ImGui::TextDisabled("[DT] = dual-tone"); + } { auto posSave = ImGui::GetCursorScreenPos(); ImGui::Text("Tx Protocol: "); @@ -980,7 +1035,7 @@ void renderMain() { { auto col = ImVec4 { 1.0f, 0.0f, 0.0f, 1.0f }; col.w = interp; - ImGui::TextColored(col, "Failed to received"); + ImGui::TextColored(col, "Failed to receive"); } break; case Message::Text: diff --git a/examples/waver/main.cpp b/examples/waver/main.cpp index 6f95395..b8e3e81 100644 --- a/examples/waver/main.cpp +++ b/examples/waver/main.cpp @@ -310,6 +310,7 @@ int main(int argc, char** argv) { SDL_GL_DeleteContext(gl_context); SDL_DestroyWindow(window); + SDL_CloseAudio(); SDL_Quit(); #endif diff --git a/include/ggwave/ggwave.h b/include/ggwave/ggwave.h index 22be23c..94d85c1 100644 --- a/include/ggwave/ggwave.h +++ b/include/ggwave/ggwave.h @@ -41,10 +41,22 @@ extern "C" { GGWAVE_TX_PROTOCOL_ULTRASOUND_NORMAL, GGWAVE_TX_PROTOCOL_ULTRASOUND_FAST, GGWAVE_TX_PROTOCOL_ULTRASOUND_FASTEST, + GGWAVE_TX_PROTOCOL_DT_NORMAL, + GGWAVE_TX_PROTOCOL_DT_FAST, + GGWAVE_TX_PROTOCOL_DT_FASTEST, } ggwave_TxProtocolId; // GGWave instance parameters // + // If payloadLength <= 0, then GGWave will transmit with variable payload length + // depending on the provided payload. Sound markers are used to identify the + // start and end of the transmission. + // + // If payloadLength > 0, then the transmitted payload will be of the specified + // fixed length. In this case, no sound markers are emitted and a slightly + // different decoding scheme is applied. This is useful in cases where the + // length of the payload is known in advance. + // // The sample rates are values typically between 8000 and 96000. // Default value: GGWave::kBaseSampleRate // @@ -53,6 +65,7 @@ extern "C" { // Default value: GGWave::kDefaultSamplesPerFrame // typedef struct { + int payloadLength; // payload length int sampleRateInp; // capture sample rate int sampleRateOut; // playback sample rate int samplesPerFrame; // number of samples per audio frame @@ -205,18 +218,20 @@ extern "C" { #include #include #include +#include class GGWave { public: static constexpr auto kBaseSampleRate = 48000; static constexpr auto kDefaultSamplesPerFrame = 1024; static constexpr auto kDefaultVolume = 10; - static constexpr auto kMaxSamplesPerFrame = 1024; + static constexpr auto kMaxSamplesPerFrame = 2048; static constexpr auto kMaxDataBits = 256; static constexpr auto kMaxDataSize = 256; - static constexpr auto kMaxLength = 140; + static constexpr auto kMaxLengthVarible = 140; + static constexpr auto kMaxLengthFixed = 16; static constexpr auto kMaxSpectrumHistory = 4; - static constexpr auto kMaxRecordedFrames = 1024; + static constexpr auto kMaxRecordedFrames = 2048; using Parameters = ggwave_Parameters; using SampleFormat = ggwave_SampleFormat; @@ -236,12 +251,15 @@ public: static const TxProtocols & getTxProtocols() { static const TxProtocols kTxProtocols { - { GGWAVE_TX_PROTOCOL_AUDIBLE_NORMAL, { "Normal", 40, 9, 3, } }, - { GGWAVE_TX_PROTOCOL_AUDIBLE_FAST, { "Fast", 40, 6, 3, } }, - { GGWAVE_TX_PROTOCOL_AUDIBLE_FASTEST, { "Fastest", 40, 3, 3, } }, - { GGWAVE_TX_PROTOCOL_ULTRASOUND_NORMAL, { "[U] Normal", 320, 9, 3, } }, - { GGWAVE_TX_PROTOCOL_ULTRASOUND_FAST, { "[U] Fast", 320, 6, 3, } }, - { GGWAVE_TX_PROTOCOL_ULTRASOUND_FASTEST, { "[U] Fastest", 320, 3, 3, } }, + { GGWAVE_TX_PROTOCOL_AUDIBLE_NORMAL, { "Normal", 40, 9, 3, } }, + { GGWAVE_TX_PROTOCOL_AUDIBLE_FAST, { "Fast", 40, 6, 3, } }, + { GGWAVE_TX_PROTOCOL_AUDIBLE_FASTEST, { "Fastest", 40, 3, 3, } }, + { GGWAVE_TX_PROTOCOL_ULTRASOUND_NORMAL, { "[U] Normal", 320, 9, 3, } }, + { GGWAVE_TX_PROTOCOL_ULTRASOUND_FAST, { "[U] Fast", 320, 6, 3, } }, + { GGWAVE_TX_PROTOCOL_ULTRASOUND_FASTEST, { "[U] Fastest", 320, 3, 3, } }, + { GGWAVE_TX_PROTOCOL_DT_NORMAL, { "[DT] Normal", 24, 9, 1, } }, + { GGWAVE_TX_PROTOCOL_DT_FAST, { "[DT] Fast", 24, 6, 1, } }, + { GGWAVE_TX_PROTOCOL_DT_FASTEST, { "[DT] Fastest", 24, 3, 1, } }, }; return kTxProtocols; @@ -321,6 +339,8 @@ public: // Rx + void setRxProtocols(const TxProtocols & rxProtocols) { m_rxProtocols = rxProtocols; } + const TxRxData & getRxData() const { return m_rxData; } const TxProtocol & getRxProtocol() const { return m_rxProtocol; } const TxProtocolId & getRxProtocolId() const { return m_rxProtocolId; } @@ -329,6 +349,9 @@ public: bool takeSpectrum(SpectrumData & dst); private: + void decode_fixed(); + void decode_variable(); + int maxFramesPerTx() const; int minBytesPerTx() const; @@ -353,13 +376,18 @@ private: const int m_nBitsInMarker; const int m_nMarkerFrames; - const int m_nPostMarkerFrames; const int m_encodedDataOffset; + // common + + bool m_isFixedPayloadLength; + int m_payloadLength; + // Rx bool m_receivingData; bool m_analyzingData; + int m_nMarkersSuccess; int m_markerFreqStart; int m_recvDuration_frames; @@ -369,12 +397,13 @@ private: int m_framesToRecord; int m_samplesNeeded; - std::vector m_fftInp; // real + std::vector m_fftInp; // real std::vector m_fftOut; // complex bool m_hasNewSpectrum; SpectrumData m_sampleSpectrum; AmplitudeData m_sampleAmplitude; + AmplitudeData m_sampleAmplitudeResampled; TxRxData m_sampleAmplitudeTmp; bool m_hasNewRxData; @@ -382,13 +411,17 @@ private: TxRxData m_rxData; TxProtocol m_rxProtocol; TxProtocolId m_rxProtocolId; + TxProtocols m_rxProtocols; - int m_historyId = 0; + int m_historyId; AmplitudeData m_sampleAmplitudeAverage; std::vector m_sampleAmplitudeHistory; RecordedData m_recordedAmplitude; + int m_historyIdFixed; + std::vector m_spectrumHistoryFixed; + // Tx bool m_hasNewTxData; float m_sendVolume; @@ -403,6 +436,11 @@ private: TxRxData m_outputBlockTmp; AmplitudeDataI16 m_outputBlockI16; AmplitudeDataI16 m_txAmplitudeDataI16; + + // Impl + // todo : move all members inside Impl + struct Impl; + std::unique_ptr m_impl; }; #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6f42d13..965cd5e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,7 @@ set(TARGET ggwave) add_library(${TARGET} ggwave.cpp + resampler.cpp ) target_include_directories(${TARGET} PUBLIC diff --git a/src/ggwave.cpp b/src/ggwave.cpp index 71bdbdc..33a7705 100644 --- a/src/ggwave.cpp +++ b/src/ggwave.cpp @@ -1,13 +1,15 @@ #include "ggwave/ggwave.h" +#include "resampler.h" + #include "reed-solomon/rs.hpp" #include #include #include -//#include #include #include +//#include // // C interface @@ -27,6 +29,7 @@ ggwave_Instance ggwave_init(const ggwave_Parameters parameters) { static ggwave_Instance curId = 0; g_instances[curId] = new GGWave({ + parameters.payloadLength, parameters.sampleRateInp, parameters.sampleRateOut, parameters.samplesPerFrame, @@ -242,7 +245,7 @@ float getTime_ms(const T & tStart, const T & tEnd) { } int getECCBytesForLength(int len) { - return std::max(4, 2*(len/5)); + return len < 4 ? 2 : std::max(4, 2*(len/5)); } int bytesForSampleFormat(GGWave::SampleFormat sampleFormat) { @@ -262,11 +265,16 @@ int bytesForSampleFormat(GGWave::SampleFormat sampleFormat) { } +struct GGWave::Impl { + Resampler resampler; +}; + const GGWave::Parameters & GGWave::getDefaultParameters() { static ggwave_Parameters result { - GGWave::kBaseSampleRate, - GGWave::kBaseSampleRate, - GGWave::kDefaultSamplesPerFrame, + -1, // vaiable payload length + kBaseSampleRate, + kBaseSampleRate, + kDefaultSamplesPerFrame, GGWAVE_SAMPLE_FORMAT_F32, GGWAVE_SAMPLE_FORMAT_F32, }; @@ -283,32 +291,62 @@ GGWave::GGWave(const Parameters & parameters) : m_sampleSizeBytesOut(bytesForSampleFormat(parameters.sampleFormatOut)), m_sampleFormatInp(parameters.sampleFormatInp), m_sampleFormatOut(parameters.sampleFormatOut), - m_hzPerSample(m_sampleRateInp/parameters.samplesPerFrame), + m_hzPerSample(float(kBaseSampleRate)/parameters.samplesPerFrame), m_ihzPerSample(1.0f/m_hzPerSample), m_freqDelta_bin(1), m_freqDelta_hz(2*m_hzPerSample), m_nBitsInMarker(16), - m_nMarkerFrames(16), - m_nPostMarkerFrames(0), - m_encodedDataOffset(3), + m_nMarkerFrames(parameters.payloadLength > 0 ? 0 : 16), + m_encodedDataOffset(parameters.payloadLength > 0 ? 0 : 3), + // common + m_isFixedPayloadLength(parameters.payloadLength > 0), + m_payloadLength(parameters.payloadLength), + // Rx m_samplesNeeded(m_samplesPerFrame), m_fftInp(kMaxSamplesPerFrame), m_fftOut(2*kMaxSamplesPerFrame), m_hasNewSpectrum(false), m_sampleSpectrum(kMaxSamplesPerFrame), - m_sampleAmplitude(kMaxSamplesPerFrame), - m_sampleAmplitudeTmp(kMaxSamplesPerFrame*m_sampleSizeBytesInp), + m_sampleAmplitude(kMaxSamplesPerFrame + 128), // small extra space because sometimes resampling needs a few more samples + m_sampleAmplitudeResampled(8*kMaxSamplesPerFrame), // min input sampling rate is 0.125*kBaseSampleRate + m_sampleAmplitudeTmp(8*kMaxSamplesPerFrame*m_sampleSizeBytesInp), m_hasNewRxData(false), m_lastRxDataLength(0), m_rxData(kMaxDataSize), + m_rxProtocol(getDefaultTxProtocol()), + m_rxProtocolId(getDefaultTxProtocolId()), + m_rxProtocols(getTxProtocols()), + m_historyId(0), m_sampleAmplitudeAverage(kMaxSamplesPerFrame), m_sampleAmplitudeHistory(kMaxSpectrumHistory), - m_recordedAmplitude(kMaxRecordedFrames*kMaxSamplesPerFrame), + m_historyIdFixed(0), + // Tx + m_hasNewTxData(false), + m_sendVolume(0.1), + m_txDataLength(0), m_txData(kMaxDataSize), m_txDataEncoded(kMaxDataSize), m_outputBlock(kMaxSamplesPerFrame), m_outputBlockTmp(kMaxRecordedFrames*kMaxSamplesPerFrame*m_sampleSizeBytesOut), - m_outputBlockI16(kMaxRecordedFrames*kMaxSamplesPerFrame) { + m_outputBlockI16(kMaxRecordedFrames*kMaxSamplesPerFrame), + m_impl(new Impl()) { + + if (m_payloadLength > 0) { + // fixed payload length + if (m_payloadLength > kMaxLengthFixed) { + throw std::runtime_error("Invalid payload legnth"); + } + + m_txDataLength = m_payloadLength; + + int totalLength = m_txDataLength + getECCBytesForLength(m_txDataLength); + int totalTxs = (totalLength + minBytesPerTx() - 1)/minBytesPerTx(); + + m_spectrumHistoryFixed.resize(totalTxs*maxFramesPerTx()); + } else { + // variable payload length + m_recordedAmplitude.resize(kMaxRecordedFrames*kMaxSamplesPerFrame); + } if (m_sampleSizeBytesInp == 0) { throw std::runtime_error("Invalid or unsupported capture sample format"); @@ -322,8 +360,13 @@ GGWave::GGWave(const Parameters & parameters) : throw std::runtime_error("Invalid samples per frame"); } - if (m_sampleRateInp < m_sampleRateOut) { - fprintf(stderr, "Error: capture sample rate (%d Hz) must be >= playback sample rate (%d Hz)\n", (int) m_sampleRateInp, (int) m_sampleRateOut); + if (m_sampleRateInp < 0.125*kBaseSampleRate) { + fprintf(stderr, "Error: capture sample rate (%d Hz) must be >= %d Hz\n", (int) m_sampleRateInp, (int) 0.125*kBaseSampleRate); + throw std::runtime_error("Invalid capture/playback sample rate"); + } + + if (m_sampleRateInp > 2.0*kBaseSampleRate) { + fprintf(stderr, "Error: capture sample rate (%d Hz) must be <= %d Hz\n", (int) m_sampleRateInp, (int) 2.0*kBaseSampleRate); throw std::runtime_error("Invalid capture/playback sample rate"); } @@ -351,9 +394,10 @@ bool GGWave::init(int dataSize, const char * dataBuffer, const TxProtocol & txPr return false; } - if (dataSize > kMaxLength) { - fprintf(stderr, "Truncating data from %d to 140 bytes\n", dataSize); - dataSize = kMaxLength; + auto maxLength = m_isFixedPayloadLength ? m_payloadLength : kMaxLengthVarible; + if (dataSize > maxLength) { + fprintf(stderr, "Truncating data from %d to %d bytes\n", dataSize, maxLength); + dataSize = maxLength; } if (volume < 0 || volume > 100) { @@ -378,6 +422,10 @@ bool GGWave::init(int dataSize, const char * dataBuffer, const TxProtocol & txPr m_hasNewTxData = true; } + if (m_isFixedPayloadLength) { + m_txDataLength = m_payloadLength; + } + // Rx m_receivingData = false; m_analyzingData = false; @@ -401,6 +449,11 @@ bool GGWave::init(int dataSize, const char * dataBuffer, const TxProtocol & txPr m_fftOut[2*i + 1] = 0.0f; } + for (auto & s : m_spectrumHistoryFixed) { + s.resize(kMaxSamplesPerFrame); + std::fill(s.begin(), s.end(), 0); + } + return true; } @@ -413,22 +466,22 @@ uint32_t GGWave::encodeSize_samples() const { return 0; } - int samplesPerFrameOut = (m_sampleRateOut/m_sampleRateInp)*m_samplesPerFrame; + int samplesPerFrameOut = (m_sampleRateOut/kBaseSampleRate)*m_samplesPerFrame; int nECCBytesPerTx = getECCBytesForLength(m_txDataLength); int sendDataLength = m_txDataLength + m_encodedDataOffset; int totalBytes = sendDataLength + nECCBytesPerTx; int totalDataFrames = ((totalBytes + m_txProtocol.bytesPerTx - 1)/m_txProtocol.bytesPerTx)*m_txProtocol.framesPerTx; return ( - m_nMarkerFrames + m_nPostMarkerFrames + totalDataFrames + m_nMarkerFrames + m_nMarkerFrames + totalDataFrames + m_nMarkerFrames )*samplesPerFrameOut; } bool GGWave::encode(const CBWaveformOut & cbWaveformOut) { - int samplesPerFrameOut = (m_sampleRateOut/m_sampleRateInp)*m_samplesPerFrame; + int samplesPerFrameOut = (m_sampleRateOut/kBaseSampleRate)*m_samplesPerFrame; - if (m_sampleRateOut != m_sampleRateInp) { - fprintf(stderr, "Resampling from %d Hz to %d Hz\n", (int) m_sampleRateInp, (int) m_sampleRateOut); + if (m_sampleRateOut != kBaseSampleRate) { + fprintf(stderr, "Resampling from %d Hz to %d Hz\n", (int) kBaseSampleRate, (int) m_sampleRateOut); } int frameId = 0; @@ -474,16 +527,19 @@ bool GGWave::encode(const CBWaveformOut & cbWaveformOut) { int totalBytes = sendDataLength + nECCBytesPerTx; int totalDataFrames = ((totalBytes + m_txProtocol.bytesPerTx - 1)/m_txProtocol.bytesPerTx)*m_txProtocol.framesPerTx; - RS::ReedSolomon rsData = RS::ReedSolomon(m_txDataLength, nECCBytesPerTx); - RS::ReedSolomon rsLength(1, m_encodedDataOffset - 1); + if (m_isFixedPayloadLength == false) { + RS::ReedSolomon rsLength(1, m_encodedDataOffset - 1); + rsLength.Encode(m_txData.data(), m_txDataEncoded.data()); + } - rsLength.Encode(m_txData.data(), m_txDataEncoded.data()); + // first byte of m_txData contains the length of the payload, so we skip it: + RS::ReedSolomon rsData = RS::ReedSolomon(m_txDataLength, nECCBytesPerTx); rsData.Encode(m_txData.data() + 1, m_txDataEncoded.data() + m_encodedDataOffset); while (m_hasNewTxData) { std::fill(m_outputBlock.begin(), m_outputBlock.end(), 0.0f); - if (m_sampleRateOut != m_sampleRateInp) { + if (m_sampleRateOut != kBaseSampleRate) { for (int k = 0; k < m_txProtocol.nDataBitsPerTx(); ++k) { double freq = bitFreq(m_txProtocol, k); @@ -512,18 +568,8 @@ bool GGWave::encode(const CBWaveformOut & cbWaveformOut) { ::addAmplitudeSmooth(bit0Amplitude[i], m_outputBlock, m_sendVolume, 0, samplesPerFrameOut, frameId, m_nMarkerFrames); } } - } else if (frameId < m_nMarkerFrames + m_nPostMarkerFrames) { - nFreq = m_nBitsInMarker; - - for (int i = 0; i < m_nBitsInMarker; ++i) { - if (i%2 == 0) { - ::addAmplitudeSmooth(bit0Amplitude[i], m_outputBlock, m_sendVolume, 0, samplesPerFrameOut, frameId - m_nMarkerFrames, m_nPostMarkerFrames); - } else { - ::addAmplitudeSmooth(bit1Amplitude[i], m_outputBlock, m_sendVolume, 0, samplesPerFrameOut, frameId - m_nMarkerFrames, m_nPostMarkerFrames); - } - } - } else if (frameId < m_nMarkerFrames + m_nPostMarkerFrames + totalDataFrames) { - int dataOffset = frameId - m_nMarkerFrames - m_nPostMarkerFrames; + } else if (frameId < m_nMarkerFrames + totalDataFrames) { + int dataOffset = frameId - m_nMarkerFrames; int cycleModMain = dataOffset%m_txProtocol.framesPerTx; dataOffset /= m_txProtocol.framesPerTx; dataOffset *= m_txProtocol.bytesPerTx; @@ -551,10 +597,10 @@ bool GGWave::encode(const CBWaveformOut & cbWaveformOut) { ::addAmplitudeSmooth(bit1Amplitude[k/2], m_outputBlock, m_sendVolume, 0, samplesPerFrameOut, cycleModMain, m_txProtocol.framesPerTx); } } - } else if (frameId < m_nMarkerFrames + m_nPostMarkerFrames + totalDataFrames + m_nMarkerFrames) { + } else if (frameId < m_nMarkerFrames + totalDataFrames + m_nMarkerFrames) { nFreq = m_nBitsInMarker; - int fId = frameId - (m_nMarkerFrames + m_nPostMarkerFrames + totalDataFrames); + int fId = frameId - (m_nMarkerFrames + totalDataFrames); for (int i = 0; i < m_nBitsInMarker; ++i) { if (i%2 == 0) { addAmplitudeSmooth(bit0Amplitude[i], m_outputBlock, m_sendVolume, 0, samplesPerFrameOut, fId, m_nMarkerFrames); @@ -649,8 +695,11 @@ bool GGWave::encode(const CBWaveformOut & cbWaveformOut) { void GGWave::decode(const CBWaveformInp & cbWaveformInp) { while (m_hasNewTxData == false) { + float factor = m_sampleRateInp/kBaseSampleRate; + // read capture data - uint32_t nBytesNeeded = m_samplesNeeded*m_sampleSizeBytesInp; + uint32_t nBytesNeededOriginal = m_samplesNeeded*m_sampleSizeBytesInp; + uint32_t nBytesNeeded = std::ceil(factor*m_samplesNeeded)*m_sampleSizeBytesInp; uint32_t nBytesRecorded = 0; uint32_t offset = m_samplesPerFrame - m_samplesNeeded; @@ -665,7 +714,7 @@ void GGWave::decode(const CBWaveformInp & cbWaveformInp) { } break; case GGWAVE_SAMPLE_FORMAT_F32: { - nBytesRecorded = cbWaveformInp(m_sampleAmplitude.data() + offset, nBytesNeeded); + nBytesRecorded = cbWaveformInp(m_sampleAmplitudeResampled.data() + offset, nBytesNeeded); } break; } @@ -684,6 +733,7 @@ void GGWave::decode(const CBWaveformInp & cbWaveformInp) { } // convert to 32-bit float + int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesInp; switch (m_sampleFormatInp) { case GGWAVE_SAMPLE_FORMAT_UNDEFINED: break; case GGWAVE_SAMPLE_FORMAT_U8: @@ -692,7 +742,7 @@ void GGWave::decode(const CBWaveformInp & cbWaveformInp) { int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesInp; 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; + m_sampleAmplitudeResampled[offset + i] = float(int16_t(*(p + offset + i)) - 128)*scale; } } break; case GGWAVE_SAMPLE_FORMAT_I8: @@ -701,7 +751,7 @@ void GGWave::decode(const CBWaveformInp & cbWaveformInp) { int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesInp; auto p = reinterpret_cast(m_sampleAmplitudeTmp.data()); for (int i = 0; i < nSamplesRecorded; ++i) { - m_sampleAmplitude[offset + i] = float(*(p + offset + i))*scale; + m_sampleAmplitudeResampled[offset + i] = float(*(p + offset + i))*scale; } } break; case GGWAVE_SAMPLE_FORMAT_U16: @@ -710,7 +760,7 @@ void GGWave::decode(const CBWaveformInp & cbWaveformInp) { int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesInp; 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; + m_sampleAmplitudeResampled[offset + i] = float(int32_t(*(p + offset + i)) - 32768)*scale; } } break; case GGWAVE_SAMPLE_FORMAT_I16: @@ -719,266 +769,32 @@ void GGWave::decode(const CBWaveformInp & cbWaveformInp) { int nSamplesRecorded = nBytesRecorded/m_sampleSizeBytesInp; auto p = reinterpret_cast(m_sampleAmplitudeTmp.data()); for (int i = 0; i < nSamplesRecorded; ++i) { - m_sampleAmplitude[offset + i] = float(*(p + offset + i))*scale; + m_sampleAmplitudeResampled[offset + i] = float(*(p + offset + i))*scale; } } break; case GGWAVE_SAMPLE_FORMAT_F32: break; } + if (nBytesRecorded == 0) { + break; + } + + if (nBytesNeeded != nBytesNeededOriginal) { + int nSamplesResampled = m_impl->resampler.resample(factor, nSamplesRecorded, m_sampleAmplitudeResampled.data(), m_sampleAmplitude.data()); + nBytesRecorded = nSamplesResampled*m_sampleSizeBytesInp; + nBytesNeeded = m_samplesNeeded*m_sampleSizeBytesInp; + } else { + for (int i = 0; i < nSamplesRecorded; ++i) { + m_sampleAmplitude[i] = m_sampleAmplitudeResampled[i]; + } + } + // we have enough bytes to do analysis - if (nBytesRecorded == nBytesNeeded) { - m_samplesNeeded = m_samplesPerFrame; - m_sampleAmplitudeHistory[m_historyId] = m_sampleAmplitude; - - if (++m_historyId >= kMaxSpectrumHistory) { - m_historyId = 0; - } - - if (m_historyId == 0 || m_receivingData) { - m_hasNewSpectrum = true; - - std::fill(m_sampleAmplitudeAverage.begin(), m_sampleAmplitudeAverage.end(), 0.0f); - for (auto & s : m_sampleAmplitudeHistory) { - for (int i = 0; i < m_samplesPerFrame; ++i) { - m_sampleAmplitudeAverage[i] += s[i]; - } - } - - float norm = 1.0f/kMaxSpectrumHistory; - for (int i = 0; i < m_samplesPerFrame; ++i) { - m_sampleAmplitudeAverage[i] *= norm; - } - - // calculate spectrum - std::copy(m_sampleAmplitudeAverage.begin(), m_sampleAmplitudeAverage.begin() + m_samplesPerFrame, m_fftInp.data()); - - FFT(m_fftInp.data(), m_fftOut.data(), m_samplesPerFrame, 1.0); - - for (int i = 0; i < m_samplesPerFrame; ++i) { - m_sampleSpectrum[i] = (m_fftOut[2*i + 0]*m_fftOut[2*i + 0] + m_fftOut[2*i + 1]*m_fftOut[2*i + 1]); - } - for (int i = 1; i < m_samplesPerFrame/2; ++i) { - m_sampleSpectrum[i] += m_sampleSpectrum[m_samplesPerFrame - i]; - } - } - - if (m_framesLeftToRecord > 0) { - std::copy(m_sampleAmplitude.begin(), - m_sampleAmplitude.begin() + m_samplesPerFrame, - m_recordedAmplitude.data() + (m_framesToRecord - m_framesLeftToRecord)*m_samplesPerFrame); - - if (--m_framesLeftToRecord <= 0) { - m_analyzingData = true; - } - } - - if (m_analyzingData) { - fprintf(stderr, "Analyzing captured data ..\n"); - auto tStart = std::chrono::high_resolution_clock::now(); - - const int stepsPerFrame = 16; - const int step = m_samplesPerFrame/stepsPerFrame; - - bool isValid = false; - for (int rxProtocolId = 0; rxProtocolId < (int) getTxProtocols().size(); ++rxProtocolId) { - const auto & rxProtocol = getTxProtocol(rxProtocolId); - - // skip Rx protocol if start frequency is different from detected one - if (rxProtocol.freqStart != m_markerFreqStart) { - continue; - } - - std::fill(m_sampleSpectrum.begin(), m_sampleSpectrum.end(), 0.0f); - - m_framesToAnalyze = m_nMarkerFrames*stepsPerFrame; - m_framesLeftToAnalyze = m_framesToAnalyze; - - // note : not sure if looping backwards here is more meaningful than looping forwards - for (int ii = m_nMarkerFrames*stepsPerFrame - 1; ii >= 0; --ii) { - bool knownLength = false; - - const int offsetStart = ii; - for (int itx = 0; itx < 1024; ++itx) { - int offsetTx = offsetStart + itx*rxProtocol.framesPerTx*stepsPerFrame; - if (offsetTx >= m_recvDuration_frames*stepsPerFrame || (itx + 1)*rxProtocol.bytesPerTx >= (int) m_txDataEncoded.size()) { - break; - } - - std::copy( - m_recordedAmplitude.begin() + offsetTx*step, - m_recordedAmplitude.begin() + offsetTx*step + m_samplesPerFrame, m_fftInp.data()); - - for (int k = 1; k < rxProtocol.framesPerTx; ++k) { - for (int i = 0; i < m_samplesPerFrame; ++i) { - m_fftInp[i] += m_recordedAmplitude[(offsetTx + k*stepsPerFrame)*step + i]; - } - } - - FFT(m_fftInp.data(), m_fftOut.data(), m_samplesPerFrame, 1.0); - - for (int i = 0; i < m_samplesPerFrame; ++i) { - m_sampleSpectrum[i] = (m_fftOut[2*i + 0]*m_fftOut[2*i + 0] + m_fftOut[2*i + 1]*m_fftOut[2*i + 1]); - } - for (int i = 1; i < m_samplesPerFrame/2; ++i) { - m_sampleSpectrum[i] += m_sampleSpectrum[m_samplesPerFrame - i]; - } - - uint8_t curByte = 0; - for (int i = 0; i < 2*rxProtocol.bytesPerTx; ++i) { - double freq = m_hzPerSample*rxProtocol.freqStart; - int bin = std::round(freq*m_ihzPerSample) + 16*i; - - int kmax = 0; - double amax = 0.0; - for (int k = 0; k < 16; ++k) { - if (m_sampleSpectrum[bin + k] > amax) { - kmax = k; - amax = m_sampleSpectrum[bin + k]; - } - } - - if (i%2) { - curByte += (kmax << 4); - m_txDataEncoded[itx*rxProtocol.bytesPerTx + i/2] = curByte; - curByte = 0; - } else { - curByte = kmax; - } - } - - if (itx*rxProtocol.bytesPerTx > m_encodedDataOffset && knownLength == false) { - RS::ReedSolomon rsLength(1, m_encodedDataOffset - 1); - if ((rsLength.Decode(m_txDataEncoded.data(), m_rxData.data()) == 0) && (m_rxData[0] > 0 && m_rxData[0] <= 140)) { - knownLength = true; - } else { - break; - } - } - - if (knownLength && itx*rxProtocol.bytesPerTx > m_encodedDataOffset + m_rxData[0] + ::getECCBytesForLength(m_rxData[0]) + 1) { - break; - } - } - - if (knownLength) { - int decodedLength = m_rxData[0]; - - RS::ReedSolomon rsData(decodedLength, ::getECCBytesForLength(decodedLength)); - - if (rsData.Decode(m_txDataEncoded.data() + m_encodedDataOffset, m_rxData.data()) == 0) { - if (m_rxData[0] != 0) { - std::string s((char *) m_rxData.data(), decodedLength); - - fprintf(stderr, "Decoded length = %d\n", decodedLength); - fprintf(stderr, "Received sound data successfully: '%s'\n", s.c_str()); - - isValid = true; - m_hasNewRxData = true; - m_lastRxDataLength = decodedLength; - m_rxProtocol = rxProtocol; - m_rxProtocolId = TxProtocolId(rxProtocolId); - } - } - } - - if (isValid) { - break; - } - --m_framesLeftToAnalyze; - } - - if (isValid) break; - } - - m_framesToRecord = 0; - - if (isValid == false) { - fprintf(stderr, "Failed to capture sound data. Please try again\n"); - m_lastRxDataLength = -1; - m_framesToRecord = -1; - } - - m_receivingData = false; - m_analyzingData = false; - - std::fill(m_sampleSpectrum.begin(), m_sampleSpectrum.end(), 0.0f); - - m_framesToAnalyze = 0; - m_framesLeftToAnalyze = 0; - - auto tEnd = std::chrono::high_resolution_clock::now(); - fprintf(stderr, "Time to analyze: %g ms\n", getTime_ms(tStart, tEnd)); - } - - // check if receiving data - if (m_receivingData == false) { - bool isReceiving = false; - - for (const auto & rxProtocol : getTxProtocols()) { - int nDetectedMarkerBits = m_nBitsInMarker; - - for (int i = 0; i < m_nBitsInMarker; ++i) { - double freq = bitFreq(rxProtocol.second, i); - int bin = std::round(freq*m_ihzPerSample); - - if (i%2 == 0) { - if (m_sampleSpectrum[bin] <= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) --nDetectedMarkerBits; - } else { - if (m_sampleSpectrum[bin] >= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) --nDetectedMarkerBits; - } - } - - if (nDetectedMarkerBits == m_nBitsInMarker) { - m_markerFreqStart = rxProtocol.second.freqStart; - isReceiving = true; - break; - } - } - - if (isReceiving) { - std::time_t timestamp = std::time(nullptr); - fprintf(stderr, "%sReceiving sound data ...\n", std::asctime(std::localtime(×tamp))); - - m_receivingData = true; - std::fill(m_rxData.begin(), m_rxData.end(), 0); - - // max recieve duration - m_recvDuration_frames = - 2*m_nMarkerFrames + m_nPostMarkerFrames + - maxFramesPerTx()*((kMaxLength + ::getECCBytesForLength(kMaxLength))/minBytesPerTx() + 1); - - m_framesToRecord = m_recvDuration_frames; - m_framesLeftToRecord = m_recvDuration_frames; - } + if (nBytesRecorded >= nBytesNeeded) { + if (m_isFixedPayloadLength) { + decode_fixed(); } else { - bool isEnded = false; - - for (const auto & rxProtocol : getTxProtocols()) { - int nDetectedMarkerBits = m_nBitsInMarker; - - for (int i = 0; i < m_nBitsInMarker; ++i) { - double freq = bitFreq(rxProtocol.second, i); - int bin = std::round(freq*m_ihzPerSample); - - if (i%2 == 0) { - if (m_sampleSpectrum[bin] >= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) nDetectedMarkerBits--; - } else { - if (m_sampleSpectrum[bin] <= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) nDetectedMarkerBits--; - } - } - - if (nDetectedMarkerBits == m_nBitsInMarker) { - isEnded = true; - break; - } - } - - if (isEnded && m_framesToRecord > 1) { - std::time_t timestamp = std::time(nullptr); - fprintf(stderr, "%sReceived end marker. Frames left = %d\n", std::asctime(std::localtime(×tamp)), m_framesLeftToRecord); - m_recvDuration_frames -= m_framesLeftToRecord - 1; - m_framesLeftToRecord = 1; - } + decode_variable(); } } else { m_samplesNeeded -= nBytesRecorded/m_sampleSizeBytesInp; @@ -1018,6 +834,440 @@ bool GGWave::takeSpectrum(SpectrumData & dst) { return true; } +// +// Variable payload length +// + +void GGWave::decode_variable() { + m_samplesNeeded = m_samplesPerFrame; + m_sampleAmplitudeHistory[m_historyId] = m_sampleAmplitude; + + if (++m_historyId >= kMaxSpectrumHistory) { + m_historyId = 0; + } + + if (m_historyId == 0 || m_receivingData) { + m_hasNewSpectrum = true; + + std::fill(m_sampleAmplitudeAverage.begin(), m_sampleAmplitudeAverage.end(), 0.0f); + for (auto & s : m_sampleAmplitudeHistory) { + for (int i = 0; i < m_samplesPerFrame; ++i) { + m_sampleAmplitudeAverage[i] += s[i]; + } + } + + float norm = 1.0f/kMaxSpectrumHistory; + for (int i = 0; i < m_samplesPerFrame; ++i) { + m_sampleAmplitudeAverage[i] *= norm; + } + + // calculate spectrum + std::copy(m_sampleAmplitudeAverage.begin(), m_sampleAmplitudeAverage.begin() + m_samplesPerFrame, m_fftInp.data()); + + FFT(m_fftInp.data(), m_fftOut.data(), m_samplesPerFrame, 1.0); + + for (int i = 0; i < m_samplesPerFrame; ++i) { + m_sampleSpectrum[i] = (m_fftOut[2*i + 0]*m_fftOut[2*i + 0] + m_fftOut[2*i + 1]*m_fftOut[2*i + 1]); + } + for (int i = 1; i < m_samplesPerFrame/2; ++i) { + m_sampleSpectrum[i] += m_sampleSpectrum[m_samplesPerFrame - i]; + } + } + + if (m_framesLeftToRecord > 0) { + std::copy(m_sampleAmplitude.begin(), + m_sampleAmplitude.begin() + m_samplesPerFrame, + m_recordedAmplitude.data() + (m_framesToRecord - m_framesLeftToRecord)*m_samplesPerFrame); + + if (--m_framesLeftToRecord <= 0) { + m_analyzingData = true; + } + } + + if (m_analyzingData) { + fprintf(stderr, "Analyzing captured data ..\n"); + auto tStart = std::chrono::high_resolution_clock::now(); + + const int stepsPerFrame = 16; + const int step = m_samplesPerFrame/stepsPerFrame; + + bool isValid = false; + for (const auto & rxProtocolPair : m_rxProtocols) { + const auto & rxProtocolId = rxProtocolPair.first; + const auto & rxProtocol = rxProtocolPair.second; + + // skip Rx protocol if start frequency is different from detected one + if (rxProtocol.freqStart != m_markerFreqStart) { + continue; + } + + std::fill(m_sampleSpectrum.begin(), m_sampleSpectrum.end(), 0.0f); + + m_framesToAnalyze = m_nMarkerFrames*stepsPerFrame; + m_framesLeftToAnalyze = m_framesToAnalyze; + + // note : not sure if looping backwards here is more meaningful than looping forwards + for (int ii = m_nMarkerFrames*stepsPerFrame - 1; ii >= 0; --ii) { + bool knownLength = false; + + int decodedLength = 0; + const int offsetStart = ii; + for (int itx = 0; itx < 1024; ++itx) { + int offsetTx = offsetStart + itx*rxProtocol.framesPerTx*stepsPerFrame; + if (offsetTx >= m_recvDuration_frames*stepsPerFrame || (itx + 1)*rxProtocol.bytesPerTx >= (int) m_txDataEncoded.size()) { + break; + } + + std::copy( + m_recordedAmplitude.begin() + offsetTx*step, + m_recordedAmplitude.begin() + offsetTx*step + m_samplesPerFrame, m_fftInp.data()); + + // note : should we skip the first and last frame here as they are amplitude-smoothed? + for (int k = 1; k < rxProtocol.framesPerTx; ++k) { + for (int i = 0; i < m_samplesPerFrame; ++i) { + m_fftInp[i] += m_recordedAmplitude[(offsetTx + k*stepsPerFrame)*step + i]; + } + } + + FFT(m_fftInp.data(), m_fftOut.data(), m_samplesPerFrame, 1.0); + + for (int i = 0; i < m_samplesPerFrame; ++i) { + m_sampleSpectrum[i] = (m_fftOut[2*i + 0]*m_fftOut[2*i + 0] + m_fftOut[2*i + 1]*m_fftOut[2*i + 1]); + } + for (int i = 1; i < m_samplesPerFrame/2; ++i) { + m_sampleSpectrum[i] += m_sampleSpectrum[m_samplesPerFrame - i]; + } + + uint8_t curByte = 0; + for (int i = 0; i < 2*rxProtocol.bytesPerTx; ++i) { + double freq = m_hzPerSample*rxProtocol.freqStart; + int bin = std::round(freq*m_ihzPerSample) + 16*i; + + int kmax = 0; + double amax = 0.0; + for (int k = 0; k < 16; ++k) { + if (m_sampleSpectrum[bin + k] > amax) { + kmax = k; + amax = m_sampleSpectrum[bin + k]; + } + } + + if (i%2) { + curByte += (kmax << 4); + m_txDataEncoded[itx*rxProtocol.bytesPerTx + i/2] = curByte; + curByte = 0; + } else { + curByte = kmax; + } + } + + if (itx*rxProtocol.bytesPerTx > m_encodedDataOffset && knownLength == false) { + RS::ReedSolomon rsLength(1, m_encodedDataOffset - 1); + if ((rsLength.Decode(m_txDataEncoded.data(), m_rxData.data()) == 0) && (m_rxData[0] > 0 && m_rxData[0] <= 140)) { + knownLength = true; + decodedLength = m_rxData[0]; + + const int nTotalBytesExpected = m_encodedDataOffset + decodedLength + ::getECCBytesForLength(decodedLength); + const int nTotalFramesExpected = 2*m_nMarkerFrames + ((nTotalBytesExpected + rxProtocol.bytesPerTx - 1)/rxProtocol.bytesPerTx)*rxProtocol.framesPerTx; + if (m_recvDuration_frames > nTotalFramesExpected || + m_recvDuration_frames < nTotalFramesExpected - 2*m_nMarkerFrames) { + knownLength = false; + break; + } + } else { + break; + } + } + + { + const int nTotalBytesExpected = m_encodedDataOffset + decodedLength + ::getECCBytesForLength(decodedLength); + if (knownLength && itx*rxProtocol.bytesPerTx > nTotalBytesExpected + 1) { + break; + } + } + } + + if (knownLength) { + RS::ReedSolomon rsData(decodedLength, ::getECCBytesForLength(decodedLength)); + + if (rsData.Decode(m_txDataEncoded.data() + m_encodedDataOffset, m_rxData.data()) == 0) { + if (m_rxData[0] != 0) { + std::string s((char *) m_rxData.data(), decodedLength); + + fprintf(stderr, "Decoded length = %d, protocol = '%s' (%d)\n", decodedLength, rxProtocol.name, rxProtocolId); + fprintf(stderr, "Received sound data successfully: '%s'\n", s.c_str()); + + isValid = true; + m_hasNewRxData = true; + m_lastRxDataLength = decodedLength; + m_rxProtocol = rxProtocol; + m_rxProtocolId = TxProtocolId(rxProtocolId); + } + } + } + + if (isValid) { + break; + } + --m_framesLeftToAnalyze; + } + + if (isValid) break; + } + + m_framesToRecord = 0; + + if (isValid == false) { + fprintf(stderr, "Failed to capture sound data. Please try again (length = %d)\n", m_rxData[0]); + m_lastRxDataLength = -1; + m_framesToRecord = -1; + } + + m_receivingData = false; + m_analyzingData = false; + + std::fill(m_sampleSpectrum.begin(), m_sampleSpectrum.end(), 0.0f); + + m_framesToAnalyze = 0; + m_framesLeftToAnalyze = 0; + + auto tEnd = std::chrono::high_resolution_clock::now(); + fprintf(stderr, "Time to analyze: %g ms\n", getTime_ms(tStart, tEnd)); + } + + // check if receiving data + if (m_receivingData == false) { + bool isReceiving = false; + + for (const auto & rxProtocol : getTxProtocols()) { + int nDetectedMarkerBits = m_nBitsInMarker; + + for (int i = 0; i < m_nBitsInMarker; ++i) { + double freq = bitFreq(rxProtocol.second, i); + int bin = std::round(freq*m_ihzPerSample); + + if (i%2 == 0) { + if (m_sampleSpectrum[bin] <= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) --nDetectedMarkerBits; + } else { + if (m_sampleSpectrum[bin] >= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) --nDetectedMarkerBits; + } + } + + if (nDetectedMarkerBits == m_nBitsInMarker) { + m_markerFreqStart = rxProtocol.second.freqStart; + isReceiving = true; + break; + } + } + + if (isReceiving) { + if (++m_nMarkersSuccess >= 1) { + } else { + isReceiving = false; + } + } else { + m_nMarkersSuccess = 0; + } + + if (isReceiving) { + std::time_t timestamp = std::time(nullptr); + fprintf(stderr, "%sReceiving sound data ...\n", std::asctime(std::localtime(×tamp))); + + m_receivingData = true; + std::fill(m_rxData.begin(), m_rxData.end(), 0); + + // max recieve duration + m_recvDuration_frames = + 2*m_nMarkerFrames + + maxFramesPerTx()*((kMaxLengthVarible + ::getECCBytesForLength(kMaxLengthVarible))/minBytesPerTx() + 1); + + m_nMarkersSuccess = 0; + m_framesToRecord = m_recvDuration_frames; + m_framesLeftToRecord = m_recvDuration_frames; + } + } else { + bool isEnded = false; + + for (const auto & rxProtocol : getTxProtocols()) { + int nDetectedMarkerBits = m_nBitsInMarker; + + for (int i = 0; i < m_nBitsInMarker; ++i) { + double freq = bitFreq(rxProtocol.second, i); + int bin = std::round(freq*m_ihzPerSample); + + if (i%2 == 0) { + if (m_sampleSpectrum[bin] >= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) nDetectedMarkerBits--; + } else { + if (m_sampleSpectrum[bin] <= 3.0f*m_sampleSpectrum[bin + m_freqDelta_bin]) nDetectedMarkerBits--; + } + } + + if (nDetectedMarkerBits == m_nBitsInMarker) { + isEnded = true; + break; + } + } + + if (isEnded) { + if (++m_nMarkersSuccess >= 1) { + } else { + isEnded = false; + } + } else { + m_nMarkersSuccess = 0; + } + + if (isEnded && m_framesToRecord > 1) { + std::time_t timestamp = std::time(nullptr); + m_recvDuration_frames -= m_framesLeftToRecord - 1; + fprintf(stderr, "%sReceived end marker. Frames left = %d, recorded = %d\n", std::asctime(std::localtime(×tamp)), m_framesLeftToRecord, m_recvDuration_frames); + m_nMarkersSuccess = 0; + m_framesLeftToRecord = 1; + } + } +} + +// +// Fixed payload length + +void GGWave::decode_fixed() { + m_samplesNeeded = m_samplesPerFrame; + + // calculate spectrum + std::copy(m_sampleAmplitude.begin(), m_sampleAmplitude.begin() + m_samplesPerFrame, m_fftInp.data()); + + FFT(m_fftInp.data(), m_fftOut.data(), m_samplesPerFrame, 1.0); + + for (int i = 0; i < m_samplesPerFrame; ++i) { + m_sampleSpectrum[i] = (m_fftOut[2*i + 0]*m_fftOut[2*i + 0] + m_fftOut[2*i + 1]*m_fftOut[2*i + 1]); + } + for (int i = 1; i < m_samplesPerFrame/2; ++i) { + m_sampleSpectrum[i] += m_sampleSpectrum[m_samplesPerFrame - i]; + } + + m_spectrumHistoryFixed[m_historyIdFixed] = m_sampleSpectrum; + + if (++m_historyIdFixed >= (int) m_spectrumHistoryFixed.size()) { + m_historyIdFixed = 0; + } + + bool isValid = false; + for (const auto & rxProtocolPair : m_rxProtocols) { + const auto & rxProtocolId = rxProtocolPair.first; + const auto & rxProtocol = rxProtocolPair.second; + + const int binStart = rxProtocol.freqStart; + const int binDelta = 16; + + const int totalLength = m_payloadLength + getECCBytesForLength(m_payloadLength); + const int totalTxs = (totalLength + rxProtocol.bytesPerTx - 1)/rxProtocol.bytesPerTx; + + int historyStartId = m_historyIdFixed - totalTxs*rxProtocol.framesPerTx; + if (historyStartId < 0) { + historyStartId += m_spectrumHistoryFixed.size(); + } + + const int nTones = 2*rxProtocol.bytesPerTx; + std::vector detectedBins(2*totalLength); + + struct ToneData { + int nMax[16]; + }; + + std::vector tones(nTones); + + bool detectedSignal = true; + for (int k = 0; k < totalTxs; ++k) { + for (auto & tone : tones) { + std::fill(tone.nMax, tone.nMax + 16, 0); + } + + for (int i = 0; i < rxProtocol.framesPerTx; ++i) { + int historyId = historyStartId + k*rxProtocol.framesPerTx + i; + if (historyId >= (int) m_spectrumHistoryFixed.size()) { + historyId -= m_spectrumHistoryFixed.size(); + } + + for (int j = 0; j < rxProtocol.bytesPerTx; ++j) { + int f0bin = -1; + int f1bin = -1; + + double f0max = 0.0; + double f1max = 0.0; + + for (int b = 0; b < 16; ++b) { + { + const auto & v = m_spectrumHistoryFixed[historyId][binStart + 2*j*binDelta + b]; + + if (f0max <= v) { + f0max = v; + f0bin = b; + } + } + + { + const auto & v = m_spectrumHistoryFixed[historyId][binStart + 2*j*binDelta + binDelta + b]; + + if (f1max <= v) { + f1max = v; + f1bin = b; + } + } + } + + tones[2*j + 0].nMax[f0bin]++; + tones[2*j + 1].nMax[f1bin]++; + } + } + + int detectedTx = 0; + int txNeeded = 0; + for (int j = 0; j < rxProtocol.bytesPerTx; ++j) { + if (k*rxProtocol.bytesPerTx + j >= totalLength) break; + txNeeded += 2; + for (int b = 0; b < 16; ++b) { + if (tones[2*j + 0].nMax[b] > rxProtocol.framesPerTx/2) { + detectedBins[2*(k*rxProtocol.bytesPerTx + j) + 0] = b; + detectedTx++; + } + if (tones[2*j + 1].nMax[b] > rxProtocol.framesPerTx/2) { + detectedBins[2*(k*rxProtocol.bytesPerTx + j) + 1] = b; + detectedTx++; + } + } + } + + if (detectedTx < txNeeded) { + detectedSignal = false; + } + } + + if (detectedSignal) { + RS::ReedSolomon rsData(m_payloadLength, getECCBytesForLength(m_payloadLength)); + + for (int j = 0; j < totalLength; ++j) { + m_txDataEncoded[j] = (detectedBins[2*j + 1] << 4) + detectedBins[2*j + 0]; + } + + if (rsData.Decode(m_txDataEncoded.data(), m_rxData.data()) == 0) { + if (m_rxData[0] != 0) { + fprintf(stderr, "Received sound data successfully: '%s'\n", m_rxData.data()); + + isValid = true; + m_hasNewRxData = true; + m_lastRxDataLength = m_payloadLength; + m_rxProtocol = rxProtocol; + m_rxProtocolId = TxProtocolId(rxProtocolId); + } + } + } + + if (isValid) { + break; + } + } +} + int GGWave::maxFramesPerTx() const { int res = 0; for (const auto & protocol : getTxProtocols()) { diff --git a/src/resampler.cpp b/src/resampler.cpp new file mode 100644 index 0000000..e31dbb3 --- /dev/null +++ b/src/resampler.cpp @@ -0,0 +1,110 @@ +#include "resampler.h" + +#include +#include + +namespace { +double linear_interp(double first_number, double second_number, double fraction) { + return (first_number + ((second_number - first_number)*fraction)); +} +} + +int Resampler::resample( + float factor, + int nSamples, + const float * samplesInp, + float * samplesOut) { + if (factor != m_lastFactor) { + make_sinc(); + m_lastFactor = factor; + } + + int idxInp = 0; + int idxOut = 0; + int notDone = 1; + double time_now = 0.0; + long num_samples = nSamples; + long int_time = 0; + long last_time = 0; + float data_in = samplesInp[idxInp]; + float data_out; + double one_over_factor = 1.0; + while (notDone) { + double temp1 = 0.0; + long left_limit = time_now - kWidth + 1; /* leftmost neighboring sample used for interp.*/ + long right_limit = time_now + kWidth; /* rightmost leftmost neighboring sample used for interp.*/ + if (left_limit<0) left_limit = 0; + if (right_limit>num_samples) right_limit = num_samples; + if (factor<1.0) { + for (int j=left_limit;j= kWidth - 1) { + return 0.0; + } else { + temp = fabs(x) * (double) kSamplesPerZeroCrossing; + low = temp; /* these are interpolation steps */ + delta = temp - low; /* and can be ommited if desired */ + return linear_interp(m_sincTable[low], m_sincTable[low + 1], delta); + } +} diff --git a/src/resampler.h b/src/resampler.h new file mode 100644 index 0000000..aae84c2 --- /dev/null +++ b/src/resampler.h @@ -0,0 +1,32 @@ +#pragma once + +class Resampler { +public: + int resample( + float factor, + int nSamples, + const float * samplesInp, + float * samplesOut); +private: + float gimme_data(long j) const; + void new_data(float data); + void make_sinc(); + double sinc(double x) const; + + /* this controls the number of neighboring samples + which are used to interpolate the new samples. The + processing time is linearly related to this width */ + static const int kWidth = 64; + + static const int kDelaySize = 140; + + /* this defines how finely the sinc function + is sampled for storage in the table */ + static const int kSamplesPerZeroCrossing = 32; + + float m_sincTable[kWidth*kSamplesPerZeroCrossing] = { 0.0 }; + + float m_delayBuffer[3*kWidth] = { 0 }; + + float m_lastFactor = -1.0f; +}; diff --git a/tests/test-ggwave.cpp b/tests/test-ggwave.cpp index a360590..d82412c 100644 --- a/tests/test-ggwave.cpp +++ b/tests/test-ggwave.cpp @@ -1,5 +1,6 @@ #include "ggwave/ggwave.h" +#include #include #include #include @@ -71,7 +72,14 @@ void convert(const std::vector & src, std::vector & dst) { } } -int main() { +int main(int argc, char ** argv) { + bool full = false; + if (argc > 1) { + if (strcmp(argv[1], "--full") == 0) { + full = true; + } + } + std::vector bufferU8; std::vector bufferI8; std::vector bufferU16; @@ -181,16 +189,15 @@ int main() { CHECK_F(instance.init(payload.size(), payload.c_str(), 101)); } - // capture / playback at different sample rates - { + // playback / capture at different sample rates + for (int srInp = GGWave::kBaseSampleRate/3; srInp <= 2*GGWave::kBaseSampleRate; srInp += 1100) { auto parameters = GGWave::getDefaultParameters(); - std::string payload = "hello"; + std::string payload = "hello123"; // encode { - parameters.sampleRateInp = 48000; - parameters.sampleRateOut = 12000; + parameters.sampleRateOut = srInp; GGWave instanceOut(parameters); instanceOut.init(payload, 25); @@ -203,10 +210,10 @@ int main() { // decode { - parameters.samplesPerFrame *= float(parameters.sampleRateOut)/parameters.sampleRateInp; - parameters.sampleRateInp = parameters.sampleRateOut; + parameters.sampleRateInp = srInp; GGWave instanceInp(parameters); + instanceInp.setRxProtocols({{instanceInp.getDefaultTxProtocolId(), instanceInp.getDefaultTxProtocol()}}); instanceInp.decode(kCBWaveformInp.at(parameters.sampleFormatInp)); GGWave::TxRxData result; @@ -217,30 +224,67 @@ int main() { } } - for (const auto & txProtocol : GGWave::getTxProtocols()) { - for (const auto & formatOut : kFormats) { - for (const auto & formatInp : kFormats) { + std::string payload = "a0Z5kR2g"; + + // encode / decode using different sample formats and Tx protocols + for (const auto & formatOut : kFormats) { + for (const auto & formatInp : kFormats) { + if (full == false) { + if (formatOut != GGWAVE_SAMPLE_FORMAT_I16) continue; + if (formatInp != GGWAVE_SAMPLE_FORMAT_F32) continue; + } + for (const auto & txProtocol : GGWave::getTxProtocols()) { printf("Testing: protocol = %s, in = %d, out = %d\n", txProtocol.second.name, formatInp, formatOut); - auto parameters = GGWave::getDefaultParameters(); - parameters.sampleFormatInp = formatInp; - parameters.sampleFormatOut = formatOut; - GGWave instance(parameters); + for (int length = 1; length <= (int) payload.size(); ++length) { + // variable payload length + { + auto parameters = GGWave::getDefaultParameters(); + parameters.sampleFormatInp = formatInp; + parameters.sampleFormatOut = formatOut; + GGWave instance(parameters); - std::string payload = "test"; + instance.setRxProtocols({{txProtocol.first, txProtocol.second}}); + instance.init(length, payload.data(), txProtocol.second, 25); + auto expectedSize = instance.encodeSize_samples(); + instance.encode(kCBWaveformOut.at(formatOut)); + printf("Expected = %d, actual = %d\n", expectedSize, nSamples); + CHECK(expectedSize == nSamples); + convertHelper(formatOut, formatInp); + instance.decode(kCBWaveformInp.at(formatInp)); - instance.init(payload, txProtocol.second, 25); - auto expectedSize = instance.encodeSize_samples(); - instance.encode(kCBWaveformOut.at(formatOut)); - printf("Expected = %d, actual = %d\n", expectedSize, nSamples); - CHECK(expectedSize == nSamples); - convertHelper(formatOut, formatInp); - instance.decode(kCBWaveformInp.at(formatInp)); + GGWave::TxRxData result; + CHECK(instance.takeRxData(result) == length); + for (int i = 0; i < length; ++i) { + CHECK(payload[i] == result[i]); + } + } + } - GGWave::TxRxData result; - CHECK(instance.takeRxData(result) == (int) payload.size()); - for (int i = 0; i < (int) payload.size(); ++i) { - CHECK(payload[i] == result[i]); + for (int length = 1; length <= (int) payload.size(); ++length) { + // fixed payload length + { + auto parameters = GGWave::getDefaultParameters(); + parameters.payloadLength = length; + parameters.sampleFormatInp = formatInp; + parameters.sampleFormatOut = formatOut; + GGWave instance(parameters); + + instance.setRxProtocols({{txProtocol.first, txProtocol.second}}); + instance.init(length, payload.data(), txProtocol.second, 10); + auto expectedSize = instance.encodeSize_samples(); + instance.encode(kCBWaveformOut.at(formatOut)); + printf("Expected = %d, actual = %d\n", expectedSize, nSamples); + CHECK(expectedSize == nSamples); + convertHelper(formatOut, formatInp); + instance.decode(kCBWaveformInp.at(formatInp)); + + GGWave::TxRxData result; + CHECK(instance.takeRxData(result) == length); + for (int i = 0; i < length; ++i) { + CHECK(payload[i] == result[i]); + } + } } } }