mailto: blog -at- heyrick -dot- eu

QuickScope

An ESP32 on a breadboard with lots of wires.
This is what this is!

Some interesting guessing about what this thing is - but alas the board on the right is very much not a USB to MIDI, or anything whatsoever to do with sound. It's a board for programming/powering the ESP32-CAM board - the ESP32-CAM slots into the sockets along each side of the programming board, and it provides +5V, serial comms, and also the controls to enable programming and to reset the board.

 

The jack plug is actually an input.

Here's a schematic.

A simple schematic
QuickScope schematic.

So let's look at what is going on here. These concepts were discussed in the recent musings on oscilloscopes, and, well, it was niggling me to have the idea in my head but not show it being done practically.

The only ADC that is tracked out on the little ESP32-CAM boards is ADC2. There are seven versions of it, but we know that pin 13 is one of the few 'safe' pins that doesn't need to be in a specific state at boot. So it's technically ADC2_4.

The ADC can sample a voltage between 0V and 1.1V (the Vref); however there is an attenuation option, and by attenuating by 12dB it is possible to sample between ~150mV and 3100mV.

We specify a read resolution of 12 bits, which means the results will be in the range of 0 to 4095.

Now, you see that audio jack? That's a sound signal input. This bounces positive and negative, so the ADC cannot directly read it. What needs to be done in order to decently read the audio is to create a potential divider and then lay the audio signal into it.

Thus, the 3.3V from the ESP32 (converted down on-board from the 5V input), goes through R2, a 10KΩ resistor, through R1, another 10KΩ resistor, and then to 0V. This is a potential divider.

As it is "3.3V → 10K →·→ 10K → 0V", the centre point (indicated by the dot) is half of 3.3V, or about 1.65V. For the purpose of what we're doing, the actual voltage doesn't matter.

Now the ESP32 is a really noisy chip, and one cannot rely upon the 3.3V being rock steady. It's actually bad enough that if you connect the ADC to the middle of the potential divider, it will look like there's some sort of sound playing when nothing else is connected. In order to help smooth things out, I have added a 22µF electrolytic capacitor between the centre tap point and 0V. You want enough capacitance that it'll help smooth out the noise, but not so much that it actually affects what we're trying to measure. I just so happened to have a 22µF and that's a perfectly decent value to use.

The audio ground is connected to 0V, so everything has a common reference point. I can do this because I'm playing audio with my phone, but if your sound is coming from something that is plugged in, check first if there's any actual voltage difference between what your audio system thinks is 0V and what the ESP board thinks is 0V before connecting them!

The final piece of the puzzle is to connect the audio left channel into the centre tap point by way of a 1µF ceramic capacitor.

But - wait - that's... an electrolytic?

Yes. It is.

The reason is because the only ceramic capacitor in my box'o'bits is marked "101" which means it is 0.0001µF.
This is a bad thing, because the capacitor in between the two resistors like that forms a high pass filter. The cutoff frequency is determined by the calculation:

      1
fc = ----
     2πRC

So if the resistors are 10KΩ and the capacitor is 100pF (or 0.0001µF), the calculation is:

                 1
fc = -------------------------
     2π × 10000 × 0.0000000001  

That's 10,000 ohms (one resistor), and 0.0000000001 farads (the capacitor).

Plugging this into a calculator (or BASIC) as something we can work with:

1÷((2×3.14)×10000×0.0000000001)
gives us the result 159,236.blahblah.

What this means is, effectively, anything below about 159kHz is filtered out. So this would be great for radio signals, but utterly rubbish for audio - which would all be filtered out!

As I had a 22µF capacitor handy, let's run this again with that instead:

1÷((2×3.14)×10000×0.000022)
gives us the result 0.723.

This means anything below three quarters of a hertz will be filtered out. Which means all useful parts of the audio will be passed - I don't think even drone metal gets down to less than one hertz: that's not bass, that's a heartbeat!

The capacitor might seem to be wired up back to front, but this is because the centre point voltage is ~1.65V, while the audio input is much closer to zero. It depends upon the volume setting, but is typically 0.3V to 0.4V max, and the EU norm EN600065 limits it to 150mV (except under specific cases). EN600065 covers just about everything that involves moving electrons, so it's in section Z1.2 e)2 on page 133.

Anyway, the middle of the divider is more positive than the audio input is ever likely to be, so we'll put the +ve leg there, so it is correctly biased at all times.

Because the electrolytic has leakage and ESR, the resistors need to be kept in the region of 10KΩ. Over 100K is too much, and the whole thing needs to 'settle' for about a tenth of a second at startup.

Well, that's it. That's the hardware.

 

Now let's look at the software. You'll be shocked by how simple it actually is.

// Quick'n'dirty ESP32-CAM oscilloscope
// GPIO13 (ADC2_CH4)

#include <WiFi.h>
#include "Esp.h"


#define ADC_PIN 13
#define INT_LED 33
#define SAMPLE_RATE_HZ 20000      // 20 kHz ideal sample rate
#define SAMPLE_PERIOD_US (1000000 / SAMPLE_RATE_HZ)

void setup()
{
   Serial.begin(1000000); // Fast serial is VERY important here
   delay(1000);           // Wait for everything to get going

   // Disable WiFi to allow ADC2 to work
   WiFi.mode(WIFI_OFF);
   btStop();

   analogReadResolution(12);       // 0-4095
   analogSetAttenuation(ADC_11db); // ~0-3.2V range

   // Print a banner
   Serial.println("ESP32-CAM QuickScope 2026/02/17");

   // Now turn on the LED so we know we've started up
   pinMode(INT_LED, OUTPUT);
   digitalWrite(INT_LED, LOW); // remember, it is inverted
}


void loop()
{
   static uint32_t next_sample = micros();
   static uint32_t count = 0;
   static uint32_t last_ms = 0;

   // Perform the sampling
   uint32_t now = micros();
   if ((int32_t)(now - next_sample) >= 0)
   {
      // Read this sample
      next_sample += SAMPLE_PERIOD_US;
      int sample = analogRead(ADC_PIN);

      // Output as plain numbers for Serial Plotter
      int centered = (sample - 2048); // push it to the centre
      Serial.print("-100 "); // *really* don't need full range for a tiny audio signal
      Serial.println(centered);

      // Count our sampling rate
      count++;

      uint32_t now_ms = millis();
      if (now_ms - last_ms >= 1000)
      {
         Serial.print("SPS=");
         Serial.println(count);
         count = 0;
         last_ms = now_ms;
      }
   }
}

That's it!

 

Let's take it bit by bit.

#define ADC_PIN 13
#define INT_LED 33
#define SAMPLE_RATE_HZ 20000      // 20 kHz ideal sample rate
#define SAMPLE_PERIOD_US (1000000 / SAMPLE_RATE_HZ)

These are our definitions. The pin we'll be using for the ADC, the internal LED, and wishful thinking that we might manage 20kHz sampling (spoiler: nope, not like this).

void setup()
{
   Serial.begin(1000000); // Fast serial is VERY important here
   delay(1000);           // Wait for everything to get going

Everything has to run really quickly; so specifying 1000000bps is essential. Each byte will take eight microseconds, which means the nine bytes that we are sending will take about 72µS. By contrast, the ADC sampling takes about 80~100µS using the normal analogue read function, so we're losing roughly half our bandwidth just in sending the data to the host.

   // Disable WiFi to allow ADC2 to work
   WiFi.mode(WIFI_OFF);
   btStop();

This right here is the big caveat and the problem with the ESP32-CAM board. ADC1 is not tracked out anywhere (it may be that those pins are used for the camera interface instead). ADC2 is available, but due to internal resource usage, you cannot use ADC2 when WiFi is active.
So WiFi needs to be shut down.

Note that this is specifically an issue with the ESP32-CAM board. Proper ESP32 boards with lots of pins will have ADC1 somewhere, and that can be used with WiFi.

   analogReadResolution(12);       // 0-4095
   analogSetAttenuation(ADC_11db); // ~0-3.2V range

As discussed above, we're just setting the attenuation to cope with full voltage levels, and asking for 12 bit results.

   // Print a banner
   Serial.println("ESP32-CAM QuickScope 2026/02/17");

   // Now turn on the LED so we know we've started up
   pinMode(INT_LED, OUTPUT);
   digitalWrite(INT_LED, LOW); // remember, it is inverted
}
And finally, at the end of the initial setup, a quick banner is output (always useful to identify the device at least once where it can be seen in a serial monitor), and then turn on the internal LED. This is just a little diagnostic thingy to permit us to know that the device has correctly booted.

Now for the "loop()" function, that just gets called continuously.

void loop()
{
   static uint32_t next_sample = micros();
   static uint32_t count = 0;
   static uint32_t last_ms = 0;

Define the variables that we want to preserve over invocations of this function.

   // Perform the sampling
   uint32_t now = micros();
   if ((int32_t)(now - next_sample) >= 0)
   {
      // Read this sample
      next_sample += SAMPLE_PERIOD_US;
      int sample = analogRead(ADC_PIN);

If it's after the time of our next sample, then take a sample. Given that our desired rate was the rather optimistic 20kHz, this will pretty much always run as this particular setup can't handle those sorts of sample rates.

      // Output as plain numbers for Serial Plotter
      int centered = (sample - 2048); // push it to the centre

For the built-in Arduino plotter, we push our value to be zero-biased. That is to say, a halfway reading (as one would expect with the potential divider) is counted as zero. Anything over is positive, anything under is negative. This means we can now better reflect the audio input swings over and under the zero point.

      Serial.print("-100 "); // *really* don't need full range for a tiny audio signal
      Serial.println(centered);

We then output a line like "-100 17". The reason for the 'ghost channel' that is always -100 is to stop the Serial Plotter from constantly trying to adjust itself to scale the input to fill the display.

I had what looked like a bug in my code, it was bouncing around crazy-like, as if there was a really loud audio signal when nothing was playing. Looking at the raw data, it turns out that it was only changing by five or six values, basic analogue ripple, but the Serial Plotter was unhelpfully scaling that to fill the screen, so what should have been a slightly wobbly line instead looked like a forest of icicles.
By giving a phantom channel, we're forcing the plotter to keep to a certain scaling. I have specified -100 (instead of -2048) because audio signals are usually quite small. That value is a normal listening value for me, so in your case you may need to fiddle with the volume to get it to fit.

      // Count our sampling rate
      count++;

      uint32_t now_ms = millis();
      if (now_ms - last_ms >= 1000)
      {
         Serial.print("SPS=");
         Serial.println(count);
         count = 0;
         last_ms = now_ms;
      }
   }
}

And, finally, it's a microcontroller. There's no need to guess what sort of speed we might be sampling when we can just measure it.

Something in the order of 8½-9kHz is how fast this setup runs.

 

When booted, hooked to some music - I was listening to "Through the Desert, Through the Storm" by Signum Regis - and the Arduino Serial Plotter is running and set to 1000000bps, this is what you can expect to see.

The waveform of a song.
Result! A waveform of a piece of music.

Of course, this will run for maybe a minute or so and then freeze......because Java is shit and would freeze up burning one core doing diddly-squat if you hooked up a toaster.

Java hammering a core to do nothing
Making heat for no good reason.

 

Future?

Oh, there's loads more that could be done.

  • Write something (in Python, perhaps?) to allow us to lose the sluggish Java nonsense.
    Bonus: With this we can send 16 bit binary values and know that the plot will be the size we determine, so each read only needs two bytes (16µS) to send.
  • Raw read for faster sampling?
  • There's something called I2S that can run quite a bit faster, but the code is hairier.
  • More stability - regular timing.
  • And... more... my pasta is nearly ready so this will do for now. ☺

 

Anyway, getting a little microcontroller (intended for use with a camera) to sample some music, just be writing a few lines of code and putting some components and wires on a bit of breadboard... I think this counts as a righteous hack, especially as it wasn't done for any reason other than the sake of just doing it. That's where all the best hacks come from, isn't it?

 

 

Your comments:

Please note that while I check this page every so often, I am not able to control what users write; therefore I disclaim all liability for unpleasant and/or infringing and/or defamatory material. Undesired content will be removed as soon as it is noticed. By leaving a comment, you agree not to post material that is illegal or in bad taste, and you should be aware that the time and your IP address are both recorded, should it be necessary to find out who you are. Oh, and don't bother trying to inline HTML. I'm not that stupid! ☺
As of February 2025, commenting is no longer available to UK residents, following the implementation of the vague and overly broad Online Safety Act. You must tick the box below to verify that you are not a UK resident, and you expressly agree if you are in fact a UK resident that you will indemnify me (Richard Murray), as well as the person maintaining my site (Rob O'Donnell), the hosting providers, and so on. It's a shitty law, complain to your MP.
It's not that I don't want to hear from my British friends, it's because your country makes stupid laws.

 
You can now follow comment additions with the comment RSS feed. This is distinct from the b.log RSS feed, so you can subscribe to one or both as you wish.

David Pilling, 19th February 2026, 01:33
I got my answer. ISTR Google suggested the programmer for the ESP32 Cam. I should have listened. Interesting, a scope. There is at least one fast replacement for the Arduino plotter. By now actually squillions of them. I'd be inclined to feed in something where one knows what the results look like - damp finger mains hum. Pure sine wave. There is pyscope a 'scope written in Python - patch in your output.
C Ferris, 21st February 2026, 10:15
Rick have you thought of doing an article for one of the Arc mags - how's your German :-)

Add a comment (v0.12) [help?] . . . try the comment feed!
Your name
Your email (optional)
Validation Are you real? Please type 71933 backwards.
UK resident
Your comment
French flagSpanish flagJapanese flag
Calendar
«   February 2026   »
MonTueWedThuFriSatSun
      
3456
1011121315
1920
23242627 

(Felicity? Marte? Find out!)

Last 5 entries

List all b.log entries

Return to the site index

Geekery
 
Alphabetical:

Search

Search Rick's b.log!

PS: Don't try to be clever.
It's a simple substring match.

Etc...

Last read at 14:16 on 2026/03/07.

QR code


Valid HTML 4.01 Transitional
Valid CSS
Valid RSS 2.0

 

© 2026 Rick Murray
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted.
RIPA notice: No consent is given for interception of page transmission.

 

Have you noticed the watermarks on pictures?
Next entry - 2026/02/21
Return to top of page