tinker.toadie
tinker.toadie

Reputation: 21

White noise but no sound // Recording audio from electret microphone preamp on Raspberry Pi Pico (ADC) // Micropython

I am pretty new to the microcontroller game. Recently I am trying to record audio on a Raspberry Pi Pico from an analogue electret microphone. Thus, I have built a (nicely working) preamp for the mic and connected it via ADC pins as explained later in detail.

For now I just want to store it as .wav file on the internal flash memory using MicroPython. At the moment this MicroPython code seems to be the main issue since it produces a .wav file that contains nothing but white noise.

What I did so far is the following...

(1) Build a MCU interface preamp for the electret microphone

I came across a usefull preamp that utilizes an MCP6001 OpAmp on microchip.com. The drawing looks like this: Conventional MCP6001 preamp for electret microphones

Here is the preamp soldered to a circuit board.

Tests with this preamp worked out pretty well. Using a two AA batteries as power supply I measure the supply voltage V_DD = 2.620(1)V and an output bias voltage V_out0 = 1.310(1)V with a multimeter. The peak-to-peak-signal is V_out ~ 0.2-0.3V measured with an oscilloscope when I whistle into the microphone. The signal-to-noise-ratio (SNR) is quite good as shown below:

Oscilloscope measurement of V_out on battery supplied preamp

I then connected the preamp with the Pico pins ADC_VREF (GPIO 35) and AGND (GPIO 31) of the Pico which is powered via USB. The preamp output is not connected to the Pico yet as shown in the measuring setup. Now I get a supply voltage V_DD = 3.2(1)V and an output bias voltage V_0 = 1.58(10)V which is much less stable in comparison to the battery version but the SNR should be high enough to record some audio I think:

Oscilloscope measurement of V_out on Pico ADC_VREF supplied preamp

(2) Sampling from ADC on the Pico / MicroPython shell

When I now connect the preamp output to ADC0 (GPIO 31 == Pin 26) and run the following code in the Thonny MicroPython shell

>>> from machine import Pin, ADC
>>> mic = ADC(Pin(26))
>>> mic.read_u16()
32551
>>> mic.read_u16()
31911
>>> mic.read_u16()
31815

I get some reasonable reading values considering 16-bit ADC with maximum value 2^16=65536.

(3) Saving samples into .wav file on the Pico / MicroPython main code

Then, I wrote a code to create and write a .wav header into test.wav in the Pico's flash memory (based on Mike Teachman's I2S examples), read binary samples into a buffer and append test.wav by this buffered values.

from machine import Pin
import time

micPin = machine.ADC(Pin(26)) 
FILE_NAME = 'test17.wav'

sampleRate      = 44100 # 44.1 kHz - CD quality
bitsPerSample   = 16 
num_channels    = 1 # 1 - MONO, 2 -  STEREO

duration        = 3 # in seconds
bufferSize      = duration * sampleRate # 1/s * s

num_samples     = bufferSize

def create_wav_header(sampleRate, bitsPerSample, num_channels, num_samples):
    datasize = num_samples * num_channels * bitsPerSample // 8
    o = bytes("RIFF", "ascii")  # 1-4 (4byte) Marks file as RIFF
    o += (datasize + 36).to_bytes(
        4, "little"
    )  # 5-8 (4byte) File size in bytes excluding this and RIFF marker
    o += bytes("WAVE", "ascii")  # 9-12 (4bytes) File type
    o += bytes("fmt ", "ascii")  # 13-16 (4bytes) Format Chunk Marker, trailing 0
    o += (16).to_bytes(4, "little")  # 17-20 (4bytes) Length of above format data
    o += (1).to_bytes(2, "little")  # 21-22 (2bytes) Format type (1 - PCM)
    o += (num_channels).to_bytes(2, "little")  # 23-24 (2bytes)
    o += (sampleRate).to_bytes(4, "little")  # 25-28 (4bytes)
    o += (sampleRate * num_channels * bitsPerSample // 8).to_bytes(4, "little")  # 29-32 (4bytes)
    o += (num_channels * bitsPerSample // 8).to_bytes(2, "little")  # 33-34 (2bytes)
    o += (bitsPerSample).to_bytes(2, "little")  # 35-36 (2bytes)
    o += bytes("data", "ascii")  # 37-40 (4bytes) Data Chunk Marker
    o += (datasize).to_bytes(4, "little")  # 41-44 (4bytes) Data size in bytes
    return o


try:
    with open('/'+FILE_NAME, 'wb') as recFile:
        
        wav_header = create_wav_header(
            sampleRate,
            bitsPerSample,
            num_channels,
            num_samples
            )
        recFile.write(wav_header)
    
    with open('/'+FILE_NAME, 'ab') as recFile:    
        
        buffer = bytearray(bufferSize)

        for i in range(bufferSize):
            buffer[i] = micPin.read_u16()
#            print(buffer[i])
            time.sleep(1/sampleRate) 
    
        recFile.write(buffer)
        
except (KeyboardInterrupt, Exception) as e:
    print("caught exception {} {}".format(type(e).__name__, e))

I am aware that running the micropython firmware occupies a lot of resources and the sound should be quite glitchy due to timing issues (found some advices to use C++ and DMA to prevent this). Still, I would expect to hear at least SOMETHING in the .wav but all I find is white noise... hexdump -C test.wav shows something like

00000000  52 49 46 46 bc 09 04 00  57 41 56 45 66 6d 74 20  |RIFF....WAVEfmt |
00000010  10 00 00 00 01 00 01 00  44 ac 00 00 88 58 01 00  |........D....X..|
00000020  02 00 10 00 64 61 74 61  98 09 04 00 a8 88 88 f8  |....data........|
00000030  c8 28 18 98 88 98 d8 18  28 38 d8 b8 c8 88 a8 08  |.(......(8......|
00000040  e8 38 78 98 b8 c8 e8 08  18 28 18 c8 b8 58 38 e8  |.8x......(...X8.|
00000050  58 38 48 18 48 88 d8 38  b8 f8 08 28 28 58 68 88  |X8H.H..8...((Xh.|
00000060  a8 88 58 98 a8 a8 88 08  08 28 58 68 68 58 68 98  |..X......(XhhXh.|
00000070  88 c8 e8 e8 08 38 48 38  28 28 18 68 18 28 38 48  |.....8H8((.h.(8H|
00000080  58 68 88 98 88 68 58 48  38 18 28 38 e8 e8 08 48  |Xh...hXH8.(8...H|
00000090  98 c8 e8 28 18 58 08 48  28 08 68 a8 c8 b8 d8 e8  |...(.X.H(.h.....|
000000a0  18 08 f8 e8 b8 e8 e8 38  88 78 88 58 e8 68 e8 28  |.......8.x.X.h.(|
000000b0  28 48 38 48 88 c8 08 08  28 58 d8 18 48 88 a8 98  |(H8H....(X..H...|
000000c0  b8 a8 c8 a8 b8 b8 b8 d8  d8 18 28 f8 08 28 18 28  |..........(..(.(|

(excerpt)

Also, when I uncomment the print(buffer[i]) command in the for loop there are values read like

84
8
88
200
184
152

These explain the white noise. But I do not understand why micPin.read_u16() seems to fail here whilst it works in the shell...

What am I doing wrong and/or how I could improve this project?

EDIT 1 Going with David Grayson's advices I solved the issue with the wrong data type of the buffer. Storing 16-bit ADC values in a bytearray is going to fail of course since the 8 bits are lost. See the following code in which for testing purposes I create a sine wave which simulates the ADC input values (2**16-1)//2+(amplitude of the sine wave). These 16-bit values are now converted first to bytes using amplitude.to_bytes(2,'little') and then stored in the bytearray-buffer. Whenever bufferSize is reached the buffer is written to recFile and cleared. This creates a perfectly audible test_sine.wav in the Pico's flash memory.

from machine import Pin, ADC
import time
import math # for sine wave

micPin = ADC(Pin(26)) 
FILE_NAME = 'test_sine.wav'

sampleRate      = 44100 # 44.1 kHz - CD quality
bitsPerSample   = 16 
num_channels    = 1 # 1 - MONO, 2 -  STEREO

duration        = 3 # in seconds
recordSize      = duration * sampleRate # 1/s * s
bufferSize      = 1000 

num_samples     = recordSize

def create_wav_header(sampleRate, bitsPerSample, num_channels, num_samples):
    datasize = num_samples * num_channels * bitsPerSample // 8
    o = bytes("RIFF", "ascii")  # 1-4 (4byte) Marks file as RIFF
    o += (datasize + 36).to_bytes(
        4, "little"
    )  # 5-8 (4byte) File size in bytes excluding this and RIFF marker
    o += bytes("WAVE", "ascii")  # 9-12 (4bytes) File type
    o += bytes("fmt ", "ascii")  # 13-16 (4bytes) Format Chunk Marker, trailing 0
    o += (16).to_bytes(4, "little")  # 17-20 (4bytes) Length of above format data
    o += (1).to_bytes(2, "little")  # 21-22 (2bytes) Format type (1 - PCM)
    o += (num_channels).to_bytes(2, "little")  # 23-24 (2bytes)
    o += (sampleRate).to_bytes(4, "little")  # 25-28 (4bytes)
    o += (sampleRate * num_channels * bitsPerSample // 8).to_bytes(4, "little")  # 29-32 (4bytes)
    o += (num_channels * bitsPerSample // 8).to_bytes(2, "little")  # 33-34 (2bytes)
    o += (bitsPerSample).to_bytes(2, "little")  # 35-36 (2bytes)
    o += bytes("data", "ascii")  # 37-40 (4bytes) Data Chunk Marker
    o += (datasize).to_bytes(4, "little")  # 41-44 (4bytes) Data size in bytes
    return o


try:
    with open('/'+FILE_NAME, 'wb') as recFile:
        
        wav_header = create_wav_header(
            sampleRate,
            bitsPerSample,
            num_channels,
            num_samples
            )
        recFile.write(wav_header)
        print('Header written')
        
    with open('/'+FILE_NAME, 'ab') as recFile:
        
        buffer = bytearray()
        
        # sine wave creation
        for i in range(recordSize):

            x = (2 * math.pi) / 1000    # devide by an arbitray number
                                        # to create steps
            y = math.sin(i * x)
            amplitude = math.floor((2**16-1//2) + ((2**16-1)//2)*y)
            
            if i%bufferSize==0:
                # write buffer to file and empty it
                recFile.write(buffer)
#                print('buffer written')
                buffer = bytearray()
                buffer.extend(amplitude.to_bytes(2,'little'))
            else:
                buffer.extend(amplitude.to_bytes(2,'little'))        

except (KeyboardInterrupt, Exception) as e:
    print("caught exception {} {}".format(type(e).__name__, e))

I should now be able to perform the same converting procedure on the real ADC values from the microphone preamp.

Upvotes: 2

Views: 274

Answers (1)

David Grayson
David Grayson

Reputation: 87486

You're trying to store a 16-bit number into a byte, which is 8 bits:

buffer = bytearray(bufferSize)
...
buffer[i] = micPin.read_u16()

Of course the top 8 bits of the sample will be cut off, leaving the lower 8 bits, which is never what you would want.

A more appropriate data type for your buffer would be an array with typecode h (16-bit signed).

Just for now, to help troubleshoot, you shouldn't try to record real ADC readings in the file. Just generate a sine wave so you can be sure your WAV encoding and file output code is working. Once that is working, then you could try integrating actual microphone hardware.

Also, to avoid large delays from the filesystem code, you might need to allocate space for your file ahead of time by writing blank data. See my comment on GitHub.

I generally expect that MicroPython's garbage collector and slowness in general will make this project impossible.

Upvotes: 2

Related Questions