Add arduino-rx + update ggwave-mod

This commit is contained in:
Georgi Gerganov
2022-05-05 22:14:20 +03:00
parent ecb604c629
commit ee9034dc86
17 changed files with 949 additions and 52 deletions

View File

@@ -79,6 +79,9 @@ if (GGWAVE_SUPPORT_SDL2)
)
endif()
# modified ggwave used by some of the examples
add_subdirectory(ggwave-mod/src)
# examples
if (EMSCRIPTEN)
@@ -93,6 +96,7 @@ if (GGWAVE_SUPPORT_SDL2)
add_subdirectory(r2t2)
endif()
add_subdirectory(arduino-rx)
if (EMSCRIPTEN)
# emscripten sdl2 examples

View File

@@ -0,0 +1,24 @@
#
# arduino-rx
set(TARGET arduino-rx)
if (NOT EMSCRIPTEN)
add_executable(${TARGET}
arduino-rx.cpp
)
target_include_directories(${TARGET} PRIVATE
..
${SDL2_INCLUDE_DIRS}
)
target_link_libraries(${TARGET} PRIVATE
ggwave-common
ggwave-mod
${SDL2_LIBRARIES}
)
else()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/index-tmpl.html ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/index.html @ONLY)
configure_file(${CMAKE_SOURCE_DIR}/examples/ggwave-mod/src/ggwave-mod.js ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/ggwave-mod.js COPYONLY)
endif()

View File

@@ -0,0 +1,402 @@
#include "ggwave-common.h"
#include "ggwave/ggwave.h"
#ifdef __EMSCRIPTEN__
#include "build_timestamp.h"
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif
#include <SDL.h>
#include <SDL_opengl.h>
#include <array>
#include <chrono>
#include <string>
#include <thread>
namespace {
// Direct-sequence spread magic numbers
// Used to xor the actual payload
const std::array<uint8_t, 64> kDSSMagic = {
0x96, 0x9f, 0xb4, 0xaf, 0x1b, 0x91, 0xde, 0xc5, 0x45, 0x75, 0xe8, 0x2e, 0x0f, 0x32, 0x4a, 0x5f,
0xb4, 0x56, 0x95, 0xcb, 0x7f, 0x6a, 0x54, 0x6a, 0x48, 0xf2, 0x0b, 0x7b, 0xcd, 0xfb, 0x93, 0x6d,
0x3c, 0x77, 0x5e, 0xc3, 0x33, 0x47, 0xc0, 0xf1, 0x71, 0x32, 0x33, 0x27, 0x35, 0x68, 0x47, 0x1f,
0x4e, 0xac, 0x23, 0x42, 0x5f, 0x00, 0x37, 0xa4, 0x50, 0x6d, 0x48, 0x24, 0x91, 0x7c, 0xa1, 0x4e,
};
std::string g_defaultCaptureDeviceName = "";
SDL_AudioDeviceID g_devIdInp = 0;
SDL_AudioDeviceID g_devIdOut = 0;
SDL_AudioSpec g_obtainedSpecInp;
SDL_AudioSpec g_obtainedSpecOut;
GGWave *g_ggWave = nullptr;
}
static std::function<bool()> g_doInit;
static std::function<void(int, int)> g_setWindowSize;
static std::function<bool()> g_mainUpdate;
void mainUpdate(void *) {
g_mainUpdate();
}
// JS interface
extern "C" {
EMSCRIPTEN_KEEPALIVE
int sendData(int textLength, const char * text, int protocolId, int volume) {
g_ggWave->init(textLength, text, g_ggWave->getTxProtocol(protocolId), volume);
return 0;
}
EMSCRIPTEN_KEEPALIVE
int getText(char * text) {
std::copy(g_ggWave->getRxData().begin(), g_ggWave->getRxData().end(), text);
return 0;
}
EMSCRIPTEN_KEEPALIVE
float getSampleRate() { return g_ggWave->getSampleRateInp(); }
EMSCRIPTEN_KEEPALIVE
int getFramesToRecord() { return g_ggWave->getFramesToRecord(); }
EMSCRIPTEN_KEEPALIVE
int getFramesLeftToRecord() { return g_ggWave->getFramesLeftToRecord(); }
EMSCRIPTEN_KEEPALIVE
int getFramesToAnalyze() { return g_ggWave->getFramesToAnalyze(); }
EMSCRIPTEN_KEEPALIVE
int getFramesLeftToAnalyze() { return g_ggWave->getFramesLeftToAnalyze(); }
EMSCRIPTEN_KEEPALIVE
int hasDeviceOutput() { return g_devIdOut; }
EMSCRIPTEN_KEEPALIVE
int hasDeviceCapture() { return g_devIdInp; }
EMSCRIPTEN_KEEPALIVE
int doInit() {
return g_doInit();
}
}
void GGWave_setDefaultCaptureDeviceName(std::string name) {
g_defaultCaptureDeviceName = std::move(name);
}
bool GGWave_init(
const int playbackId,
const int captureId,
const int payloadLength,
const float sampleRateOffset) {
if (g_devIdInp && g_devIdOut) {
return false;
}
if (g_devIdInp == 0 && g_devIdOut == 0) {
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s\n", SDL_GetError());
return (1);
}
SDL_SetHintWithPriority(SDL_HINT_AUDIO_RESAMPLING_MODE, "medium", SDL_HINT_OVERRIDE);
{
int nDevices = SDL_GetNumAudioDevices(SDL_FALSE);
printf("Found %d playback devices:\n", nDevices);
for (int i = 0; i < nDevices; i++) {
printf(" - Playback device #%d: '%s'\n", i, SDL_GetAudioDeviceName(i, SDL_FALSE));
}
}
{
int nDevices = SDL_GetNumAudioDevices(SDL_TRUE);
printf("Found %d capture devices:\n", nDevices);
for (int i = 0; i < nDevices; i++) {
printf(" - Capture device #%d: '%s'\n", i, SDL_GetAudioDeviceName(i, SDL_TRUE));
}
}
}
bool reinit = false;
if (g_devIdOut == 0) {
printf("Initializing playback ...\n");
SDL_AudioSpec playbackSpec;
SDL_zero(playbackSpec);
playbackSpec.freq = GGWave::kBaseSampleRate + sampleRateOffset;
playbackSpec.format = AUDIO_S16SYS;
playbackSpec.channels = 1;
playbackSpec.samples = 16*1024;
playbackSpec.callback = NULL;
SDL_zero(g_obtainedSpecOut);
if (playbackId >= 0) {
printf("Attempt to open playback device %d : '%s' ...\n", playbackId, SDL_GetAudioDeviceName(playbackId, SDL_FALSE));
g_devIdOut = SDL_OpenAudioDevice(SDL_GetAudioDeviceName(playbackId, SDL_FALSE), SDL_FALSE, &playbackSpec, &g_obtainedSpecOut, 0);
} else {
printf("Attempt to open default playback device ...\n");
g_devIdOut = SDL_OpenAudioDevice(NULL, SDL_FALSE, &playbackSpec, &g_obtainedSpecOut, 0);
}
if (!g_devIdOut) {
printf("Couldn't open an audio device for playback: %s!\n", SDL_GetError());
g_devIdOut = 0;
} else {
printf("Obtained spec for output device (SDL Id = %d):\n", g_devIdOut);
printf(" - Sample rate: %d (required: %d)\n", g_obtainedSpecOut.freq, playbackSpec.freq);
printf(" - Format: %d (required: %d)\n", g_obtainedSpecOut.format, playbackSpec.format);
printf(" - Channels: %d (required: %d)\n", g_obtainedSpecOut.channels, playbackSpec.channels);
printf(" - Samples per frame: %d (required: %d)\n", g_obtainedSpecOut.samples, playbackSpec.samples);
if (g_obtainedSpecOut.format != playbackSpec.format ||
g_obtainedSpecOut.channels != playbackSpec.channels ||
g_obtainedSpecOut.samples != playbackSpec.samples) {
g_devIdOut = 0;
SDL_CloseAudio();
fprintf(stderr, "Failed to initialize playback SDL_OpenAudio!");
return false;
}
reinit = true;
}
}
if (g_devIdInp == 0) {
SDL_AudioSpec captureSpec;
captureSpec = g_obtainedSpecOut;
captureSpec.freq = GGWave::kBaseSampleRate + sampleRateOffset;
captureSpec.format = AUDIO_F32SYS;
captureSpec.samples = 1024;
SDL_zero(g_obtainedSpecInp);
if (captureId >= 0) {
printf("Attempt to open capture device %d : '%s' ...\n", captureId, SDL_GetAudioDeviceName(captureId, SDL_TRUE));
g_devIdInp = SDL_OpenAudioDevice(SDL_GetAudioDeviceName(captureId, SDL_TRUE), SDL_TRUE, &captureSpec, &g_obtainedSpecInp, 0);
} else {
printf("Attempt to open default capture device ...\n");
g_devIdInp = SDL_OpenAudioDevice(g_defaultCaptureDeviceName.empty() ? nullptr : g_defaultCaptureDeviceName.c_str(),
SDL_TRUE, &captureSpec, &g_obtainedSpecInp, 0);
}
if (!g_devIdInp) {
printf("Couldn't open an audio device for capture: %s!\n", SDL_GetError());
g_devIdInp = 0;
} else {
printf("Obtained spec for input device (SDL Id = %d):\n", g_devIdInp);
printf(" - Sample rate: %d\n", g_obtainedSpecInp.freq);
printf(" - Format: %d (required: %d)\n", g_obtainedSpecInp.format, captureSpec.format);
printf(" - Channels: %d (required: %d)\n", g_obtainedSpecInp.channels, captureSpec.channels);
printf(" - Samples per frame: %d\n", g_obtainedSpecInp.samples);
reinit = true;
}
}
GGWave::SampleFormat sampleFormatInp = GGWAVE_SAMPLE_FORMAT_UNDEFINED;
GGWave::SampleFormat sampleFormatOut = GGWAVE_SAMPLE_FORMAT_UNDEFINED;
switch (g_obtainedSpecInp.format) {
case AUDIO_U8: sampleFormatInp = GGWAVE_SAMPLE_FORMAT_U8; break;
case AUDIO_S8: sampleFormatInp = GGWAVE_SAMPLE_FORMAT_I8; break;
case AUDIO_U16SYS: sampleFormatInp = GGWAVE_SAMPLE_FORMAT_U16; break;
case AUDIO_S16SYS: sampleFormatInp = GGWAVE_SAMPLE_FORMAT_I16; break;
case AUDIO_S32SYS: sampleFormatInp = GGWAVE_SAMPLE_FORMAT_F32; break;
case AUDIO_F32SYS: sampleFormatInp = GGWAVE_SAMPLE_FORMAT_F32; break;
}
switch (g_obtainedSpecOut.format) {
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({
payloadLength,
(float) g_obtainedSpecInp.freq,
(float) g_obtainedSpecOut.freq,
512,
GGWave::kDefaultSoundMarkerThreshold,
sampleFormatInp,
sampleFormatOut});
}
return true;
}
GGWave *& GGWave_instance() { return g_ggWave; }
bool GGWave_mainLoop() {
if (g_devIdInp == 0 && g_devIdOut == 0) {
return false;
}
static GGWave::CBWaveformOut cbWaveformOut = [&](const void * data, uint32_t nBytes) {
SDL_QueueAudio(g_devIdOut, data, nBytes);
};
static GGWave::CBWaveformInp cbWaveformInp = [&](void * data, uint32_t nMaxBytes) {
return SDL_DequeueAudio(g_devIdInp, data, nMaxBytes);
};
if (g_ggWave->hasTxData() == false) {
SDL_PauseAudioDevice(g_devIdOut, SDL_FALSE);
static auto tLastNoData = std::chrono::high_resolution_clock::now();
auto tNow = std::chrono::high_resolution_clock::now();
if ((int) SDL_GetQueuedAudioSize(g_devIdOut) < g_ggWave->getSamplesPerFrame()*g_ggWave->getSampleSizeBytesOut()) {
SDL_PauseAudioDevice(g_devIdInp, SDL_FALSE);
if (::getTime_ms(tLastNoData, tNow) > 500.0f) {
g_ggWave->decode(cbWaveformInp);
GGWave::TxRxData rxData;
int n = g_ggWave->takeRxData(rxData);
if (n > 0) {
for (int i = 0; i < n; i++) {
rxData[i] ^= kDSSMagic[i%kDSSMagic.size()];
}
std::time_t timestamp = std::time(nullptr);
std::string tstr = std::asctime(std::localtime(&timestamp));
tstr.back() = 0;
printf("[%s] Received: '%s'\n", tstr.c_str(), rxData.data());
}
if ((int) SDL_GetQueuedAudioSize(g_devIdInp) > 32*g_ggWave->getSamplesPerFrame()*g_ggWave->getSampleSizeBytesInp()) {
fprintf(stderr, "Warning: slow processing, clearing queued audio buffer of %d bytes ...", SDL_GetQueuedAudioSize(g_devIdInp));
SDL_ClearQueuedAudio(g_devIdInp);
}
} else {
SDL_ClearQueuedAudio(g_devIdInp);
}
} else {
tLastNoData = tNow;
}
} else {
SDL_PauseAudioDevice(g_devIdOut, SDL_TRUE);
SDL_PauseAudioDevice(g_devIdInp, SDL_TRUE);
g_ggWave->encode(cbWaveformOut);
}
return true;
}
bool GGWave_deinit() {
if (g_devIdInp == 0 && g_devIdOut == 0) {
return false;
}
delete g_ggWave;
g_ggWave = nullptr;
SDL_PauseAudioDevice(g_devIdInp, 1);
SDL_CloseAudioDevice(g_devIdInp);
SDL_PauseAudioDevice(g_devIdOut, 1);
SDL_CloseAudioDevice(g_devIdOut);
g_devIdInp = 0;
g_devIdOut = 0;
return true;
}
int main(int argc, char** argv) {
#ifdef __EMSCRIPTEN__
printf("Build time: %s\n", BUILD_TIMESTAMP);
printf("Press the Init button to start\n");
if (argv[1]) {
GGWave_setDefaultCaptureDeviceName(argv[1]);
}
#else
printf("Usage: %s [-cN] [-lN]\n", argv[0]);
printf(" -cN - select capture device N\n");
printf(" -lN - fixed payload length of size N, N in [1, %d]\n", GGWave::kMaxLengthFixed);
printf("\n");
#endif
const GGWave::TxProtocols protocols = {
{ GGWAVE_TX_PROTOCOL_CUSTOM_0, { "[R2T2] Normal", 64, 9, 1, } },
{ GGWAVE_TX_PROTOCOL_CUSTOM_1, { "[R2T2] Fast", 64, 6, 1, } },
{ GGWAVE_TX_PROTOCOL_CUSTOM_2, { "[R2T2] Fastest", 64, 3, 1, } },
{ GGWAVE_TX_PROTOCOL_CUSTOM_3, { "[R2T2] Low Normal", 16, 9, 1, } },
{ GGWAVE_TX_PROTOCOL_CUSTOM_4, { "[R2T2] Low Fast", 16, 6, 1, } },
{ GGWAVE_TX_PROTOCOL_CUSTOM_5, { "[R2T2] Low Fastest", 16, 3, 1, } },
};
const auto argm = parseCmdArguments(argc, argv);
const int captureId = argm.count("c") == 0 ? 0 : std::stoi(argm.at("c"));
const int payloadLength = argm.count("l") == 0 ? 16 : std::stoi(argm.at("l"));
bool isInitialized = false;
g_doInit = [&]() {
if (GGWave_init(0, captureId, payloadLength, 0) == false) {
fprintf(stderr, "Failed to initialize GGWave\n");
return false;
}
g_ggWave->setRxProtocols(protocols);
isInitialized = true;
printf("Listening for payload with length = %d bytes ..\n", payloadLength);
return true;
};
g_mainUpdate = [&]() {
if (isInitialized == false) {
return true;
}
GGWave_mainLoop();
return true;
};
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(mainUpdate, NULL, 60, true);
#else
if (g_doInit() == false) {
printf("Error: failed to initialize audio\n");
return -2;
}
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
if (g_mainUpdate() == false) break;
}
GGWave_deinit();
// Cleanup
SDL_CloseAudio();
SDL_Quit();
#endif
return 0;
}

View File

@@ -0,0 +1,235 @@
<!doctype html>
<html lang="en-us">
<head>
<title>ggwave : arduino-rx</title>
<style>
.block {
display: block;
width: 100%;
height: 100%;
border: none;
background-color: #FFFFFF00;
color: #000000;
padding: 14px 28px;
padding-top: 40vh;
font-size: 100px;
cursor: pointer;
text-align: center;
}
</style>
</head>
<body>
<div id="main-container">
<div id="controls">
<div id="controls0">
<button id="captureStart" class="block">Start</button>
</div>
<div id="controls1" hidden>
<button id="rxData" class="block">[no data]</button>
</div>
</div>
<div id="debug" hidden>
<br><br>
<button onclick="onSend('RRR');">Red</button>
<button onclick="onSend('GGG');">Green</button>
<button onclick="onSend('BBB');">Blue</button>
<br><br>
<textarea name="textarea" id="rxData2" style="width:300px;height:100px;" disabled></textarea><br>
<button id="captureStart2">Start capturing</button>
<button id="captureStop" hidden>Stop capturing</button>
<br><br>
<div class="cell-version">
<span>
|
Build time: <span class="nav-link">@GIT_DATE@</span> |
Commit hash: <a class="nav-link" href="https://github.com/ggerganov/ggwave/commit/@GIT_SHA1@">@GIT_SHA1@</a> |
Commit subject: <span class="nav-link">@GIT_COMMIT_SUBJECT@</span> |
<a class="nav-link" href="https://github.com/ggerganov/ggwave/tree/master/examples/ggwave-js">Source Code</a> |
</span>
</div>
</div>
</div>
<script type="text/javascript" src="ggwave-mod.js"></script>
<script type='text/javascript'>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
var context = null;
var recorder = null;
// the ggwave module instance
var ggwave = null;
var parameters = null;
var instance = null;
const kPayloadLength = 16;
var kDSSMagic = [
0x96, 0x9f, 0xb4, 0xaf, 0x1b, 0x91, 0xde, 0xc5, 0x45, 0x75, 0xe8, 0x2e, 0x0f, 0x32, 0x4a, 0x5f,
0xb4, 0x56, 0x95, 0xcb, 0x7f, 0x6a, 0x54, 0x6a, 0x48, 0xf2, 0x0b, 0x7b, 0xcd, 0xfb, 0x93, 0x6d,
0x3c, 0x77, 0x5e, 0xc3, 0x33, 0x47, 0xc0, 0xf1, 0x71, 0x32, 0x33, 0x27, 0x35, 0x68, 0x47, 0x1f,
0x4e, 0xac, 0x23, 0x42, 0x5f, 0x00, 0x37, 0xa4, 0x50, 0x6d, 0x48, 0x24, 0x91, 0x7c, 0xa1, 0x4e,
];
// instantiate the ggwave instance
// ggwave_factory comes from the ggwave.js module
ggwave_factory().then(function(obj) {
ggwave = obj;
});
var txData = document.getElementById("txData");
var rxData = document.getElementById("rxData");
var captureStart = document.getElementById("captureStart");
var captureStop = document.getElementById("captureStop");
// helper function
function convertTypedArray(src, type) {
var buffer = new ArrayBuffer(src.byteLength);
var baseView = new src.constructor(buffer).set(src);
return new type(buffer);
}
// initialize audio context and ggwave
function init() {
if (!context) {
context = new AudioContext({sampleRate: 48000});
parameters = ggwave.getDefaultParameters();
parameters.payloadLength = kPayloadLength;
parameters.samplesPerFrame = 512;
parameters.sampleRateInp = context.sampleRate;
parameters.sampleRateOut = context.sampleRate;
instance = ggwave.init(parameters);
}
}
//
// Tx
//
function onSend(text) {
init();
// DSS : xor the text with the magic numbers
var payload = new Uint8Array(kPayloadLength);
for (var i = 0; i < kPayloadLength; i++) {
payload[i] = 0;
}
for (var i = 0; i < text.length; i++) {
payload[i] = text.charCodeAt(i);
}
for (var i = 0; i < kPayloadLength; i++) {
payload[i] = payload[i] ^ kDSSMagic[i];
}
// generate audio waveform
var waveform = ggwave.encode(instance, payload, ggwave.TxProtocolId.GGWAVE_TX_PROTOCOL_DT_FASTEST, 25)
// play audio
var buf = convertTypedArray(waveform, Float32Array);
var buffer = context.createBuffer(1, buf.length, context.sampleRate);
buffer.getChannelData(0).set(buf);
var source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.start(0);
}
//
// Rx
//
captureStart.addEventListener("click", function () {
init();
let constraints = {
audio: {
// not sure if these are necessary to have
echoCancellation: false,
autoGainControl: false,
noiseSuppression: false
}
};
navigator.mediaDevices.getUserMedia(constraints).then(function (e) {
mediaStream = context.createMediaStreamSource(e);
var bufferSize = 1024;
var numberOfInputChannels = 1;
var numberOfOutputChannels = 1;
if (context.createScriptProcessor) {
recorder = context.createScriptProcessor(
bufferSize,
numberOfInputChannels,
numberOfOutputChannels);
} else {
recorder = context.createJavaScriptNode(
bufferSize,
numberOfInputChannels,
numberOfOutputChannels);
}
recorder.onaudioprocess = function (e) {
var source = e.inputBuffer;
var res = ggwave.decode(instance, convertTypedArray(new Float32Array(source.getChannelData(0)), Int8Array));
if (res && res.length == kPayloadLength) {
// DSS
var payload = "";
res8 = convertTypedArray(res, Uint8Array);
for (var i = 0; i < kPayloadLength; i++) {
res8[i] = res8[i] ^ kDSSMagic[i];
payload += String.fromCharCode(res8[i]);
if (res8[i] == 0) {
break;
}
}
console.log(res8);
document.getElementById("rxData").value = payload;
document.getElementById("rxData").innerHTML = payload;
document.body.style.backgroundColor = '#aaffaa';
setTimeout(function () {
document.body.style.backgroundColor = '#FFFFFF';
}, 250);
}
}
mediaStream.connect(recorder);
recorder.connect(context.destination);
}).catch(function (e) {
console.error(e);
});
rxData.value = 'Listening ...';
document.getElementById("controls0").hidden = true;
document.getElementById("controls1").hidden = false;
captureStart.hidden = true;
captureStop.hidden = false;
});
captureStop.addEventListener("click", function () {
if (recorder) {
recorder.disconnect(context.destination);
mediaStream.disconnect(recorder);
recorder = null;
}
rxData.value = 'Audio capture is paused! Press the "Start capturing" button to analyze audio from the microphone';
captureStart.hidden = false;
captureStop.hidden = true;
});
captureStop.click();
</script>
</body>
</html>

View File

@@ -25,5 +25,23 @@ void loop() {
GGWave::send(kPinSpeaker, "This is GGWave!!", GGWave::TX_ARDUINO_512_FASTEST);
digitalWrite(kPinLed0, LOW);
delay(250);
digitalWrite(kPinLed0, HIGH);
GGWave::send(kPinSpeaker, "This is NORMAL", GGWave::TX_ARDUINO_512_NORMAL);
digitalWrite(kPinLed0, LOW);
delay(250);
digitalWrite(kPinLed0, HIGH);
GGWave::send(kPinSpeaker, "This is FAST", GGWave::TX_ARDUINO_512_FAST);
digitalWrite(kPinLed0, LOW);
delay(250);
digitalWrite(kPinLed0, HIGH);
GGWave::send(kPinSpeaker, "This is FASTEST", GGWave::TX_ARDUINO_512_FASTEST);
digitalWrite(kPinLed0, LOW);
delay(5000);
}

View File

@@ -90,6 +90,23 @@ extern "C" {
// the python module and unfortunately had to do it this way
typedef int ggwave_Instance;
// Change file stream for internal ggwave logging. NULL - disable logging
//
// Intentionally passing it as void * instead of FILE * to avoid including a header
//
// // log to standard error
// ggwave_setLogFile(stderr);
//
// // log to standard output
// ggwave_setLogFile(stdout);
//
// // disable logging
// ggwave_setLogFile(NULL);
//
// Note: not thread-safe. Do not call while any GGWave instances are running
//
GGWAVE_API void ggwave_setLogFile(void * fptr);
// Helper method to get default instance parameters
GGWAVE_API ggwave_Parameters ggwave_getDefaultParameters(void);
@@ -189,8 +206,10 @@ extern "C" {
// If the return value is -1 then there was an error during the decoding process.
// Usually can occur if there is a lot of background noise in the audio.
//
// If the return value is greater than 0, then there will be that number of bytes
// decoded in the outputBuffer
// If the return value is greater than 0, then there are that number of bytes decoded.
//
// IMPORTANT:
// Notice that the decoded data written to the outputBuffer is NOT null terminated.
//
// Example:
//
@@ -218,6 +237,39 @@ extern "C" {
int dataSize,
char * outputBuffer);
// Memory-safe overload of ggwave_decode
//
// outputSize - optionally specify the size of the output buffer
//
// If the return value is -2 then the provided outputBuffer was not big enough to
// store the decoded data.
//
// See ggwave_decode for more information
//
GGWAVE_API int ggwave_ndecode(
ggwave_Instance instance,
const char * dataBuffer,
int dataSize,
char * outputBuffer,
int outputSize);
// Toggle Rx protocols on and off
//
// instance - the GGWave instance to use
// rxProtocolId - Id of the Rx protocol to modify
// state - 0 - disable, 1 - enable
//
// If an Rx protocol is enabled, the GGWave instance will attempt to decode received
// data using this protocol. By default, all protocols are enabled.
// Use this function to restrict the number of Rx protocols used in the decoding
// process. This helps to reduce the number of false positives and improves the transmission
// accuracy, especially when the Tx/Rx protocol is known in advance.
//
GGWAVE_API void ggwave_toggleRxProtocol(
ggwave_Instance instance,
ggwave_TxProtocolId rxProtocolId,
int state);
#ifdef __cplusplus
}
@@ -272,15 +324,19 @@ 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_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, } },
//{ 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, } },
{ GGWAVE_TX_PROTOCOL_DT_NORMAL, { "Arduino", 16, 9, 1, } },
{ GGWAVE_TX_PROTOCOL_DT_FAST, { "Arduino", 16, 6, 1, } },
{ GGWAVE_TX_PROTOCOL_DT_FASTEST, { "Arduino", 16, 3, 1, } },
};
return kTxProtocols;
@@ -306,6 +362,15 @@ public:
GGWave(const Parameters & parameters);
~GGWave();
// set file stream for the internal ggwave logging
//
// By default, ggwave prints internal log messages to stderr.
// To disable logging all together, call this method with nullptr.
//
// Note: not thread-safe. Do not call while any GGWave instances are running
//
static void setLogFile(FILE * fptr);
static const Parameters & getDefaultParameters();
// set Tx data to encode
@@ -369,7 +434,7 @@ public:
// Tx
static TxProtocolId getDefaultTxProtocolId() { return GGWAVE_TX_PROTOCOL_AUDIBLE_FAST; }
static TxProtocolId getDefaultTxProtocolId() { return GGWAVE_TX_PROTOCOL_DT_NORMAL; }
static const TxProtocol & getDefaultTxProtocol() { return getTxProtocols().at(getDefaultTxProtocolId()); }
static const TxProtocol & getTxProtocol(int id) { return getTxProtocols().at(TxProtocolId(id)); }
static const TxProtocol & getTxProtocol(TxProtocolId id) { return getTxProtocols().at(id); }
@@ -388,6 +453,8 @@ public:
void setRxProtocols(const RxProtocols & rxProtocols) { m_rxProtocols = rxProtocols; }
const RxProtocols & getRxProtocols() const { return m_rxProtocols; }
int lastRxDataLength() const { return m_lastRxDataLength; }
const TxRxData & getRxData() const { return m_rxData; }
const RxProtocol & getRxProtocol() const { return m_rxProtocol; }
const RxProtocolId & getRxProtocolId() const { return m_rxProtocolId; }

View File

@@ -0,0 +1,60 @@
set(TARGET ggwave-mod)
add_library(${TARGET}
ggwave.cpp
resampler.cpp
)
target_include_directories(${TARGET} PUBLIC
.
../include
)
if (BUILD_SHARED_LIBS)
target_link_libraries(${TARGET} PUBLIC
${CMAKE_DL_LIBS}
)
target_compile_definitions(${TARGET} PUBLIC
GGWAVE_SHARED
)
endif()
if (MINGW)
target_link_libraries(${TARGET} PUBLIC
stdc++
)
endif()
if (EMSCRIPTEN)
set(TARGET libggwave-mod)
add_executable(${TARGET}
${PROJECT_SOURCE_DIR}/bindings/javascript/emscripten.cpp
)
target_link_libraries(${TARGET} PRIVATE
ggwave-mod
)
unset(EXTRA_FLAGS)
if (GGWAVE_WASM_SINGLE_FILE)
set(EXTRA_FLAGS "-s SINGLE_FILE=1")
message(STATUS "Embedding WASM inside ggwave-mod.js")
add_custom_command(
TARGET libggwave-mod POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_BINARY_DIR}/bin/libggwave-mod.js
${CMAKE_CURRENT_SOURCE_DIR}/ggwave-mod.js
)
endif()
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS " \
--bind \
-s MODULARIZE=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORT_NAME=\"'ggwave_factory'\" \
${EXTRA_FLAGS} \
")
endif()

File diff suppressed because one or more lines are too long

View File

@@ -11,12 +11,26 @@
#include <map>
//#include <random>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#define ggprintf(...) \
g_fptr && fprintf(g_fptr, __VA_ARGS__)
//
// C interface
//
namespace {
FILE * g_fptr = stderr;
std::map<ggwave_Instance, GGWave *> g_instances;
std::map<ggwave_Instance, GGWave::RxProtocols> g_rxProtocols;
}
extern "C"
void ggwave_setLogFile(void * fptr) {
GGWave::setLogFile((FILE *) fptr);
}
extern "C"
@@ -58,12 +72,12 @@ int ggwave_encode(
GGWave * ggWave = (GGWave *) g_instances[instance];
if (ggWave == nullptr) {
fprintf(stderr, "Invalid GGWave instance %d\n", instance);
ggprintf("Invalid GGWave instance %d\n", instance);
return -1;
}
if (ggWave->init(dataSize, dataBuffer, ggWave->getTxProtocol(txProtocolId), volume) == false) {
fprintf(stderr, "Failed to initialize GGWave instance %d\n", instance);
ggprintf("Failed to initialize GGWave instance %d\n", instance);
return -1;
}
@@ -85,7 +99,7 @@ int ggwave_encode(
};
if (ggWave->encode(cbWaveformOut) == false) {
fprintf(stderr, "Failed to encode data - GGWave instance %d\n", instance);
ggprintf("Failed to encode data - GGWave instance %d\n", instance);
return -1;
}
@@ -112,7 +126,7 @@ int ggwave_decode(
ggWave->decode(cbWaveformInp);
// todo : avoid allocation
// TODO : avoid allocation
GGWave::TxRxData rxData;
auto rxDataLength = ggWave->takeRxData(rxData);
@@ -120,12 +134,72 @@ int ggwave_decode(
// failed to decode message
return -1;
} else if (rxDataLength > 0) {
std::copy(rxData.begin(), rxData.end(), outputBuffer);
memcpy(outputBuffer, rxData.data(), rxDataLength);
}
return rxDataLength;
}
extern "C"
int ggwave_ndecode(
ggwave_Instance instance,
const char * dataBuffer,
int dataSize,
char * outputBuffer,
int outputSize) {
// TODO : avoid duplicated code
GGWave * ggWave = (GGWave *) g_instances[instance];
GGWave::CBWaveformInp cbWaveformInp = [&](void * data, uint32_t nMaxBytes) -> uint32_t {
uint32_t nCopied = std::min((uint32_t) dataSize, nMaxBytes);
std::copy(dataBuffer, dataBuffer + nCopied, (char *) data);
dataSize -= nCopied;
dataBuffer += nCopied;
return nCopied;
};
ggWave->decode(cbWaveformInp);
// TODO : avoid allocation
GGWave::TxRxData rxData;
auto rxDataLength = ggWave->takeRxData(rxData);
if (rxDataLength == -1) {
// failed to decode message
return -1;
} else if (rxDataLength > outputSize) {
// the outputBuffer is not big enough to store the data
return -2;
} else if (rxDataLength > 0) {
memcpy(outputBuffer, rxData.data(), rxDataLength);
}
return rxDataLength;
}
extern "C"
void ggwave_toggleRxProtocol(
ggwave_Instance instance,
ggwave_TxProtocolId rxProtocolId,
int state) {
// if never called - initialize with all available protocols
if (g_rxProtocols.find(instance) == g_rxProtocols.end()) {
g_rxProtocols[instance] = GGWave::getTxProtocols();
}
if (state == 0) {
// disable Rx protocol
g_rxProtocols[instance].erase(rxProtocolId);
} else if (state == 1) {
// enable Rx protocol
g_rxProtocols[instance][rxProtocolId] = GGWave::getTxProtocols().at(rxProtocolId);
}
g_instances[instance]->setRxProtocols(g_rxProtocols[instance]);
}
//
// C++ implementation
//
@@ -259,7 +333,7 @@ int bytesForSampleFormat(GGWave::SampleFormat sampleFormat) {
case GGWAVE_SAMPLE_FORMAT_F32: return sizeof(float); break;
};
fprintf(stderr, "Invalid sample format: %d\n", (int) sampleFormat);
ggprintf("Invalid sample format: %d\n", (int) sampleFormat);
return 0;
}
@@ -270,6 +344,10 @@ struct GGWave::Impl {
Resampler resampler;
};
void GGWave::setLogFile(FILE * fptr) {
g_fptr = fptr;
}
const GGWave::Parameters & GGWave::getDefaultParameters() {
static ggwave_Parameters result {
-1, // vaiable payload length
@@ -366,12 +444,12 @@ GGWave::GGWave(const Parameters & parameters) :
}
if (m_sampleRateInp < kSampleRateMin) {
fprintf(stderr, "Error: capture sample rate (%g Hz) must be >= %g Hz\n", m_sampleRateInp, kSampleRateMin);
ggprintf("Error: capture sample rate (%g Hz) must be >= %g Hz\n", m_sampleRateInp, kSampleRateMin);
throw std::runtime_error("Invalid capture/playback sample rate");
}
if (m_sampleRateInp > kSampleRateMax) {
fprintf(stderr, "Error: capture sample rate (%g Hz) must be <= %g Hz\n", m_sampleRateInp, kSampleRateMax);
ggprintf("Error: capture sample rate (%g Hz) must be <= %g Hz\n", m_sampleRateInp, kSampleRateMax);
throw std::runtime_error("Invalid capture/playback sample rate");
}
@@ -382,11 +460,11 @@ GGWave::~GGWave() {
}
bool GGWave::init(const std::string & text, const int volume) {
return init(text.size(), text.data(), getDefaultTxProtocol(), volume);
return init((int) 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);
return init((int) text.size(), text.data(), txProtocol, volume);
}
bool GGWave::init(int dataSize, const char * dataBuffer, const int volume) {
@@ -395,18 +473,18 @@ bool GGWave::init(int dataSize, const char * dataBuffer, 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);
ggprintf("Negative data size: %d\n", dataSize);
return false;
}
auto maxLength = m_isFixedPayloadLength ? m_payloadLength : kMaxLengthVarible;
if (dataSize > maxLength) {
fprintf(stderr, "Truncating data from %d to %d bytes\n", dataSize, maxLength);
ggprintf("Truncating data from %d to %d bytes\n", dataSize, maxLength);
dataSize = maxLength;
}
if (volume < 0 || volume > 100) {
fprintf(stderr, "Invalid volume: %d\n", volume);
ggprintf("Invalid volume: %d\n", volume);
return false;
}
@@ -481,7 +559,7 @@ uint32_t GGWave::encodeSize_samples() const {
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;
int totalDataFrames = 2*((totalBytes + m_txProtocol.bytesPerTx - 1)/m_txProtocol.bytesPerTx)*m_txProtocol.framesPerTx;
return (
m_nMarkerFrames + totalDataFrames + m_nMarkerFrames
@@ -737,14 +815,14 @@ void GGWave::decode(const CBWaveformInp & cbWaveformInp) {
}
if (nBytesRecorded % m_sampleSizeBytesInp != 0) {
fprintf(stderr, "Failure during capture - provided bytes (%d) are not multiple of sample size (%d)\n",
ggprintf("Failure during capture - provided bytes (%d) are not multiple of sample size (%d)\n",
nBytesRecorded, m_sampleSizeBytesInp);
m_samplesNeeded = m_samplesPerFrame;
break;
}
if (nBytesRecorded > nBytesNeeded) {
fprintf(stderr, "Failure during capture - more samples were provided (%d) than requested (%d)\n",
ggprintf("Failure during capture - more samples were provided (%d) than requested (%d)\n",
nBytesRecorded/m_sampleSizeBytesInp, nBytesNeeded/m_sampleSizeBytesInp);
m_samplesNeeded = m_samplesPerFrame;
break;
@@ -888,7 +966,7 @@ bool GGWave::takeRxAmplitude(AmplitudeData & dst) {
bool GGWave::computeFFTR(const float * src, float * dst, int N, float d) {
if (N > kMaxSamplesPerFrame) {
fprintf(stderr, "computeFFTR: N (%d) must be <= %d\n", N, GGWave::kMaxSamplesPerFrame);
ggprintf("computeFFTR: N (%d) must be <= %d\n", N, GGWave::kMaxSamplesPerFrame);
return false;
}
@@ -945,7 +1023,7 @@ void GGWave::decode_variable() {
}
if (m_analyzingData) {
fprintf(stderr, "Analyzing captured data ..\n");
ggprintf("Analyzing captured data ..\n");
auto tStart = std::chrono::high_resolution_clock::now();
const int stepsPerFrame = 16;
@@ -1054,8 +1132,8 @@ void GGWave::decode_variable() {
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());
ggprintf("Decoded length = %d, protocol = '%s' (%d)\n", decodedLength, rxProtocol.name, rxProtocolId);
ggprintf("Received sound data successfully: '%s'\n", s.c_str());
isValid = true;
m_hasNewRxData = true;
@@ -1078,7 +1156,7 @@ void GGWave::decode_variable() {
m_framesToRecord = 0;
if (isValid == false) {
fprintf(stderr, "Failed to capture sound data. Please try again (length = %d)\n", m_rxData[0]);
ggprintf("Failed to capture sound data. Please try again (length = %d)\n", m_rxData[0]);
m_lastRxDataLength = -1;
m_framesToRecord = -1;
}
@@ -1092,7 +1170,7 @@ void GGWave::decode_variable() {
m_framesLeftToAnalyze = 0;
auto tEnd = std::chrono::high_resolution_clock::now();
fprintf(stderr, "Time to analyze: %g ms\n", getTime_ms(tStart, tEnd));
ggprintf("Time to analyze: %g ms\n", getTime_ms(tStart, tEnd));
}
// check if receiving data
@@ -1131,7 +1209,7 @@ void GGWave::decode_variable() {
if (isReceiving) {
std::time_t timestamp = std::time(nullptr);
fprintf(stderr, "%sReceiving sound data ...\n", std::asctime(std::localtime(&timestamp)));
ggprintf("%sReceiving sound data ...\n", std::asctime(std::localtime(&timestamp)));
m_receivingData = true;
std::fill(m_rxData.begin(), m_rxData.end(), 0);
@@ -1180,7 +1258,7 @@ void GGWave::decode_variable() {
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(&timestamp)), m_framesLeftToRecord, m_recvDuration_frames);
ggprintf("%sReceived end marker. Frames left = %d, recorded = %d\n", std::asctime(std::localtime(&timestamp)), m_framesLeftToRecord, m_recvDuration_frames);
m_nMarkersSuccess = 0;
m_framesLeftToRecord = 1;
}
@@ -1321,10 +1399,7 @@ void GGWave::decode_fixed() {
if (rsData.Decode(m_txDataEncoded.data(), m_rxData.data()) == 0) {
if (m_rxData[0] != 0) {
std::time_t timestamp = std::time(nullptr);
std::string tstr = std::asctime(std::localtime(&timestamp));
tstr.back() = 0;
fprintf(stderr, "[%s] Received: '%s'\n", tstr.c_str(), m_rxData.data());
ggprintf("Received sound data successfully: '%s'\n", m_rxData.data());
isValid = true;
m_hasNewRxData = true;

View File

@@ -6,18 +6,15 @@ set(TARGET r2t2)
if (NOT EMSCRIPTEN)
add_executable(${TARGET}
main.cpp
ggwave-mod/src/ggwave.cpp
ggwave-mod/src/resampler.cpp
)
target_include_directories(${TARGET} PRIVATE
..
ggwave-mod/include
ggwave-mod/src
)
target_link_libraries(${TARGET} PRIVATE
ggwave-common
ggwave-mod
)
endif()
@@ -29,19 +26,16 @@ set(TARGET r2t2-rx)
if (NOT EMSCRIPTEN)
add_executable(${TARGET}
r2t2-rx.cpp
ggwave-mod/src/ggwave.cpp
ggwave-mod/src/resampler.cpp
)
target_include_directories(${TARGET} PRIVATE
..
ggwave-mod/include
ggwave-mod/src
${SDL2_INCLUDE_DIRS}
)
target_link_libraries(${TARGET} PRIVATE
ggwave-common
ggwave-mod
${SDL2_LIBRARIES}
)
else()
@@ -49,19 +43,16 @@ else()
add_executable(${TARGET}
r2t2-rx.cpp
ggwave-mod/src/ggwave.cpp
ggwave-mod/src/resampler.cpp
)
target_include_directories(${TARGET} PRIVATE
..
ggwave-mod/include
ggwave-mod/src
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/
)
target_link_libraries(${TARGET} PRIVATE
ggwave-common
ggwave-mod
)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/index-tmpl.html ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/index.html @ONLY)