Add start/stop tones and rotate WAV sessions
Some checks failed
Build and Push EVS Bridge Image / docker (push) Has been cancelled

This commit is contained in:
Kai
2026-02-13 15:53:45 +01:00
parent 72c0fa19c6
commit bd3b73e387
5 changed files with 316 additions and 37 deletions

View File

@@ -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
}