audio tryout
This commit is contained in:
0
Software/UPSoftware/.codex
Normal file
0
Software/UPSoftware/.codex
Normal file
@@ -1,3 +1,4 @@
|
||||
#include "HardwareSerial.h"
|
||||
#include <iterator>
|
||||
#include "esp32-hal-gpio.h"
|
||||
#include "MeyCan.h";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,79 +1,220 @@
|
||||
#include <Arduino.h>
|
||||
#include <driver/i2s.h>
|
||||
#include <MP3DecoderHelix.h>
|
||||
#include "positive_sound_mp3.h"
|
||||
#include "opus.h"
|
||||
#include "opus_data.h"
|
||||
#include "MeyCan.h"
|
||||
|
||||
using namespace libhelix;
|
||||
|
||||
constexpr i2s_port_t I2S_PORT = I2S_NUM_0;
|
||||
constexpr int I2S_BCLK = 6;
|
||||
constexpr int I2S_LRC = 7;
|
||||
constexpr int I2S_DIN = 5;
|
||||
constexpr float OUTPUT_GAIN = 0.35f;
|
||||
constexpr int RX_PIN = 2; // CAN_TRANCEIVER_RX_PIN
|
||||
constexpr int TX_PIN = 3; // CAN_TRANCEIVER_TX_PIN
|
||||
bool driver_installed = false;
|
||||
|
||||
MP3DecoderHelix mp3;
|
||||
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.0–1.0)
|
||||
|
||||
bool i2s_initialized = false;
|
||||
int current_sample_rate = 0;
|
||||
int current_channels = 0;
|
||||
static OpusDecoder *dec = nullptr;
|
||||
static int16_t pcm[MAX_FRAME];
|
||||
static int16_t stereo[MAX_FRAME * 2];
|
||||
|
||||
void configureI2s(int sample_rate, int channels) {
|
||||
if (!i2s_initialized) {
|
||||
i2s_config_t i2s_config = {};
|
||||
i2s_config.mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_TX);
|
||||
i2s_config.sample_rate = sample_rate;
|
||||
i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
|
||||
i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
|
||||
i2s_config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
|
||||
i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1;
|
||||
i2s_config.dma_buf_count = 8;
|
||||
i2s_config.dma_buf_len = 256;
|
||||
i2s_config.use_apll = false;
|
||||
i2s_config.tx_desc_auto_clear = true;
|
||||
i2s_config.fixed_mclk = 0;
|
||||
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 pin_config = {};
|
||||
pin_config.bck_io_num = I2S_BCLK;
|
||||
pin_config.ws_io_num = I2S_LRC;
|
||||
pin_config.data_out_num = I2S_DIN;
|
||||
pin_config.data_in_num = I2S_PIN_NO_CHANGE;
|
||||
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, &i2s_config, 0, nullptr);
|
||||
i2s_set_pin(I2S_PORT, &pin_config);
|
||||
i2s_driver_install(I2S_PORT, &cfg, 0, nullptr);
|
||||
i2s_set_pin(I2S_PORT, &pins);
|
||||
i2s_zero_dma_buffer(I2S_PORT);
|
||||
i2s_initialized = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (sample_rate != current_sample_rate || channels != current_channels) {
|
||||
i2s_channel_t channel_mode =
|
||||
channels == 1 ? I2S_CHANNEL_MONO : I2S_CHANNEL_STEREO;
|
||||
i2s_set_clk(I2S_PORT, sample_rate, I2S_BITS_PER_SAMPLE_16BIT, channel_mode);
|
||||
current_sample_rate = sample_rate;
|
||||
current_channels = channels;
|
||||
size_t written;
|
||||
i2s_write(I2S_PORT, stereo, samples * 2 * sizeof(int16_t), &written, portMAX_DELAY);
|
||||
CheckMeyPinsTriggered();
|
||||
}
|
||||
|
||||
void DebugBlink(int d) {
|
||||
pinMode(20, OUTPUT);
|
||||
while (true) {
|
||||
|
||||
digitalWrite(20, HIGH);
|
||||
delay(d);
|
||||
digitalWrite(20, LOW);
|
||||
delay(d);
|
||||
}
|
||||
}
|
||||
|
||||
void audioDataCallback(MP3FrameInfo &info, int16_t *pcm_buffer, size_t len, void *) {
|
||||
configureI2s(info.samprate, info.nChans);
|
||||
void ConfigureAndSetupMeyCan() {
|
||||
// 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_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();
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
pcm_buffer[i] = static_cast<int16_t>(pcm_buffer[i] * OUTPUT_GAIN);
|
||||
if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) {
|
||||
Serial.println("Driver installed");
|
||||
} else {
|
||||
DebugBlink(100);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytes_written = 0;
|
||||
i2s_write(I2S_PORT, pcm_buffer, len * sizeof(int16_t), &bytes_written,
|
||||
portMAX_DELAY);
|
||||
esp_err_t e = twai_start();
|
||||
// Start TWAI driver
|
||||
if (e == ESP_OK) {
|
||||
driver_installed = true;
|
||||
} else {
|
||||
DebugBlink(500);
|
||||
return;
|
||||
}
|
||||
|
||||
SetDevicedId(0x09, 0x09);
|
||||
|
||||
SetMeyPin(1, 5);
|
||||
SetMeyPin(2, 6);
|
||||
SetMeyPin(3, 7);
|
||||
SetMeyPin(4, 8);
|
||||
|
||||
SetupMeyCan(8, 1, 3);
|
||||
|
||||
}
|
||||
|
||||
// Ogg-Container-Format:
|
||||
// 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 6–13 : Granule Position (Zeitstempel, 8 Bytes)
|
||||
// Byte 14–17 : Stream Serial Number
|
||||
// Byte 18–21 : Page Sequence Number
|
||||
// Byte 22–25 : 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 (0–255).
|
||||
// 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)
|
||||
|
||||
while (pos + 27 <= buf_len) {
|
||||
|
||||
// --- 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);
|
||||
mp3.setDataCallback(audioDataCallback);
|
||||
mp3.begin();
|
||||
Serial.println("MP3 playback started");
|
||||
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;
|
||||
}
|
||||
Serial.println("Opus playback started");
|
||||
ConfigureAndSetupMeyCan();
|
||||
|
||||
xTaskCreate(canTaskFunc, "can", 4096, nullptr, 5, nullptr);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mp3.write(positive_sound_short_mp3, positive_sound_short_mp3_len);
|
||||
mp3.begin();
|
||||
delay(20);
|
||||
play_ogg_opus(music_opus, music_opus_len);
|
||||
opus_decoder_ctl(dec, OPUS_RESET_STATE);
|
||||
}
|
||||
|
||||
BIN
Software/UPSoftware/arduino-libhelix-main.zip
Normal file
BIN
Software/UPSoftware/arduino-libhelix-main.zip
Normal file
Binary file not shown.
71009
Software/UPSoftware/opus_data.h
Normal file
71009
Software/UPSoftware/opus_data.h
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user