added sync frames every 512 bytes
parent
a58f026d35
commit
5ec348c27b
BIN
capture.wav
BIN
capture.wav
Binary file not shown.
|
|
@ -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; // 2‑byte 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
BIN
serial_to_stdout
BIN
serial_to_stdout
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
BIN
serial_to_wav
BIN
serial_to_wav
Binary file not shown.
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 won’t 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
/home/littlesophia/serial_audio_catcher/serial_to_stdout /dev/ttyESP32_A | \
|
||||
pacat --raw --rate=16000 --channels=2 --format=s16le --device=esp32
|
||||
# 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 | \
|
||||
/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
|
||||
|
|
|
|||
Loading…
Reference in New Issue