Part 10: Achieving Simple PCM with Wavetable Synthesis

The idea behind pulse-code modulated sound (PCM) is remarkably simple. A PCM waveform consists of a set of samples which describe the relative volume at a given time interval of constant length. (Note the terminology of "sample" in this context, which has nothing to with a sample as we know it from MOD/XM, for example). For playback, each sample is translated into a discrete voltage level, which is then amplified and ultimately sent to an output device, typically a loudspeaker. The samples are read and output sequentially at a constant rate until the end of the waveform has been reached.

When attempting this on a 1-bit device, we face the problem that we obviously can't output variable voltages. Instead we only have the choice between two levels, silence or "full blast". So how can we do it, then?

In order to understand how we can output PCM on a 1-bit device, let's first recap how Pulse Interleaving works. The underlying principle of P.I. is that we can keep the speaker cone in a floating state between full extension and contraction by changing the output state of our 1-bit port at a very fast rate, thanks to the inherent latency of the cone. So we're actually creating multiple volume levels. I'm sure you've realized by now that the same principle can be applied for PCM playback.

So, say we want to output a single PCM waveform at a constant pitch. All we need to do is interpret the volume levels described by the samples as the amount of time we need to keep our 1-bit port switched on. So we just create a loop of constant length, in which we

- read a sample
- switch the 1-bit port on for a the amount of time which corresponds to the sample volume
- switch the 1-bit port off for the remaining loop time
- check if we've reached the end of the waveform, and loop if we haven't.

That's all - on we go with the next sample, rinse and repeat until the entire waveform has been played.

Loop duration is a critical parameter here, of course. We can't make our loop too long, or else the "floating speaker state" trick won't work. It seems that
a loop time of around 1/15000 seconds is the absolute maximum, but ideally you should do it a bit faster than that.

With common PCM WAVs, we'll run into a problem at this point. An 8-bit PCM WAV has samples which can take 256 different volume levels, take the more popular 16-bit ones and you've already got 65536 levels. How are we supposed to control timing that precisely in our loop? 1/15000 seconds corresponds to around 233 cycles on the ZX Spectrum. The fastest output command - OUT (n),A - takes 11 cycles, which means we can squeeze at most 21 of those into the loop - and that's not taking into account all the tasks we need to perform besides outputting. So how do we output 256 or even 65536 levels? The answer is: We don't. Instead, we'll reduce the sample depth (that is, the number of possible volume levels) to a suitable level. This will obviously degrade sound quality, but hey, it's better than nothing.

As far as the Spectrum is concerned, 10 levels seems to be a convenient choice. You might be able to do more with clever code (or on a faster machine), but for the purpose of this tutorial, let's keep it at 10. That is, if we want to output just a single waveform. But of course we want to mix multiple waveforms at variable pitches, let's say two of them. In this case, our source PCM waveforms should have 5 volume levels.

As you might have already guessed, we'll need to develop our own PCM data format to encode these 5 levels. How this format will look like depends on your sound loop code as well as the device you're targetting - anything goes to make things as fast as possible. On the Spectrum, we may take two things into account:

- bit 4 sets the output state (let's ignore the details for now...)
- we have a fast command available for rotating the accumulator.

So, our samples bytes might look like this:

volume  binary    hex
level   76543210
______________________
  0%    00000000  #00
 25%    00010000  #10
 50%    00011000  #18
 75%    00011100  #1c
100%    00011110  #1e

This reasoning behind this may not be self-evident, but it'll become clear when we look at a possible sound loop.

Unfortunately, this custom PCM format still won't allow us to create a sound loop that is fast enough, so let's apply another restriction - use waveforms with a fixed length of 256 byte-sized samples. You'll see in a moment why this comes in handy.

Our sound loop might look like this:

  set up sample pointer channel 1                             ld bc,waveform1
  set base frequency ch1                                      ld de,noteval1
  clear add counter ch1                                       ld hl,0
                                                              exx
  set up sample pointer channel 2                             ld bc,waveform2
  set base frequency ch2                                      ld de,noteval2
  clear add counter ch2                                       ld hl,0
  set timer                                                   ld ix,0

loop:
  load channel 1 sample byte to accumulator                   ld a,(bc)
  output accu to beeper                                       out (#fe),a
  rotate left accumulator                                     rlca
  output accu to beeper                                       out (#fe),a
  rotate left accumulator                                     rlca
  output accu to beeper                                       out (#fe),a
  rotate left accumulator                                     rlca
  output accu to beeper                                       out (#fe),a
  add base frequency ch1 to counter ch1                       add hl,de 
    IF counter overflows, advance sample pointer ch1          adc a,0 \ add a,c \ ld c,a
                                                              exx
  load channel 2 sample byte to accumulator                   ld a,(bc)
  output accu to beeper                                       out (#fe),a
  rotate left accumulator                                     rlca
  output accu to beeper                                       out (#fe),a
  rotate left accumulator                                     rlca
  output accu to beeper                                       out (#fe),a
  rotate left accumulator                                     rlca
  output accu to beeper                                       out (#fe),a
  add base frequency ch2 to counter ch2                       add hl,de
    IF counter overflows, advance sample pointer ch2          adc a,0 \ add a,c \ ld c,a
    
  decrement timer and loop if not 0                           dec iy \ ld a,iyh \ or iyl \ jp nz,loop

Now you also see why limiting waveforms to 256 bytes is useful - this way, we can loop through them without ever having to reset the sample pointer, which of course saves time.

However, there's a whole array of problems with this code. First of all, it's still quite slow - 218 cycles. Secondly, you can see that the last output from each channel last significantly longer than the first 3. A bit of difference in length is actually not a big problem, but in this case, the last frame is 3 times longer - that's simply too much. Thirdly and most critically, I/O contention has not been taken care of (this mainly concerns the Speccy, of course).

If you've followed the discussion in this thread, you'll have noticed that I normally don't pay as much attention to I/O contention as other coders, but in this case, aligning the outputs to 8 t-state limits does make a huge difference. I'll let you figure this out on your own though. Check my wtfx code if you need further inspiration.

I will tell you one important trick for speeding up the sound loop though. Credits for this one go to sorchard from World of Dragon.

In the above sample, we're actually using 24 bit frequency resolution, since we're keeping track of the overflow from adding our 16-bit counters. But 16 bits are quite enough to generate a sufficiently accurate 7-8 octave scale. So in the above example, instead of doing "adc a,0 \ add a,c \ ld c,a" to update the sample counter, you could simply do "ld c,h", saving a whopping 22 cycles in total. The high byte of our add counter thus becomes the low byte of our sample pointer. The downside of this is that our waveforms need to be simple - e.g. just one iteration of a basic wave (triangle, saw, square, etc.). It's less of a problem than it sounds though, as you won't be creating really complex waveforms in 256 bytes anyway. And for a kick drum or noise, you can simply use a frequency value <256, making sure that you step through every sample in the waveform.

And that's all for now, hope you find the information useful, and as always, let me know if you find any errors or have any further suggestions/ideas.

827

(5 replies, posted in Sinclair)

Yes, sadly the number of 1tracker users remains low, even though gotoandplay's new Phaser3 track might suck a few new people into trying it out. It's a shame, I certainly prefer the pattern-less approach. And I understand it would be rather messy to include wtfx. The same for Beepola, of course. I think the sample part could be solved in 1tracker by displaying a 16x16 hex matrix (plus WAV import), but the fx table part is indeed tricky. Another, somewhat smaller issue might also be that certain instruments (noise, kicks) need an alternate frequency table. Anyway, I don't have any real hopes to see wtfx in a tracker anytime soon.

Nevertheless, I've got to figure out how I can write plugins for 1tracker. It looks not even that difficult, seems rather I'm having some technical issues. I can copy and rename existing engines and they'll work, but as soon as I change anything in the code 1tracker will not "see" them anymore.

Btw just realized that I wasted a few cycles in the note data loader, will update in the next days.

Edit: Done. Optimized the data loader, and also replaced the noise samples, which had the volume levels wrong.

828

(135 replies, posted in Sinclair)

Sure thing, here you go. May not be the most useful thing though as it's hardly commented.

If you want to learn the basics about keyboard reading and such, check out the wonderful "How to Write ZX Spectrum Games" by Jonathan Cauldwell: http://www.sendspace.com/file/alhxcq

829

(5 replies, posted in Sinclair)

Thanks wink Yeah, definately worth doing the alignments for this one. I'm amazed how much it reduces noise in this case.

I'm afraid it'll be a while till someone makes a proper track with this. At least till I can figure out how to make 1tracker plugins (still haven't gotten beyond the "1tracker-will-simply-ignore-whatever-I-throw-at-it" phase), or number9 integrates it into Beepola.

830

(5 replies, posted in Sinclair)

First up, some bad news: I'm releasing this engine as source-only, because some of it's major features can't be simulated via an XM template. Sorry 'bout that...

Anyway, wtfx (aka "wavetable player with effects") is my latest attempt at creating some decent-sounding triangle waves on the beeper. While not perfect, it's quite an improvement over qaop and the like, I think.

Features:
- 2 channel wavetable mixing, 256 byte wavetables
- 4 volume levels per sample
- 17.5 KHz mixing
- tick-based effects (can change wavetables/pitches without having to set a new note)
- per-step tempo control

download
browse source

Enjoy! And thanks to introspec for stressing the importance of 8t output alignment wink

831

(135 replies, posted in Sinclair)

In the worst case you could still use my rom2pwm craptility, though it's definately not very useful.
My website is down again so I'm attaching it just in case.

832

(135 replies, posted in Sinclair)

Doing this without display output would indeed save a lot of time. Why do you need to write it in C/Basic with inline asm though? I think just asm might be easier even if you don't know a lot of asm.

Yeah, a vid would be awesome!

@gotoandplay: I just read somewhere that on newer Windowses, installers that require direct hw access might need to be run via right-click-"run as administrator" even if you are already admin. Dunno if that helps in your case?

834

(135 replies, posted in Sinclair)

Yeah, I've been wanting to make something like this for a long time. However, it's no trivial project, I'm looking at at least 2-3 months of work here. I still know near to nothing about Spectrum graphics, so I'd need to learn a lot of new things. I'm afraid I won't get around to it in 2016.

Ok, fixed the mute indicator bug, and AutoInc is now off by default. But the thing I can't figure out is how you manage to crash the calc. I'm on F00, pushing buttons like a maniac, and the damn thing just won't die. How do you do it exactly? Could you by any chance record a video of you crashing the thing?

836

(22 replies, posted in Sinclair)

The xm2squeek package has been updated, it now enables you to set the duty cycle per pattern.

837

(65 replies, posted in Sinclair)

True that, seems like it's recorded from TV speakers.

Edit: Just polished up the yawp package a little, correcting some errors in the documentation and fixing a faulty sample in the xm template.

I'm glad you're still trying to push HT2 to the limits, otherwise these bugs might go undiscovered. And it's probably me who is to blame for the RAM clears yikes Thanks for reporting in any case, I'll look into these. I'll also consider deactivating autoinc by default, since you're not the first to ask wink

839

(65 replies, posted in Sinclair)

Add 2 nops immediately after every out (#af),a instruction.

Not a wine user (my PC is too old/slow, and I consider it a security risk), so I'll have to try dcvg5k on my win xp box after all hmm Judging from the recording the guy put on soundcloud it sounds good ok though, except that the noise drums seem a bit quiet.

840

(65 replies, posted in Sinclair)

Glad you got it working!

Could you ask Daniel if he would be so kind to release the sources for the new version of dcvg5k, so I can build it on linux? I failed to build the old version btw, because ld can't find the DAsm function in dasmz80.c for some reason.

object/dcvg5kdesass.o: In function `Displaydesass':
dcvg5kdesass.c:(.text+0x26f): undefined reference to `DAsm'
collect2: error: ld returned 1 exit status
makefile:9: recipe for target 'dcvg5k' failed

Btw, on forum.system-cfg.com you advised to remove the "exit" section. This isn't such a good idea, because it will cause a stack imbalance if the routine actually exits (eg. if loop is disabled [jump in line 47 enabled] or if you put the keyboard check back in). A better way would be to remove lines 28-29 and 460-461 - these are only necessary because ZX Spectrum BASIC expects a certain value in HL'. You probably don't need to do this on VG5000.

841

(65 replies, posted in Sinclair)

Hello jeremielapuree,

Very cool that you are porting this routine to VG5000! I'm busy today so I can't check out your work right now, but I will do so as soon as possible.
Is there a version of dcvg5k for regular linux, btw?

The _skip labels are so-called local labels, they are only valid till the next non-local label. Prefixing with an underscore (_) is how pasmo does it, other assemblers will use a different syntax.

You can in fact remove lines 19-26 in main.asm, this code is just there to detect the presence of a Kempston joystick interface. So it's ZX Spectrum specific, you don't need it on VG5000. Then, you can replace the code you listed in your post with a standard keyboard check on your machine.
Hope that helps wink

842

(65 replies, posted in Sinclair)

Aaaand another attempt at fixing the /§$(/§&$§! quattropic converter. Rewrote the entire mode selection code (the one that sets the row mode flag based on the presence/absence of noise and slides). Who knows, maybe it will actually work for once? Going on previous experience I don't have very high hopes.

843

(22 replies, posted in Sinclair)

Ah, I see. That's indeed a promising concept. It'd be especially interesting to attempt an implementation that uses it for harmonics.

What was the PIC implementation used for, just a stand-alone thingy? Do you still have sources for it?

844

(22 replies, posted in Sinclair)

Zilog wrote:

Is it really more efficient than mine?

Not at all, yours is certainly more efficient! The idea was more to smoothen row transitions by speeding up the data loading. I think that worked out reasonably well, though as you can probably tell from the code it's not optimized at all yet.

8bit mixing with postscaling? Sounds intriguing, but to be honest I don't quite understand how that would work. Could you explain a bit more about this idea? In any case, I'd love to see a new engine from you, given the unique approach you took in Squeeker smile

845

(65 replies, posted in Sinclair)

Ah yes, some test files will help to analyze the problem, though at this point I'm considering rewriting the whole converter, as I've patched it like 5 times already without success.

The idea of the slides is indeed to simulate kick drums and toms. However, seems it's not that feasible after all, since their volume is too low. So perhaps I'm gonna add some good old-fashioned click drums after all.

846

(3 replies, posted in Sinclair)

Hehe, enough of the honey big_smile The problem is that at the moment there's less than a handful of active developers on here, none of which really have their head in Zeddy stuff. Hmm, perhaps you could poke the nollkolltroll guy, he seems to be one of the most talented ZX81 coders out there at the moment...
Another problem is that at the moment there are no emulators which accurately emulate ZX81 port behaviour (you may remember this was giving us a lot of trouble with 1k2b). And I can't really be bothered to buy a ZXpand just for the purpose.

Well, if you have the time please do post some info about the ZX81 "loud tape" mod here, just in case someone wants to have a go at it.

Oh wow, that's exactly the type of timbre effect I've been phantasizing about.
I could try to implement it for TI (6 MHz), but on the other hand the target audience for that is tiny (basically boils down to garvalf and myself at the moment), so I'm not really motivated to spend my time on it.
Do you think you could get it to work in 3.5 MHz by going back to Phaser1 (ie, one channel with the effect and one without), and perhaps using a partially unrolled sound loop (ie updating frequency counters one at a time instead of both each loop iteration)?

848

(65 replies, posted in Sinclair)

Gah! Not again... Yeah, that's a bug in the converter alright. For some reason I just can't get it to output the proper combination of flags and counter values. Ok, I'll look into it in the coming days.

The slides should theoretically behave as you might expect, except that (unlike in the template) they will reset on every row.

849

(7 replies, posted in General Discussion)

Indeed, you have a good point there.

I've experimented a little with combining pin-pulses with Phaser-like synthesis. It certainly leads to some interesting results (overtones and such). However, volume balance is a problem, so the effect tends to be quite subtle. I haven't yet managed to get it right.

There's also another interesting idea by Alone Coder, based on a kind of wavetable synthesis, or duty cycle modulation, if you will:

Alone Coder wrote:

I make a series of long impulses between natural divisions of the
period.

For example:
- 128,256 (I list the positions where the phase is changed) gives a
simple meander.
- 85,92,128,128+85,128+92,256 gives lighter sound with acute 3rd, 5th,
7th harmonics. Sadly we can't avoid even harmonics but we can minimize
them.
- 21,92,128,128+21,128+92,256 gives even lighter sound with more power
in higher harmonics.

Edit: Just saw your post in /sinclair. Too tired now, will check it out tomorrow.

850

(5 replies, posted in Sinclair)

Yeah, kickass track. Also, glad to see this powerful engine is finally getting some well-deserved attention.