diff --git a/capture.wav b/capture.wav index c98a3d2..e74ff53 100644 Binary files a/capture.wav and b/capture.wav differ diff --git a/dump.bin b/dump.bin new file mode 100644 index 0000000..4463f35 Binary files /dev/null and b/dump.bin differ diff --git a/firmware/sender.ino b/firmware/sender.ino new file mode 100644 index 0000000..cd6e203 --- /dev/null +++ b/firmware/sender.ino @@ -0,0 +1,70 @@ +#include +#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; + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9cf595a --- /dev/null +++ b/readme.md @@ -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 diff --git a/serial_to_stdout b/serial_to_stdout index 197bd47..9389cba 100755 Binary files a/serial_to_stdout and b/serial_to_stdout differ diff --git a/serial_to_stdout.c b/serial_to_stdout.c index 10cd9de..d2172df 100644 --- a/serial_to_stdout.c +++ b/serial_to_stdout.c @@ -1,13 +1,16 @@ +// serial_to_stdout.c #include #include #include -#include #include #include #include +#include #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; } diff --git a/serial_to_wav b/serial_to_wav index 1574b3c..64d6d3c 100755 Binary files a/serial_to_wav and b/serial_to_wav differ diff --git a/serial_to_wav.c b/serial_to_wav.c index 9c07153..3a814fa 100644 --- a/serial_to_wav.c +++ b/serial_to_wav.c @@ -2,24 +2,22 @@ #include #include #include -#include #include #include #include #include -#include -#include #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); diff --git a/service/esp32-audio.service b/service/esp32-audio.service new file mode 100644 index 0000000..dbcccf6 --- /dev/null +++ b/service/esp32-audio.service @@ -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 diff --git a/setup required.txt b/setup required.txt new file mode 100644 index 0000000..699cff4 --- /dev/null +++ b/setup required.txt @@ -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 < test.wav + diff --git a/start_esp32_audio.sh b/start_esp32_audio.sh index d442fdf..c59b3dd 100755 --- a/start_esp32_audio.sh +++ b/start_esp32_audio.sh @@ -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 diff --git a/test.wav b/test.wav index f7cc9fb..66a8c3b 100644 Binary files a/test.wav and b/test.wav differ