Well, XXL and me did this a while ago, based on LSengine: http://battleofthebits.org/arena/Entry/circls/12238/
Also, there is http://www.pouet.net/prod.php?which=58051, based on Squeaker engine by Zilogat0r.
A pity that the dl for Beep'em all 2600 is broken... paging XXL, please upload it somewhere again!

The Lynx has fairly complex audio hardware on board, so it's not really interesting for 1-bit development, I think. But we're still waiting for Mono to finish his Music Studio port for the Atari Portfolio wink

927

(2 replies, posted in Sinclair)

Gnnnnrrrrr! There goes my wonderful plan for a new 256b sample engine... yikes

928

(2 replies, posted in Sinclair)

Encountered something strange a while ago. It seems that on ZX Spectrum the carry flag is reset after OUTI (and possibly OUTD). However, in documentation both official and unofficial, it is claimed that OUTI has no effect on the carry flag. Is this a special "feature" of the Z80 used in the Speccy, or is the documentation incorrect?

Just to clarify, plugging in earphones isn't harmful. Just don't do it during normal TI-OS operation, ie. plug them in after you've started DoorsCS/Mirage/whatever.

930

(164 replies, posted in Sinclair)

Latest builds have 3 new engines: OctodeXL, ZX-7, and the newly created prdr.

@garvalf: What Linux flavour are you using? Haven't encountered these problems under Debian, though I haven't tested it thoroughly yet.

@Shiru: ZX-7 engine is giving me problems, complains about "the engine does not provide any data".

@introspec: hehe thanks wink It's gonna be a tough battle... TI has nearly the same setup as the Speccy, but it's also nearly twice as fast wink

garvalf wrote:

would the 84+ be much better than the 83+? It's more expensive anyway.
So far the 83+ I got seems ok. It's a "TI-83 Plus.fr" in fact, for making music with the "French Touch" wink

I got the cable link we talked about. It's working as a "grey link" (serial) cable. I didn't manage to make it work on linux with tilp, but I'll try on another computer. It just says the cable is not connected but my system is detecting it (with dmesg). On windows I got tilp to work (with efforts), I had to test several serial ports. The official TI Connect works the best. (I had to install by hand this driver http://www.prolific.com.tw/US/ShowProdu … mp;pcid=41)
I don't know if a silverlink would have worked better, I've read it was quite complicated to connect to linux. But I'll try more with mine...

Hmm, SilverLink works out-of-the-box for me (Debian+TiLP from the repos). Never had any probs with it.
I think the problem is that the cable is interpreted as serial when it is really USB. On the one hand it's not too surprising because originally GrayLink was a indeed a serial cable. Hmm... maybe you can force TiLP to use it as SilverLink via command line? Otherwise, if problems persist shoot Lionel Debroux (TilP dev) a mail, he's a really nice guy and he might be curious about this cable, too.

A few things which didn't work at first (not your fault):
- The first time I added some new patterns to your sample tune, it could compile but not be executed on the TI83, probably a memory problem. The message during execution was more saying something like "syntax error" so it wasn't easy to understand it was because of this.

Hmm, if there's a memory problem you usually wouldn't be able to transfer it to the machine in the first place. Perhaps you can send me the problematic song file so I can have a look? Maybe I made an error somewhere...

- Adding 2 rawp songs in the TI83 were also filling its memory badly.

Yes, undoubtedly so, because the rawp engine itself is rather huge. Best to recycle the player and just swap out the song data (have a look at rawp.asm, the pointer to the music data is on line 32)

- it's a pity they used those tiny jack connectors. I only found a earphone for using with it, so I'll search more or buy a new converter. When the earphones are connected, the calculator is very unresponsive. Probably with a jack converter it won't do this.
- no speakers on the TI, it's a pity too...

Yes, 2.5mm jack suxx big_smile The unresponsiveness is due to the calculator picking up phantom voltage from the earphones. Plugging them in after you've started the shell should help to avoid the problem.

- Thank you for telling me about the 8xp. I also tried the 83p files but they were also using the extra patterns so it couldn't work anyway.
- http://irrlichtproject.de/downloads/rawp-ti.zip is not responding...

Yeah, my stupid server is down again.

So in conclusion I managed to create some music and send it to the calculator, so I'm quite happy with this. I experienced some heavy detuning in comparison with the sounds on milkytracker so I need to work more with it to learn what works ok and what works less. It sounds amazing anyway, thanks for this work!

Thank you for trying it out! Unfortunately this engine uses 8-bit frequency counters, so detuning is to be expected from upper 3rd octave on. I'm working on a version with 16-bit counters though.

Edit re: 83+ vs 84+. Starting with the 83+ SE, the processor can be used in 15 MHz mode, which is awesome. However, I don't own any calcs of this generation (yet), so I'm not supporting this. Also, the size of the Flash RAM varies among models (though it's not necessarily bigger on 84+ models). Other than that, the 83+ and 84+ are practically identical.

Yes, the 83+ is decent, just got one myself. However, don't try the 83p on it - it needs the 8xp wink
The naming of these calcs is ridiculous, btw. Especially in France where all the machines get different names. Ie. TI-82 STATS = TI-83, it has nothing to do with TI-82. There is also TI-82 Plus which is a modified TI-84 Plus, and the TI-82 Advanced, which is a more locked down TI-82 Plus.

garvalf: Thanks, that is quite useful. I'll integrate that into the first part of the tutorial.
Shiru, Hikaru: Since Hikaru deleted his posts, I've moved the rest of the "discussion" to OT for the time being. Will delete it in a while if nobody objects.

That in itself would not be such a big problem as the later models use a similar setup, but unfortunately there is hardly any documentation regarding the consequences in this case. From what I've deduced over the years, the 82 Parcus is basically a TI-83 hardware-wise (using the same port layout and such), but running a modified 82 OS (which has not been documented properly either). I've been able to run software on it, but it seems the relocation process in CrASH19.0006 is buggy (or at least there is some feature of it that isn't documented), so usually stuff will crash on exit.

I've never actually tested any of my software on TI-82 STATS. Supposedly it's fully compatible with TI-83, but it would be great to get some confirmation of this. Well, whatever you do, just don't get a TI-82 Parcus. Since years I'm trying to figure out why stuff doesn't work on it like it's supposed to, but in the end the answer remains: Because it just doesn't.

Thanks! I was surprised, too, that 6 MHz was enough to get rid of the nasty discretion noise. I should revisit the Speccy version, see if I can speed it up some more, and perhaps add 16-bit counters while I'm at it.

Re the link cable, I use the "SilverLink" USB cable nowadays, works fine with all models despite what TI claims. I'm getting a new batch of them shortly, so I can give you one, provided we can work out a way to send it. Do you know anyone in the Netherlands who could take it, or do you think it's safe enough to try it via mail?

937

(2 replies, posted in Sinclair)

Amazing. Congratulations on this outstanding achievement, you've raised the bar to near unreachable levels once again!
Btw just recompiled 1tracker from source, still works fine with krue's patch.

As a byproduct of stuff I'm currently working on, I ported my rawp engine to TI Z80. Thanks to the faster CPU, it sounds much, much better than the original ZX Spectrum version.

For those of you who aren't aware of the original engine, here's the feature list:

- 2 tone channels
- 13 different sounds
- limited volume control
- 1 interrupting hihat sound
- per-step tempo control

download (includes XM converter, requires Perl)
sound example

939

(164 replies, posted in Sinclair)

Yes, it works! Thanks a ton, krue!

940

(164 replies, posted in Sinclair)

Ah, thanks krue. Alas, I only have a 32 bit system - any idea what changes I'd need to make? At the moment gcc chokes on scriptbuilder.cpp, with some weird errors like

angelscript/scriptbuilder/scriptbuilder.cpp:808:83: error: invalid conversion from ‘int*’ to ‘asUINT* {aka unsigned int*}’ [-fpermissive]
     t = engine->ParseToken(&modifiedScript[pos], modifiedScript.size() - pos, &len);
                                                                                   ^
In file included from angelscript/scriptbuilder/scriptbuilder.h:25:0,
                 from angelscript/scriptbuilder/scriptbuilder.cpp:1:
./angelscript/include/angelscript.h:764:24: note: initializing argument 3 of ‘virtual asETokenClass asIScriptEngine::ParseToken(const char*, size_t, asUINT*) const’
  virtual asETokenClass ParseToken(const char *string, size_t stringLength = 0, asUINT *tokenLength = 0) const = 0;

Here's a list of multi-channel routines for the ZX Spectrum and ZX-81. Who will be the first to use them all? wink


ZX Spectrum

Cross-Platform Multi-Engine Editors:
1tracker
Beepola
bintracker


anteater (utz)
2ch square wave, click drums, simple PWM, compact size
XM converter, 1tracker

beepertoy (utz)
meta-engine with multiple cores, including Squeeker, Tritone, and Octode emulation, wavetable synthesis, lo-pass/hi-pass filters
source, editor n/a

BeepTracker (Alone Coder)
5ch pin pulse, envelopes/volume control, fx, non-interrupting sample drums
native editor

betaphase (utz)
experimental 3ch phaser type engine, slides, noise mode
bintracker

BM-1 aka BeepModular-1 (utz)
experimental 2ch engine with JIT code modification, can emulate almost any other beeper engine
source, editor n/a

BuzzKick (Shiru)
Improved clone of FuzzClick (SpecialFX) with sampled drums.
1tracker

BT'man (Alone Coder)
5ch custom synthesis, volume control, fx
native editor

Ear Shaver (Shiru)
2ch Earth Shaker or PuInt Synthesis, pwm drums
1tracker

Earth Shaker (Michael Batty)
1,5ch custom synthesis, click drums
1tracker

fluidcore (utz)
4ch wavetable synthesis
XM converter

Fuzz Click see SpecialFX

Huby (Shiru)
2ch PFM, synth drum, compact size
XM converter, 1tracker, Beepola

LSengine '89 (Lyndon Sharp)
2ch custom synthesis, non-interrupting sampled drums
XM converter, 1tracker

LSengine '91 (Lyndon Sharp)
2ch custom synthesis, non-interrupting sampled drums
1tracker

LSMB (Lyndon Sharp)
Lyndon Sharp's Wham! The Music Box clone with sampled drums
1tracker

Music Box, the see Wham

Music Studio, the (Saša Pušica)
2ch custom synthesis, interrupting synth drums
native editor, Beepola, 1tracker

Music Synth (Simon C. Tillson)
2ch PFM, envelopes, interrupting synth drums
native editor, native editor (tracker edition)

nanobeep (utz)
2ch custom synthesis, click drum, compact size (56-77 bytes)
XM converter, 1tracker

nanobeep2 (utz)
2ch square wave, slightly less compact than original nanobeep (64-99 bytes depending on configuration), but more feature-rich
source, editor n/a

ntropic (utz)
2ch square wave, 1ch noise, click drum, compact size
XM converter, 1tracker

Octode (Shiru)
8ch PFM, click drums
XM converter, 1tracker

Octode XL (introspec)
8ch PFM, volume control, click drums
XM converter, 1tracker

Octode 2k15 (utz)
8ch PFM, click drums
XM converter, 1tracker

Octode 2k16 (utz)
8ch square wave (digital), click drums
XM converter, 1tracker

Octode PWM (utz)
8ch custom synthesis, click drums
XM converter, 1tracker

Orfeus see SpecialFX

Oleg Origin's Engines (Oleg Origin)
various engines with different capabilities
no public release available yet

Phaser1 (Shiru)
1ch square wave, 1ch dual generator square wave, fx, interrupting sample/synth drums
native editor, 1tracker, Beepola

Phaser2 (Shiru)
2ch dual generator square wave, fx, non-interrupting synth drums
1tracker

Phaser3 (Shiru)
Even more awesome than Phaser2.
1tracker

PhaserX (utz)
2ch Phaser type engine with a drawbar organ effect and click drums. Ch2 ops can be decoupled to form 2 Squeeker type channels.
bintracker

PhaseSqueek (utz)
Powerful Phaser/Squeeker hybrid with 2-4 channels, noise, drawbar organ fx, fx tables, click drums
bintracker

PlipPlop (✝Jonathan Smith)
1ch custom synthesis, click drums
Beepola

povver (utz)
3ch pulse wave with simple volume envelopes, noise mode, click drums
1tracker

poww (utz)
2ch custom synthesis, click drums
1tracker

prdr (Shiru)
2ch custom synthesis, pitch slides
1tracker

Pytha (utz)
2ch tri/rect/saw/noise, click drums
1tracker, bintracker

qaop (utz)
2ch wavetable synthesis, click drums
XM converter

Qchan (Shiru)
4ch PFM, envelopes/volume control, click drums
XM converter, Beepola

quattropic (utz)
4ch square wave, variable pulse width, noise, pitch slides
XM converter, 1tracker

rawp (utz)
2ch wavetable synthesis, click drums
XM converter

SampleTracker (CBM)
3ch digi
native editor

Savage (Jason C. Brooke)
2ch square wave, variable pulse width, fx, click drums
Creador Musical (native editor), Beepola

Savage HD (introspec)
2ch square wave, variable pulse width, fx, click drums
editor n/a, can patch Beepola output against source

Squat (Shiru)
4ch OR synthesis, noise mode, sampled drums
1tracker

Squeeker (Zilogat0r)
4ch OR synthesis, variable pulse width
native editor, XM converter

Squeeker Plus (utz)
4ch OR synthesis, pulse width envelopes, noise mode, slides, click drums
1tracker

SpecialFX (✝Jonathan Smith)
2ch PFM, envelopes, click drums
Orfeus (native editor), Beepola

Spectone-1 (Zoltan Janosy)
4ch custom synthesis, envelopes
editor n/a

Stocker (Shiru)
2ch PFM, full envelopes/volume control, click drums
Vortex Tracker converter, 1tracker

StringKS (utz)
2ch Karplus-Strong string synthesis, volume control, pwm drums
source

tbeepr (introspec)
2ch custom synthesis, variable pulse width, duty cycle sweep, interrupting click drums
1tracker, source

Tim Follin 3ch (Tim Follin)
3ch PFM, volumes
1tracker

Tritone (Shiru)
3ch square wave, variable pulse width, click drums
XM converter, Beepola, 1tracker

Tritone Digi (Shiru)
3ch square wave, variable pulse width, pwm drums
1tracker

Tritone FX (utz)
3ch square wave, variable pulse width, noise, tick-based fx, click drums
source, editor n/a

Vibra (utz)
2ch  tone, 1ch noise, vibrato, slides
1tracker

Wham (Mark Alexander)
2ch square wave, interrupting synth drums
native editor, native editor (tracker edition), Beepola

wtbeep (utz)
3ch tone with 32 selectable waveforms, click drums
1tracker

wtfx (utz)
2ch wavetable synthesis, tick-based fx
editor n/a, source

xtone (utz)
6ch square wave, variable pulse width, click drums
XM converter, 1tracker

yawp (utz)
3ch wavetable synthesis
XM converter

YU The Music Box see Music Studio, The

zbmod (utz)
3ch digi/samples
XM converter

ZX-3 (✝Ján Deák)
3ch PFM, envelopes
native editor, 1tracker

ZX-7 (✝Ján Deák)
8ch PFM
native editor, 1tracker

ZX-10 (✝Ján Deák)
4ch PFM, envelopes
1tracker

ZX-16 (✝Ján Deák)
16ch PFM, pitch slides
XM converter, MIDI converter



ZX81

1k2b (utz)
2ch square wave, click drums
XM converter

942

(164 replies, posted in Sinclair)

Yes, that did the trick, thanks. Btw took me a while to figure out how to operate the instrument editor, you might want to add those keys to the help page.

Unfortunately I'm very busy at the moment so it'll be a while before I get around to really digging into this, but nevertheless a huge thank you, Shiru.

943

(164 replies, posted in Sinclair)

Yippee! Yes, Phaser2! I'm very, very happy right now.
However, not completely happy because unfortunately I get an "not a valid win32 executable" error when trying to run this (XP SP3).
Ok, I'll see if I can compile it from source for Linux. Though without krue's compile guide I think it'll be tricky...

edit: not getting very far:

angelscript/scriptarray/scriptarray.cpp:15:34: error: ‘asAllocMem’ was not declared in this scope
 static asALLOCFUNC_t userAlloc = asAllocMem;
                                  ^
angelscript/scriptarray/scriptarray.cpp:16:34: error: ‘asFreeMem’ was not declared in this scope
 static asFREEFUNC_t  userFree  = asFreeMem;
                                  ^
angelscript/scriptarray/scriptarray.cpp: In member function ‘void CScriptArray::Precache()’:
angelscript/scriptarray/scriptarray.cpp:1525:11: error: ‘class asIScriptFunction’ has no member named ‘GetParam’
     func->GetParam(0, &paramTypeId, &flags);
           ^
makefile:6: recipe for target 'angelscript/scriptarray/scriptarray.o' failed
make: *** [angelscript/scriptarray/scriptarray.o] Error 1

Haha awesome, great job! So that's how you made that kickass pink floyd cover track... and I was fooled into thinking it was Octode XL wink
btw what about this? Also, welcome to the new 1-bit forum, good to have you aboard again.

ed: A small issue - you might want to restore HL' on exit (original zx16 misses this, too).

Thanks Shiru, that's a great explanation.
Now there's one thing that I still don't understand myself - if you or someone else has the time, please do explain: How do engines like e.g. Zilog's Squeeker or the infamous Spectone-1 demo (the one in the "hidden" part that can be reached with key A) work?

Ok folks, I just updated the rules - submissions to the Code category don't need to be anonymous if you're worried about people stealing your code.

Part 7: Variable Pulse Width

Alright, if you've come this far, you should be able to write a pretty decent basic 1-bit sound routine. But the real fun of 1-bit coding has only started. From this point on, coding for 1-bit sounds becomes somewhat of an art form - you've got to use your creativity and imagination in order to build a routine that does something out of the ordinary.

I'm by no means an assembly expert, and don't understand half of what all these crazy beeper engines out there are doing. So I can only share those few tricks and techniques that I have so far discovered/reverse-engineered/been told about.

Ok, let's talk about variable pulse width. Varying the pulse width has a number of useful effects, most importantly the ability to produce more interesting timbres when used in conjunction with the pulse interleaving method. (In conjunction with PFM, it can be used to create volume envelopes, but this is not what this part of the tutorial is about.)

Imagine a classic pulse interleaving routine with 16-bit counters, as explained in part 4. The basic procedure for updating a channel's state and counters is:

  counter := base + counter
  IF carry THEN
    state := state XOR ch_toggle
  ENDIF
  OUTPUT state

This will output a square wave with a 50:50 duty cycle, because half of the time the output is 0, and the other half it's 1.
Well, there is another of way of doing this.


  counter := base + counter                       ld hl,nnnn \ add hl,bc \ ld b,h \ ld c,l

  IF counter < 8000h THEN                         ld a,h \ cp #80
    state := off                                  ld a,0 \ jr nc,skip
  ELSE
    state := on                                   ld a,#10
  ENDIF                                    skip:
 
  OUTPUT state                                    out (#fe),a

So, instead of waiting until the counter wraps from FFFFh to 0h, we now check if it has wrapped from FFFFh to 0h or from 7FFFh to 8000h. So in effect we change the state twice as often, but we will still get a 50:50 square wave. Now what happens if we compare against a value other than 8000h? You probably can guess: Yes, that will change our duty cycle. So, to get a 25:75 square wave for example, we'd compare against 4000h, for 12.5:87:5 we compare against 2000h, and so forth. Simple, right?

If only we wouldn't have to deal with that ugly conditional jump that ruins our timing. Well, in Z80 asm there's a handy trick. It is used in Shiru's Tritone, for example.

  ld hl,nnnn \ add hl,bc \ ld b,h \ ld c,l       ;do the counter math as usual
  ld a,h \ cp nn                                 ;compare against our chosen value
  sbc a,a                                        ;A will become 0 if there was no carry, else it becomes FFh
  and #10                                        ;ANDing 10h will leave A set to either 0 or 10h, depending the on previous result
  out (#fe),a
Part 6: Drums

We got ourselves a nicely working 1-bit routine now, but something is missing. Now what could that be? Oh right, we need some drums!

As usual, there are several approaches to realize drum sounds. The by far most common one is the "interrupting click drum" method. The idea is that in order to play drum sounds, you briefly pause playback of the tone channels and squeeze the drums in between the notes. In order for listeners to not realize that tone playback has been interrupted, the drum sounds need to be quite short, typically in the range of a few hundred up to a couple of thousand t-states.

There are countless ways of actually producing the drum sounds - pretty much anything that makes noise goes. I'll only post a very primitive example here to get you started, the rest is entirely up to your creativity wink

We'll need 3 variables:

data_pointer - a pointer into ROM or another array of arbitrary values. On ZX Spectrum, we'll use HL.
timer - a counter to keep track of the drum length. We'll use B on the Speccy.
state - our good friend, the output state. Let's use the accumulator A.

and a constant which equals the state being on/1, let's call it ch_on.

Now we do the following:

PSEUDOCODE                                        ZX ASM
drumloop:                                         
  state := (data_pointer) AND ch_on               ld a,(hl) \ and #10
  OUTPUT state                                    out (#fe),a
  INCREMENT data_pointer                          inc hl
  DECREMENT timer                                 
  IF timer != 0 THEN                              djnz drumloop
    GOTO drumloop
  ELSE
    EXIT                                          ret
  ENDIF

This will create a very short noise burst - for better sound, you may want to add some bogus commands for wasting a few cycles in the loop. You would typically trigger this code at some point during reading in data for the next sound loop.

One last thing, you will need to adjust the main soundloop timer. Otherwise you will get an unwanted groove effect every time a drum is played. So you need to count the number of t-states your drum code takes to execute. Divide this number by the amount of t-states your sound loop takes, and subtract the result from the main timer every time you trigger a drum.



Another approach to creating drums is the "PWM sample" method. PWM (pulse-width modulation) samples are a distant relative of the more widely known PCM (WAV) samples. In PCM data, each data value (also known as sample) represents the relative volume at a given time. However, for 1-bit devices, volume is rather meaningless as you have only two volume states - nothing (off, 0) or full blast (on, 1). So instead, in PWM data each sample represents the time taken until the 1-bit output state will be toggled again. Sounds a bit confusing? Well, you can also think of PWM data as a sequence of frequencies. So, think about how a kickdrum sounds: It starts at a very high frequency, then quickly drops and ends with a somewhat longer low tone. So, as a PWM sample, we could create something like this:

    db #80, #80, #70, #70, #60, #60, #60, #50       ;high start and quick drop
    db #50, #50, #50, #40, #40, #40, #40, #40
    db #30, #30, #30, #30, #30, #30, #20, #20
    db #20, #20, #20, #20, #20, #20, #20, #10
    db #10, #10, #10, #10, #10, #10, #10, #08
    db #08, #08, #08, #08, #08, #08, #08, #08
    db #04, #04, #04, #04, #04, #04, #04, #04    ;slow low end
    db #04, #04, #04, #04, #04, #04, #04, #04
    db #02, #02, #02, #02, #02, #02, #02, #02
    db #02, #02, #02, #02, #02, #02, #02, #02
    db #02, #02, #02, #02, #02, #02, #02, #02
    db #00                        ;end marker

Still confused? Well, luckily there's a utility that you can use to convert PCM to PWM. It's called pcm2pwm and can be downloaded here.

Now, how to play back this data? It couldn't be simpler. We need 3 variables:

data_pointer - a pointer that points to the memory location of the PWM data. We'll use HL in our ZX Spectrum Z80 asm example.
counter      - a counter that is fed with the sample values. We'll use B.
state        - the output state. We'll use A' (the "shadow" accumulator).

Also, we need the ch_toggle constant as usual.

PSEUDOCODE                                   ZX ASM

  state := on                                ld a,#10

drumloop:
  counter := (data_pointer)                  ex af,af' \ ld b,(hl)
  IF counter == 0 THEN                       xor a \ or b
    EXIT                                     ret z
  ENDIF
                                             ex af,af'
innerloop:
  OUTPUT state                               out (#fe),a
  DECREMENT counter
  IF counter != 0 THEN                       djnz innerloop
    GOTO innerloop                       
  ELSE
    INCREMENT data_pointer                   inc hl
    state := state XOR ch_toggle             xor #10
    GOTO drumloop                            jr drumloop
  ENDIF  

You can call this code inbetween notes, just like with the interrupting click drum method. However, this will lead to the usual problems - the drum sound needs to be very short, and you need to correct the main soundloop timer. A much better way to use PWM samples is to treat them like an extra channel, and trigger them within the soundloop alongside with the regular tone channels. The above code should be easy to adjust, so I'll leave that to you wink

Edit 15-12-01: Link to new version of pcm2pwm added.

Part 5: Improving the Sound

You've followed this tutorial series and have come up with a little 1-bit sound routine of your own design. Only problem - it still sounds crap - notes are detuned, there are clicks and crackles all over the place, and worse of all, you hear a constant high-pitched whistle over the music. So, it's time to address some of the most common sources of unwanted noise in 1-bit sound routines, and how to deal with them.


Timing Issues

In order to keep the pitch stable, you need to make sure that the timing of your sound routine is as accurate as possible, ie. that each iteration of the sound loop takes exactly the same time to execute.

Let's take a look again at the first example from part 1. We can see that that code is not well timed at all:

soundLoop:
  
       xor a                    ;4
       dec b                    ;4
       jr nz,skip1              ;12/7
       ld a,#10                 ;7
       ld b,c                   ;4
skip1: 
       out (#fe),a              ;11
  
       xor a                    ;4
       dec d                    ;4
       jr nz,skip2              ;12/7
       ld a,#10                 ;7
       ld d,e                   ;4
skip2:
       out (#fe),a              ;11
  
       dec hl                   ;6
       ld a,h \ or l            ;8
       jr nz,soundLoop          ;12
                                ;78/92t

Due to the two jumps, there are four different paths through the core (no jump taken, jump 1 taken, jump 2 taken, both jumps taking), and the sound loop length thus varies up to 12 t-states - that's more than 15% of the sound loop length, and therefore clearly unacceptable. We need to make sure that the sound loop will always take the same amount of time regardless of the code path taken. One possible solution would be to introduce an additional time-wasting jump:

soundLoop:
  
       xor a                    ;4
       dec b                    ;4
       jr nz,wait1              ;12/7----
       ld a,#10                 ;7
       ld b,c                   ;4
       nop                      ;4-------7+7+4+4=22
skip1: 
       out (#fe),a              ;11
  
       xor a                    ;4
       dec d                    ;4
       jr nz,wait2              ;12/7
       ld a,#10                 ;7
       ld d,e                   ;4
       nop                      ;4
skip2:
       out (#fe),a              ;11
  
       dec hl                   ;6
       ld a,h \ or l            ;8
       jr nz,soundLoop          ;12
                                ;100/100t
                
wait1:
       jp skip1                ;12+10=22
       
wait2:
       jp skip2

There are other possibilities, but I'll leave that for another part of this tutorial.


Row Transition Noise

A common moment for unwanted noise to occur is ironically not during the sound loop, but between notes - the moment when you're reading in new data and updating your counters etc. This is called row transition noise.

Row transition noise is very difficult to avoid. Your focus should therefore be on reducing transition noise rather than trying eliminating it. The key to this is to read in data as fast and efficiently as possible. Not much else can be said about this, except: Make sure you optimize your code. For starters, WikiTI has an excellent article on optimizing Z80 code.

Theoretically, there is a way for eliminating transition noise, though in practise very few existing beeper engines use it (Jan Deak's ZX-16 being a notable example). That way is to do parallel computation, ie. read in data while the sound loop is running. Obviously this is not only rather difficult, but also it is usually only feasible on faster machines - on ZX Spectrum, it will most likely slow down your sound loop too much.

Which brings us to another problem...


Discretion Noise

Discretion noise, also known as parasite tone, commonly takes the form of a high-pitched whistling, whining, or hissing. It inevitably occurs when mixing software channels into a 1-bit output and cannot be avoided. It is usually not a big deal when doing PFM, but can be a major hassle with Pulse Interleaving. The solution is to push the parasite tone's frequency above the audible range. In other words, if you hear discretion noise, your sound loop is too slow. As a rule of thumb, on ZX Spectrum (3,5 MHz) your sound loop should not exceed 250 t-states.

Let's take a look at the asm example from part 4 again. At the end of the sound loop, there is a relative jump back to the start (jr nz,soundLoop). A better solution would be to use an absolute jump (jp nz,soundLoop) instead, because an absolute jump always takes 10 t-states, but a relative jump takes 12 if the jump is actually taken, which we assume to be the case here.

Also, leading up to the jump we have

   dec ix
   ld a,ixh \ or ixl
   jr nz,soundLoop

which takes a whopping 38 t-states. It may be a good idea to replace it with

   dec ixl
   jr nz,soundLoop
   dec ixh
   jp nz,soundLoop

This will take only 20 t-states except when the first jump is not taken. It will introduce a timing shift every 256 sound loop iterations, but this is usually not a major problem, as it happens at a frequency below audible range.

I'll cover some more tricks for speeding up synthesis in one of the following parts.


IO Contention

This section addresses a problem that is specific to the ZX Spectrum. You can most likely skip this section if you're targetting another platform.

IO Contention is an issue that occurs on all older Spectrum models up to and including the +2. The implication is that in certain circumstances, writing values to the ULA will introduce an additional delay in the program execution. You don't need to understand the full details of this, but if you are curious you can read all about IO contention here.

What's important to know is that delay caused by IO contention affects our sound loop timing. Which is bad, as I've explained above. For sound cores with only one OUT command the solution is rather trivial: You just need to make sure that the number of t-states your sound loop takes is a multiple of 8. For ideal sound in cores with multiple OUTs however, the timing distance between each OUT command must be a multiple of 8. Naturally this is pretty tricky to achieve (and chances are your core will sound ok without observing this), but keep it in mind as a general guideline.

Edit 15-12-01: Added/changed info as suggested by introspec.

Part 4: 16-Bit Counting

If you have tried the methods in Part 1, you might have noticed that it produces a lot of detuned notes at higher frequencies. This is because 8-bit values are too small to properly represent the common range of musical notes.

So, in order to increase the usable tonal range of your engine, you should use 16-bit values for your frequency counters. However, this poses another problem: As 1-bit DACs are usually hooked up to slow 8-bit CPUs, 16-bit maths are generally rather slow in execution. So simply decrementing our frequency counters like in Part 1 will most likely be too slow. We therefore need to use a trick to speed up counting.

The trick, in this case, is to add up counters repeatedly and check for carry (ie, see if the result was >FFFFh.)
I'll explain how this works for the Pulse Interleaving method. We need the following variables:

base1    - the base frequency of channel 1. We'll put this in memory on ZX Spectrum.
base2    - the base frequency of channel 2. We'll put this in memory on ZX Spectrum.
counter1 - the actual frequency counter of channel 1. We'll use BC on the Spectrum.
counter2 - the actual frequency counter of channel 2. We'll use DE on the Spectrum.
state1   - output state of channel 1. Let's use IYh.
state2   - output state of channel 2. Let's use IYl.
timer    - note length counter. We'll use IX.

and our usual ch_toggle constant.

  disable interrupts                               di
  state1 := 0                                      ld iy,0
  state2 := 0
  counter1 := 0                                    ld bc,0
  counter2 := 0                                    ld de,0
  
soundLoop:                                  soundLoop:

  counter1 := base1 + counter1                     ld hl,nnnn \ add hl,bc \ ld b,h \ ld c,l  ;nnnn = base1
  IF previous operation resulted in carry          jr nc,skip1
    state1 := state1 XOR ch_toggle                 ld a,iyh \ xor #10 \ ld iyh,a
  ENDIF                                     skip1:
  OUTPUT state1                                    ld a,iyh \ out (#fe),a
  
  counter2 := base2 + counter2                     ld hl,nnnn \ add hl,de \ ex de,hl         ;nnnn = base2
  IF previous operation resulted in carry          jr nc,skip2
    state2 := state2 XOR ch_toggle                 ld a,iyl \ xor #10 \ ld iyl,a
  ENDIF                                     skip2:
  OUTPUT state2                                    ld a,iyl \ out (#fe),a
  
  DECREMENT timer                                  dec ix
  IF timer == 0 then                               ld a,ixh \ or ixl
    GOTO soundLoop                                 jr nz,soundLoop
  ELSE
    ENABLE INTERRUPTS                              ei
    EXIT                                           ret
  ENDIF

 

Of course the above asm code can be optimized further, but that I will leave to you, the programmer wink

Beware that in order to calculate the counter values, you will need to adapt the formula from Part 3. Simply change it to
fn = f0 * (a)^n.