Reputation: 21
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
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