Double Buffering the Copper

Do you live and breathe hexadecimal? Do you like speaking to hardware directly?

Moderator: Programming Moderators

Steve Monks
Posts: 5
Joined: Sat Mar 28, 2020 6:42 pm

Double Buffering the Copper

Postby Steve Monks » Fri Jun 12, 2020 11:53 am

I've been experimenting with playing back PCM audio using the copper. I've managed to get this working to some extent with a copper list that essentially pokes the Specdrum DAC with a new sample every two scanlines, giving me a roughly 8KHz playback rate.

As you might imagine I'm having to update the copper list with new samples every frame, but the problem I've got is that I can't see a way to update the copper list without writing over the currently active portion of the list and causing some distortion in the audio as a result.

Back in the Amiga days, I'd have used two copperlists and modified one while the other was being executed, then swap them over for the next frame, however I can't see a simple way to do this due to how the copper is implemented on the Next. As far as I can tell the copper always begins executing from address 0 in copper memory at the start of each frame.

Currently, the only way I can think of doing this is to set up a pair of line interrupts and update the copper list in two halves, this seems a bit messy and will upset a number of other things (such as my game's frame syncing), this wouldn't be the end of the world, but if anyone could suggest a simpler solution I'd appreciate it.

seedy1812
Posts: 91
Joined: Tue May 30, 2017 11:31 am

Re: Double Buffering the Copper

Postby seedy1812 » Fri Jun 12, 2020 2:20 pm

have you looked at the dma ?

Steve Monks
Posts: 5
Joined: Sat Mar 28, 2020 6:42 pm

Re: Double Buffering the Copper

Postby Steve Monks » Fri Jun 12, 2020 5:01 pm

seedy1812 wrote:
Fri Jun 12, 2020 2:20 pm
have you looked at the dma ?
I don't think the DMA would help as it can't access copper memory (it has to be written to a byte at a time via a nextreg port).

I've had a bit of a think about it and actually it turned out to be not as difficult as I thought it might be doing two interrupt driven copper list updates per frame (each doing half a frames worth of samples). I've put together a proof of concept and it sounds so much better than how it did before.

Now I've just got to integrate this back into my game without breaking everything ;-)

seedy1812
Posts: 91
Joined: Tue May 30, 2017 11:31 am

Re: Double Buffering the Copper

Postby seedy1812 » Fri Jun 12, 2020 5:12 pm

You can play samples with the dma - so you can create a double buffer so when one half is playing you populate the other half

Steve Monks
Posts: 5
Joined: Sat Mar 28, 2020 6:42 pm

Re: Double Buffering the Copper

Postby Steve Monks » Fri Jun 12, 2020 6:07 pm

seedy1812 wrote:
Fri Jun 12, 2020 5:12 pm
You can play samples with the dma - so you can create a double buffer so when one half is playing you populate the other half
Ah, you're right, I wasn't aware of that and completely missed it when I skimmed the DMA documentation. That might be a simpler approach. Another one to try out over the weekend. Thanks.

User avatar
SevenFFF
Posts: 565
Joined: Mon Jun 05, 2017 5:30 pm
Location: USA

Re: Double Buffering the Copper

Postby SevenFFF » Fri Jun 12, 2020 8:47 pm

The DMA can be set up with a prescaler value which effectively streams bytes at a constant sample rate regardless of the video mode and the master clock rate. The downside is that the DMA is very useful for other tasks, so it's a bit of a waste to dedicate it to a single mono DAC stream.

The copper is probably a better use of resources, and can output at least a stereo stream, if not all four DAC channels. The downside is that the copper pixel clock and hence the sample rate will change depending on the video mode, leaving you with many different permutations of lines per frame and frames per second to accommodate.
Robin Verhagen-Guest
SevenFFF / Threetwosevensixseven / colonel32
NXtel NXTP ESP Update ESP Reset CSpect Plugins

lpotamianos
Posts: 2
Joined: Mon May 29, 2017 7:34 pm

Re: Double Buffering the Copper

Postby lpotamianos » Fri Jun 12, 2020 9:09 pm

I have written code to output digital sound using the copper. You have to output the copper list, and every frame use two raster interrupts. The first interrupt should be somewhere on the first half of the screen and should feed the second half of the copper list. Then, upload the second part of the copper list, preferably using dma. The second interrupt should be on the second half of the screen and feed the first part of the copper list, and upload it to the copper. This is the general idea, it can get pretty complex code wise. Plus you have to ensure you have to load new data for the sound/music. It is recommended you use the streaming routines offered by NextZXos, but one could get away with using seek and read Esxdos routines to load data.

lpotamianos
Posts: 2
Joined: Mon May 29, 2017 7:34 pm

Re: Double Buffering the Copper

Postby lpotamianos » Fri Jun 12, 2020 9:14 pm

Plus, as mentioned above, you should cater for different number of lines for 50 and 60hz. The copper list length should be adjusted accordingly and you should prepare two different files for each case, at an appropriate sampling rate.

Steve Monks
Posts: 5
Joined: Sat Mar 28, 2020 6:42 pm

Re: Double Buffering the Copper

Postby Steve Monks » Sat Jun 13, 2020 4:46 pm

I've got both approaches (copper and DMA) working now, so I have options :-)

Programming the DMA controller was "interesting". Simple enough once you grasp what's going on with the byzantine register access mechanism, but it took a bit of staring blankly at the docs before that clicked.

I was undermined somewhat by the prescale bug in the version of CSpect I was using, plus there appears to be another bug in CSpect when reading progress back from the DMAC as I can't get that to work at all, but the code works fine on my actual Next.

Thanks everyone for your help.

Ped7g
Posts: 256
Joined: Mon Jul 16, 2018 7:11 pm

Re: Double Buffering the Copper

Postby Ped7g » Sat Jun 13, 2020 5:43 pm

> As far as I can tell the copper always begins executing from address 0 in copper memory at the start of each frame.

No? Depends on the mode how you start it.
https://wiki.specnext.dev/Copper_Control_High_Byte
%00 stops the copper %01 reset copperPC to 0, and starts it (note there's nothing about start of frame), %10 just resumes from current copperPC (if it was stopped by %00), and only %11 restarts copper when offset-video-line does reach 0 (each frame, where precisely depends on the new offset register $64 ... btw no emulator does this (except my zesarux fork, but that is not precise enough for good copper sound too).

Also keep in mind the copper-write-index ($61 + $62 index) is not "copper-PC", those are independent.

You have 1024 instructions in copper, so with 8kHz mono you need to reload copper only about 16 times per second (wait + set DAC = two instructions if you don't do anything else in copper code = 512x sample data per full load). Also you can interleave this at half-point with copper write-to-$7F some value, which you can check game-loop to see if that particular half of copper data was already fully played and could be reloaded.

And you can load copper code by DMA too, you have to select I/O port $243B https://wiki.specnext.dev/TBBlue_Register_Select to $63, and then start the DMA to port $253B https://wiki.specnext.dev/TBBlue_Register_Access -> 1024 byte transfer = half of copper memory loaded, much faster then doing `nextreg $63,imm8|a`

The truly annoying part is calculating the correct wait lines for every possible video mode, as the total amount of scanlines does change per video mode, and overall machine speed changes per video mode, so if you want to have precise 8kHz, you will have either quite some tables in your code or to do some calculations.

Plus the video modes characteristics keep changing with cores (only rarely, but does, the last one I remember was around core3.0 fixing HDMI and giving 60Hz modes few more scanlines, and there is strong long-time ongoing effort to fix HDMI timings to match original Spectrum, there's still some hope this may eventually get fixed ... breaking your speed-tables for copper audio tuned for older cores... :P :) )

(CSpect DMA prescaler works for me in V2.12.29 correctly ... I wasn't aware of V2.12.30 .. time to re-test it and refresh the "known bugs" page on wiki, hm)

edit2: plus the absolutely most annoying part for **me** is, that I have almost always some neat idea how to use copper for graphics, which doesn't mix well with audio ... actually the idea I will probably try to bring into reality as next project (unless I change my mind while finalizing some tooling/tests stuff) seems to leave DMA for audio rather easily while using copper heavily. But the DMA audio can't be stereo? I need to check the DAC nextregs, if there is some kind of interleaved stereo register to write left+right data to the same I/O port, but IIRC there's no such thing, only "centre" options to play the mono at both speakers with single write.


Who is online

Users browsing this forum: No registered users and 1 guest