Add start/stop tones and rotate WAV sessions
Some checks failed
Build and Push EVS Bridge Image / docker (push) Has been cancelled
Some checks failed
Build and Push EVS Bridge Image / docker (push) Has been cancelled
This commit is contained in:
167
src/main.cpp
167
src/main.cpp
@@ -1,10 +1,18 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <ArduinoWebsockets.h>
|
||||
#include <math.h>
|
||||
#include "driver/i2s.h"
|
||||
#include "secrets.h"
|
||||
using namespace websockets;
|
||||
|
||||
static constexpr bool kDefaultStreamMode = EVS_DEFAULT_STREAM_MODE;
|
||||
static constexpr bool kSerialCommandEcho = EVS_SERIAL_COMMAND_ECHO;
|
||||
static constexpr bool kMicUseRightChannel = EVS_MIC_USE_RIGHT_CHANNEL;
|
||||
static constexpr int kMicS24ToS16Shift = EVS_MIC_S24_TO_S16_SHIFT;
|
||||
static constexpr float kMicGain = EVS_MIC_GAIN;
|
||||
static constexpr float kLoopbackMonitorGain = EVS_LOOPBACK_MONITOR_GAIN;
|
||||
|
||||
// ---------------------------
|
||||
// Project config
|
||||
// ---------------------------
|
||||
@@ -24,6 +32,8 @@ static constexpr int PWM_CHANNEL = 0;
|
||||
static constexpr uint32_t PWM_FREQ = 22050;
|
||||
static constexpr uint8_t PWM_RES_BITS = 8;
|
||||
static constexpr uint32_t SPEAKER_SAMPLE_RATE = 16000;
|
||||
static constexpr uint32_t MIC_TELEMETRY_INTERVAL_MS = 1000;
|
||||
static constexpr float PI_F = 3.14159265358979323846f;
|
||||
|
||||
// WiFi / WebSocket
|
||||
|
||||
@@ -40,6 +50,7 @@ static bool g_wsConnected = false;
|
||||
static uint32_t g_lastConnectTryMs = 0;
|
||||
static uint32_t g_nextOutUs = 0;
|
||||
static bool g_streamingActive = false;
|
||||
static uint32_t g_lastMicTelemetryMs = 0;
|
||||
|
||||
static constexpr size_t RX_SAMPLES_CAP = 16000;
|
||||
static int16_t g_rxSamples[RX_SAMPLES_CAP];
|
||||
@@ -52,7 +63,7 @@ static bool initMicI2s() {
|
||||
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
|
||||
.sample_rate = MIC_SAMPLE_RATE,
|
||||
.bits_per_sample = MIC_BITS,
|
||||
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||
.channel_format = kMicUseRightChannel ? I2S_CHANNEL_FMT_ONLY_RIGHT : I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||
.intr_alloc_flags = 0,
|
||||
.dma_buf_count = 8,
|
||||
@@ -129,6 +140,49 @@ static void enqueuePcmFrame(const int16_t* frame, size_t count) {
|
||||
}
|
||||
}
|
||||
|
||||
static void enqueueTone(uint16_t freqHz, uint16_t durationMs, int16_t amplitude) {
|
||||
if (freqHz == 0 || durationMs == 0) {
|
||||
return;
|
||||
}
|
||||
const uint32_t sampleCount = (uint32_t)SPEAKER_SAMPLE_RATE * durationMs / 1000U;
|
||||
const float phaseStep = 2.0f * PI_F * (float)freqHz / (float)SPEAKER_SAMPLE_RATE;
|
||||
float phase = 0.0f;
|
||||
for (uint32_t i = 0; i < sampleCount; ++i) {
|
||||
const float s = sinf(phase);
|
||||
const int16_t sample = (int16_t)(s * (float)amplitude);
|
||||
if (!enqueuePcmSample(sample)) {
|
||||
return;
|
||||
}
|
||||
phase += phaseStep;
|
||||
if (phase > 2.0f * PI_F) {
|
||||
phase -= 2.0f * PI_F;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void enqueueSilenceMs(uint16_t durationMs) {
|
||||
const uint32_t sampleCount = (uint32_t)SPEAKER_SAMPLE_RATE * durationMs / 1000U;
|
||||
for (uint32_t i = 0; i < sampleCount; ++i) {
|
||||
if (!enqueuePcmSample(0)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void playStartTone() {
|
||||
// Ascending double beep: A5 -> C6.
|
||||
enqueueTone(880, 90, 5000);
|
||||
enqueueSilenceMs(35);
|
||||
enqueueTone(1047, 110, 6000);
|
||||
}
|
||||
|
||||
static void playStopTone() {
|
||||
// Descending double beep: C6 -> A5.
|
||||
enqueueTone(1047, 90, 5000);
|
||||
enqueueSilenceMs(35);
|
||||
enqueueTone(880, 110, 6000);
|
||||
}
|
||||
|
||||
static void onWsMessageCallback(WebsocketsMessage message) {
|
||||
if (!message.isBinary()) {
|
||||
return;
|
||||
@@ -157,9 +211,13 @@ static void onWsEventCallback(WebsocketsEvent event, String) {
|
||||
if (g_mode == DeviceMode::StreamToServer && !g_streamingActive) {
|
||||
g_ws.send("{\"type\":\"start\"}");
|
||||
g_streamingActive = true;
|
||||
playStartTone();
|
||||
}
|
||||
Serial.println("WS connected");
|
||||
} else if (event == WebsocketsEvent::ConnectionClosed) {
|
||||
if (g_streamingActive) {
|
||||
playStopTone();
|
||||
}
|
||||
g_wsConnected = false;
|
||||
g_streamingActive = false;
|
||||
Serial.println("WS disconnected");
|
||||
@@ -206,11 +264,13 @@ static void setMode(DeviceMode mode) {
|
||||
if (g_mode == DeviceMode::StreamToServer && g_wsConnected && g_streamingActive) {
|
||||
g_ws.send("{\"type\":\"stop\"}");
|
||||
g_streamingActive = false;
|
||||
playStopTone();
|
||||
}
|
||||
g_mode = mode;
|
||||
if (g_mode == DeviceMode::StreamToServer && g_wsConnected && !g_streamingActive) {
|
||||
g_ws.send("{\"type\":\"start\"}");
|
||||
g_streamingActive = true;
|
||||
playStartTone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,19 +282,71 @@ static void handleFrameForServer(const int16_t* frame, size_t count) {
|
||||
g_ws.sendBinary(reinterpret_cast<const char*>(frame), count * sizeof(int16_t));
|
||||
}
|
||||
|
||||
static void serviceSpeaker() {
|
||||
const uint32_t periodUs = 1000000UL / SPEAKER_SAMPLE_RATE;
|
||||
const uint32_t now = micros();
|
||||
if ((int32_t)(now - g_nextOutUs) < 0) {
|
||||
static void publishMicTelemetryIfDue(const int16_t* frame, size_t count) {
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
g_nextOutUs += periodUs;
|
||||
|
||||
int16_t s = 0;
|
||||
if (dequeuePcmSample(s)) {
|
||||
pwmWrite(pcm16ToPwm8(s));
|
||||
} else {
|
||||
pwmWrite(128);
|
||||
int16_t peak = 0;
|
||||
int64_t sum_abs = 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
int16_t v = frame[i];
|
||||
int16_t av = (v < 0) ? (int16_t)(-v) : v;
|
||||
if (av > peak) peak = av;
|
||||
sum_abs += av;
|
||||
}
|
||||
const uint32_t avg_abs = (uint32_t)(sum_abs / (int64_t)count);
|
||||
|
||||
const uint32_t now = millis();
|
||||
if ((now - g_lastMicTelemetryMs) < MIC_TELEMETRY_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
g_lastMicTelemetryMs = now;
|
||||
|
||||
Serial.print("MIC peak=");
|
||||
Serial.print(peak);
|
||||
Serial.print(" avg_abs=");
|
||||
Serial.print(avg_abs);
|
||||
Serial.print(" ws=");
|
||||
Serial.println(g_wsConnected ? "connected" : "disconnected");
|
||||
|
||||
if (g_wsConnected) {
|
||||
String msg = "{\"type\":\"mic_level\",\"peak\":";
|
||||
msg += String(peak);
|
||||
msg += ",\"avg_abs\":";
|
||||
msg += String(avg_abs);
|
||||
msg += ",\"samples\":";
|
||||
msg += String((unsigned)count);
|
||||
msg += "}";
|
||||
g_ws.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static inline int16_t convertMicSampleToPcm16(int32_t raw32) {
|
||||
// INMP441 uses 24-bit signed samples packed into 32-bit I2S slots.
|
||||
int32_t s24 = raw32 >> 8;
|
||||
int32_t s16 = s24 >> kMicS24ToS16Shift;
|
||||
float scaled = (float)s16 * kMicGain;
|
||||
if (scaled > 32767.0f) scaled = 32767.0f;
|
||||
if (scaled < -32768.0f) scaled = -32768.0f;
|
||||
return (int16_t)scaled;
|
||||
}
|
||||
|
||||
static void serviceSpeaker() {
|
||||
const uint32_t periodUs = 1000000UL / SPEAKER_SAMPLE_RATE;
|
||||
uint32_t now = micros();
|
||||
int processed = 0;
|
||||
// Catch up if we lagged behind, but cap work per loop iteration.
|
||||
while ((int32_t)(now - g_nextOutUs) >= 0 && processed < 8) {
|
||||
g_nextOutUs += periodUs;
|
||||
int16_t s = 0;
|
||||
if (dequeuePcmSample(s)) {
|
||||
pwmWrite(pcm16ToPwm8(s));
|
||||
} else {
|
||||
pwmWrite(128);
|
||||
}
|
||||
now = micros();
|
||||
++processed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,15 +354,21 @@ static void printHelp() {
|
||||
Serial.println();
|
||||
Serial.println("Commands:");
|
||||
Serial.println(" i = idle");
|
||||
Serial.println(" s = stream mode (stub)");
|
||||
Serial.println(" s = stream mode");
|
||||
Serial.println(" l = local loopback mode");
|
||||
Serial.println(" p = print network status");
|
||||
Serial.println(" h = help");
|
||||
Serial.print("Default on boot: ");
|
||||
Serial.println(kDefaultStreamMode ? "StreamToServer" : "LocalLoopback");
|
||||
}
|
||||
|
||||
static void handleSerialCommands() {
|
||||
while (Serial.available()) {
|
||||
const char c = (char)Serial.read();
|
||||
if (kSerialCommandEcho && c != '\r' && c != '\n') {
|
||||
Serial.print("RX cmd: ");
|
||||
Serial.println(c);
|
||||
}
|
||||
if (c == 'i') {
|
||||
setMode(DeviceMode::Idle);
|
||||
Serial.println("Mode -> Idle");
|
||||
@@ -269,6 +387,8 @@ static void handleSerialCommands() {
|
||||
Serial.println(g_wsConnected ? "connected" : "disconnected");
|
||||
} else if (c == 'h') {
|
||||
printHelp();
|
||||
} else if (c != '\r' && c != '\n') {
|
||||
Serial.println("Unknown command");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,7 +411,11 @@ void setup() {
|
||||
g_ws.onEvent(onWsEventCallback);
|
||||
g_nextOutUs = micros();
|
||||
|
||||
setMode(DeviceMode::LocalLoopback);
|
||||
if (kDefaultStreamMode) {
|
||||
setMode(DeviceMode::StreamToServer);
|
||||
} else {
|
||||
setMode(DeviceMode::LocalLoopback);
|
||||
}
|
||||
Serial.println("Audio init ok");
|
||||
Serial.println("Set local environment values in include/secrets.h");
|
||||
printHelp();
|
||||
@@ -299,8 +423,10 @@ void setup() {
|
||||
|
||||
void loop() {
|
||||
handleSerialCommands();
|
||||
ensureConnectivity();
|
||||
g_ws.poll();
|
||||
if (g_mode != DeviceMode::LocalLoopback) {
|
||||
ensureConnectivity();
|
||||
g_ws.poll();
|
||||
}
|
||||
serviceSpeaker();
|
||||
|
||||
size_t bytesRead = 0;
|
||||
@@ -313,14 +439,19 @@ void loop() {
|
||||
const size_t sampleCount = bytesRead / sizeof(int32_t);
|
||||
static int16_t pcm16[MIC_FRAME_SAMPLES];
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
// INMP441 delivers meaningful data in the high bits for 32-bit slot formats.
|
||||
pcm16[i] = (int16_t)(g_micBuffer[i] >> 14);
|
||||
pcm16[i] = convertMicSampleToPcm16(g_micBuffer[i]);
|
||||
}
|
||||
|
||||
if (g_mode == DeviceMode::StreamToServer) {
|
||||
handleFrameForServer(pcm16, sampleCount);
|
||||
publishMicTelemetryIfDue(pcm16, sampleCount);
|
||||
} else if (g_mode == DeviceMode::LocalLoopback) {
|
||||
enqueuePcmFrame(pcm16, sampleCount);
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
float v = (float)pcm16[i] * kLoopbackMonitorGain;
|
||||
if (v > 32767.0f) v = 32767.0f;
|
||||
if (v < -32768.0f) v = -32768.0f;
|
||||
enqueuePcmSample((int16_t)v);
|
||||
}
|
||||
} else {
|
||||
// idle
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user