2
\$\begingroup\$

I want to sample audio as a learning process.

In the future i want to make my dorbell "smart" and stream the audio via VoIP. But as beginning i want to sample a simple audio signal from my laptop (e.g. simple sign wave) and send it via UDP over the network and listen to it.

As board i want to use a ESP32-12. Since i never had to sample a analog audio, i want to know if this code is "ok". I know its just software based and has the potential to be inprecise due to the lack of interrupts, but as said, its for the purpose of learning.

#include <WiFi.h>
#include <WiFiUdp.h>

// network setuf
#define WIFI_SSID "YOUR WIFI NETWORK SSID"
#define WIFI_PASS "YOUR WIFI PASSWORD"
const char *udpAddress = "192.168.1.100";
const int udpPort = 4210;

const int analogIn = A0;
const int sampleRate = 8000;

int samples[sampleRate];
int sampleIndex = 0;

int timerLastSample;

WiFiUDP UDP;

void setup()
{

    // setup serial
    Serial.begin(115200);

    // give some time for serial connection
    delay(1000);

    // feedback
    Serial.println("Connect to wifi...");

    // connect to wifi
    WiFi.begin(WIFI_SSID, WIFI_PASS);

    // wait for wifi connection
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(100);
        Serial.print(".");
    }
}

void loop()
{

    int now = micros();

    if (now - timerLastSample >=  (1 / sampleRate) * 1000)
    {
        timerLastSample = now;
        samples[sampleIndex] = analogRead(analogIn);
        sampleIndex += 1;
    }

    if (sampleIndex == sampleRate)
    {

        // reset
        sampleIndex = 0;

        // send data
        UDP.beginPacket(udpAddress, udpPort);
        UDP.write(samples);
        UDP.endPacket();
    }
}
```
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Fun project!

power draw

The arduino event loop is one level up, repeatedly calling into your loop(). I hope it occasionally does a brief sleep, to conserve processor power. If not, you might wish to sleep a moment in the case where micros() has not yet hit your sampling deadline.

Oh, it seems the smallest interval one can LowPower.idle() is SLEEP_15MS. Might be relevant for noticing 60 Hz peak values. But, sigh!, it's far too coarse to be applicable here.

what goes on the wire?

Document the spec for the wire protocol. It wouldn't hurt to version it. And to occasionally send that version number to the peer host. The sample rate might sometimes change, so send it as metadata or bundle such changes when you bump the version number.

int samples[sampleRate];

You told us we have a sign bit, that it's not floating point, and that's about it. Maybe it's int32? Maybe int64? Maybe the peer host has an int of the same size? And is of same endianess?

Be explicit when you serialize. I didn't even see any htonl() calls.

Also, in addition to scheduling ADC readings, it appears we want to send IP packets on a one-second isochronous schedule. Be explicit about that.

Reconsider whether that's really what you want. My reading is that a 32-bit host would send 32000 bytes of UDP payload in a single call, which gets fragged out to IDK about 22 IP fragments. That seems super bursty, which encourages router drops. And it grossly magnifies the app-level effect of even a tiny underlying drop rate. Much better to send ~350 samples at a time, so they fit within a single wifi frame.

timestamp

        UDP.write(samples);

In this prototype you're not sending redundant info in case of dropped packet.

You should prepend an "actual" timestamp at beginning of packet, ahead of the audio samples. That lets a receiver buffer and play out the sound at proper rate, even in the presence of delayed / dropped packets.

A simple approach for dealing with isolated drops would be to send every sample twice. Each packet would replay samples from previous interval, and would include a bunch of new samples from current interval. Receiver would typically ignore the replayed portion. Alternatively, consider sending audio data via TCP.

\$\endgroup\$
4
  • \$\begingroup\$ This seems to be programmed using the Arduino environment. There setup and loop are standard functions required for the code to run. Sure, OP could create a function streamAudio and call that from loop, but anyone even vaguely familiar with Arduino coding will immediately recognize setup and loop functions. \$\endgroup\$
    – TomG
    Commented Dec 26, 2023 at 13:40
  • 1
    \$\begingroup\$ @TomG, granted, thank you. Duly updated. // This appears to be a while (1) { } battery burner that just keeps a core busy with looping. (Plus it occasionally grabs an audio sample.) On a bigger host an app would communicate to the OS scheduler, via sleep(), how often it would like to run in order to meet its deadlines. And the OS would ask the CPU to reduce voltage and clock speed, momentarily halt an individual core, that sort of thing. In an Arduino context, how do we convince it to consume battery charge at an appropriate rate? \$\endgroup\$
    – J_H
    Commented Dec 26, 2023 at 15:57
  • \$\begingroup\$ here's Arduino documentation on power saving. It talks about a LowPower library that you use to configure the device according to your needs. docs.arduino.cc/learn/electronics/low-power \$\endgroup\$
    – TomG
    Commented Dec 26, 2023 at 18:17
  • 1
    \$\begingroup\$ @TomG Arduino low-power is not compatible with ESP32-12. ESP32-12 has a different API. I would suggest using the light sleep mode with the Wifi modem turned on. If power was an issue I would move the ADC sampling code to the ULP coprocessor which still runs in sleep mode. Sample a bunch of data and send it over wifi in chunks. It might cause a delay in audio reception so there is a tradeoff to be made here. \$\endgroup\$ Commented Dec 26, 2023 at 23:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.