Skip to main content
I misunderstood how sampling error might accumulate, in part because identifiers + comments did not speak of deadlines. Withdrawn.
Source Link
J_H
  • 35.1k
  • 3
  • 32
  • 129

conserve powerpower draw

cumulative error

Here's a thought experiment.

Imagine there's a CD player next to the microphone. You insert a compact disc that offers 2 KHz test tones.

const int sampleRate = 8000;

This was mostly clear, if we assume S.I. units, but an explicit ... = 8000; // Hz would be a fine addition.

So positive x-axis crossings of the test tone will happen every 0.5 msec. And we should observe these samples: zero-crossing upward, +1 peak, zero-crossing downwardOh, -1 peak. And then it repeats. Or we see that with some fixed phase offset.

    int now = micros();

    if (now - timerLastSample >=  (1 / sampleRate) * 1000)
    {

Theseems the smallest interval one can now = microsLowPower.idle() assignment is just fine, and perfectly clear. But clarity of the comparison could be improved, since instead of S.I. units we're dealing with μsec. It's not like 1000SLEEP_15MS is obscure or anything. But phrasing it as a unity conversion factor like USEC_PER_MILLISEC wouldMight be helpfulrelevant for noticing 60 Hz peak values. And putting it in a SECONDS_TO_USEC() macro wouldBut, sigh!, it's far too coarse to be even betterapplicable here.

what goes on the wire?

As I write this, I confess I do not understand whatDocument the meaning ofspec for the (1 / sampleRate) factor iswire protocol. I initially thoughtIt wouldn't hurt to version it was sample period with units of seconds. But now it seems like it would haveAnd to be period in milliseconds? Yet I'm unwillingoccasionally send that version number to believe we're scheduling 8 million samples per secondthe peer host. Sampling rates of much more than 44 KHz tend to be overkillThe sample rate might sometimes change, at least for audio playback intended for humans.

That timerLastSample identifier is a lovely identifier. But consider abandoningso send it in favor of a different concept: nextSampleDeadline. Then we can just ask if current time has hit as metadata or bundle such changes when you bump the deadline yet. Suppose we have 1000 usec until next deadline. Then we could safely sleep for a millisecond, saving on battery chargeversion number.

        timerLastSample =int now;samples[sampleRate];

That seems like trouble. Using the current code's approachYou told us we have a sign bit, prefer something like timerLastSample += samplePeriod (assuming appropriate units)that it's not floating point, and that's about it.

The Maybe it's ifint32 compares? Maybe a >= bint64.? Number of microseconds beyondMaybe the peer host has an bint is a random variable. So we're introducing a continually increasing phase shift that has random jitter.

To verify, send a known test tone toof the mic andsame size? graphically plot the sampled waveform.

EDITAnd is of same endianess?

It seems the smallest interval one canBe explicit when you serialize. I didn't even see any LowPower.idlehtonl() is SLEEP_15MS. Might be relevant for noticing 60 Hz peak values calls. But

Also, sigh!in addition to scheduling ADC readings, it's far too coarse it appears we want to be applicable heresend IP packets on a one-second isochronous schedule. Be explicit about that.

co-existing with other tasks

If down the roadReconsider whether that's really what you want. My reading is that a maintenance engineer32-bit host would send 32000 bytes definesof UDP payload in a serialEventRun routinesingle call, which gets fragged out then details of scheduling andto IDK about 22 IP fragments. That seems super bursty, which encourages router drops. And it grossly magnifies the app-level deadlineseffect of will become more importanteven a tiny underlying drop rate. As it standsMuch better to send ~350 samples at a time, each serial character processed will introduce significant audio jitterso they fit within a single wifi frame.

You'reIn this prototype you're not sending redundant info in case of dropped packet -- we'll just ignore that in this prototype.

conserve power

cumulative error

Here's a thought experiment.

Imagine there's a CD player next to the microphone. You insert a compact disc that offers 2 KHz test tones.

const int sampleRate = 8000;

This was mostly clear, if we assume S.I. units, but an explicit ... = 8000; // Hz would be a fine addition.

So positive x-axis crossings of the test tone will happen every 0.5 msec. And we should observe these samples: zero-crossing upward, +1 peak, zero-crossing downward, -1 peak. And then it repeats. Or we see that with some fixed phase offset.

    int now = micros();

    if (now - timerLastSample >=  (1 / sampleRate) * 1000)
    {

The now = micros() assignment is just fine, and perfectly clear. But clarity of the comparison could be improved, since instead of S.I. units we're dealing with μsec. It's not like 1000 is obscure or anything. But phrasing it as a unity conversion factor like USEC_PER_MILLISEC would be helpful. And putting it in a SECONDS_TO_USEC() macro would be even better.

As I write this, I confess I do not understand what the meaning of the (1 / sampleRate) factor is. I initially thought it was sample period with units of seconds. But now it seems like it would have to be period in milliseconds? Yet I'm unwilling to believe we're scheduling 8 million samples per second. Sampling rates of much more than 44 KHz tend to be overkill, at least for audio playback intended for humans.

That timerLastSample identifier is a lovely identifier. But consider abandoning it in favor of a different concept: nextSampleDeadline. Then we can just ask if current time has hit the deadline yet. Suppose we have 1000 usec until next deadline. Then we could safely sleep for a millisecond, saving on battery charge.

        timerLastSample = now;

That seems like trouble. Using the current code's approach, prefer something like timerLastSample += samplePeriod (assuming appropriate units).

The if compares a >= b. Number of microseconds beyond b is a random variable. So we're introducing a continually increasing phase shift that has random jitter.

To verify, send a known test tone to the mic and graphically plot the sampled waveform.

EDIT

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.

co-existing with other tasks

If down the road a maintenance engineer defines a serialEventRun routine, then details of scheduling and app-level deadlines will become more important. As it stands, each serial character processed will introduce significant audio jitter.

You're not sending redundant info in case of dropped packet -- we'll just ignore that in this prototype.

power draw

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.

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

added 192 characters in body
Source Link
J_H
  • 35.1k
  • 3
  • 32
  • 129

EDIT

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.

EDIT

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.

added 269 characters in body
Source Link
J_H
  • 35.1k
  • 3
  • 32
  • 129

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.

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.

added 284 characters in body
Source Link
J_H
  • 35.1k
  • 3
  • 32
  • 129
Loading
added 11 characters in body
Source Link
J_H
  • 35.1k
  • 3
  • 32
  • 129
Loading
added 11 characters in body
Source Link
J_H
  • 35.1k
  • 3
  • 32
  • 129
Loading
Source Link
J_H
  • 35.1k
  • 3
  • 32
  • 129
Loading