DanB91
Posts: 10
Joined: Tue Jan 08, 2019 3:04 pm

Understanding PWM audio

Wed Apr 07, 2021 1:25 am

Hi All,

I am trying to get and understanding of PWM sound for the Raspberry Pi Pico. I have a passive buzzer hooked up to pin 10 (GPIO 7) of my pico and ran an example Python program that plays some tones and it seems to work fine.

Eventually I'd like to try to play a wav file and I understand I'll probably need to swap out my buzzer for an actual speaker. But for now, I am trying to play a simple WAV file that is just a tone of middle C. My goals is to conceptually understand how PWM works. I have some understanding, but I feel there are some gaps here.

I've dumped this file into a byte array in Python. These are the details of this sound:

8000 samples/sec
1 second long
8 bit (1 byte) unsigned samples

This means, the sound is exactly 8000 samples (i.e. 8000 bytes) large. As far as I understand, every 1/8000 of a second, I should set the PWM duty cycle to that sample since the PWM duty cycle will "fake" the analog equivalent of that sample. And since the PWM duty cycle can go from 0 to 65535, I multiply each 1 byte sample by (65535/255). Is my understanding of this correct?

The issue I am having right now is that the passive buzzer makes this glitchy high pitched sound, which is definitely not middle C. I am not sure if it is because of my understanding of PWM, if I have a buzzer rather than a speaker, or if my code is incorrect.

One of my confusions is I am not sure what to set the frequency of the PWM to since I am using a timer to feed the sample. Right now I have it set to 8000 which is the same as the timer frequency, but I have a feeling this is wrong.

This is my code:

Code: Select all

from machine import Pin, PWM, Timer
buzzer = PWM(Pin(7))

def tick(timer):
    global samples, pos
    if pos >= len(samples):
        buzzer.duty_u16(0)
        timer.deinit()
        return
    buzzer.duty_u16(samples[ pos ]*(65535//255))
    pos += 1

buzzer.freq(8000) #is this right??
timer = Timer()
timer.init(freq=8000, mode=Timer.PERIODIC, callback=tick)
pos = 44 #skip the WAVE header which is 44 bytes long
samples = [
#WAV file here
]

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Wed Apr 07, 2021 2:09 am

Setting your frequency at 8000Hz will be detected to be a sound. You need to go higher until we won't detect that noise.

The PWM is to create a analog sound from a digital timing signal. The ratio ON/(ON+OFF) in time determine the analog value of your signal.
the frequency of the digital cycle need to be way above our hearing frequency. Something above 48Khz.

Now change the PWM every 1/8000 Hz.

Also add a RC filter to smooth the output.

Class PWM8 to convert PWM to 8 bits

Code: Select all

from machine import PWM

class PWM8(PWM):
    
    def duty(self,value=None):
        if value is None:
            return self.duty_u16() >> 8
        
        if value > 254:
            self.duty_u16(0xfffe)
        else:
            self.duty_u16((value << 8) | value)
        
    
    
if __name__ == "__main__":
    from machine import Pin
    from PWM8 import PWM8
    pwm = PWM8(Pin(10))
    pwm.duty(128)
    print("current duty is ",pwm.duty())

pidd
Posts: 1905
Joined: Fri May 29, 2020 8:29 pm
Location: Wirral, UK
Contact: Website

Re: Understanding PWM audio

Wed Apr 07, 2021 2:47 am

If you wanted to produce a 200Hz square wave using your 8KHz frequency, this would need 8000/200 = 40 samples, the first 20 you would write "0" and the second 20 you would write "65535" then repeat.

The most normal sample rate to use is 44.1KHz

For a middle C square wave using 44.1KHz frequency there would be around 168 samples so 84 lows followed by 84 highs.

Once you get into sine waves you will notice that you only have 168 samples so you can't use the full 65535 bits of resolution, it goes in steps.

For more accuracy instead of looking at one cycle, you can do a number of cycles or continuous depending how your application generates a note.

Sound file data is a lot easier because it is normally sampled at 44.1KHz so you don't have to do any sample rate conversion, if not it is usually a simple multiple or divisor of 44.1KHz.

Be aware of the offset, no sound will be at 32767 or 32768 - I'm not sure which is conventional.
Last edited by pidd on Wed Apr 07, 2021 2:56 am, edited 1 time in total.

DanB91
Posts: 10
Joined: Tue Jan 08, 2019 3:04 pm

Re: Understanding PWM audio

Wed Apr 07, 2021 2:50 am

danjperron wrote:
Wed Apr 07, 2021 2:09 am
Setting your frequency at 8000Hz will be detected to be a sound. You need to go higher until we won't detect that noise.

The PWM is to create a analog sound from a digital timing signal. The ratio ON/(ON+OFF) in time determine the analog value of your signal.
the frequency of the digital cycle need to be way above our hearing frequency. Something above 48Khz.

Now change the PWM every 1/8000 Hz.

Also add a RC filter to smooth the output.

Class PWM8 to convert PWM to 8 bits

Code: Select all

from machine import PWM

class PWM8(PWM):
    
    def duty(self,value=None):
        if value is None:
            return self.duty_u16() >> 8
        
        if value > 254:
            self.duty_u16(0xfffe)
        else:
            self.duty_u16((value << 8) | value)
        
    
    
if __name__ == "__main__":
    from machine import Pin
    from PWM8 import PWM8
    pwm = PWM8(Pin(10))
    pwm.duty(128)
    print("current duty is ",pwm.duty())
Thank you very much. So I changed the frequency to 64000, kept the timer frequency the same and merged some of your code into mine (I also got rid of that 65535/255 multiplier). I am not sure if the new tone matches, but it notice the sound plays for fraction of a second, cuts out and then resumes for the rest of the second. It's also pretty quiet and not exactly pleasant sounding. The sound is so jagged that I can't tell if it's middle C. Maybe I need an RC filter to tell? This is my new code. Does anything stand out as wrong?

EDIT: I have the code now just repeat from the sample array when it reaches the end. After I did that, that glitch only comes up at the begging, but it doesn't reappear. It might be middle C, but I have hard time telling. I'll have to try other WAVE files

Code: Select all

from machine import Pin, PWM, Timer

class PWM8(PWM):
    def duty(self,value=None):
        if value is None:
            return self.duty_u16() >> 8
        if value > 254:
            self.duty_u16(0xfffe)
        else:
            self.duty_u16((value << 8) | value)
buzzer = PWM8(Pin(7))

def tick(timer):
    global samples, pos
    if pos >= len(samples):
        pos = 44
    buzzer.duty(samples[ pos ])
    pos += 1

buzzer.freq(64000)
timer = Timer()
timer.init(freq=8000, mode=Timer.PERIODIC, callback=tick)
pos = 44
samples = [
#WAVE file here
]

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Wed Apr 07, 2021 2:39 pm

Is your passive buzzer a piezo or a speaker?

You know that piezo has up to 40db difference between 100Hz and 1KHz!

User avatar
shabtronic
Posts: 71
Joined: Mon Feb 08, 2021 11:13 pm

Re: Understanding PWM audio

Thu Apr 08, 2021 1:32 am

The key parts of PWM audio are:

Integrate one cycle of a square wave - what do you get? zero!!

Integrate one cycle of a square wave with a duty cycle - what do you get,
some value + or - depending on the %duty cycle - why? because the + and - parts
of the square wave are no longer equal in time length and no longer cancel out to zero.

How do we Integrate in the electronics world - using a capacitor, a coil, speaker
e.t.c

All of those are "leaky" integrators - meaning they decrease value to zero over time.

A few things effect the quality of PWM audio:

1) Timing jitter - obviously if the square wave timing varies at all, it
will mess up the integration.

2) Quality of the caps/coils - component tolerance e.t.c.


Things to research if you're interested:

Noise shaping
Class-D amplifiers
Class-T amplifiers (tripath)
Switching power supply
Delta Sigma modulation/encoders
Pulse Density Modulation Audio
DSD Audio
Early 8bit games (Chronos zx spectrum) e.t.c.
Last edited by shabtronic on Thu Apr 08, 2021 4:34 am, edited 2 times in total.

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Thu Apr 08, 2021 2:04 am

Fun Fact!

I was thinking of using DMA but I found out that the PWM register need to be written with a 32 bits path. Bummer!
Using the DMA with the special included timer will have removed the lag you get with the interrupt.

To do that I did subclass the PWM to force by hardware a PWM output from 0 to 255. The current PWM use a formula to modify the input
values (0,65534) to (0, Max count provide by the frequency). Then since we want a very high frequency, I assume around 60Khz, I force the
hardware register to set the TOP count to be 255. This way the PWM is directly inserted to the PWM register.

My intention was to use the DMA at a specific rate to send the raw ".wav" sound but since register only allows 32 bits I'm stuck on that path.
But it is possible to send 16 bits, ( channel A and B will be the same). If one of the PWM is used there is no problem. It should work!.

This is my subclass HD_PWM.py

Code: Select all

from machine import PWM,Pin,mem32


class  HD_PWM(PWM):
    def __init__(self,pin):
        self.id = int(str(pin)[4:-1].split(',')[0])
        self.A_B = self.id & 1
        self.channel = self.id >> 1
        super().__init__(pin)
        super().freq(500_000)
        super().duty_u16(0)
        # set memory base
        self.PWM_BASE = 0x4005_0000 + (self.channel * 0x14)
        # set divider base
        self.PWM_DIV = self.PWM_BASE + 4
        # set  top base
        self.PWM_TOP = self.PWM_BASE + 16
        # set  cc base
        self.PWM_CC = self.PWM_BASE + 12
        
        #ok we want frequency around 60KHz and max top at 255
        # 125Mhz / (255 * 60000) => 8.1
        # then  125MHZ / ( 8 * 255) = 61275Hz
        
        # set divider to 8
        mem32[self.PWM_DIV] =  8 << 4
        # set top to 255
        mem32[self.PWM_TOP] = 255   
        
    def duty(self, value):
        if value > 255:
            value = 255
        reg = mem32[self.PWM_CC]
        if self.A_B == 0:
            # ok change channel A
            mem32[self.PWM_CC]=  (reg & 0xffff0000) | value
        else:
            # change channel B
            mem32[self.PWM_CC]= ( reg & 0xffff) | (value << 16)
            
            
if __name__ == "__main__":
    import utime
    pwm = HD_PWM(Pin(15))
    try:
        value = 0
        increment = 1
        while True:
            value = value % 256
            pwm.duty(value)
            if value == 0:
                increment = increment * (-1)
            value += increment
            utime.sleep_ms(1)
    except KeyboardInterrupt:
        pwm.deinit()


And this is my DMA trial class.

Code: Select all

from machine import mem32
import uctypes


class myDMA:
    
    def __init__(self, channel,timer=None, clock_MUL=1, clock_DIV=1):
        self.channel = channel
        self.timer = timer
        self.DMA_BASE = 0x50000000
        self.DMA_CH_BASE = self.DMA_BASE + (0x40 * channel)
        self.READ_ADDR = self.DMA_CH_BASE + 0
        self.WRITE_ADDR = self.DMA_CH_BASE + 4 
        self.TRANS_COUNT = self.DMA_CH_BASE + 8
        self.CTRL_TRIG = self.DMA_CH_BASE + 12
        self.MULTI_TRIG = self.DMA_BASE + 0x430
        self.timer_channel = timer
        if self.timer_channel is None:
            self.TIMER = None
        else:
            self.TIMER = self.DMA_BASE + 0x420 + ( 4 * self.timer_channel)
        self.clock_MUL= clock_MUL
        self.clock_DIV = clock_DIV

    def  move(self, src_add, dst_add,count,data_size=1,src_inc=True, dst_inc=True):

        if data_size == 1 :
           DATA_SIZE = 0
        elif data_size == 2:
           DATA_SIZE = 1
        elif data_size == 4:
           DATA_SIZE = 2
        else:
            return False

        mem32[self.CTRL_TRIG] = 0
#        mem32[self.WRITE_ADDR] = uctypes.addressof(dst)
#        mem32[self.READ_ADDR] = uctypes.addressof(src)
        mem32[self.WRITE_ADDR] = dst_add
        mem32[self.READ_ADDR] =  src_add
        mem32[self.TRANS_COUNT] = count // data_size
        ctrl = 1
        ctrl += (DATA_SIZE << 2)
        
        if self.timer_channel is None:
            ctrl += (0x3f << 15)
        else:
            mem32[self.TIMER]= self.clock_MUL << 16 | self.clock_DIV
            ctrl += ((0x3b + self.timer_channel) << 15)

            
            
        ctrl += (self.channel << 11)

        if src_inc:
            ctrl += 0x10
        if dst_inc:
            ctrl += 0x20
        mem32[self.CTRL_TRIG] = ctrl
        while True:
            flag = mem32[self.CTRL_TRIG]
            if ( flag & 0x8000_0000) == 0x8000_0000:
                mem32[self.CTRL_TRIG] = 0
                return False
            if (flag & ( 1<<24)) == 0:
                mem32[self.CTRL_TRIG] = 0
                return True
            
if __name__ == "__main__":
    import urandom, time
    tSize = 96
    src = bytearray(tSize)
    dst = bytearray(tSize)

    for i in range(tSize):
        src[i]= urandom.randint(0,255)
 
   # initialize DMA channel 11 , use timer 3 and set clock to 125MHz/15625 = 8000Hz
    dma = myDMA(11,timer=3,clock_MUL=1, clock_DIV=15625)
    start = time.ticks_us()
    dma.move(uctypes.addressof(src),uctypes.addressof(dst),tSize)
    end = time.ticks_us() 

    print("src= ",src)
    print("\ndst= ",dst)

    length_us = end - start
    if length_us > 0:
        print("\ntook {} us  rate = {} bytes /sec\n".format(length_us,1_000_000.0 * tSize / length_us))     

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Fri Apr 09, 2021 2:37 pm

Ok I made a python script that could read a 8KHz stereo *.wav file and output the audio via PWM.

https://github.com/danjperron/PicoAudioPWM

I should have use C but I tried with micropython.

The bottleneck is the conversion from signed 16 bits of the wave data to unsigned 8 bit to the PWM.

I use a single DMA but it should be chaining DMA . When one DMA is done it should trigger another DMA channel to send the next
chunk of audio data.

It is limited to 8KHz. I tried other frequency but python is too slow.

to convert your wave file to 8KHz sample use sox.

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Fri Apr 09, 2021 7:06 pm

I updated my code on github.

it is now possible to use 22KHz wave file in stereo.

I forget that I can't overwrite a buffer when I do a DMA on It.
The simple method was to use two buffers and two DMA channels and alternate them.

Also I change the way I convert wav 16 bit signed to (-32768..32768) to (0.. 255) unsigned 16 bits.
Using a lambda method was more elegant and way faster.

blippy
Posts: 149
Joined: Fri Nov 03, 2017 3:07 pm

Re: Understanding PWM audio

Sat Apr 10, 2021 6:51 pm

I am trying to get and understanding of PWM sound for the Raspberry Pi Pico. I have a passive buzzer hooked up to pin 10 (GPIO 7) of my pico and ran an example Python program that plays some tones and it seems to work fine.

Eventually I'd like to try to play a wav file
You're probably going to have to ditch Python, and use C/C++. Python is likely to be way too slow.



Eventually I'd like to try to play a wav file and I understand I'll probably need to swap out my buzzer for an actual speaker. But for now, I am trying to play a simple WAV file that is just a tone of middle C.
WAV files contain headers. You can try to decode them if you like, but it will be easier if you convert the files to RAW format of the desired bitrate and datatype. RAW just means raw binaries of the samples.

You may be interested in a little project that I did that demonstrates playing hard-coded audio:
https://github.com/blippy/rpi/tree/mast ... -hard-pico

It seems to work quite well.

I've made some notes about converting wav to raw here:

https://github.com/blippy/rpi/tree/master/audio

You might also consider Audacity if you on Windows.





My goals is to conceptually understand how PWM works. I have some understanding, but I feel there are some gaps here.
I've dumped this file into a byte array in Python. These are the details of this sound:

8000 samples/sec
1 second long
8 bit (1 byte) unsigned samples

This means, the sound is exactly 8000 samples (i.e. 8000 bytes) large. As far as I understand, every 1/8000 of a second, I should set the PWM duty cycle to that sample since the PWM duty cycle will "fake" the analog equivalent of that sample. And since the PWM duty cycle can go from 0 to 65535, I multiply each 1 byte sample by (65535/255). Is my understanding of this correct?
Note sure 65535 is correct. This may not work. But you're on the right lines, very close.

I have presented some calculations on this page:
https://github.com/blippy/rpi/blob/master/pico/pwm.md


There are two things you want as inputs:
* "freq" - your sample rate (e.g. 8000Hz)
* "top" - the maximum value of each sample. If you're using unsigned 8-bit values, for example, the top will be 255.

Given freq and top, you set the pwm clk divider. The divider is calculated like so:

Code: Select all

/** NB clock divider must be in range 1.f <= value < 256.f
*/

float pwm_divider(int freq, int top)
{
        uint32_t f_sys = clock_get_hz(clk_sys); // typically 125'000'000 Hz
        float scale = (top+1) * freq;
        return f_sys / scale;
}
and you set it like so:

Code: Select all

        uint slice_num = pwm_gpio_to_slice_num(SPK);

        int top = 255;
        int sampling_freq = 44'100;
        sampling_freq = 8000;
        float divider = pwm_divider(sampling_freq, top);
        pwm_set_clkdiv(slice_num, divider); // pwm clock should now be running at 1MHz

        gpio_set_function(SPK, GPIO_FUNC_PWM);
        pwm_set_wrap(slice_num, top);
        pwm_set_enabled(slice_num, true);
        pwm_set_gpio_level(SPK, top/2);
Now, you must ensure that divider is at least 1, and less that 256, otherwise you're going to get strange results.

Now, it so happens that you can do a little check to ensure that this is so: run my utility ucalcs, available here:
https://github.com/blippy/rpi/tree/master/pico

The issue I am having right now is that the passive buzzer makes this glitchy high pitched sound, which is definitely not middle C. I am not sure if it is because of my understanding of PWM, if I have a buzzer rather than a speaker, or if my code is incorrect.
The following issues spring to mind:
* yeah, a passive buzzer is likely to be a bad choice
* your sample frequency is 8000Hz. So, 8000 times a second, the output pin will go high. This is well within human hearing, creating a very disagreeable high-pitch tone. You need to up the frequency to double or more.
* middle c has a frequency of 278Hz, which doesn't divide into 8000Hz exactly. So if you wrap around your samples, you'll find that the volume of the end of the samples is not the same as at the beginning, creating a jump in volume, which you'll notice.


One of my confusions is I am not sure what to set the frequency of the PWM to since I am using a timer to feed the sample. Right now I have it set to 8000 which is the same as the timer frequency, but I have a feeling this is wrong.
You haven't quite made it clear if you want to generate pure square-wave tones at a given frequency, or play audio samples. The former is actually quite straightforward, and you don't need wav files. I'm assuming the latter.

In my example code (link above) I actually recorded the samples at 8000Hz. I tripled it up to 32kHz to get rid of the high-pitch tone. This means that I only change the level every third call to the interrupt I set up.

I also used a top of 1023 instead of 255, making me scale up the volume. I created unnecessary work on that, though. I left it as is for now, as it's not doing any harm.

Note that I use an interrupt to reset the level every (third) wrap of the pwm clock. The use of interrupts on the wrap ensures that my setting of the level value correctly coincides with a wrap. Otherwise I could just be setting the new level willy-nilly, and the whole thing would just be wrong.

Hope that helps.

User avatar
scruss
Posts: 4029
Joined: Sat Jun 09, 2012 12:25 pm
Location: Toronto, ON
Contact: Website

Re: Understanding PWM audio

Sat Apr 10, 2021 8:35 pm

If you want to go beyond PWM sampling to generation of "1-bit" PWM music, The 1-Bit Instrument: The Fundamentals of 1-Bit Synthesis, Their Implementational Implications, and Instrumental Possibilities by Dr. Blake “PROTODOME” Troise is a great overview.

Blake's produced some rather lovely 1-bit music over the years, including the wonderful 4000AD album that he sold for a while playing from an ATMega328p microcontroller.
‘Remember the Golden Rule of Selling: “Do not resort to violence.”’ — McGlashan.
Pronouns: he/him

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Sat Apr 10, 2021 8:49 pm

I also used a top of 1023 instead of 255, making me scale up the volume
I disagree unless you didn't change the scale.

This is why I made a subclass of the Pico PWM to set the max scale to 1023. Then there is no difference between a system with a range of 255,1023 or 65355 for volume if you are using the specific max scale. The only difference is the resolution. ( 8 bits , 10 bits or 16 bits).

The current Pico PWM is set to maximise the scale near 65535 which is practically never. 65535. It use a ratio factor to boost it up!
Using a maximum PWM scale of 1023 allows to increase the PWM frequency then it is simple to remove the PWM cycle from the audio itself
since it is higher!

PWM audio use the duty cycle difference to create sound wave. Normally you should use a low band filter to remove the PWM cycle. This way you are only with the output (TON / (TON+TOFF).

Then it is compulsory to set the PWM frequency so you could remove it using a Low pass filter. On my post I just a 1K resistor on headphone. I assume that the frequency of the PWM (~ 122Khz) will be high enough that the mechanical of the headphone it self will do the trick. And because our ears are also limited to ~18..20KHz it does the trick!

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Sat Apr 10, 2021 8:53 pm

Yes 1 bit music is another method. I'm on image analysis and the term is dithering.

this mean average last bit true time and decide if the next bit will zero or ones depending if you are above or below the target value.
Works very well on image when you print on laser printer.

I don't think I will be able to run this on micropython!

blippy
Posts: 149
Joined: Fri Nov 03, 2017 3:07 pm

Re: Understanding PWM audio

Sun Apr 11, 2021 4:59 pm

This actually inspired me to see if I could make a "bytebeat".
https://github.com/blippy/rpi/tree/master/pico/bytebeat

A nice simple project that you can compile and upload to the Pico. It plays a goofy tune generated algorithmically. Stick a speaker in GP14 and the other end in ground. You can search YouTube for other algorithms to play with, or even invent your own.

A fun, quick, simple project.

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Mon Apr 12, 2021 8:25 pm

I added the SDCard into my github to play longer wave file.

I also made a small youtube demo to display the sound quality, which is not perfect but ok. 16Khz in stereo and 44KHz in mono 8 bits.

On my video I specified 22KHz but it was 16KHz, 16 bits , stereo.
https://youtu.be/dgIQz5uy2vA

blippy
Posts: 149
Joined: Fri Nov 03, 2017 3:07 pm

Re: Understanding PWM audio

Mon Apr 12, 2021 9:07 pm

danjperron wrote:
Mon Apr 12, 2021 8:25 pm

On my video I specified 22KHz but it was 16KHz, 16 bits , stereo.
https://youtu.be/dgIQz5uy2vA
Cool.

I'd like to get an SD card working on my Pico. I have a Wemos Micro SD shield, but I'm not sure how I should hook it up.

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Mon Apr 12, 2021 9:59 pm

I'd like to get an SD card working on my Pico. I have a Wemos Micro SD shield, but I'm not sure how I should hook it up.
Just use the SDCard adapter which came with the microSD.
viewtopic.php?t=307275#p1838662


To convert mp3 to wav I use mpg123 and after I use sox to change the rate.

blippy
Posts: 149
Joined: Fri Nov 03, 2017 3:07 pm

Re: Understanding PWM audio

Tue Apr 13, 2021 6:36 am

danjperron wrote:
Mon Apr 12, 2021 9:59 pm
I'd like to get an SD card working on my Pico. I have a Wemos Micro SD shield, but I'm not sure how I should hook it up.
Just use the SDCard adapter which came with the microSD.
viewtopic.php?t=307275#p1838662
I mean without soldering pins to an SD card, but using a shield.

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Sun Apr 18, 2021 1:42 pm

I'm able to run wave at 44.1KHz .

The problem with micropython is that we need to unpack the binary string to signed short list, do the math and then pack it again to unsigned short.

Code: Select all

 value = struct.unpack('hh'*(nbFrame),s)
value = [PWM_HALF + (i // PWM_CONVERSION) for i in value]
V1 = struct.pack('hh'*nbFrame,*value)
Micropython has a method to do assembly using "@micropython.asm_thumb" before the function declaration. This allows me to do the calculation directly to the binary string without unpack and pack.

I added the new file into my github,
https://github.com/danjperron/PicoAudio ... ard_asm.py

danjperron
Posts: 3768
Joined: Thu Dec 27, 2012 4:05 am
Location: Québec, Canada

Re: Understanding PWM audio

Tue Apr 20, 2021 7:40 pm

I figure out how to do DMA chaining !

I modified my code and no more glitches between dma buffer transfer!

https://github.com/danjperron/PicoAudioPWM

DanB91
Posts: 10
Joined: Tue Jan 08, 2019 3:04 pm

Re: Understanding PWM audio

Thu Apr 22, 2021 2:27 am

    danjperron wrote:
    Sat Apr 10, 2021 8:49 pm
    I also used a top of 1023 instead of 255, making me scale up the volume
    I disagree unless you didn't change the scale.

    This is why I made a subclass of the Pico PWM to set the max scale to 1023. Then there is no difference between a system with a range of 255,1023 or 65355 for volume if you are using the specific max scale. The only difference is the resolution. ( 8 bits , 10 bits or 16 bits).

    The current Pico PWM is set to maximise the scale near 65535 which is practically never. 65535. It use a ratio factor to boost it up!
    Using a maximum PWM scale of 1023 allows to increase the PWM frequency then it is simple to remove the PWM cycle from the audio itself
    since it is higher!

    PWM audio use the duty cycle difference to create sound wave. Normally you should use a low band filter to remove the PWM cycle. This way you are only with the output (TON / (TON+TOFF).

    Then it is compulsory to set the PWM frequency so you could remove it using a Low pass filter. On my post I just a 1K resistor on headphone. I assume that the frequency of the PWM (~ 122Khz) will be high enough that the mechanical of the headphone it self will do the trick. And because our ears are also limited to ~18..20KHz it does the trick!
    Thanks for the help. My eventual goal is to actually get this running in C and I found a nic video and code to get a headphone jack hooked up along with example code (https://www.youtube.com/watch?v=rwPTpMuvSXg and https://github.com/rgrosset/pico-pwm-audio). I was able to get a sound file converted to 22000khz and 8 bit samples and it runs fine in the example.

    However, the sound files I want to you use have 16 bit samples and converting them to use 8-bit samples make them sound kind of crappy. These files are also 22050hz and 44100hz, but I can convert them to 22000hz and 44000hz fine if need be. I am little lost as how I can convert the sample code to use 16-bit samples for the following reasons:

    • This example code uses pwm_set_gpio_level() to set the PWM using the sample from the audio. From reading the documentation pwm_set_gpio_level()set the level between 0 to TOP, but here the samples can go up to 0xFFFF but as you said the level almost never goes near 0xFFFF, so I am little confused by this.
    • The samples are signed 16-bit, so not sure if that affects anything. So the range is really -32768 to 32767
    • 0xFFFF is not an even multiple of the 176mhz as the Pico is clocked to in the example and I'd imagine there is no clock speed that the Pico can have that is an even multiple of 0xFFFF so I'm thinking this might not work well. Plus once you divide 176000000/65535, there are not enough cycles in a second to reach 22000hz!

    I feel I'm thinking about this in the completely wrong way, but I can't seem to find any examples that use 16-bit samples. Any extra help would be greatly appreciated to understand how I can use PWM to play 16-bit samples (-32768 to 32767). Extra points if I can get it working in 22050hz and 44100hz, but not too big of a deal if that is difficult (as they are not a clean division of the clock speed). It seems you were able to do that above, but I am having difficulty understand exactly what you did there. Thanks!

    blippy
    Posts: 149
    Joined: Fri Nov 03, 2017 3:07 pm

    Re: Understanding PWM audio

    Thu Apr 22, 2021 1:20 pm

    DanB91 wrote:
    Thu Apr 22, 2021 2:27 am

    • This example code uses pwm_set_gpio_level() to set the PWM using the sample from the audio. From reading the documentation pwm_set_gpio_level()set the level between 0 to TOP, but here the samples can go up to 0xFFFF but as you said the level almost never goes near 0xFFFF, so I am little confused by this.
    • The samples are signed 16-bit, so not sure if that affects anything. So the range is really -32768 to 32767
    • 0xFFFF is not an even multiple of the 176mhz as the Pico is clocked to in the example and I'd imagine there is no clock speed that the Pico can have that is an even multiple of 0xFFFF so I'm thinking this might not work well. Plus once you divide 176000000/65535, there are not enough cycles in a second to reach 22000hz!

    I feel I'm thinking about this in the completely wrong way, but I can't seem to find any examples that use 16-bit samples. Any extra help would be greatly appreciated to understand how I can use PWM to play 16-bit samples (-32768 to 32767). Extra points if I can get it working in 22050hz and 44100hz, but not too big of a deal if that is difficult (as they are not a clean division of the clock speed). It seems you were able to do that above, but I am having difficulty understand exactly what you did there. Thanks!
    I created a little util for myself that, given a sampling frequency and a top, calculates the clock divider. Here's the code:

    Code: Select all

    #!/usr/bin/env perl
    
    # perform various calculations
    
    
    my $input;
    
    sub get_defaulted {
    	my ($text, $default) = @_;
    	print "$text ($default):";
    	my $input = <STDIN>;
    	chomp($input);
    	if($input eq "") { $input = $default; }
    	return $input;
    }
    
    my $freq = int(get_defaulted("Freq", "16000"));
    my $top = int(get_defaulted("Top", "1023"));
    my $divider = 125000000/($top+1)/$freq;
    my $ok = (1 <= $divider && $divider < 256) ? "OK" : "FAIL";
    print "Divider: $divider ($ok)\n";
    
    It is available here: https://github.com/blippy/rpi/blob/master/pico/ucalcs. It's useful to tinker around with to see if you have a valid clock divider in the first place.

    OK, next problem. Instead of working with 16-bit signed audio, why not work with unsigned audio? Programs like Audacity, or command-line tools like sox should be able to do the conversion for you. Failing that, you could write a small proggy to convert 16-bit signed to unsigned.

    Alternatively/additionally, you may want to reduce the resolution of of your samples. Typical values to use for audio are 12-bit and 10-bit, if you think 8-bit is too low. 12 or 10 ought to be plenty.

    How do you do this? Well, say you wanted to work with 10-bit unsigned audio, but you have 16-bit signed. The transformation you'd apply is (I think I've got my maths right):

    Code: Select all

    vol = (vol >> 6) + (1<<9)
    
    The first term on the right downgrades the sample, whilst the second term converts from signed to unsigned.

    Does that help?

    danjperron
    Posts: 3768
    Joined: Thu Dec 27, 2012 4:05 am
    Location: Québec, Canada

    Re: Understanding PWM audio

    Thu Apr 22, 2021 1:45 pm

    Hi DanB91,

    ok maybe I should explain what I did.

    This example code uses pwm_set_gpio_level() to set the PWM using the sample from the audio. From reading the documentation pwm_set_gpio_level()set the level between 0 to TOP, but here the samples can go up to 0xFFFF but as you said the level almost never goes near 0xFFFF, so I am little confused by this.
    The current Pico PWM use a range of 65535. if you divide 125MHz by 65535 you get 1907Hz PWM cycle which is quite bad to filter out because is right at our most sensitive earing.

    The thing I did was to change the PWM behavior to get an higher PWM frequency cycle. Divide 125Mhz by 1023 you get 122KHz PWM cycle frequency. This is way better to filter out and this is not audible by our ears!

    It is true that it is not 16 bits output but 10 bits output. But I listen to music yesterday and it is still good.
    The samples are signed 16-bit, so not sure if that affects anything. So the range is really -32768 to 32767
    Now the range needs to be 0 to 1023 but wave format is -32768 to 32767. This is a problem in python since you need to convert it . Using a function in assembler allows me to be quick since I didn't need to pack an unpack. No choice, I had to do it in assembler.
    The assembler add 32768 and divide by 64 (by shifting right 6 times). This gives the range between 0 and 1023.
    0xFFFF is not an even multiple of the 176mhz as the Pico is clocked to in the example and I'd imagine there is no clock speed that the Pico can have that is an even multiple of 0xFFFF so I'm thinking this might not work well. Plus once you divide 176000000/65535, there are not enough cycles in a second to reach 22000hz!
    The PWM frequency is irrelevant to the sample rate. You want a PWM frequency outside the audible range. You don't want to ear the PWM at all. You want to filter the PWM as much as possible to get the DC effect of the PWM signal. The PWM tries to mimic analog signal. If you want to get 1.65V you need to set the PWM at 50%. but the PWM is a waveform, audio signal also. you want the PWM signal to be outside the audio ones! The only thing left is to remove the PWM frequency using filter. A simple RC filter will work. On headphone I'm using 2K resistor and a 0.1uF capacitor (2200pF for amplifier). BT.W. our ears don't go higher then 20Khz so a 122 KHz square wave is not an issue.

    One Trick for Headphone
    if you listen when there is no sound you will get the digital noise on the power bus of the cpu. The simple trick to get ridd of it is to add another PWM Pin and set it at the same PWM frequency cycle and set it at 512 (50%). This way the noise from the digital power bus will be cancelled since there are on both side. Very efficient! Then the new ground is not the ground but the new PWM Pin. The capacitors ground from the RC filter, will be connected to that PWM and the GND of the headphone also.


    The DMA
    Using the DMA register with its own timer allows me to get perfect timing between PWM change. The Timer is set to the current sample rate and because I set two DMA channels in chain mode the transition are seamless. No need to create an interrupt!


    Way to convert mp3 to WAV
    I'm using mpg123 to convert to wav and correct the wave using sox.

    This is a bash example on how to use sox and mpg123 to create wave file into a folder wav
    I should use stdout but I'm too lazy. I made it in 2 operations instead. use the wav in the wav folder

    Code: Select all

    mkdir wav
    for i in *.mp3; do mpg123 -w "${i%.mp3}.wav" "i"; done
    for i in *.wav; do sox  "$i" -r 44100 -b 16 -c 2  "wav/$i"; done
    Possible improvements
    Using DMA it is possible the send the PWM signal to a D/A I.C. using SPI instead. This could send 16bits.

    My next goal
    Right now I'm learning how to do C in MPY. I will try to implement the MP3 decoder.
    I did look at the circuitpython mp3 player and I should be able to use the same code but instead of using
    interrupts I will use the DMA method which is better I.M.O.


    https://github.com/danjperron/PicoAudioPWM
    The third PWM to cancel the power noise is not on github yet.


    I want to stay on micropython.

    Daniel

    DanB91
    Posts: 10
    Joined: Tue Jan 08, 2019 3:04 pm

    Re: Understanding PWM audio

    Thu Apr 29, 2021 4:41 pm

    danjperron wrote:
    Thu Apr 22, 2021 1:45 pm
    Hi DanB91,

    ok maybe I should explain what I did.



    The current Pico PWM use a range of 65535. if you divide 125MHz by 65535 you get 1907Hz PWM cycle which is quite bad to filter out because is right at our most sensitive earing.

    The thing I did was to change the PWM behavior to get an higher PWM frequency cycle. Divide 125Mhz by 1023 you get 122KHz PWM cycle frequency. This is way better to filter out and this is not audible by our ears!

    It is true that it is not 16 bits output but 10 bits output. But I listen to music yesterday and it is still good.

    The PWM frequency is irrelevant to the sample rate. You want a PWM frequency outside the audible range. You don't want to ear the PWM at all. You want to filter the PWM as much as possible to get the DC effect of the PWM signal. The PWM tries to mimic analog signal. If you want to get 1.65V you need to set the PWM at 50%. but the PWM is a waveform, audio signal also. you want the PWM signal to be outside the audio ones! The only thing left is to remove the PWM frequency using filter. A simple RC filter will work. On headphone I'm using 2K resistor and a 0.1uF capacitor (2200pF for amplifier). BT.W. our ears don't go higher then 20Khz so a 122 KHz square wave is not an issue.

    One Trick for Headphone
    if you listen when there is no sound you will get the digital noise on the power bus of the cpu. The simple trick to get ridd of it is to add another PWM Pin and set it at the same PWM frequency cycle and set it at 512 (50%). This way the noise from the digital power bus will be cancelled since there are on both side. Very efficient! Then the new ground is not the ground but the new PWM Pin. The capacitors ground from the RC filter, will be connected to that PWM and the GND of the headphone also.
    Thank you for this. I converted my WAV file to 22khz 12-bit unsigned samples (TOP of 4000, not 4095, since 4000 divides 176mhz), and minus a few occasional pops, it sounds pretty good (and minimal background noise!).

    To be more specific, I am overclocking the PI to 176mhz and using a divisor of 2 with a TOP of 4000 to make the sample rate 22khz.

    I haven't tried the second PWM Pin trick yet, but will definitely try that once i get more things in order.
    The DMA
    Using the DMA register with its own timer allows me to get perfect timing between PWM change. The Timer is set to the current sample rate and because I set two DMA channels in chain mode the transition are seamless. No need to create an interrupt!


    https://github.com/danjperron/PicoAudioPWM
    The third PWM to cancel the power noise is not on github yet.

    Daniel
    This is definitely the route I am gonna go with, since I am working on a game and if I used interrupts, I feel I'd probably need to utilize the second core for the sound, which I'd like to avoid. I am looking at your code in wavePlayer.py and for the most part it makes sense but I am little confused about these 2 lines of code:

    Code: Select all

    self.dma0.setCtrl(src_inc=True, dst_inc=False,data_size=4,chainTo=self.dma1.channel)
    
    and

    Code: Select all

    self.dma0.move(uctypes.addressof(t0),self.leftPWM.PWM_CC,nbFrame*4)
    
    It looks like from these 2 lines are you are copying 4 bytes at a time to the PWM address. But, in the case of 10-bit audio each sample is only 2 bytes. At first, i thought you might be doubling the buffer size and zero extending each sample to make it 4 bytes, but looking at convert2PWM(), that doesn't seem to be the case.

    What this seems to do is copy 2 samples simultaneously in one DMA tick, which doesn't make much to me. Am I missing something?

    Thanks for all the help!

    danjperron
    Posts: 3768
    Joined: Thu Dec 27, 2012 4:05 am
    Location: Québec, Canada

    Re: Understanding PWM audio

    Thu Apr 29, 2021 6:09 pm

    self.dma0.setCtrl(src_inc=True, dst_inc=False,data_size=4,chainTo=self.dma1.channel)
    The way I made the DMA is to set the number of byte instead of number of transfer.

    the arguments,

    - src_inc Do you want to increment the pointer from the source file on every transfer.
    - dst_inc Do you want to increment the pointer to the destination address. (ON PWM the address is always the same. Its a register)
    - data_size 8, 16 or 32 bits bus transfer.

    We are using PWM on register for destination. Then registers are always 32 bits.

    A 8 bits transfer to a register is a 32 bits transfer with the same 8 bits on each 4 bytes.
    A 16 bits transfer to a register is a 32 bits transfer with the same word twice.
    ex: 8 bits transfer of 0xaa on a register is a 32 bits transfer of 0xaaaaaaaa.

    - chainTo This is the magic! when this DMA channel is done it will automatically start the next DMA by itself.
    This is a merry-go round style when DMA0 is done it starts DMA1 and vice versa.
    self.dma0.move(uctypes.addressof(t0),self.leftPWM.PWM_CC,nbFrame*4)
    This is the actual command to fill the DMA number of byte to transfer, the source and destination address.

    Code: Select all

    def move(self, src_add, dst_add,count,start=False):
    The start argument is, if you want, to start the DMA. Since the other DMA channel will trigger it, we don't need to start the DMA . We only need to start it once!


    Ok the four byte of data,

    32 bits to a register minimum. The PWM register for the PWM is two 16 bits counter compare values for channel A and B (CHX_CC).
    Channel A for left and channel B for right. The data written on each channel is a unsigned 8 or 10 bits PWM mimicking the A/D value.


    N.B. The DMA implementation doesn't use the 'C' claimed method in the pico-sdk . I don't have access to it , the DMA claimed variable is not public. This is why I use the highest DMA channel to be safe from the micropython code itself.


    In 'C' I should modify the code to use the claimed variable.
    Last edited by danjperron on Thu Apr 29, 2021 6:38 pm, edited 2 times in total.

    Return to “General”