Using the PCM chip to make sweet beautiful music!
Moderator: Mask of Destiny
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9:33 pm
Using the PCM chip to make sweet beautiful music!
Well, at least to some folks...
My internet has been pretty bad the last few weeks... damn the providers and their fear of competition.
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.
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.
My internet has been pretty bad the last few weeks... damn the providers and their fear of competition.
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.
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.
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9:33 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.
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.
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9:33 pm
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):
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.
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
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.
-
- Very interested
- Posts: 292
- Joined: Sat Apr 21, 2007 1:14 am
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:
Not sure if any of these ideas are really related to what the Sega CD is doing, of course.
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
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9:33 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:
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.
Maybe make the write routine like this:
Code: Select all
loop:
move.b d0,$800F.w
cmpi.b d0,$800F.w
bne.b 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.
-
- Very interested
- Posts: 616
- Joined: Thu Nov 30, 2006 6:30 am
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:
And the main CPU code for a particular command:
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.
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
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
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9:33 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.
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.
-
- Very interested
- Posts: 292
- Joined: Sat Apr 21, 2007 1:14 am
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9:33 pm
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.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?
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.
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9: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:
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:
The part that concerns us is here:
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
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
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
Code: Select all
move.b 0xA1200F,d0
beq.b cmd_loop
bmi.b cmd_done /* error - resync */
move.b d0,0xA1200E /* command ack */
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
-
- Very interested
- Posts: 2984
- Joined: Fri Aug 17, 2007 9:33 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:
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
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
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