Jump to content

PWM experiments

Recommended Posts

I 've been doing some testing of the PWM technique to play samples (thanks Phaeron and Xuel for the examples).
Is nothing great, but I think that maybe the code could be of use to people doing similar experiments.



It's for a 1Mb cart and there are two small songs only, but in different qualities.

The options are:

1 - Song 1, PWM at 15720 Hz, sample values between 0 and 109, a little lower than 7bit depth. "Normal" PWM quality, with one sample per scan line.

2 - Song 1, PWM at 21013 Hz, sample values between 0 and 80, higher than 6bit depth. This is a failed experiment.. I tried to play 4 samples every 3 scan lines, but getting the perfect timing for this seems impossible, because of the refresh cycles.

I wasn't that far off, but the small imperfections in timing are enough to add an undesired frequency sound.

3 - Song 1, PWM at 31440 Hz, sample values between 0 and 52, lower than 6bit depth. This worked well but it eats memory x) (30 seconds max for a 1Mb cart).

This plays two samples per scan line, the timing was a little tricky because you need to avoid the refresh cycles and also balance your worst case code branch with the rest of the code.

The good thing is that the normal carrier frequency produced by PWM (that at 15KHz is still annoying for some people) is not an issue.

The bad thing is that playing more samples in the same time, implies that the bit depth is lower (for PWM).. so at the end of the day I don't think this is an improvement over the 15KHz PWM.

4 - Song 1, at 15720 Hz, sample values between 0 and 15, classic 4bit depth. This could sound better, but I didn't allowed much clipping, so maybe the volume is too low.

5 - Song 2, PWM at 15720 Hz, same as the first option, but with a larger song.

The 15720 Hz rate sounds perfect in NTSC and just a bit slower in PAL (there the rate should be 15600 Hz).
All samples converted with Sox and then with some scripts in C#, to move the data to the correct range allowed by every method.
Main code is in rtech_cart.asm, done in Mads.

I think that using 15KHz PWM is an improvement over the classic 4bit sound, if you are playing songs without much silence (where the 15Khz carrier frequency can bother some people).

Also you can do some simple track sequencing with the screen on, so a nice intro for a game in a 1MB cart can use this. I already have code for this, but that's coming later :)



  • Like 14
Link to comment
Share on other sites

An other question. If 31440Hz is possible, turned screen off, shouldn't it be possible to have gr. 7 active, using 15720Hz?

It's just that the timing had to be adjusted in the VBI time to fit to every 2nd scanline timing, when no DMA is active.

Link to comment
Share on other sites

With 15KHz you can leave the screen "on" in any graphics mode probably.

And at least in narrow playfield (32 bytes wide), with the menu "on" (the six lines of graphic 0) it sounded almost equal to me.

First scan line of a char line is "nasty" for the timing, but it seems ok with this.. should test it more anyway.


I would try to do the second song in 31KHz later, it just fit in the 1Mb cart.

  • Like 2
Link to comment
Share on other sites

And the last one..

A one-minute "megamix" of songs from old arcade games.

There is a NTSC version and a PAL version this time, 1 MB cart bins as always.

Maybe I should give a prize to the people that can name them all :D







Edited by NRV
  • Like 12
Link to comment
Share on other sites

My subjective view (for song 1):


- Test 1 has a strange sound/noise which is a bit annoying (I am hard hearing, but I can hear this !?!)

- Test 2 has a high frequency sound/noise, that is louder?, clearer? and even more annoying than in test 1

- Test 3 sounds the best to me, no high frequency noise

- Test 4 to my surprise sounds better than test 1 or test 2 to me (but not as good as test 3), since it does not have that high frequency noise


But only tested with Altirra yet, have to test it on the real A8 with my Ultimate Cart.

  • Like 1
Link to comment
Share on other sites

Updated code to play the "song 2" and the "megamix" with the PDM technique.

Also included the 1Mb cart images for both, and in NTSC and PAL versions.




Now it plays at 15.7KHz (15.6KHz in pal), with 8 bits of depth, same memory usage as before, without the carrier tone.

Also there is no click sound at the end, the wave fits on the screen, and it can be aborted with the start key.

A win-win x) .. thanks to Xuel and kool kitty89.


So maybe the conclusion of this thread is to not use PWM and only use PDM when possible :)


  • Like 10
Link to comment
Share on other sites

fun fact - the first mod players (like one from Intel Outside demo side b) used mixing of 4 channels into 3 using a lookup table built from voltage measurements on pokey output. reason was the pokey was not adding much, or at all, of a volume (amplitude) when all 4 channels were used and with a lookup table better characteristic could be achieved. we used to think then of pokey as of a 6-bit DAC with funny inputs. nowadays people understand how pokey really worked so this might be irrelevant, but at least show that such a downmixing of 4 4-bit channels into one 6- or 7-bit is not impossible.

Edited by pirx
  • Like 1
Link to comment
Share on other sites

Yeah... all in all volume.

Things could get interesting to adjust the volume for all 4 channels on the fly instead of mixing them all together. The high replaying speed could help solving some volume issues.

And, as it seems, it's just a conversion mistake from 8 bits to the 4 bits of the POKEY channels, as it even is possible to play 4 channel POKEY tunes without distortions.

Edited by emkay
Link to comment
Share on other sites

would it be possible to use this technique to play mods?


Well, that's what I already asked here: http://atariage.com/forums/topic/244946-using-pulse-density-modulation-for-8-bit-pcm/page-2?do=findComment&comment=4022992

but it got "overlooked" because of the enthusiasm it seems...

  • Like 1
Link to comment
Share on other sites

If you preprocess the mod's samples to 6-bit, adding the four channels together yields you an 8-bit value. The problem is that this value cannot simply be written to pokey. You need two LUTs (hi,x and lo,x), which is slower than four covox writes, but I guess would not be that different from three lookups that were needed to get 5.5 bits by using three pokey channels volume-only.

Link to comment
Share on other sites

yea, 5.5 bits, forgot this funky arithmetic : ]

so looks like a doable feat. alternatively, leaving (nicely antialiased) 4 bit samples in mods could yield 6 bits with less lookups than in this old technique and possibly a superior sound as well.

Link to comment
Share on other sites

The key is in the replaying speed. Pulse width programming can exchange bit correctness by the resulting "power package. We had some discussion in the past. And POKEY can do that with more than 3MHz ;)


Searching for the maximum sampling speed, but not playing the samples every time, just fill it up with some "unrecognizable sound" . To reduce the sampling noise. This probably works better with PCM .


Or replay in to different replay speeds, hacking the resulting sampling noise to some less recognizable noise.

And this is what POKEY can do with it's timers already. It's just the replay speed, that has to be as fast as possible.

A high tone with some 25kHz sampling noise, will sound clear.

A low tone with some 8kHz sampling noise eventually sounds cool, as far as the sampling ratio fits to the harmonics of the tone.


And so on...

Link to comment
Share on other sites

>All samples converted with Sox and then with some scripts in C#, to move the data to the correct range allowed by every method.

How dou you actually generate these PWM samples ?

Link to comment
Share on other sites

Nothing fancy I think..
I normally start with a 44KHz or 48KHz wav file.
Sometimes I take a look at it with Audacity and apply a Normalize effect there.
But in general I use a sox command like this:

sox --no-dither --norm song.wav --type raw --encoding un --channels 1 --rate 15720 --bits 8 song_ntsc_15k.raw
sox --no-dither --norm song.wav --type raw --encoding un --channels 1 --rate 15600 --bits 8 song_pal_15k.raw

That means no dither, default normalize level (could use also --norm=2, or --norm=3, if you want more volume and don't mind some clipping), generate a raw file without headers, unsigned numbers (0 - 255 in this case), mono, the specific rate for your project (one sample per scanline in this case, ntsc or pal), and 8 bits depth (one byte per sample).

You can use the same kind of command to generate a wav file, if you want to look at it with Audacity:
sox --no-dither --norm song.wav --type wav --encoding un --channels 1 --rate 15720 --bits 8 song_ntsc_15k.wav
For example to see if you are using the full amplitude range.

Other rates I have used for my ntsc examples: 7860 (every two scan lines), 21013, 31440.

In my final experiments I was using the rate command as an effect, because you can use more parameters like this:
sox --no-dither mix2.wav --type raw --encoding un --channels 1 --bits 8 mix2_15k_ms.raw rate -m -s 15720
It would be better that you read the Sox documentation for the explanation of the "-m -s" parameters, but after experimenting with the quality settings that was the option that sounded better for me.

I also experimented with the compand command:
sox --no-dither song.wav --type wav song_amp.wav compand 0.3,1 6:−70,−60,−20 −5 −90 0.2
This one, with the same parameters, is explained in the documentation.
It's a kind of "normalization" but can apply different levels in different parts of the song.

Well, after all this you have an 8 bit song that can be directly used in the A8, with the pcm4+4 (ex pdm) technique.
But if you wanted to use the pwm technique, for example at 15720 Hz (one sample per scanline), you need to reduce the 0-255 range to something like 0-109.
For that I used a script in C# where the algorithm is a simple scaling like this:

private void ByteArrayRemapRange(byte[] byteArray, int arraySize, byte minValue, byte maxValue, byte newMinValue, byte newMaxValue)
 for (int i = 0; i < arraySize; i++)
  float remapFactor = (float)(newMaxValue - newMinValue) / (maxValue - minValue);
  float newValue = (byteArray[i] - minValue) * remapFactor + newMinValue;
  int newValueInt = Mathf.RoundToInt(newValue);
  byteArray[i] = (byte)newValueInt;

This lives in a Unity project, along other code that I use to transform data to the A8 needs, so is not exactly a useful tool right now :)

  • Like 3
Link to comment
Share on other sites

That quantization stuff was the part the way rather clear to me (except maybe where the 109 comes from). I but I thought the signals would have to be processed further. But now I found time to check the code. Basically you join two channel to run at 1.79 Mhz, trigger, set the frequency for 1 scanline and start the pulse generation for 1 scanline using STIMER. That generates a pulse of a certain length each scanline. And then you assume there are RC parts that flatten this out.


Is that understanding correct?


And why set 1.79 MhZ/16 bit and then keep the high value of the divisior at 255? More linear results? Or is the CPU simply to slow to set lo/hi of the frequency fas enough?

Link to comment
Share on other sites

That quantization stuff was the part the way rather clear to me (except maybe where the 109 comes from).


Beyond 109, the pulse doesn't occur before the next scan line resets everything again with STIMER. The effect is that you get clipping.

  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
  • Create New...