Skip to content

CircuitPython I2S Audio: Testing Real Hardware in the Wild

J
Jordan Miles
May 27, 2026
12 min read
Travel & Places
CircuitPython I2S Audio: Testing Real Hardware in the Wild - Image from the article

Quick Summary

Discover how CircuitPython handles I2S audio recording across ESP32-S3 and Raspberry Pi RP2350 hardware. Real-world testing, bit-depth math, and RAM tradeoffs explained.

In This Article

When the Code Compiles but the Real World Disagrees

There's a particular kind of nervous energy that comes from pushing a firmware build to a microcontroller for the first time. You've stared at the code, you've reasoned through the logic, and you think it's right — but until you plug in a microphone, hit record, and open up Audacity, you genuinely don't know. That tension is exactly where CircuitPython's I2S audio input work lives right now, and it's one of the most instructive places to watch embedded systems development happen in real time.

I2S — Inter-IC Sound — is a serial communication protocol designed specifically for passing digital audio data between chips. It's how your microphone module talks to your microcontroller, and how your microcontroller eventually hands off audio to a DAC, a speaker driver, or in this case, a WAV file on an SD card. Getting I2S working cleanly in CircuitPython across multiple hardware targets is deceptively complex, and the ongoing development work on the I2SIn library pulls back the curtain on exactly why.

Let's dig in.

What I2S Actually Does (and Why Bit Depth Makes It Complicated)

At its core, I2S uses three wires: a bit clock (BCLK), a word select line (LRCLK or WS), and a data line (SD). The word select line tells the receiver whether the current sample belongs to the left or right audio channel. Simple enough in principle — but the moment you introduce mismatched bit depths, things get interesting fast.

The challenge at the heart of this particular development sprint involves 24-bit audio data travelling inside a 32-bit container. Most I2S microphones output 24-bit samples, but many microcontroller peripherals work most naturally with 32-bit words. That leaves 8 bits of padding to deal with, and where you put those padding bits — and what values you assign them — has a real impact on the audio quality you get out the other end.

The previous implementation used a loop that replicated the high bits into the low bits, essentially trying to fill that extra space with meaningful data. The revised approach, following feedback from a senior contributor, simply shifts the data and leaves the top bits blank rather than attempting to mirror them. It's a cleaner operation, computationally cheaper, and — crucially — it still produces audio that sounds correct when tested against a real microphone.

This is a good reminder that in embedded audio work, the proof is always in the playback. Mathematical elegance matters, but a waveform in Audacity that shows clean amplitude variation across a voice recording is the real test that passes.

CircuitPython's Cross-Platform Promise and Its Hardware Realities

One of CircuitPython's most compelling selling points is consistency: write your Python code once, and it should behave the same whether you're running it on an Adafruit Feather ESP32-S3, a Raspberry Pi Pico, or a Fruit Jam board built around the RP2350. In practice, hardware differences inevitably surface, and I2S audio recording is a vivid example of where those differences actually matter to the end user.

Testing the I2S input changes across two very different hardware families — the Espressif ESP32-S3 and the Raspberry Pi RP2350-based Fruit Jam — reveals a fascinating RAM-versus-write-speed tradeoff that shapes how you have to architect your recording code on each platform.

On the ESP32-S3, the write speed to an SD card is fast enough that you can record audio in a streaming loop: grab a chunk, write it, grab another chunk, write it again. The device doesn't have enormous amounts of RAM, but it doesn't need to because the writes keep pace with the recording. You end up with a complete WAV file built incrementally, and it works.

Flip over to the RP2350-based Fruit Jam, and the story changes. The SD card writes — and even writes to CircuitPython's internal flash filesystem — are slow enough that the streaming approach introduces audible gaps in the recording. The write operation takes long enough that the next audio chunk isn't captured cleanly. But the Fruit Jam has a significant compensating advantage: it ships with around 8MB of SRAM. That's an enormous amount by microcontroller standards, and it means you can record an entire 5- or 10-second audio clip directly into RAM, then write the whole thing to disk in one shot at the end. No gaps, clean audio, happy developer.

This isn't a flaw in either platform — it's an architectural characteristic. Understanding it is what lets you write code that actually works in deployment rather than just on the bench.

Wiring I2S Hardware: The Details That Trip People Up

CircuitPython I2S Audio: Testing Real Hardware in the Wild

If you've ever spent twenty minutes debugging an I2S connection only to discover your wires were in the wrong pins, you'll appreciate the kind of careful pin-assignment work that goes into testing this stuff properly. On the Fruit Jam, the I2S peripheral requires that bit clock and data pins be consecutive GPIO numbers. Six and seven work. Seven and eight work. But six and eight? That won't function, because the hardware requires adjacency — not just any available pins.

This is the sort of constraint that doesn't appear prominently in marketing materials and can easily be missed in a quick skim of a datasheet. It's exactly the kind of hard-won knowledge that gets embedded into example code, library documentation, and — yes — live development streams where someone is working through the wiring in real time.

For anyone setting up I2S audio input on CircuitPython hardware, here's a practical checklist worth keeping close:

  • Check consecutive pin requirements for your specific microcontroller's I2S peripheral before you solder anything.
  • Confirm 3.3V logic compatibility on your microphone module — some MEMS mics have specific power requirements.
  • Start with a short recording test (5–10 seconds) and validate it in Audacity or a similar tool before writing longer recording logic.
  • Account for your RAM budget when deciding between streaming writes and full-capture-then-write approaches.
  • Don't assume SD card write speed — benchmark it on your target hardware before committing to an architecture.

Open Source Embedded Development: Slower Than It Looks, More Valuable Than It Seems

Watching a live development stream of someone iterating on a CircuitPython pull request is a useful corrective to the myth that good open source software just happens. The work is incremental, occasionally unglamorous, and filled with moments of genuine uncertainty — like staring at bit-manipulation code and knowing conceptually what it should do without being able to trace every flip and shift at the bit level.

That honesty is actually one of the most valuable things about open, community-driven embedded development. When a contributor says "I don't fully grok all of this down to the bit, but the audio recordings work and the NeoPixel frequency analysis behaves correctly," they're modelling something important: pragmatic validation. You don't always need to hold the entire implementation in your head simultaneously. You need clear inputs, clear expected outputs, and a reliable way to verify that the outputs match expectations.

In this case, that verification is Audacity showing a clean waveform. For a NeoPixel frequency visualiser, it's watching the lights respond correctly to different frequency bands in a music signal. Both of these are real, observable, reproducible tests — and they're what give you confidence to merge a PR even when the bit mathematics makes your eyes glaze over slightly.

Adafruit's model of funding open source embedded work — paying engineers to write code that anyone can use freely, and sustaining that through hardware sales — is worth acknowledging here too. CircuitPython is genuinely free and open source under the MIT licence. You don't owe anyone a royalty to build a product with it, to fork it, or to add support for your own hardware. The social contract is simple: if you find it useful, buying hardware from adafruit.com keeps the lights on and keeps the engineers employed.

Debugging the Unexpected: When a New Build Breaks Something Unrelated

One of the more relatable moments in any hardware development session is when you flash a new build and something that has nothing to do with your changes suddenly stops working. In this case, flashing a fresh CircuitPython build to the Fruit Jam caused a display initialisation error related to how the settings.toml file was being parsed — specifically, a display width value that was previously read as a string now potentially being returned as an integer.

This is a known growing pain in CircuitPython's evolution. The settings.toml configuration system has been expanding its type support, moving toward behaviour closer to what you'd expect from a standard Python environment. A value like 640 in a TOML file should come back as an integer, not a string — but if your code was written expecting a string and passing it to a function that needs an integer (or vice versa), you'll hit exactly this kind of subtle breakage.

The takeaway for anyone maintaining CircuitPython projects: when you update your firmware, skim the CircuitPython release notes for any changes to settings.toml parsing, os.getenv() behaviour, or display configuration APIs. These are areas of active development, and small type-handling changes can cascade into unexpected errors that look unrelated to your actual project work.

Practical Takeaways for Your Own CircuitPython Audio Projects

Free Weekly Newsletter

Enjoying this guide?

Get the best articles like this one delivered to your inbox every week. No spam.

CircuitPython I2S Audio: Testing Real Hardware in the Wild

All of this testing and iteration distils into practical wisdom you can apply directly. Whether you're building a voice-activated display, a field recorder, a music visualiser, or just experimenting with microphones for the first time, here's what the real-world I2S development work teaches:

Start simple, validate early. Before you build any complex audio processing logic, confirm that raw recording works and that the output file is a valid WAV with the correct sample rate and bit depth. Open it in Audacity. Look at the waveform. Play it back.

Know your RAM budget before you architect your loop. If you're on an ESP32-S3 or similar, plan for streaming writes. If you're on an RP2350 with abundant SRAM, capture to RAM and write once.

Consecutive pins are non-negotiable for I2S. Double-check this in your specific board's pinout documentation, not just the general CircuitPython I2S API docs.

Bit depth conversions are subtle. If you're working with 24-bit microphones on 32-bit I2S peripherals, understand that the implementation choices around padding bits affect audio quality. Test with real recordings, not just oscilloscope traces.

Keep your CircuitPython version consistent during a project. When you do update, check the changelog carefully — especially for anything touching configuration parsing or display APIs.

The work being done on CircuitPython's I2S input is a small but genuinely meaningful piece of making audio accessible on microcontrollers. When it lands cleanly, anyone will be able to record a voice note, analyse music frequencies, or build a wake-word detector with nothing more than a few lines of Python and a sub-ten-dollar microphone module. That's the promise, and it's getting closer with every tested iteration.

Frequently Asked Questions

What is I2S and why is it used for audio in CircuitPython projects?

I2S (Inter-IC Sound) is a serial bus protocol designed specifically for transmitting digital audio data between integrated circuits. It uses three signal lines — bit clock, word select, and data — to stream stereo audio samples reliably. CircuitPython supports I2S for both audio output (playing sounds through a DAC or amplifier) and audio input (recording from MEMS microphones), making it a practical choice for voice, music, and sound-reactive projects on microcontrollers.

Why does I2S audio recording work differently on ESP32-S3 versus Raspberry Pi RP2350 boards?

The difference comes down to RAM capacity and SD card write speed. ESP32-S3 devices have limited RAM but can write to an SD card fast enough to stream audio in chunks — record a little, write a little, repeat. RP2350-based boards like the Adafruit Fruit Jam have much more SRAM (around 8MB), but their storage writes are slower. On those boards, the better approach is to capture the entire recording into RAM first, then write the complete file at the end. Neither approach is universally superior — it depends entirely on the hardware you're using.

Do I2S pins need to be in a specific order or position on a microcontroller?

Yes, and this is a common source of confusion. Many microcontroller I2S peripherals require the bit clock and data pins to be consecutive GPIO numbers — for example, pins 6 and 7 will work, but pins 6 and 8 typically won't, because they're not adjacent. Always consult the pinout documentation for your specific board before wiring up an I2S microphone, and look for notes about which pin combinations are valid for the I2S peripheral rather than assuming any available pins will work.

How do you verify that CircuitPython I2S audio recording is working correctly?

The most practical validation method is to record a short audio clip (5–10 seconds of speech or known sound) and open the resulting WAV file in a tool like Audacity. A working recording will show a waveform with clear variation between silence and audio — you'll see the amplitude rise when you speak and fall when you stop. A broken recording typically shows either flat silence throughout, a wall of uniform maximum-amplitude data, or a waveform with regular gaps suggesting missed chunks. Listening to the playback is the final confirmation that the bit-depth handling and sample rate are correct.

What should I check when a CircuitPython firmware update breaks something that was previously working?

Start by reading the release notes for the new CircuitPython version, particularly any sections covering changes to settings.toml parsing, os.getenv() behaviour, display APIs, or peripheral driver updates. A common source of subtle breakage is type-handling changes — for example, a value that previously came back from settings.toml as a string might now correctly return as an integer, which can cause type mismatch errors in code written against the old behaviour. If the breakage seems unrelated to your project, search the CircuitPython GitHub issues and changelog for anything touching the specific API or feature that stopped working.

Frequently Asked Questions

When the Code Compiles but the Real World Disagrees

There's a particular kind of nervous energy that comes from pushing a firmware build to a microcontroller for the first time. You've stared at the code, you've reasoned through the logic, and you think it's right — but until you plug in a microphone, hit record, and open up Audacity, you genuinely don't know. That tension is exactly where CircuitPython's I2S audio input work lives right now, and it's one of the most instructive places to watch embedded systems development happen in real time.

I2S — Inter-IC Sound — is a serial communication protocol designed specifically for passing digital audio data between chips. It's how your microphone module talks to your microcontroller, and how your microcontroller eventually hands off audio to a DAC, a speaker driver, or in this case, a WAV file on an SD card. Getting I2S working cleanly in CircuitPython across multiple hardware targets is deceptively complex, and the ongoing development work on the I2SIn library pulls back the curtain on exactly why.

Let's dig in.

What I2S Actually Does (and Why Bit Depth Makes It Complicated)

At its core, I2S uses three wires: a bit clock (BCLK), a word select line (LRCLK or WS), and a data line (SD). The word select line tells the receiver whether the current sample belongs to the left or right audio channel. Simple enough in principle — but the moment you introduce mismatched bit depths, things get interesting fast.

The challenge at the heart of this particular development sprint involves 24-bit audio data travelling inside a 32-bit container. Most I2S microphones output 24-bit samples, but many microcontroller peripherals work most naturally with 32-bit words. That leaves 8 bits of padding to deal with, and where you put those padding bits — and what values you assign them — has a real impact on the audio quality you get out the other end.

The previous implementation used a loop that replicated the high bits into the low bits, essentially trying to fill that extra space with meaningful data. The revised approach, following feedback from a senior contributor, simply shifts the data and leaves the top bits blank rather than attempting to mirror them. It's a cleaner operation, computationally cheaper, and — crucially — it still produces audio that sounds correct when tested against a real microphone.

This is a good reminder that in embedded audio work, the proof is always in the playback. Mathematical elegance matters, but a waveform in Audacity that shows clean amplitude variation across a voice recording is the real test that passes.

CircuitPython's Cross-Platform Promise and Its Hardware Realities

One of CircuitPython's most compelling selling points is consistency: write your Python code once, and it should behave the same whether you're running it on an Adafruit Feather ESP32-S3, a Raspberry Pi Pico, or a Fruit Jam board built around the RP2350. In practice, hardware differences inevitably surface, and I2S audio recording is a vivid example of where those differences actually matter to the end user.

Testing the I2S input changes across two very different hardware families — the Espressif ESP32-S3 and the Raspberry Pi RP2350-based Fruit Jam — reveals a fascinating RAM-versus-write-speed tradeoff that shapes how you have to architect your recording code on each platform.

On the ESP32-S3, the write speed to an SD card is fast enough that you can record audio in a streaming loop: grab a chunk, write it, grab another chunk, write it again. The device doesn't have enormous amounts of RAM, but it doesn't need to because the writes keep pace with the recording. You end up with a complete WAV file built incrementally, and it works.

Flip over to the RP2350-based Fruit Jam, and the story changes. The SD card writes — and even writes to CircuitPython's internal flash filesystem — are slow enough that the streaming approach introduces audible gaps in the recording. The write operation takes long enough that the next audio chunk isn't captured cleanly. But the Fruit Jam has a significant compensating advantage: it ships with around 8MB of SRAM. That's an enormous amount by microcontroller standards, and it means you can record an entire 5- or 10-second audio clip directly into RAM, then write the whole thing to disk in one shot at the end. No gaps, clean audio, happy developer.

This isn't a flaw in either platform — it's an architectural characteristic. Understanding it is what lets you write code that actually works in deployment rather than just on the bench.

Wiring I2S Hardware: The Details That Trip People Up

If you've ever spent twenty minutes debugging an I2S connection only to discover your wires were in the wrong pins, you'll appreciate the kind of careful pin-assignment work that goes into testing this stuff properly. On the Fruit Jam, the I2S peripheral requires that bit clock and data pins be consecutive GPIO numbers. Six and seven work. Seven and eight work. But six and eight? That won't function, because the hardware requires adjacency — not just any available pins.

This is the sort of constraint that doesn't appear prominently in marketing materials and can easily be missed in a quick skim of a datasheet. It's exactly the kind of hard-won knowledge that gets embedded into example code, library documentation, and — yes — live development streams where someone is working through the wiring in real time.

For anyone setting up I2S audio input on CircuitPython hardware, here's a practical checklist worth keeping close:

  • Check consecutive pin requirements for your specific microcontroller's I2S peripheral before you solder anything.
  • Confirm 3.3V logic compatibility on your microphone module — some MEMS mics have specific power requirements.
  • Start with a short recording test (5–10 seconds) and validate it in Audacity or a similar tool before writing longer recording logic.
  • Account for your RAM budget when deciding between streaming writes and full-capture-then-write approaches.
  • Don't assume SD card write speed — benchmark it on your target hardware before committing to an architecture.
Open Source Embedded Development: Slower Than It Looks, More Valuable Than It Seems

Watching a live development stream of someone iterating on a CircuitPython pull request is a useful corrective to the myth that good open source software just happens. The work is incremental, occasionally unglamorous, and filled with moments of genuine uncertainty — like staring at bit-manipulation code and knowing conceptually what it should do without being able to trace every flip and shift at the bit level.

That honesty is actually one of the most valuable things about open, community-driven embedded development. When a contributor says "I don't fully grok all of this down to the bit, but the audio recordings work and the NeoPixel frequency analysis behaves correctly," they're modelling something important: pragmatic validation. You don't always need to hold the entire implementation in your head simultaneously. You need clear inputs, clear expected outputs, and a reliable way to verify that the outputs match expectations.

In this case, that verification is Audacity showing a clean waveform. For a NeoPixel frequency visualiser, it's watching the lights respond correctly to different frequency bands in a music signal. Both of these are real, observable, reproducible tests — and they're what give you confidence to merge a PR even when the bit mathematics makes your eyes glaze over slightly.

Adafruit's model of funding open source embedded work — paying engineers to write code that anyone can use freely, and sustaining that through hardware sales — is worth acknowledging here too. CircuitPython is genuinely free and open source under the MIT licence. You don't owe anyone a royalty to build a product with it, to fork it, or to add support for your own hardware. The social contract is simple: if you find it useful, buying hardware from adafruit.com keeps the lights on and keeps the engineers employed.

Debugging the Unexpected: When a New Build Breaks Something Unrelated

One of the more relatable moments in any hardware development session is when you flash a new build and something that has nothing to do with your changes suddenly stops working. In this case, flashing a fresh CircuitPython build to the Fruit Jam caused a display initialisation error related to how the settings.toml file was being parsed — specifically, a display width value that was previously read as a string now potentially being returned as an integer.

This is a known growing pain in CircuitPython's evolution. The settings.toml configuration system has been expanding its type support, moving toward behaviour closer to what you'd expect from a standard Python environment. A value like 640 in a TOML file should come back as an integer, not a string — but if your code was written expecting a string and passing it to a function that needs an integer (or vice versa), you'll hit exactly this kind of subtle breakage.

The takeaway for anyone maintaining CircuitPython projects: when you update your firmware, skim the CircuitPython release notes for any changes to settings.toml parsing, os.getenv() behaviour, or display configuration APIs. These are areas of active development, and small type-handling changes can cascade into unexpected errors that look unrelated to your actual project work.

Practical Takeaways for Your Own CircuitPython Audio Projects

All of this testing and iteration distils into practical wisdom you can apply directly. Whether you're building a voice-activated display, a field recorder, a music visualiser, or just experimenting with microphones for the first time, here's what the real-world I2S development work teaches:

Start simple, validate early. Before you build any complex audio processing logic, confirm that raw recording works and that the output file is a valid WAV with the correct sample rate and bit depth. Open it in Audacity. Look at the waveform. Play it back.

Know your RAM budget before you architect your loop. If you're on an ESP32-S3 or similar, plan for streaming writes. If you're on an RP2350 with abundant SRAM, capture to RAM and write once.

Consecutive pins are non-negotiable for I2S. Double-check this in your specific board's pinout documentation, not just the general CircuitPython I2S API docs.

Bit depth conversions are subtle. If you're working with 24-bit microphones on 32-bit I2S peripherals, understand that the implementation choices around padding bits affect audio quality. Test with real recordings, not just oscilloscope traces.

Keep your CircuitPython version consistent during a project. When you do update, check the changelog carefully — especially for anything touching configuration parsing or display APIs.

The work being done on CircuitPython's I2S input is a small but genuinely meaningful piece of making audio accessible on microcontrollers. When it lands cleanly, anyone will be able to record a voice note, analyse music frequencies, or build a wake-word detector with nothing more than a few lines of Python and a sub-ten-dollar microphone module. That's the promise, and it's getting closer with every tested iteration.

Frequently Asked Questions

What is I2S and why is it used for audio in CircuitPython projects?

I2S (Inter-IC Sound) is a serial bus protocol designed specifically for transmitting digital audio data between integrated circuits. It uses three signal lines — bit clock, word select, and data — to stream stereo audio samples reliably. CircuitPython supports I2S for both audio output (playing sounds through a DAC or amplifier) and audio input (recording from MEMS microphones), making it a practical choice for voice, music, and sound-reactive projects on microcontrollers.

Why does I2S audio recording work differently on ESP32-S3 versus Raspberry Pi RP2350 boards?

The difference comes down to RAM capacity and SD card write speed. ESP32-S3 devices have limited RAM but can write to an SD card fast enough to stream audio in chunks — record a little, write a little, repeat. RP2350-based boards like the Adafruit Fruit Jam have much more SRAM (around 8MB), but their storage writes are slower. On those boards, the better approach is to capture the entire recording into RAM first, then write the complete file at the end. Neither approach is universally superior — it depends entirely on the hardware you're using.

Do I2S pins need to be in a specific order or position on a microcontroller?

Yes, and this is a common source of confusion. Many microcontroller I2S peripherals require the bit clock and data pins to be consecutive GPIO numbers — for example, pins 6 and 7 will work, but pins 6 and 8 typically won't, because they're not adjacent. Always consult the pinout documentation for your specific board before wiring up an I2S microphone, and look for notes about which pin combinations are valid for the I2S peripheral rather than assuming any available pins will work.

How do you verify that CircuitPython I2S audio recording is working correctly?

The most practical validation method is to record a short audio clip (5–10 seconds of speech or known sound) and open the resulting WAV file in a tool like Audacity. A working recording will show a waveform with clear variation between silence and audio — you'll see the amplitude rise when you speak and fall when you stop. A broken recording typically shows either flat silence throughout, a wall of uniform maximum-amplitude data, or a waveform with regular gaps suggesting missed chunks. Listening to the playback is the final confirmation that the bit-depth handling and sample rate are correct.

What should I check when a CircuitPython firmware update breaks something that was previously working?

Start by reading the release notes for the new CircuitPython version, particularly any sections covering changes to settings.toml parsing, os.getenv() behaviour, display APIs, or peripheral driver updates. A common source of subtle breakage is type-handling changes — for example, a value that previously came back from settings.toml as a string might now correctly return as an integer, which can cause type mismatch errors in code written against the old behaviour. If the breakage seems unrelated to your project, search the CircuitPython GitHub issues and changelog for anything touching the specific API or feature that stopped working.

Z

About Zeebrain Editorial

Our editorial team is dedicated to providing clear, well-researched, and high-utility content for the modern digital landscape. We focus on accuracy, practicality, and insights that matter.

More from Travel & Places

Related Guides

Keep exploring this topic

Explore More Categories

Keep browsing by topic and build depth around the subjects you care about most.