added sync frames every 512 bytes

master
jake 2025-11-15 15:57:17 +00:00
parent a58f026d35
commit 5ec348c27b
12 changed files with 312 additions and 32 deletions

Binary file not shown.

BIN
dump.bin Normal file

Binary file not shown.

70
firmware/sender.ino Normal file
View File

@ -0,0 +1,70 @@
#include <Arduino.h>
#include "driver/i2s.h"
#define I2S_PORT I2S_NUM_0
#define SAMPLE_RATE 16000
#define BUFFER_SIZE 256
#define BLOCK_FRAMES 128 // 128 stereo frames = 512 bytes
const uint16_t MAGIC = 0xABCD; // 2byte header marker
void setup() {
Serial.begin(1500000);
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = BUFFER_SIZE,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t pin_config = {
.bck_io_num = 6,
.ws_io_num = 7,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = 8
};
i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
i2s_set_pin(I2S_PORT, &pin_config);
Serial.println("I2S microphone initialized!");
}
void loop() {
int32_t buffer[BUFFER_SIZE];
size_t bytes_read;
// Collect one I2S DMA buffer
i2s_read(I2S_PORT, (void*)buffer, sizeof(buffer), &bytes_read, portMAX_DELAY);
int samples = bytes_read / 4; // 32-bit words
static int16_t block[BLOCK_FRAMES * 2]; // stereo frames
static int blockIndex = 0;
for (int i = 0; i < samples; i += 2) {
int32_t right32 = buffer[i];
int32_t left32 = buffer[i + 1];
int16_t right16 = (int16_t)(right32 >> 16);
int16_t left16 = (int16_t)(left32 >> 16);
block[blockIndex++] = left16;
block[blockIndex++] = right16;
if (blockIndex >= BLOCK_FRAMES * 2) {
// Send header
Serial.write((uint8_t*)&MAGIC, sizeof(MAGIC));
// Send block
Serial.write((uint8_t*)block, BLOCK_FRAMES * 4);
blockIndex = 0;
}
}
}

40
readme.md Normal file
View File

@ -0,0 +1,40 @@
esp32-audio.service goes to ~/.config/systemd/users/
# Enable it so it starts automatically at login
systemctl --user enable esp32-audio.service
# Start it immediately
systemctl --user start esp32-audio.service
# Check its status
systemctl --user status esp32-audio.service
# Causes user login to linger
sudo loginctl enable-linger littlesophia
NEED TO LOCK IN ID OF ESP32 SO ITS ALWAYS THE SAME PORT
#Get ID
udevadm info -a -n /dev/ttyACM0 | grep serial
EXAMPLE RESPONSE
(radxa) littlesophia@radxa-cubie-a7z:~/serial_audio_catcher$ udevadm info -a -n /dev/ttyACM0 | grep serial ATTRS{product}=="USB JTAG/serial debug unit" ATTRS{serial}=="B4:3A:45:AD:CA:90" ATTRS{serial}=="xhci-hcd.41.auto"
CREATE rules file and add listing
sudo nano /etc/udev/rules.d/99-esp32.rules
add this line
SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", ATTRS{serial}=="B4:3A:45:AD:CA:90", SYMLINK+="ttyESP32_A"
Now device should be locked at port "/dev/ttyESP32_A"
# Test Record
parec -d esp32.monitor --file-format=wav > test.wav

Binary file not shown.

View File

@ -1,13 +1,16 @@
// serial_to_stdout.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#define DEFAULT_PORT "/dev/ttyESP32_A"
#define DEFAULT_BAUD B1500000
#define BLOCK_SIZE 512 // PCM bytes per block
#define MAGIC 0xABCD // header marker
int open_serial(const char *path, speed_t baud) {
int fd = open(path, O_RDONLY | O_NOCTTY);
@ -39,18 +42,60 @@ int main(int argc, char *argv[]) {
const char *port = (argc > 1) ? argv[1] : DEFAULT_PORT;
speed_t baud = DEFAULT_BAUD;
int fd = open_serial(port, baud);
if (fd < 0) return 1;
int fd = -1;
uint8_t buf[8192];
while (1) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
fwrite(buf, 1, n, stdout);
if (fd < 0) {
// Try to open serial port
fd = open_serial(port, baud);
if (fd < 0) {
fprintf(stderr, "Waiting for device %s...\n", port);
sleep(2);
continue;
}
fprintf(stderr, "Connected to %s\n", port);
}
// Read header
uint8_t hbuf[2];
ssize_t hn = read(fd, hbuf, 2);
if (hn != 2) {
// Error or disconnect
if (hn == 0 || (hn < 0 && errno != EINTR)) {
fprintf(stderr, "Device disconnected, retrying...\n");
close(fd);
fd = -1;
sleep(2);
}
continue;
}
uint16_t hdr = hbuf[0] | (hbuf[1] << 8);
if (hdr != MAGIC) {
// Not aligned, skip
continue;
}
// Accumulate one full PCM block
uint8_t block[BLOCK_SIZE];
size_t got = 0;
while (got < BLOCK_SIZE) {
ssize_t m = read(fd, block + got, BLOCK_SIZE - got);
if (m <= 0) {
// Disconnect mid-block
fprintf(stderr, "Read error/disconnect mid-block, retrying...\n");
close(fd);
fd = -1;
break;
}
got += (size_t)m;
}
if (fd >= 0 && got == BLOCK_SIZE) {
fwrite(block, 1, BLOCK_SIZE, stdout);
fflush(stdout);
}
}
close(fd);
return 0;
}

Binary file not shown.

View File

@ -2,24 +2,22 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#define SERIAL_PORT "/dev/ttyESP32_A"
#define BAUD_RATE B1500000
#define RATE 16000
#define CHANNELS 2
#define BPS 16
#define BLOCK_SIZE 512 // PCM bytes per block (128 stereo frames)
#define MAGIC 0xABCD // header marker
static volatile sig_atomic_t stop_flag = 0;
void on_sigint(int sig) { (void)sig; stop_flag = 1; }
// Write RIFF/WAVE header placeholders; sizes will be patched on exit
void write_wav_header(FILE *f, uint32_t data_bytes) {
uint32_t byte_rate = RATE * CHANNELS * (BPS/8);
uint16_t block_align = CHANNELS * (BPS/8);
@ -48,7 +46,6 @@ void write_wav_header(FILE *f, uint32_t data_bytes) {
fwrite(&data_bytes, 4, 1, f);
}
// Configure serial port for raw 8N1, no flow control, blocking reads
int open_serial(const char *path) {
int fd = open(path, O_RDONLY | O_NOCTTY);
if (fd < 0) { perror("open"); return -1; }
@ -59,16 +56,15 @@ int open_serial(const char *path) {
cfsetispeed(&tio, BAUD_RATE);
cfsetospeed(&tio, BAUD_RATE);
// Raw mode: disable all translations
tio.c_iflag &= ~(IGNBRK | BRKINT | ICRNL | INLCR | IXON | IXOFF | IXANY | PARMRK | INPCK | ISTRIP);
tio.c_iflag &= ~(IGNBRK | BRKINT | ICRNL | INLCR | IXON | IXOFF | IXANY |
PARMRK | INPCK | ISTRIP);
tio.c_oflag &= ~(OPOST);
tio.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
tio.c_cflag &= ~(CSIZE | PARENB | CSTOPB | CRTSCTS);
tio.c_cflag |= (CS8 | CLOCAL | CREAD);
// Blocking read: return when at least N bytes available
tio.c_cc[VMIN] = 1; // at least 1 byte
tio.c_cc[VTIME] = 0; // no timeout
tio.c_cc[VMIN] = 1;
tio.c_cc[VTIME] = 0;
if (tcsetattr(fd, TCSANOW, &tio) != 0) { perror("tcsetattr"); close(fd); return -1; }
@ -84,21 +80,35 @@ int main() {
FILE *out = fopen("capture.wav", "wb+");
if (!out) { perror("fopen"); close(fd); return 1; }
// Reserve header space
uint32_t data_bytes = 0;
fseek(out, 44, SEEK_SET);
// Capture loop
uint8_t buf[8192];
while (!stop_flag) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
fwrite(buf, 1, (size_t)n, out);
data_bytes += (uint32_t)n;
// Read header bytes explicitly
uint8_t hbuf[2];
ssize_t hn = read(fd, hbuf, 2);
if (hn != 2) continue;
uint16_t hdr = hbuf[0] | (hbuf[1] << 8); // little-endian decode
if (hdr != MAGIC) {
// Not aligned, skip one byte and retry
continue;
}
// Accumulate one full PCM block
uint8_t block[BLOCK_SIZE];
size_t got = 0;
while (got < BLOCK_SIZE && !stop_flag) {
ssize_t m = read(fd, block + got, BLOCK_SIZE - got);
if (m > 0) got += (size_t)m;
}
if (got == BLOCK_SIZE) {
fwrite(block, 1, BLOCK_SIZE, out);
data_bytes += BLOCK_SIZE;
}
}
// Finalize header
write_wav_header(out, data_bytes);
fflush(out);
fclose(out);

View File

@ -0,0 +1,34 @@
[Unit]
Description=ESP32 Serial Audio to PulseAudio
After=pulseaudio.service
Requires=pulseaudio.service
# Tie service start to the device node so it wont launch until udev creates it
Requires=dev-ttyESP32_A.device
After=dev-ttyESP32_A.device
[Service]
# Ensure PulseAudio runtime dir is set so pactl can connect
Environment=XDG_RUNTIME_DIR=/run/user/%U
Environment=LANG=en_US.UTF-8
WorkingDirectory=/home/littlesophia/serial_audio_catcher
# Wait until PulseAudio socket and serial device exist
ExecStartPre=/bin/sh -c 'until [ -S "$XDG_RUNTIME_DIR/pulse/native" ] && [ -e /dev/ttyESP32_A ]; do sleep 2; done'
# Create the null sink before starting the pipeline
ExecStartPre=/usr/bin/pactl unload-module module-null-sink
ExecStartPre=/usr/bin/pactl load-module module-null-sink sink_name=esp32 rate=16000 channels=2 format=s16le
# Run the pipeline through a shell so the pipe is interpreted correctly
ExecStart=/bin/sh -c '/home/littlesophia/serial_audio_catcher/serial_to_stdout /dev/ttyESP32_A | /usr/bin/pacat --raw --rate=16000 --channels=2 --format=s16le --device=esp32'
# Restart automatically if it crashes or exits
Restart=always
RestartSec=5
# Log output to journal
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target

70
setup required.txt Normal file
View File

@ -0,0 +1,70 @@
NEED TO LOCK IN ID OF ESP32 SO ITS ALWAYS THE SAME PORT
#Get ID
udevadm info -a -n /dev/ttyACM0 | grep serial
EXAMPLE RESPONSE
(radxa) littlesophia@radxa-cubie-a7z:~/serial_audio_catcher$ udevadm info -a -n /dev/ttyACM0 | grep serial ATTRS{product}=="USB JTAG/serial debug unit" ATTRS{serial}=="B4:3A:45:AD:CA:90" ATTRS{serial}=="xhci-hcd.41.auto"
CREATE rules file and add listing
sudo nano /etc/udev/rules.d/99-esp32.rules
add this line
SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", ATTRS{serial}=="B4:3A:45:AD:CA:90", SYMLINK+="ttyESP32_A"
Now device should be locked at port "/dev/ttyESP32_A"
UNTESTED SHELL SCRIPT TO ACCOMPLISH ABOVE
#!/bin/bash
# Detect ESP32 ACM device and create a persistent udev rule
RULE_FILE="/etc/udev/rules.d/99-esp32.rules"
# Find the first ACM device with Espressif vendor ID
DEV=$(ls /dev/ttyACM* | head -n1)
if [ -z "$DEV" ]; then
echo "No /dev/ttyACM device found."
exit 1
fi
# Extract attributes
VENDOR=$(udevadm info -a -n $DEV | grep '{idVendor}' -m1 | awk -F'"' '{print $2}')
PRODUCT=$(udevadm info -a -n $DEV | grep '{idProduct}' -m1 | awk -F'"' '{print $2}')
SERIAL=$(udevadm info -a -n $DEV | grep 'ATTRS{serial}' -m1 | awk -F'"' '{print $2}')
if [ -z "$VENDOR" ] || [ -z "$PRODUCT" ] || [ -z "$SERIAL" ]; then
echo "Could not extract vendor/product/serial."
exit 1
fi
echo "Detected ESP32 ACM device:"
echo " Vendor: $VENDOR"
echo " Product: $PRODUCT"
echo " Serial: $SERIAL"
# Write rule
sudo tee $RULE_FILE > /dev/null <<EOF
SUBSYSTEM=="tty", ATTRS{idVendor}=="$VENDOR", ATTRS{idProduct}=="$PRODUCT", ATTRS{serial}=="$SERIAL", SYMLINK+="ttyESP32"
EOF
echo "Rule written to $RULE_FILE"
# Reload udev
sudo udevadm control --reload-rules
sudo udevadm trigger
echo "Now unplug/replug your ESP32. It should appear as /dev/ttyESP32"
parec -d esp32.monitor --file-format=wav --rate=16000 --channels=2 test.wav
parec -d esp32.monitor --file-format=wav > test.wav

View File

@ -1,10 +1,21 @@
#!/bin/bash
# Start ESP32 audio capture into PulseAudio
# 1. Ensure null sink exists at 16kHz
# Ensure null sink exists
pactl unload-module module-null-sink 2>/dev/null
pactl load-module module-null-sink sink_name=esp32 rate=16000 channels=2 format=s16le
pactl load-module module-null-sink sink_name=esp32 rate=16000 channels=2 format=s16le || {
echo "Failed to create esp32 sink"
exit 1
}
# 2. Run serial capture and feed into sink
# Loop until serial device is available
while true; do
if [ -e /dev/ttyESP32_A ]; then
echo "Device found, starting stream..."
/home/littlesophia/serial_audio_catcher/serial_to_stdout /dev/ttyESP32_A | \
pacat --raw --rate=16000 --channels=2 --format=s16le --device=esp32
/usr/bin/pacat --raw --rate=16000 --channels=2 --format=s16le --device=esp32
echo "Stream ended, retrying..."
else
echo "Waiting for /dev/ttyESP32_A..."
sleep 2
fi
done

BIN
test.wav

Binary file not shown.