Compare commits

...

3 Commits

Author SHA1 Message Date
Martin Linkwitz - NUC
f8547c9ff3 audio tryout 2026-05-17 15:11:32 +02:00
Martin Linkwitz - NUC
75672d4ec4 Add ESP32-C3 Opus loopback example 2026-04-08 16:32:11 +02:00
Martin Linkwitz - NUC
3551b073d7 Add embedded MP3 playback test sketch 2026-04-08 16:30:34 +02:00
11 changed files with 124748 additions and 297 deletions

View File

View File

@@ -1,3 +1,4 @@
#include "HardwareSerial.h"
#include <iterator> #include <iterator>
#include "esp32-hal-gpio.h" #include "esp32-hal-gpio.h"
#include "MeyCan.h"; #include "MeyCan.h";

View File

@@ -1,104 +0,0 @@
#include "MeyRule.h"
#include "MeyCan.h"
#include <Arduino.h>
#include <mcp2515.h>;
RemotePinInfo remotePinInfo = RemotePinInfo();
Rule *rules = NULL;
void PutRule(Rule *rule)
{
if (rules == NULL)
rules = rule;
else
rules->AddRule(rule);
}
void AddSimple(uint16_t sourceDevId, byte sourceMeyPinId, uint16_t targetDevId, byte targetMeyPinId)
{
Rule *rule = new Rule();
rule->sourceDevId = sourceDevId;
rule->sourceMeyPinId = sourceMeyPinId;
rule->targetDevId = targetDevId;
rule->targetMeyPinId = targetMeyPinId;
rule->toggle = false;
rule->inverse = false;
PutRule(rule);
}
void AddToggle(uint16_t sourceDevId, byte sourceMeyPinId, uint16_t targetDevId, byte targetMeyPinId)
{
Rule *rule = new Rule();
rule->sourceDevId = sourceDevId;
rule->sourceMeyPinId = sourceMeyPinId;
rule->targetDevId = targetDevId;
rule->targetMeyPinId = targetMeyPinId;
rule->toggle = true;
rule->inverse = false;
PutRule(rule);
}
void AddToggleInverse(uint16_t sourceDevId, byte sourceMeyPinId, uint16_t targetDevId, byte targetMeyPinId)
{
Rule *rule = new Rule();
rule->sourceDevId = sourceDevId;
rule->sourceMeyPinId = sourceMeyPinId;
rule->targetDevId = targetDevId;
rule->targetMeyPinId = targetMeyPinId;
rule->toggle = true;
rule->inverse = true;
PutRule(rule);
}
void CheckRule(uint16_t deviceId, uint8_t dt, uint8_t state, Rule *rule)
{
RemotePinInfo *currentPinState = remotePinInfo.FindOrAdd(rule->targetDevId);
if (currentPinState == NULL) return;
bool pinState = state > 0;
if (rule->inverse)
pinState = !pinState;
if (rule->toggle)
pinState = (currentPinState->getPinState(rule->targetMeyPinId) ^ true);
BroadcastTriggerMeyPinCanPackage(rule->targetDevId, rule->targetMeyPinId, pinState);
currentPinState->setPinState(rule->targetMeyPinId, pinState);
}
void HandleTriggered(can_frame *frame)
{
if (GetPackageType(frame->can_id) == SWITCH_TRIGGERED_CAN_ID)
{
RemotePinInfo *currentPinState = remotePinInfo.FindOrAdd(GetDeviceId(frame->can_id) );
if (currentPinState == NULL)
return;
currentPinState->setPinState(frame->data[0], frame->data[1]);
}
}
void HandleRules(can_frame *frame)
{
HandleTriggered(frame);
if (rules == NULL) return;
if (GetPackageType(frame->can_id) == SWITCH_TRIGGERED_CAN_ID)
{
uint16_t deviceId = GetDeviceId(frame->can_id);
uint8_t dt = frame->data[1];
uint8_t state = frame->data[0];
rules->Traverse(deviceId, dt, state, CheckRule);
}
}

View File

@@ -1,111 +0,0 @@
#ifndef MEYRULE_H
#define MEYRULE_H
#include <Arduino.h>
#include <mcp2515.h>;
struct Rule
{
uint16_t sourceDevId;
byte sourceMeyPinId;
uint16_t targetDevId;
byte targetMeyPinId;
bool toggle;
bool inverse;
Rule *nextRule = NULL;
void AddRule(Rule *rule)
{
if (this->nextRule == NULL)
{
this->nextRule = rule;
rule->nextRule = NULL;
} else {
this->nextRule->AddRule(rule);
}
}
void Traverse( uint16_t deviceId, uint8_t dt, uint8_t state, void (*handle)(uint16_t, uint8_t, uint8_t, Rule*))
{
if ( this->sourceDevId == deviceId && this->sourceMeyPinId == state)
handle(deviceId, dt, state, this);
if (this->nextRule != NULL)
this->nextRule->Traverse(deviceId, dt, state, handle);
}
};
typedef struct RemotePinInfo
{
const byte MAX_REMOTE_PIN_COUNT = 64;
uint16_t DeviceId = 0; // the id of the device
uint8_t pinState = 0; // bitmap of 8 MeyPin states of the device. 0000 0100, MeyPin #3 is HIGH in this example
RemotePinInfo *next = NULL;
bool getPinState(byte meyPin)
{
return (this->pinState >> (meyPin - 1)) & 1;
}
void setPinState(byte meyPin, bool state)
{
if (state)
this->pinState = this->pinState | (1 << (meyPin - 1)); // 0001 0000
else
this->pinState = this->pinState & (~(1 << (meyPin - 1))); // 1110 1111 -> not
}
int16_t Count()
{
if (this->next == NULL) return 1;
return this->next->Count() + 1;
}
RemotePinInfo* FindOrAdd(uint16_t deviceId, byte count = 0)
{
if (count > MAX_REMOTE_PIN_COUNT)
return NULL;
if (this->DeviceId == 0 && this->pinState == 0)
{
this->DeviceId = deviceId;
this->pinState = 0;
}
if (this->DeviceId == deviceId)
{
//ToggleDebug();
return this;
}
if (next != NULL)
{
return next->FindOrAdd(deviceId, count + 1);
}
RemotePinInfo *theNext = new RemotePinInfo;
theNext->DeviceId = deviceId;
theNext->pinState = 0;
theNext->next = NULL;
this->next = theNext;
return this->next;
}
};
extern RemotePinInfo remotePinInfo;
extern Rule *rules;
void AddSimple(uint16_t sourceDevId, byte sourceMeyPinId, uint16_t targetDevId, byte targetMeyPinId);
void AddToggle(uint16_t sourceDevId, byte sourceMeyPinId, uint16_t targetDevId, byte targetMeyPinId);
void AddToggleInverse(uint16_t sourceDevId, byte sourceMeyPinId, uint16_t targetDevId, byte targetMeyPinId);
void HandleRules(can_frame *frame);
void PutRule(Rule *rule);
void CheckRule(uint16_t deviceId, uint8_t dt, uint8_t state, Rule *rule);
#endif

View File

@@ -1,11 +1,65 @@
#include <Arduino.h> #include <Arduino.h>
#include "driver/twai.h" #include <driver/i2s.h>
#include "opus.h"
#include "opus_data.h"
#include "MeyCan.h" #include "MeyCan.h"
// https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/TWAI/TWAIreceive/TWAIreceive.ino
#define RX_PIN 2 constexpr int RX_PIN = 2; // CAN_TRANCEIVER_RX_PIN
#define TX_PIN 3 constexpr int TX_PIN = 3; // CAN_TRANCEIVER_TX_PIN
bool driver_installed = false;
constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S-Peripherie-Instanz (0 = erster Controller)
constexpr int I2S_DIN = 9; // GPIO-Pin für serielle Datenleitug (Data-Out → DAC)
constexpr int I2S_BCLK = 10; // GPIO-Pin für Bit-Clock (BCLK)
constexpr int I2S_LRC = 20; // GPIO-Pin für Word-Select / Left-Right-Clock
constexpr int SAMPLE_RATE = 48000; // Abtastrate in Hz (Opus-Standard: 48 kHz)
constexpr int CHANNELS = 1; // Anzahl Audiokanäle (1 = Mono)
constexpr int MAX_FRAME = 5760; // Maximale Samples pro Opus-Frame (120 ms @ 48 kHz)
float OUTPUT_GAIN = 0.02f; // Lautstärkeskalierung vor I2S-Ausgabe (0.01.0)
static OpusDecoder *dec = nullptr;
static int16_t pcm[MAX_FRAME];
static int16_t stereo[MAX_FRAME * 2];
static void i2s_setup() {
i2s_config_t cfg = {};
cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
cfg.sample_rate = SAMPLE_RATE;
cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
cfg.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
cfg.communication_format = I2S_COMM_FORMAT_STAND_I2S;
cfg.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1;
cfg.dma_buf_count = 8;
cfg.dma_buf_len = 512;
cfg.tx_desc_auto_clear = true;
i2s_pin_config_t pins = {};
pins.bck_io_num = I2S_BCLK;
pins.ws_io_num = I2S_LRC;
pins.data_out_num = I2S_DIN;
pins.data_in_num = I2S_PIN_NO_CHANGE;
i2s_driver_install(I2S_PORT, &cfg, 0, nullptr);
i2s_set_pin(I2S_PORT, &pins);
i2s_zero_dma_buffer(I2S_PORT);
}
static void decode_packet(const uint8_t *pkt, size_t len) {
int samples = opus_decode(dec, pkt, (opus_int32)len, pcm, MAX_FRAME, 0);
if (samples <= 0) return;
// Mono → Stereo mit Gain
for (int i = 0; i < samples; i++) {
int16_t s = (int16_t)(pcm[i] * OUTPUT_GAIN);
stereo[i * 2] = s;
stereo[i * 2 + 1] = s;
}
size_t written;
i2s_write(I2S_PORT, stereo, samples * 2 * sizeof(int16_t), &written, portMAX_DELAY);
CheckMeyPinsTriggered();
}
void DebugBlink(int d) { void DebugBlink(int d) {
pinMode(20, OUTPUT); pinMode(20, OUTPUT);
@@ -18,21 +72,12 @@ void DebugBlink(int d) {
} }
} }
bool driver_installed = false; void ConfigureAndSetupMeyCan() {
void setup() {
Serial.begin(9600);
SPI.begin();
// Explicit GND for LED and Input
pinMode(21, OUTPUT);
digitalWrite(21, LOW);
// Initialize configuration structures using macro initializers // Initialize configuration structures using macro initializers
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)TX_PIN, (gpio_num_t)RX_PIN, TWAI_MODE_NORMAL); twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)TX_PIN, (gpio_num_t)RX_PIN, TWAI_MODE_NORMAL);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_1MBITS(); //Look in the api-reference for other speed sets. twai_timing_config_t t_config = TWAI_TIMING_CONFIG_1MBITS(); //Look in the api-reference for other speed sets.
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) { if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) {
Serial.println("Driver installed"); Serial.println("Driver installed");
} else { } else {
@@ -40,8 +85,6 @@ void setup() {
return; return;
} }
esp_err_t e = twai_start(); esp_err_t e = twai_start();
// Start TWAI driver // Start TWAI driver
if (e == ESP_OK) { if (e == ESP_OK) {
@@ -51,32 +94,127 @@ void setup() {
return; return;
} }
SetDevicedId(0x09, 0x09);
SetDevicedId(0x05, 0x1F);
SetMeyPin(1, 5); SetMeyPin(1, 5);
SetMeyPin(2, 6); SetMeyPin(2, 6);
SetMeyPin(3, 7); SetMeyPin(3, 7);
SetMeyPin(4, 8); SetMeyPin(4, 8);
SetMeyPin(5, 9);
SetMeyPin(6, 10);
SetMeyPin(7, 20);
SetupMeyCan(8, 1, 3); SetupMeyCan(8, 1, 3);
} }
twai_message_t frame; // Ogg-Container-Format:
void loop() { // Der Stream besteht aus aufeinanderfolgenden "Pages" (Seiten).
// Jede Page beginnt mit dem 4-Byte-Capture-Pattern "OggS".
// Der Page-Header ist immer mindestens 27 Bytes lang:
// Byte 0 3: "OggS" (Capture Pattern)
// Byte 4 : Stream-Struktur-Version (immer 0)
// Byte 5 : Header-Type-Flag (0=normal, 1=continued, 2=first, 4=last)
// Byte 613 : Granule Position (Zeitstempel, 8 Bytes)
// Byte 1417 : Stream Serial Number
// Byte 1821 : Page Sequence Number
// Byte 2225 : CRC Checksum
// Byte 26 : Anzahl der Segmente (num_segs)
// Danach folgt die Segment-Tabelle (num_segs Bytes),
// und anschließend die eigentlichen Nutzdaten.
//
// Ogg-Lacing-Regel für Paketgrenzen:
// Jeder Eintrag in der Segment-Tabelle gibt die Größe eines Segments an (0255).
// Ist ein Segment genau 255 Bytes → das Paket geht weiter (nächstes Segment gehört dazu).
// Ist ein Segment < 255 Bytes → das Paket endet hier.
// So können Pakete größer als 255 Bytes über mehrere Segmente verteilt sein.
//
// Bei Opus in Ogg sind die ersten zwei Pages immer Metadaten:
// Page 0: OpusHead (Samplerate, Kanalanzahl, Preskip …)
// Page 1: OpusTags (Künstler, Titel, Encoder …)
// Ab Page 2 folgen die eigentlichen Audio-Pakete (je ~20 ms Opus-Frame).
static void play_ogg_opus(const uint8_t *buf, size_t buf_len) {
size_t pos = 0; // aktuelle Leseposition im Buffer
int page_n = 0; // Page-Zähler (0 und 1 sind Header-Pages)
if (!driver_installed) { while (pos + 27 <= buf_len) {
// Driver not installed
DebugBlink(2000); // --- 1. Capture Pattern "OggS" suchen ---
// Falls pos nicht direkt auf eine Page zeigt, byte-weise vorwärts suchen.
if (!(buf[pos] == 'O' && buf[pos + 1] == 'g' && buf[pos + 2] == 'g' && buf[pos + 3] == 'S')) {
pos++;
continue;
}
// --- 2. Page-Header lesen ---
uint8_t num_segs = buf[pos + 26]; // Anzahl Segmente aus Byte 26
size_t hdr_end = pos + 27 + num_segs; // Ende des Headers (= Beginn der Nutzdaten)
if (hdr_end > buf_len) break;
// Gesamtgröße der Nutzdaten = Summe aller Segmentlängen
size_t data_size = 0;
for (int s = 0; s < num_segs; s++) data_size += buf[pos + 27 + s];
// --- 3. Audio-Pages verarbeiten (Page 0+1 sind Metadaten → überspringen) ---
if (page_n >= 2) {
size_t pkt_start = hdr_end; // Startposition des aktuellen Pakets
size_t pkt_size = 0; // akkumulierte Paketgröße über Segmente
for (int s = 0; s < num_segs; s++) {
uint8_t seg = buf[pos + 27 + s]; // Größe dieses Segments
pkt_size += seg;
if (seg < 255) {
// Paketende erreicht → vollständiges Opus-Paket dekodieren und ausgeben
decode_packet(buf + pkt_start, pkt_size);
pkt_start += pkt_size; // nächstes Paket beginnt direkt dahinter
pkt_size = 0;
}
// seg == 255 → Paket geht im nächsten Segment weiter (Lacing)
}
}
// --- 4. Zur nächsten Page springen ---
pos = hdr_end + data_size;
page_n++;
}
}
int changeVolume = 0;
static void canTaskFunc(void *) {
twai_message_t frame;
for (;;) {
// blockiert ohne CPU-Last; der interne TWAI-Interrupt weckt den Task sobald ein Frame ankommt
if (twai_receive(&frame, portMAX_DELAY) == ESP_OK) {
HandleFrame(&frame);
if (changeVolume > 20)
changeVolume = 0;
OUTPUT_GAIN = 0.1f + changeVolume*0.1f;
changeVolume++;
}
}
}
void setup() {
Serial.begin(115200);
i2s_setup();
// Explicit GND for LED and Input
pinMode(21, OUTPUT);
digitalWrite(21, LOW);
int err;
dec = opus_decoder_create(SAMPLE_RATE, CHANNELS, &err);
if (err != OPUS_OK) {
Serial.printf("opus_decoder_create failed: %d\n", err);
return; return;
} }
Serial.println("Opus playback started");
ConfigureAndSetupMeyCan();
CheckMeyPinsTriggered(); xTaskCreate(canTaskFunc, "can", 4096, nullptr, 5, nullptr);
if (twai_receive(&frame, 0) == ESP_OK) { }
HandleFrame(&frame); void loop() {
} play_ogg_opus(music_opus, music_opus_len);
opus_decoder_ctl(dec, OPUS_RESET_STATE);
} }

Binary file not shown.

View File

@@ -0,0 +1,56 @@
#include <Arduino.h>
#include <AudioTools.h>
#include <AudioTools/AudioCodecs/CodecOpus.h>
using namespace audio_tools;
constexpr int I2S_BCLK = 6;
constexpr int I2S_LRC = 7;
constexpr int I2S_DIN = 5;
// Keep the test intentionally light so it is realistic for an ESP32-C3.
AudioInfo audioInfo(16000, 1, 16);
SineWaveGenerator<int16_t> sineWave(10000);
GeneratedSoundStream<int16_t> source(sineWave);
I2SStream i2s;
OpusAudioEncoder opusEncoder;
OpusAudioDecoder opusDecoder;
EncodedAudioStream decodedAudio(&i2s, &opusDecoder);
EncodedAudioStream encodedAudio(&decodedAudio, &opusEncoder);
StreamCopy copier(encodedAudio, source);
void setup() {
Serial.begin(115200);
AudioToolsLogger.begin(Serial, AudioToolsLogLevel::Warning);
auto i2sConfig = i2s.defaultConfig(TX_MODE);
i2sConfig.copyFrom(audioInfo);
i2sConfig.pin_bck = I2S_BCLK;
i2sConfig.pin_ws = I2S_LRC;
i2sConfig.pin_data = I2S_DIN;
i2sConfig.buffer_count = 8;
i2sConfig.buffer_size = 256;
i2s.begin(i2sConfig);
auto sineConfig = sineWave.defaultConfig();
sineConfig.copyFrom(audioInfo);
sineWave.begin(sineConfig, N_B4);
auto &encoderConfig = opusEncoder.config();
encoderConfig.copyFrom(audioInfo);
encoderConfig.application = OPUS_APPLICATION_RESTRICTED_LOWDELAY;
encoderConfig.bitrate = 24000;
encoderConfig.complexity = 1;
encoderConfig.frame_sizes_ms_x2 = OPUS_FRAMESIZE_20_MS;
decodedAudio.begin(audioInfo);
encodedAudio.begin(audioInfo);
Serial.println("Opus loopback test started");
}
void loop() {
if (copier.copy() == 0) {
delay(1);
}
}

View File

@@ -0,0 +1,26 @@
This sketch is a minimal ESP32-C3 Opus codec smoke test.
What it does:
- Generates a sine wave locally
- Encodes it with Opus
- Decodes it immediately again
- Sends the decoded PCM to I2S
Why this example:
- It tests the raw Opus codec path without Ogg or CAN framing
- It is closer to the later Pi -> CAN -> ESP32 decoder path than MP3
- It keeps CPU load down by using 16 kHz mono and low encoder complexity
Expected hardware:
- ESP32-C3
- I2S DAC / amp on:
- BCLK = GPIO 6
- LRCK = GPIO 7
- DIN = GPIO 5
Required Arduino libraries:
- `arduino-audio-tools`
- `arduino-libopus`
If this sketch runs and outputs a stable sine tone, the basic Opus encode/decode
chain is working on the ESP32-C3.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff