Digital audio · Decode before interpreting
Digital Microphone: A Quiet Average Can Hide Clipped Peaks
Capture one finite digital microphone buffer, inspect its signed sample format and compare peak amplitude with average and root-mean-square amplitude. Then clip a constructed signal in software and turn it down again. A quieter number does not restore the missing peak shape.
1. Choose the exact microphone and the exact claim
General-purpose input/output (GPIO) names identify Pico pins. Left/right clock (LRCLK), also called word select (WS), marks the audio slot. Units: V is volt, Ω is ohm, kΩ is kilohm, µF is microfarad, nF is nanofarad, pF is picofarad, MHz is megahertz, kHz is kilohertz, ns is nanosecond, µs is microsecond and ms is millisecond.
The fixture is an original RP2040 Raspberry Pi Pico with stock MicroPython 1.26.0 and the Adafruit PID 3421 breakout carrying a Knowles SPH0645LM4H-B microphone. A microelectromechanical system (MEMS) combines tiny mechanical structures with electronics. This microphone converts sound into digital samples internally. Inter-IC Sound (I2S) is the clocked serial audio interface carrying those samples; it is unrelated to the two-wire I2C control bus.
Adafruit’s current guide covers two different boards. This lesson uses the SPH0645 version, not the newer ICS-43434 board in the similarly named MEMS repository. The guide’s SPH0645 download links the separate Adafruit-I2S-Microphone-Breakout-PCB repository. Its pinned schematic has the six-pad header, a 68 Ω series data resistor, a 10 kΩ channel-select pull-down and 1 µF plus 220 pF supply capacitors. Check your actual board identity and continuity before applying power; a photograph or product-family name does not prove the populated part.
The author executed the actual decoder, arithmetic and capture-control tests on the host. No Pico firmware was built, no target program was executed and no microphone was physically sampled. The capture program is a complete intended target program, not evidence of target success. Known synthetic words exercise byte order, sign extension and error paths. They do not establish acoustic sensitivity, physical bit alignment, sample continuity or sound-pressure level.
2. Trace power, clock ownership and every return
Power the Pico through its normal USB connector only. Universal Serial Bus (USB) supplies the board; use Pico’s regulated 3V3 OUT terminal, physical pin 36, for the microphone’s 3V header pad 1. Ground, the shared voltage reference, runs from Pico physical pin 13 to carrier GND pad 2 and SEL pad 6. Selecting LOW assigns this microphone to the left channel. Never connect the carrier to VBUS or a 5 V signal: its normal supply range is 1.62–3.6 V, not the datasheet’s 5 V stress limit.
| Pico terminal | Carrier header | Owner and purpose |
|---|---|---|
| GP10, physical 14 | BCLK, pad 3 | Pico output: bit clock |
| GP11, physical 15 | LRCLK/WS, pad 5 | Pico output: left/right word-select clock |
| GP12, physical 16 | DOUT, pad 4 | Microphone output; Pico input only |
| 3V3 OUT, physical 36 | 3V, pad 1 | One regulated 3.3 V supply domain |
| GND, physical 13 | GND pad 2 and SEL pad 6 | Common return and explicit left-channel selection |
Add a 100 kΩ resistor from DOUT pad 4 to common ground so the single microphone’s undriven slot discharges. Add a 100 nF ceramic bypass capacitor, rated at least 10 V, directly between carrier 3V and GND. These external parts supplement the verified carrier components; they are not invented microphone internals. Keep the clock/data connections short, with nearby returns. No speaker, amplifier, second microphone or analog input is connected.
In the timing inset, SCK means serial clock, OUTOVER is the RP2040 output override and IN is its hardware input-sample instruction. Native means the signal inside the peripheral before the GPIO output override.

For reference, the bare Knowles package uses pin 1 WS, pin 2 SELECT, pin 3 GND, pin 4 BCLK, pin 5 VDD and pin 6 DATA. Those package numbers differ from the carrier header numbers in the table. The diagram intentionally labels the accessible carrier terminals. The source review records the actual carrier schematic nets, including its series resistor. Do not solder this experiment using package pin numbers on the header.
3. Derive the word format from the pinned RP2 source
Pulse-code modulation (PCM) represents each audio sample as a signed number. The Knowles datasheet specifies an I2S 24-bit two’s-complement word with 18 bits of precision and unused low bits set to zero. Two’s complement is the signed binary convention in which the top bit carries a negative weight. The most-significant bit (MSB) is transmitted first, one bit-clock period after the word-select transition.
The selected sample rate is nominally 32,000 samples per second. A stereo frame contains 64 bit clocks, so BCLK is nominally 32,000 × 64 = 2.048 MHz, the lower specified operating clock. A frame is 31.25 µs and one bit period is about 488.28 ns. Fractional clock division and the board clock affect the actual rate; no oscillator accuracy was measured here. Do not choose a convenient 16 kHz rate for this microphone: its resulting 1.024 MHz clock would fall outside the specified normal range.
MicroPython’s RP2 implementation requires WS to be SCK + 1 because both clocks use adjacent programmable input/output (PIO) side-set pins. Our GP10/GP11 pair satisfies this. PIO is RP2040’s programmable pin-timing engine. The pinned receive program shifts in 32 bits per slot. Its direct memory access (DMA) engine moves stereo frames in the background. The frame map for 32-bit stereo preserves bytes 0–7; the documented user buffer is little-endian, left word before right word.
The stock RP2 receive program alone is not correctly phased for this selected Knowles Rev B part. Its IN instructions sample while raising native SCK; its WS transition occurs in the same instruction. Knowles specifies WS setup and hold around the microphone’s rising clock edge, and DATA can change up to 65.92 ns after that edge. Merely guessing another decoder shift does not fix this timing mismatch.
The delivered correction inverts only the physical GP10 clock through the RP2040 output override (OUTOVER). Memory-mapped input/output (MMIO) accesses the documented GPIO10 CTRL register at 0x40014054. Bits 9:8 use mask 0x300; value 0x100 selects INVERT. Native SCK rising now appears as physical BCLK falling, so WS changes on the falling edge and IN samples data established after the previous physical rising edge. The SDK’s GPIO function setup clears overrides, so the correction is applied and read back only after the I2S constructor returns. No external inverter or undocumented decoder padding trick is used.
This path requires the original RPI_PICO build string “Raspberry Pi Pico with RP2040”, MicroPython 1.26.0, platform rp2 and a 125 MHz system clock before any MMIO access. It rejects RP2350 and other board names, even at the same frequency. Use stock firmware and one run after a power cycle; reserve I2S0, its PIO state machine/program space, two DMA channels and GP10/11/12 exclusively, with no second core or interrupt handler changing their configuration. Untouched GPIO CTRL values are checked first; these checks do not identify a maliciously modified firmware image.
At 125 MHz, the fractional divider’s shortest half-cycle is 30 system clocks, or 240 ns. That gives 154.55 ns beyond the microphone’s 85.45 ns WS setup/hold requirement in the digital edge model. After the 65.92 ns maximum DATA delay, reserving three system clocks (24 ns) for normal input synchronization/phase and another 20 ns model allowance leaves 130.08 ns before IN. These are source-derived timing budgets at this one rate, not measured pad delays, breadboard loading or a 64 kHz compatibility claim.
Qualify the actual DATA levels at the Pico: HIGH must be at least 2.0 V and LOW at most 0.8 V. The microphone’s minimum HIGH of 0.7×VDD is 2.31 V at 3.3 V, but its maximum LOW of 0.3×VDD is 0.99 V under the stated 2 mA test condition; that number alone does not guarantee the Pico LOW margin. This fixture presents a much lighter load, yet “3.3 V logic” is not a substitute for checking the loaded levels, edge phase and settling. Stop if these conditions are not met.
With the corrected clock phase and the expected standard-I2S word alignment, the 18 significant left-channel bits occupy signed 32-bit positions 31–14. The decoder first unpacks a little-endian signed word, requires the low 14 bits to be zero, then shifts right by 14. This combines the microphone’s six zero precision bits with the remaining eight discharged slot bits. The right word must be zero for this explicitly grounded-SEL, one-microphone fixture. Neither predicate proves correct physical framing: a disconnected line can produce zeros, and a wrong signal can coincidentally fit the mask. Before trusting captured numbers, inspect actual WS/BCLK/DATA timing with suitable equipment and confirm the bit-delay/channel assignment. No physical alignment was verified by the author.
4. Calculate amplitude without hiding peaks
For N signed samples x, the mean is Σx/N. Positive and negative audio can cancel, so a near-zero mean does not establish a quiet waveform. Root mean square (RMS) amplitude is √(Σx²/N); it measures the energy-related magnitude including any direct-current (DC) offset. The code also reports alternating-current (AC) RMS, which removes the mean first. Peak amplitude is max(|x|). Full scale (FS) is the signed digital range, here −131072 through +131071.
Digital rail hits count samples exactly at either representable extreme. They are a warning, not proof that the microphone acoustically clipped. The host gain function additionally counts samples whose proposed multiplied value exceeded that representable range before saturation. That count is known digital clipping caused by our own arithmetic. Analog overload inside a microphone can happen without producing exact digital endpoint codes; this experiment does not diagnose it from endpoint counts.
The constructed example has 120 zeros followed by four pairs of +100000 and −100000. Its mean is exactly zero. Applying gain 4 forces eight samples beyond the signed range, so the function saturates them. Reducing those already-clipped samples by one eighth makes RMS and peak smaller, but the lost values cannot be recovered by scaling. No loud acoustic stimulation is needed: the distortion is created in software.

5. One finite capture, then stop the peripheral
The complete capture uses I2S peripheral 0, 32-bit stereo, nominal 32 kHz and a 16,384-byte internal ring buffer. It discards 4,096 startup frames, nominally 128 ms, before requesting 4,096 retained frames, nominally 128 ms. The constructor briefly starts the uncorrected native clock before the override can be applied. The initial discard exceeds the maximum 16,383 queued ring bytes plus 256 DMA bytes, 32 joined-FIFO bytes and four shift-register bytes. It then includes over 62 ms of newly corrected-phase data, above the datasheet’s 50 ms power-up figure. This capacity/rate argument is a nominal startup allowance, not a measured settling or continuity qualification. Both buffers are preallocated before starting the peripheral.
Nonblocking mode uses a completion callback. A returned request length means the buffer was queued, not that DMA filled it. The code waits with a 500 ms gross clock deadline and a separate 502-poll budget, handles ordinary tick-counter wrap through ticks_diff and rejects short requests, late completion or a frozen-clock/no-progress budget exhaustion. The callback only flips a flag. Blocking native calls, sleeps or a full hidden tick-counter wrap are not bounded by this Python polling budget. Cleanup independently attempts peripheral deinitialization and restoration of only the owned OUTOVER field before decoding, printing or writing files.
The deinitialization guarantee starts only after the native factory returns an object. Exact RP2 construction configures PIO and pins before a later DMA allocation can fail; Python cannot safely recover that unreturned partial state. Any constructor, phase, transfer or cleanup failure requires a Pico power cycle before another attempt. Restoring the override field is not proof that a failed native cleanup released its resources.
The pinned RP2 code silently declines a DMA-to-ring copy when the ring lacks space. It exposes no overflow counter through this API. Therefore a complete callback and plausible elapsed time cannot prove lossless or continuous sampling. Every report explicitly says continuity is unverified. The program intentionally makes no frequency, elapsed-waveform or acoustic-performance claim from this buffer. A disconnected DATA line can yield a timely zero buffer, which is reported as ZERO_STREAM_UNPROVEN.
Complete capture_i2s.py — intended target code, not author-executed
"""Original RPI_PICO RP2040 / stock MicroPython 1.26.0 / 125 MHz only.
GPIO10 OUTOVER corrects the selected SPH0645 clock phase. Source-qualified
intended target code, not author target execution or a physical timing guarantee.
Run once after a power cycle, with I2S0 and GP10/11/12 exclusively reserved.
"""
FRAMES, STARTUP_FRAMES, RATE, IBUF = 4096, 4096, 32000, 16384
GPIO_CTRL = (0x40014054, 0x4001405C, 0x40014064) # GP10,11,12
OUTOVER_MASK, OUTOVER_INVERT = 0x300, 0x100
class CaptureFailure(RuntimeError):
def __init__(self, phase, cause, cleanup):
self.phase = phase
self.cause_type = type(cause).__name__ if cause else None
self.cleanup_errors = cleanup
self.reset_required = True
super().__init__('CAPTURE_FAILED_' + phase + '_POWER_CYCLE_REQUIRED')
def guard_identity(implementation, platform_name):
if (implementation.name != 'micropython' or
tuple(implementation.version[:3]) != (1, 26, 0)):
raise RuntimeError('REQUIRES_MICROPYTHON_1_26_0')
if (platform_name != 'rp2' or
getattr(implementation, '_machine', '') != 'Raspberry Pi Pico with RP2040'):
raise RuntimeError('REQUIRES_ORIGINAL_RPI_PICO_RP2040')
def guard_target(implementation, platform_name, frequency):
guard_identity(implementation, platform_name)
if frequency != 125000000:
raise RuntimeError('REQUIRES_125_MHZ')
class ClockPhase:
"""Own only GPIO10 OUTOVER; native I2S owns function selection.
No other core, interrupt handler or user may change these pins/registers.
The three pins must have untouched reset CTRL values before construction.
"""
def __init__(self, mem32):
self.mem = mem32
self.attempted = False
if any(self.mem[a] != 0x1F for a in GPIO_CTRL):
raise RuntimeError('FRESH_EXCLUSIVE_PINS_REQUIRED_POWER_CYCLE')
self.saved = 0
def enable(self):
values = [self.mem[a] for a in GPIO_CTRL]
function = values[0] & 0x1F
if (function not in (6, 7) or
any(v != function for v in values)):
raise RuntimeError('UNEXPECTED_NATIVE_PIO_PIN_CONFIGURATION')
before = values[0]
self.attempted = True
desired = (before & ~OUTOVER_MASK) | OUTOVER_INVERT
self.mem[GPIO_CTRL[0]] = desired
if self.mem[GPIO_CTRL[0]] != desired:
raise OSError('CLOCK_PHASE_READBACK')
def restore(self):
if self.attempted:
now = self.mem[GPIO_CTRL[0]]
desired = (now & ~OUTOVER_MASK) | self.saved
self.mem[GPIO_CTRL[0]] = desired
if self.mem[GPIO_CTRL[0]] != desired:
raise OSError('CLOCK_RESTORE_READBACK')
def read_complete(device, buf, clock, done, timeout_ms=500):
if type(timeout_ms) is not int or not 1 <= timeout_ms <= 1000:
raise ValueError('TIMEOUT_RANGE')
done[0] = False
started = clock.ticks_ms()
queued = device.readinto(buf)
if queued != len(buf):
raise OSError('READ_REQUEST_SIZE')
for _ in range(timeout_ms + 2):
if done[0]:
break
elapsed = clock.ticks_diff(clock.ticks_ms(), started)
if elapsed < 0 or elapsed >= timeout_ms:
raise OSError('CALLBACK_TIMEOUT')
clock.sleep_ms(1)
if not done[0]:
raise OSError('CALLBACK_POLL_BUDGET')
elapsed = clock.ticks_diff(clock.ticks_ms(), started)
if elapsed < 0 or elapsed >= timeout_ms:
raise OSError('LATE_COMPLETION')
return elapsed
def capture(factory=None, clock=None, phase=None):
import sys
if factory is None:
# Check exact identity before importing/accessing the RP2040 MMIO adapter.
guard_identity(sys.implementation, sys.platform)
from machine import I2S, Pin, mem32, freq
guard_target(sys.implementation, sys.platform, freq())
phase = ClockPhase(mem32)
def factory():
return I2S(0, sck=Pin(10), ws=Pin(11), sd=Pin(12), mode=I2S.RX,
bits=32, format=I2S.STEREO, rate=RATE, ibuf=IBUF)
elif phase is None:
raise ValueError('INJECTED_FACTORY_REQUIRES_PHASE_ADAPTER')
if clock is None:
import time
clock = time
# Preallocate. Discard > ring + DMA/FIFO backlog, then >50 ms of corrected
# phase data. This is not a continuity guarantee or physical settling test.
startup = bytearray(STARTUP_FRAMES * 8)
raw = bytearray(FRAMES * 8)
done = [False]
def completed(_):
done[0] = True
device = None
stage, error, cleanup = 'CONSTRUCT', None, []
try:
device = factory()
stage = 'CLOCK_PHASE'
phase.enable() # after constructor: SDK GPIO setup clears overrides
stage = 'IRQ'
device.irq(completed)
stage = 'STARTUP_DRAIN'
startup_ms = read_complete(device, startup, clock, done)
stage = 'CAPTURE'
capture_ms = read_complete(device, raw, clock, done)
except BaseException as exc:
error = exc
finally:
if device is not None:
try:
device.deinit()
except BaseException as exc:
cleanup.append('deinit:' + type(exc).__name__)
# Independent attempt, including after deinit failure. No native recovery
# is claimed when a constructor raises without returning an object.
try:
phase.restore()
except BaseException as exc:
cleanup.append('clock_restore:' + type(exc).__name__)
if error is not None or cleanup:
raise CaptureFailure(stage, error, cleanup) from error
from audio_metrics import decode_stereo, metrics
values = decode_stereo(raw)
return raw, {'runtime': str(sys.implementation), 'rate_requested_Hz': RATE,
'frames': FRAMES, 'format': 'little-endian signed32 stereo, left first',
'left_decode': 'signed32 >> 14 after zero-padding check',
'GPIO10_OUTOVER_during_capture': 'INVERT; restored after deinit attempt',
'startup_frames_discarded': STARTUP_FRAMES,
'startup_elapsed_ms': startup_ms, 'capture_elapsed_ms': capture_ms,
'continuity': 'UNVERIFIED_NO_OVERFLOW_COUNTER',
'physical_alignment_verified_by_author': False,
'metrics': metrics(values)}
if __name__ == '__main__':
import json
raw, report = capture()
with open('capture.pcm', 'wb') as f:
if f.write(raw) != len(raw):
raise OSError('SHORT_FILE_WRITE')
with open('capture-metadata.json', 'w') as f:
f.write(json.dumps(report))
print(json.dumps(report))
Complete audio_metrics.py — actual host-tested decoder and calculations
"""Host/MicroPython finite PCM decoder. No sound-pressure calibration."""
import math
import struct
MIN18, MAX18 = -131072, 131071
MAX_FRAMES = 4096
def decode_stereo(raw):
if not isinstance(raw, (bytes, bytearray)) or not raw or len(raw) % 8:
raise ValueError('STEREO_FRAME_SIZE')
if len(raw) > MAX_FRAMES * 8:
raise ValueError('FRAME_LIMIT')
samples = []
for i in range(0, len(raw), 8):
left, right = struct.unpack_from('<ii', raw, i)
if left & 0x3fff:
raise ValueError('LEFT_PADDING_OR_ALIGNMENT')
if right != 0:
raise ValueError('INACTIVE_RIGHT_NOT_ZERO')
samples.append(left >> 14)
return samples
def check_samples(samples):
if not isinstance(samples, (list, tuple)) or not 1 <= len(samples) <= MAX_FRAMES:
raise ValueError('SAMPLE_COUNT')
if any(type(x) is not int or not MIN18 <= x <= MAX18 for x in samples):
raise ValueError('SIGNED_18_BIT')
def metrics(samples):
check_samples(samples)
n = len(samples)
mean = sum(samples) / n
rms = math.sqrt(sum(x * x for x in samples) / n)
ac_rms = math.sqrt(sum((x - mean) ** 2 for x in samples) / n)
peak = max(abs(x) for x in samples)
return {'samples': n, 'mean_codes': mean, 'rms_codes': rms,
'ac_rms_codes': ac_rms, 'peak_codes': peak,
'peak_fraction_FS': peak / 131072,
'rail_hits': sum(x in (MIN18, MAX18) for x in samples),
'stream_state': 'ZERO_STREAM_UNPROVEN' if peak == 0 else 'DIGITAL_VALUES_ONLY'}
def gain_clip(samples, gain):
check_samples(samples)
if type(gain) not in (int, float) or not math.isfinite(gain) or not .125 <= gain <= 16:
raise ValueError('GAIN')
out, saturated = [], 0
for x in samples:
y = int(round(x * gain))
if y < MIN18 or y > MAX18:
saturated += 1
out.append(min(MAX18, max(MIN18, y)))
return out, saturated
def synthetic_demo():
# Exactly zero mean sparse +/- peaks before asymmetric digital saturation.
original = [0] * 120 + [100000, -100000] * 4
clipped, count = gain_clip(original, 4)
quieter, _ = gain_clip(clipped, .125)
return {'synthetic_not_microphone_capture': True,
'original': metrics(original), 'gain4_clipped': metrics(clipped),
'gain4_samples_saturated': count, 'after_gain_one_eighth': metrics(quieter),
'warning': 'Rescaling does not restore the removed peak shapes; no SPL or acoustic-clipping diagnosis.'}
if __name__ == '__main__':
import json
import sys
if len(sys.argv) == 1:
print(json.dumps(synthetic_demo(), indent=2))
elif len(sys.argv) == 2:
with open(sys.argv[1], 'rb') as f:
raw = f.read(MAX_FRAMES * 8 + 1)
values = decode_stereo(raw)
scaled, count = gain_clip(values, 4)
print(json.dumps({'raw': metrics(values), 'digital_gain4': metrics(scaled),
'gain_saturated_samples': count, 'continuity': 'UNVERIFIED'}, indent=2))
else:
raise SystemExit('Usage: python audio_metrics.py [capture.pcm]')
Capture program · Decoder and analysis program · Blank worksheet · Complete programs and tests ZIP
6. Reproduce arithmetic first, then qualify your own fixture
- On the host, run
python audio_metrics.pyand inspect the explicitly synthetic example. Runpython -B test_audio.pyfor the delivered decoder and capture-wrapper control tests. Fake transfer devices exercise Python cleanup/deadlines only; they are not a Pico emulator or DMA validation. - With all power removed, identify the SPH0645 carrier and check the table’s continuity, common ground, pull-down, capacitor and SEL connection. Leave its bottom acoustic port clear. Do not probe inside the port or blast compressed air into it.
- Copy both complete Python files to an original RP2040 Pico running the specified MicroPython version. Use the stock RPI_PICO build at 125 MHz, power-cycle first and leave the reserved pins/I2S0/PIO/DMA resources unused by other code. The capture rejects other identities and dirty pin configuration; power-cycle again after an initialization or cleanup error.
- Power through USB and configure high-impedance, suitable-bandwidth probes referenced to common ground for triggered acquisition before starting the code. During step 5’s single finite run, record loaded DATA HIGH≥2.0 V/LOW≤0.8 V, nominal 2.048 MHz physical BCLK, 32 kHz WS, 64 clocks per frame, WS transition on physical BCLK falling, data settling before the falling sampling edge, left-channel selection and serial bit delay. After capture stops, inspect that recorded trace before trusting the samples. Short leads matter; datasheet edge/load conditions are not guaranteed by a breadboard.
- Use normal quiet-room sounds only. Run
capture_i2s.pyonce. It stops I2S, writes capture.pcm and capture-metadata.json, and prints diagnostics. A storage error or failed decode is an error, not a valid measurement. The worksheet is blank because no such physical run was performed by the author. - Copy the binary file to the host and run
python audio_metrics.py capture.pcm. Preserve the raw file and metadata together. Compare mean, RMS, peak, rail hits and the intentionally applied digital-gain saturation count. Never turn these values into decibels of sound pressure without calibration and a reviewed acoustic chain.
7. Interpret faults and scope honestly
The author’s 40 host groups exercise exact boundary words, little-endian decoding, negative sign extension, padding faults, wrong channel, truncated/oversized frames, RMS/DC arithmetic, saturation, rescaling and selected-code round trips. Capture-control tests execute the actual wrapper with a fake transfer object for timeout, short queue return, late callback, tick wrap, data rejection, finite frozen-clock polls, phase-register ownership/readback, partial constructor failure and independent cleanup. A model executes the exact eight pinned native RX opcodes to verify the physical edge inversion; it is not target execution. They explicitly do not run the RP2040 PIO, MicroPython native I2S implementation or physical microphone.
The zero-padding predicate is deliberately strict for this fixture. If it fails on a real board, preserve the raw bytes and diagnose clocks, alignment, pull-down and exact microphone identity; do not “repair” numbers by guessing a new shift until they look plausible. If the inactive right channel is nonzero, check SEL and bus behavior. If both channels are zero, check power and timing instead of declaring perfect silence. Clock completion and electrical/acoustic health are separate observations.
For basic component and circuit terminology use the learning course and Circuitpedia. This lesson adds a bounded digital-format experiment; it does not claim a calibrated audio instrument or an owner-unit hardware test.
8. Challenge: can the lower number prove recovery?
A captured sequence has a mean near zero. After multiplying by four and saturating, you turn the result down until its RMS is below the original RMS. Has the clipping been removed?
Answer
No. Clipping mapped many possible input values to the same rail. Later attenuation changes amplitude, not the missing information. A zero mean also allows large equal positive and negative peaks. Inspect peak and known saturation counts, preserve raw data, and keep digital clipping separate from acoustic overload and continuity claims.
9. Primary references
- Adafruit PID 3421 and its SPH0645 schematic downloads; pinned carrier source in the evidence.
- Knowles SPH0645LM4H-B Rev B, operating limits, channel/format, timing, pull-down and package terminals. Rev B corrects the timing diagram; no separate erratum was identified from these current official links.
- MicroPython 1.26.0 I2S API and pinned RP2/extmod implementation: technical-preview API, frame mapping, nonblocking descriptor and silent ring-drop limitation.
- RP2040 datasheet, GPIO10 CTRL/OUTOVER, PIO input synchronization and 3.3 V input thresholds; the exact MicroPython SDK submodule GPIO source confirms that function setup clears overrides.
- Original Pico pinout for physical terminal numbers.