Using the PCM chip to make sweet beautiful music!

Ask anything your want about Mega/SegaCD programming.

Moderator: Mask of Destiny

Post Reply
Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Using the PCM chip to make sweet beautiful music!

Post by Chilly Willy » Mon Apr 01, 2013 7:49 pm

Well, at least to some folks...

My internet has been pretty bad the last few weeks... damn the providers and their fear of competition. :x

So I've been working on how to make the PCM chip in the SegaCD make noise... it was pretty simple on emulators - and a big pain on real hardware. I eventually got it all figured out. Here's the source for a few things:

libpcm - My low-level handling of the PCM chip. It includes functions for setting the registers properly and loading samples (will pass samples straight through, or optionally convert signed 8-bit samples to the proper format).

cdmodplayer - A conversion of my MOD player code to load MODs from CD and play them on the PCM. Very nifty code suitable for homebrew. Handles 4, 6, and 8 channel MODs. It will resample the instruments to make them fit in the PCM ram... it sounds better with some instruments than others. Best if it doesn't need to resample the instruments (duh).

libmikmod-3.3.1 - A port of mikmod to the SegaCD. Works great for plain MODs... a bit slow on XMs. Clearly needs optimizing for the 68000 (duh). Not as fast or as efficient as my MOD code, but handles more formats. The code here is set to load IT/MOD/S3M/XM/M15 files.

mikmodplayer - What good is libmikmod without a player? This is a modification of cdmodplayer to use mikmod instead of my MOD handling.

cdboot - While working on the players, I found another bug in the filesystem code in my SCD boot loader. Here's the latest.

I'll upload a couple example ISOs when the internet gets better... I can't keep a connection long enough to upload the ISOs right now. :cry:

One thing I wanted to ask - on real hardware, if you let the players sit and do nothing for a few minutes, the SCD hangs. As long as you keep doing SOMETHING (navigate the menus, select MODs to play) it works fine. It's like there's some kind of watchdog timer that looks for activity and tries to reset the CD if nothing happens for too long. I noticed that on all my SCD stuff. It runs forever on emulators just fine, but hangs on real hardware if you do nothing for a period. Any ideas? I can't find squat about this in the SCD dev info available. :?

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Tue Apr 02, 2013 6:29 pm

Okay! Finally managed to get a binary posted. Here's the full archive for everything needed to build the player, along with three ISO images - one for each region.

cdmodplayer

Press A to enter subdirectories and to start loading/playing a MOD file. You can press C to stop a MOD, otherwise it plays through once before going back to the browser. Remember that the CD may/will spin down during a long MOD, and that navigating to another directory or loading another MOD will require the CD to spin up, which takes a few seconds on the SCD.

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Wed Apr 03, 2013 3:35 am

Okay - solved the hang on real hardware when just sitting. It's a race condition involving the SCD communication registers. You can't just set one side and start watching for the other side to respond - you need to put a few cycles in there to allow for some kind of propagation delay. Anywho, the "error" is in hw_md.s, and you stick in a bunch of nops like this (not the only place fixed, just an example):

Code: Select all

send_md_cmd:
        tst.b   0x800E.w
        bne.b   send_md_cmd             /* wait on previous md command done */
        nop
        nop
        nop
        nop
        move.b  d0,0x800F.w             /* send md command */
        nop
        nop
        nop
        nop
0:
        cmp.b   0x800E.w,d0
        bne.b   0b                      /* wait on md command ACK */
        nop
        nop
        nop
        nop
        cmpi.b  #MD_CMD_DMA_SCREEN,d0
        beq.b   1f                      /* don't do command ACK handshake on MD_CMD_DMA_SCREEN! */
        move.b  #0,0x800F.w             /* send md command ACK handshake */
1:
        rts
If you have enough regular code between the comm register accesses, you don't need to worry about it. In any case, I can let the player sit and it doesn't hang anymore. I'll post an updated archive tomorrow.

EDIT: Well, it's better, but still not 100%. Apparently, this cross console communication is trickier than I first thought. What's going on is somewhere the communications between the two sides gets out of sync and one side or the other gets stuck waiting for the other side, which then also gets stuck waiting for the other side. I thought my comm protocol was incapable of getting in a state like that... it looks great on paper, and NEVER gets in the wrong state under emulation. But real hardware is another thing altogether. I'm rewriting the sync code. Hopefully better this time.

notaz
Very interested
Posts: 193
Joined: Mon Feb 04, 2008 11:58 pm
Location: Lithuania

Post by notaz » Thu Apr 04, 2013 1:22 pm

This com stuff is really painful to emulate too, I remember spending days on it trying to avoid game deadlocks. It seems Eke had beeter luck getting that emulated at least.

Charles MacDonald
Very interested
Posts: 292
Joined: Sat Apr 21, 2007 1:14 am

Post by Charles MacDonald » Thu Apr 04, 2013 3:44 pm

Do you think it's possible if one side writes to a comm reg at the instant the other side reads it, that data is lost? If so, I wonder if writing the response several times might help to help ensure at least one of them gets through.

In an unrelated project I had to deal with status flags that had some glitches in them before settling, and the glitches were causing loops to exit too early. What I had to do was test not only if the flag was in the desired state, but then check if it then stayed in that state for n consecutive reads. This filtered out the glitches. The dbne/dbeq instructions were helpful for that. Maybe the following kind of loop could be changed to do that:

Code: Select all

send_md_cmd:
        tst.b   0x800E.w
        bne.b   send_md_cmd             /* wait on previous md command done */
        nop 
Not sure if any of these ideas are really related to what the Sega CD is doing, of course. :)

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Thu Apr 04, 2013 4:16 pm

I've been looking over the reversed code for the cinepak player since we KNOW it works on real hardware. That's fixed the problems I've run into so far... and one thing I've noticed now is that the cinepak code studiously avoids using $E and $F for communication - it uses $10 and $20. So I'm wondering if the SCD sometimes has trouble with byte operations on registers. $E and $F are BYTE registers as opposed to all others, and maybe it's like you say - reading one can interfere with the other being written due to being the same word. I'm going to change from using $E/$F to $10/$20 and see what happens. But I think I will try your idea first - write each response a few times. That would certainly explain why the nops helped, but didn't cure the problem in the long run.

Maybe make the write routine like this:

Code: Select all

loop:
    move.b  d0,$800F.w
    cmpi.b  d0,$800F.w
    bne.b   loop
Then remove any nops on writing and put some in the reading loop.

That is one issue the 32X specifically states as not a problem - a simultaneous read and write will work properly on the 32X comm registers, but it cannot say what will happen with simultaneous writes. I guess that's why I've not seen a similar issue on the 32X. Thanks for the suggestion. :D

Mask of Destiny
Very interested
Posts: 615
Joined: Thu Nov 30, 2006 6:30 am

Post by Mask of Destiny » Thu Apr 04, 2013 5:33 pm

First off - excellent work! A PCM MOD player was one of the projects I wanted to tackle when I was active in SegaCD dev and it's really good to see someone actually do it.

As for your current problem, I don't think there are any weird quirks with the com registers. SLO (my boot loader/menu) used the following setup and I never had any sync issues:

$E - Command from Main to Sub
$F - Handshake from Sub to Main
$10+ - Command arguments
$20+ - Command response data (if applicable)

The sub CPU sets $F to 0 and waits for the main to set $E to 0. Once $E is zero, it sets $F to 1 to indicate that it is ready for a command and waits for $E to be non-zero. The value of $E is then used as the command number. Once the command is done executing, $F is set back to 0 to indicate completion.

Here's the sub CPU code:

Code: Select all

move.b	#0, $FF800F


Wait:
	tst.b	$FF800E
	bne	Wait
	move.b	#1, $FF800F
Wait2:
	tst.b	$FF800E
	beq	Wait2

	moveq	#0, d0
	move.b	$FF800E, d0
And the main CPU code for a particular command:

Code: Select all

	move.b	#0, $A1200E	;clear out Handshake
	
	bset	#1, $A12003	;give Sub CPU control of WordRAM

Wait:
	tst.b	$A1200F		;wait for SUB CPU to respond
	beq	Wait
	move.b	#1, $A1200E	;request file list
Wait2:
	tst.b	$A1200F		;wait for sub CPU to finish
	bne	Wait2
I do vaguely remember having some problem in which the CD block would stop working if I wasn't triggering the sub CPU level-2 interrupt from my VINT handler. However, it's been a while so I may be mis-remembering or I may have misunderstood the original problem.

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Thu Apr 04, 2013 7:38 pm

Yes, if you don't trigger the INT2 periodically, the SCD doesn't run. I do that. However, I still get a lockup somewhere in the SCD<>MD sync.

Here's my protocol...

SCD side:
Wait prev CMD done - wait for E == 0
Send next CMD - set F = CMD
Wait for CMD ACK - wait for E == CMD
Ack handshake - set F = 0
Loop

MD side:
Wait for CMD - wait for F != 0
Send CMD ACK - set E = CMD
Do CMD code
Wait for Ack handshake - wait for F == 0
Set CMD Done - set E = 0
Loop

It works perfectly under emulation, and on real hardware for a time depending on what else you are doing. But it eventually fails on real hardware.

Even my last test of setting the registers until they return the correct value, and putting nops in the read loops still fails eventually.
:(

Charles MacDonald
Very interested
Posts: 292
Joined: Sat Apr 21, 2007 1:14 am

Post by Charles MacDonald » Fri Apr 05, 2013 6:53 pm

Have you tried changing the polling code to have a very long (like several minutes) timeout, and then when the lockup condition occurs print out the comms register contents to see what's stored in there?

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Sat Apr 06, 2013 12:12 am

Charles MacDonald wrote:Have you tried changing the polling code to have a very long (like several minutes) timeout, and then when the lockup condition occurs print out the comms register contents to see what's stored in there?
Not yet... I'd need to make both sides time out since you can't print anything from the SCD without the help of the MD side. That means I need to make both sides use subroutines to both write and read the comm registers so that I can check for a timeout or a resync event in the case of a time. Then I can print what happened.

I'm really curious what the deal is... I left Gens/GS running for a day without it locking, but with my current best code, I can't go more than several minutes before it locks solid on real hardware.

I'm also wondering if I should burn SLO to a CD and see how if it works without locking. I've already determined from the source that I can't count on the SegaCD version of Frog - it does many things that I've already figured out don't work on real hardware. It's clear Frog was only ever tested on an emulator.

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Mon Apr 08, 2013 11:33 pm

Well, I found WHERE the hang occurs, but no idea WHY. I get a timeout in the cmd acknowledge. Here's the send cmd routine:

Code: Select all

send_md_cmd:
        move.w  d0,-(sp)

        /* wait on previous md command done */
        bsr     sub_wait_done
        bmi.b   2f                      /* error - resync */

        move.w  (sp)+,d0
        move.b  d0,0x800F.w             /* send md command */

        /* wait on md command ACK */
        bsr     sub_wait_ack
        bmi.b   1f                      /* error - resync */

        cmpi.b  #MD_CMD_DMA_SCREEN,d0
        beq.b   1f                      /* no immediate command ACK handshake on MD_CMD_DMA_SCREEN! */
        move.b  #0,0x800F.w             /* send md command ACK handshake */
1:
        rts
2:
        addq.l  #2,sp
        rts
It waits for the command to be done (with a timeout as well, which I've never seen happen), then it sets the command value in the comm register. Then it waits for the main 68k to acknowledge the command - that happens by the main 68k storing the command value into its comm register. We know the main 68k is already waiting for the next command byte - we just finished waiting for the last command to be done. Since it didn't timeout, we know where it is on the MD side.

So far, all timeouts I've seen are that second one, sub_wait_ack. So let's look at the MD side:

Code: Select all

cmd_done:
        tst.b   0xA1200F
        bmi.b   1f                      /* error - resync */
        bne.b   cmd_done                /* wait on ACK handshake */
1:
        move.b  #0,0xA1200E             /* command done */

        /* main loop */
        moveq   #0,d0
cmd_loop:
        tst.b   d7
        beq.b   md_check                /* 32X not initialized */
        move.w  0xA15120,d0
        beq.b   md_check

        /* handle 32X command */
        cmpi.w  #1,d0
        beq.b   md_check                /* copy data to 32X */


| unknown command

mars_done:
        moveq   #0,d0
        move.w  d0,0xA15120             /* command done */
md_check:
        move.b  0xA1200F,d0
        beq.b   cmd_loop
        bmi.b   cmd_done                /* error - resync */

        move.b  d0,0xA1200E             /* command ack */

        /* push args on stack */
        move.l  0xA1202C,-(sp)
        move.l  0xA12028,-(sp)
        move.l  0xA12024,-(sp)
        move.l  0xA12020,-(sp)

        cmpi.b  #'M,d0
        bne.b   2f
1:
        btst    #0,0xA12003            /* Main-CPU has Word RAM? */
        beq.b   1b
        jmp     0x200000
2:
        cmpi.b  #'I,d0
        bne.b   3f
        bset    #1,0xA12003             /* give Sub-CPU Word RAM */
        bra.b   4f
3:
        move.l  #ERR_UNKNOWN_CMD,0xA12010
        cmpi.b  #MD_CMD_END,d0
        bhs.b   4f                      /* unknown command */
        add.w   d0,d0
        lea     cmd_table(pc),a0
        move.w  0(a0,d0.w),d0
        jsr     0(a0,d0.w)
        move.l  d0,0xA12010             /* return value */
4:
        lea     16(sp),sp
        bra     cmd_done
The part that concerns us is here:

Code: Select all

        move.b  0xA1200F,d0
        beq.b   cmd_loop
        bmi.b   cmd_done                /* error - resync */

        move.b  d0,0xA1200E             /* command ack */
For some reason, the cd side has stored the command to F, but the MD side doesn't see it. This loop is done with ints enabled, but the only int on the MD side is the vertical blank. The vertical blank just reads the pads and increments the ticks count. The timeout is long enough that perhaps a couple dozen vblanks occur before it would timeout, so it's not an issue with the vblank interrupting the code.

Frankly, I'm stumped... both my CDX and Model 2 CD act the same way. Could someone try this on their real hardware and see what they get? If a timeout occurs, you get a two second error message on line 24.

http://www.mediafire.com/download.php?jrm4yuzp5wtb2r8

Chilly Willy
Very interested
Posts: 2984
Joined: Fri Aug 17, 2007 9:33 pm

Post by Chilly Willy » Thu Apr 11, 2013 7:38 pm

Here's an update to the SCD MOD player. Changes since last version...

I made a change in the way instruments are chosen to be resampled that improves the quality for larger total instrument sizes... don't expect miracles - anything more than 150K or so will start to drop the quality drastically.

I added filtering to the resampling. The old method was a simple average; now you have two more filters - the first is roughly equivalent to a sample and hold curve (coeff = 0.25, 0.5, 0.25), and the second is an exponential drop off for a "warmer" sound (coeff = 0.5, 0.25, 0.125). Filtering is specified when the MOD is loaded, so you could choose the filtering on a file to file basis.

I added a compile-time option to pre-process the pattern data. If you are familiar with MODs, pattern data looks like this:

Code: Select all

iiii pppp pppp pppp iiii eeee aaaa aaaa
where:
iiiiiiii = set sample i-1 (0 = no sample)
pppppppppppp = Amiga period value for the sample playing
eeee = score effect
aaaaaaaa = effect argument
The period may specify a note event, so the way player figures that out is to use a for next loop to look up the period in a table of three octaves of notes (finetune = 0). If the period is in the table, it's a note event and the index gives the note; if it's not in the table, it's not a note event and the period is used for something like porta. You COULD make a 4K table to look up the period in one array access, but that's wasting ram. What I do instead is when the pattern data is loaded to ram, I go through all the data and look up the period THEN rather than in the player. If the period is a note, I replace the period with note+4060. In the player, all I have to do is check if the period is greater than or equal to 4060. If so, it's a note where the note is period-4060. I can look up the note's period in the table mentioned above. If it's less than 4060, it's a plain period value. This works rather well and saves time in the player, and memory in not needing the big reverse lookup table.

Finally, I added another MOD signature to the init code ('FLT4' => Star Trekker four channel... there's also a Star Trekker 8 channel, but I've never seen one - if someone has one maybe they could send it to me so I could add that and verify it works correctly).

cdmodplayer-src-20130411.7z
cdmodplayer-bin-20130411.7z

Post Reply