Feedback on failed project

Video below.

Not sure where to go from here.
Happy to leave it behind for now but if anyone has any feedback I’ll give it a go. :slight_smile:

Watch this space for some code and recordings.

audio

You can read the code here.

See full code on Github :point_left:

key code excerpts

class SineWave(Oscilator):
    def generator(self, x : float) -> float:
        b = (2 * pi * x) / self.frq
        return sin(b)


class Transport:
    ''' code removed for brevity '''
    def tick(self):
        self.cache = self.clock
        self.clock = ticks_us() / 1000000.0

class DAC:
   '''code removed for brevity'''
   @rp2.asm_pio(**pio_config)
    def update_DAC():
        out(pins, 10) # punch x register onto all pins instantaneously

    def punch_signal(self, sig : Signal):
        if not isinstance(sig, Signal):
            raise NotImplementedError
        output = (sig.val + 1) / 2 * (2**10 - 1)
        self.punch(output)

# the global transport
tp = Transport()

# the makerverse r2 ladder
dac = DAC(list(range(6, 16)))

# oscilator
cycle = SineWave(amplitude=1, frequency=150)

while 1:
    sig = Signal(cycle)
    dac.punch_signal(sig)
    tp.tick()

early 8bit prototype

Just for anyones curiosity.
It was deceptively promising. Figured i was just spending too many clock cycles the shift register but clearly it was more fundermental than that.

1 Like

Hi Pix
Hi Fi at its best eh!!! Looks like about 90% distortion. Must be easier ways to generate an audio signal than that conglomeration of bits.
Cheers Bob

Yeah for sure :stuck_out_tongue:
This was for curiosity, education, and joy.
Low budget synths have gotten so incredibly good and diverse since the pandemic, you could probably acquire a half way decent and working synth by borrowing your dads lawnmower and spending a weekend cutting the neighborhood grass. :laughing:
I’ve … definitely thrown more than a weekend on this …

1 Like

Hi Pix

You should certainly get some of that
Cheers Bob

Im not a python coder so not sure how your code is really working (but thats on me).

The way I understand a “bit bang” DAC should work is via PWM, where the on part is longer when the “amplitude” is higher. (i.e. the longer the X Volts is on the higher the average V will get).

So, lets say you have a very rough sin wave, you would have a bare minimum of 0 1 0 -1 (and repeat). You would need to output 4 PWM states per cycle (with this very rough wave); normally you would have many more samples. Then for say your sample rate was 14.4 K you would output those 4 states 14400 times per second. (i.e. each pwm cycle = 1 sample)

Since the uC does not have negative values, lets shift that up to a range from 0-3 V (for easy math)
1.5V 3V 1.5V 0V (1.5V repeat)
So to get the 1.5V from the digital pin then the duty cycle will be (approx for easy math) 50% on then 50% off (at your sample freq)
That should yield the on duty cycles of
50% 100% 50% 0% … and repeat

The idea here is that the pwm frequency is fast enough so the “amp” would see the average of the on/off time; kind of like how the Voltmeter will see a Voltage, but a scope will see a wave.

If you scope out the “pwm” digital pin you should see the square wave that will have a varying “on” time. But a fixed cycle “on+off time”

Edit/Add
Just trying to think of clearer/simpler way to explain this…
If you take an 8bit mono wave sound file with a sample rate of 14.4 K sps. Then that has “record” 14,400 voltages per second. At 8 bits per sample that = 14,400 bytes per second (mono).
So the objective here is to setup the PWM to (at a min) output each sample voltage. e.g. 0 - All off, 255 = All on for the pwm cycle for that sample. (Note wav file will be storing - to + values so a conversion will most likely be needed)

So one way would be something like
Setup timer for PWM frequency (say 256 * PWM Freq)
You would need the 256 to cover 8Bit “voltage” levels

if (PWM timer count = 0) 
   read next SampleOnTime

if (PWM timer count  <= SampleOnTime) 
   Set Pin High
else   
   Set Pin Low

You may need to make the code smarter to only set the PIN start if it changes (not every tick) as GPIO can be slow.

I kinda get what you mean but how do I cast my pwm to a 10bit binary number?
This seems like a cool way to build a pwm synth but i wanna see if I can use this dac because i think its interesting.

Ok, I think I missed your DAC, whats the part number of the DAC?
You may need to do a “dc” to “ac” conversion on your generated data.

e.g. DC 0-1023 (10 Bit) may need to be convert to a -511 to 511 which may need a 2s compliment or something.

I remember when I was playing with wav files I had all sorts of distortion until I did the correct conversion.

Lets have a look at the data sheet.

Edit:
Any chance I can get you to dump the wave form data to a log file for me. i.e. the actual data you are sending to the DAC/Ladder.

1 Like

While this wont affect your quality, I think the SineWave function is wrong.

class SineWave(Oscilator):
    def generator(self, x : float) -> float:
        b = (2 * pi * x) / self.freq
        return sin(b)

To adjust the freq. its normally sin(b*freq);
So that would make it

b = (2 * pi * x)  * self.freq

I did a quick excel data generation and plot and the original way decreased the frequency as “freq” was bigger; the adjust way increase the sine wave freq as you increase freq.

but thats not quality issue.

output = (sig.val + 1) / 2 * (2**10 - 1)

The above snippet seems to adjust correctly for the sine wave -1 to 1 range to 0 to 1024 range

So that looks how I would expect for that dac/ladder.
So I’m now thinking its the actual number of samples pre cycle of the sin wave. At low’ish values you can see a less smooth waveform, so it will be interesting to see the actual values from your code.

If that looks good, I would then move onto the output from the DAC and into the Amp. it could be as simple as you are overdriving the amp. e.g. "The normal level of the audio signal in a professional studio is +4 dBu or about 1.23 volts "
and would swing around 0v, where as the output from the dac would be 0 + 3.3V (I assume it was 3.3V). So you try to to reduce that.

What was the model # for the Amp ?

1 Like

Hey Michael.
Thanks for diving in.

Let’s start with parts I’m using.

Hey Micahel.
There isn’t a .wav file. This is all function generators. :slight_smile:
They all stem from the oscillator class.

Keen eye.
In this case expressing the sine function as sin(b/f * x) is a deliberate choice.
When the frequency is a denominator then it represents the period rather than the cycles.
This is good for my code because it’s how the other objects that inherit from oscillator express a frequency.
Here’s a quick graph I whipped up to demonstrate.
In my case I’m wanting the red line, not the black line.

The waveform seems to produce the correct functions it’s just that the sample rate is too low. Waves become down-sampled as the pico’s clock run out of puff.
However, at bass freqs, like 20-80hertz, it works perfectly (as long as you have a sub woofer).

Concerning amp distortion I’m not particularly worried there because I’ve hooked my oscilloscope directly to the VCC of the DAC. The wave function I’m throwing into the amplifier already lacks resolution.

Ok that seems to make sense. Note sometimes I tend to mix up wave file with the wav as in the signal you are processing. In this case I was interesting in dumping the output of what the values getting passed to the dac so I could plot and review; but I suspect it wont be needed as per the following comments.

So if its working fine at low freqs, then it does sound like the issue is the sin wave resolution as you said.

Been a while since I played with the Pi Pico and when i did I was using C code and direct flash so no python getting in the way.

from memory the clock rate is 125 MHz. That being the case, it sounds like it should be fast enough, but i do understand there is overhead getting in the way.
I have an esp32 dual core running at 240 Mhz trying to bit bang to a jtag interface to read the Chip ID (and a few other things) and the fastest I could get that was 1Mhz at the jtag clock pin; so yeah… i understand.

A few things I learnt about trying to get it faster was tracking down the overhead. So the following are more thoughts as I dont know enough about python to say for sure.

  1. Calling functions to do work while trying to clock out data will have overhead. the more inline code you have, the faster it should run. e.g. Memory permitting, you could create a higher resolution sin wave at what ever freq you need, store that in an array, then simple clock that out. That should drop sum funcation calls and the math, all taking up cpu ticks.

  2. As you already found setting pin outputs takes time so any faster ways to do that would help. e.g. is there a direct register way or do you need to use a function call.

  3. IRQ and running code on events takes too much cpu time.

The simplest way to work out the 100% upper limit would be to have a very tight loop and just toggle a single IO pin and check that on the scope.

e.g. assuming no other instructions and guessing over simplified, something like the following would happen

while (true) 
    setpin high
    setbin low

If we said 2 CPU ticks per line (for an example) and we need to jump back to the while, we have 8 CPU ticks per loop pass; so 125 Mhz / 8 = 15 Mhz (rounded)

We can also look at this in reverse.
if we can clock at say 1 Mhz, then 125Mhz/1 = 125 ticks per loop pass to do all the work; which I would say is not much work is going to get done.
but if we have a pre calculated sin wave at 1000 samples per cycle, then we have 125,000 cpu ticks to clock it out.

Of course all this depends on the python and other overhead.

Please note: The key point I am trying to make with some made up example data, is to increase the wave resolution, you need more samples. to get more samples you need to minimize anything getting in the way.

This is interesting to see written out.
It does intuitively feel like there should be plenty of room here.
Micropython is slow but I am surprised it is thaaat slow.
One thing I’m noticing is that every loop in my while loop I’m declaring a new Signal() object.
Technically that means I’m allocating memory for that object every loop.
Maybe if I declare it once outside the loop I’d have better luck?
I’d like to think the compiler would have handled that for me but maybe not.

Back of the envelope calculations.
A good audio sample rate is 48khz.
Pico is playing at 125000khz
(125 megahertz) / (48 kilohertz) = ~2600 cycles.
Admittedly, once you include to cycles to manage GPIO memory and keep track of adding signal objects that’s maybe not enough. Hard to say without digging into the assembly.

@Michael you’ve probably got the most experience in the world with this product. Any ideas? :slight_smile:
Things I could do better next time?

The trick/challenge is to work out what you can actually deliver. this will involve the hardware limits (e.g. how much time does it take to make a pin go high), then there is the “os” overhead in this case the python (in ESP32 its RTOS).
You should be able to get close to full CD quality in theory as I was playing 32 wave files on an ATMega32 running at 15 Mhz (but it did not have any “os” overheads).

As a rule fast code is not nice code :slight_smile: What I mean by that is its likly that functions will slow down your code, so to get speed you “inline” your functions. In most c compilers the inline keyword is a suggestion, so to ensure the code is inline, put it their.

In not up to speed on python and how it tunes the code, but a high level approach will be to device a way to “time” it.
e.g. You have your sin waveform, run it an see how it looks on the scope. Make a change then retest, did it improve, stay the same or make it worse… you will learn what works.

Bottom line and my golden rule, never do anything that does not need to be done. So check all the code in loops and see what only needs to be done once and put it outside the loop.

Next look at what is using CPU time and see if there is a better way. There are some very cleaver ways to speed up math, e.g. a = a * 2; or a <<= 1; normally the bitwise operates are faster. Don’t do string comparisons if number comparisons can work. With numbers if you can stay within the native hardware supported sizes. e.g. If the cpu is 32 bits then don’t assign 64bit numbers unless needed.

For this project, I think the best thing you could do would be to create a 1 Cycle wave at your selected frequency, outside of the loop playing it. Do this over just enough samples so it sounds good, but not too many that it takes too much time.
then in your main “playing” loop, just DAC(samples[x++]) sort of thing. If this creates notaciable breaks in sound when you change frequency, then you can look into memory/speed trade offs. (some math needed here to work out the best) but an idea (not thought threw) is to create a supper High samples per second wav for the lowest frequency (on boot/pre stored) then play every X sample for higher frequencies.
e.g. Lets say the lowest freq. is 50 hrz, 1 cycle could be over 100,000 samples (just guessing here) which will be way over the top.
Now to play a 100 hrz sound, play ever 2nd sample (50K sps) or 200 hrz would be every 4th sample at 25K sps)
but as you can see the sample rate falls very fast in this example, so a collection of samples may work.
the key point here is that you are trading nice code with fast code and using memory for speed.

Just a thought… the pico has 2 cores. as such if you could pin the “os” stuff to one core and a thread for the sound on the other.
Then you could do something like

Pre-calc wave table
while (true) {
   wait X CPU ticks
   Output wave[idx++]
   if (idx >= maxsamples) idx =0;
}

where X would be adjusted for your frequency. i.e. a smaller x = higher freq.

1 Like

That’ll work :). You’d be building a wavetable synth at that point.
I dunno it’s not really the direction I wanted this project to go.
Using the other core might help. I don’t love the idea of multiprocessing in python. I think I would switch and to a systems level language.
At the point I’m considering multiprocessing the project starts to balloon beyond what I think it gives back to me in value. It was supposed to be an easy christmas project for the lols. :stuck_out_tongue:

Hi Pix
Next XMAS might be OK. Ha Ha
Cheers Bob

1 Like

Yeah, understand. Wait until you start working on projects where the information needed is not public, but there seems to be plenty of example code on github. You start to spend more time reverse engineering and analyzing code then actually building your project.

I have lots of little projects that are more about the learning then a final build. I normally get to the point of saying “Im sure I can do it from here” then move on to the next think I want to know more about.

1 Like

I had a bit of free time today, so I had a play.
I setup a sin wave over 256 samples (for 1 complete cycle)
then clock that out using bit bang pwm (on an esp32 @240Mhz)
once working, at my given settings I was getting just on 3Khz sin wave at the scope.
I then took some math out of the loop and adjusted the array day to have pre-calculated. This was the only change…
Post that change the scope was showing a frequency of 6.4Khz

So the key point here was to demonstrate how removing things from the loop (especially if they need to happen outside “wait” time) can improve things.

3Khz to 6.4Khz is over double the rate with tuned code.

@Pixmusix, If you want to have a zoom sometime to have a play and discuss, Im happy to do so, just need to find a non-public way to arrange and a suitable time.

2 Likes

HI Michael.
Thanks for waiting.
I’ve thought about it and I’ve decided I do want to pursue this project a little longer.
It might be that some per-calculated waveforms are required. Not quite what I wanted but the perfect shouldn’t be the enemy of the good.
Let me try some stuff and get back to you.

I’ve been mulling this over and decided I want to give this another go but with a different design.

So I’m back with the pico and micropython and doing super simple science.


''' ** What's my top speed?' ** '''

from machine import Pin, ADC

pin = machine.Pin(15, machine.Pin.OUT)

while True:
    pin.high()
    pin.low()

I’m literally toggling a pin and getting only ~62khz.
It’s not even a reliable 62khz.
There is no shot it takes 2000 clock cycles to toggle a pin… right?
What am I doing wrong?