Here's something I've been working on for the past couple of days:
http://www.youtube.com/watch?v=AUlwVFGTmzY
It's a VGM player running on the SPC. SN76489 only, naturally. VGM files are played as-is. The SNES CPU sends the entire file over to the SPC along with my playback code and some additional data, and then the SPC takes over.
There are still some issues to sort out before I make a proper release.
mic_ wrote:
Here's something I've been working on for the past couple of days:
http://www.youtube.com/watch?v=AUlwVFGTmzYIt's a VGM player running on the SPC. SN76489 only, naturally. VGM files are played as-is. The SNES CPU sends the entire file over to the SPC along with my playback code and some additional data, and then the SPC takes over.
There are still some issues to sort out before I make a proper release.
Great! This is a cool start of a engine for SNES/SPC-700
Very impressive stuff!
I've created
a tool that converts from .VGM to .SPC.
Give it a .VGM/.VGZ file and it'll spit out an .SPC file with the VGM data and my playback code.
The zip also includes the SPC-700 assembly code for the player.
Oh man, I'm really tempted to see if I can get that technique for crisp square waves at any frequency working with this. Glad you included the source.
Looks like I had forgot to include the frequency table in the zip though (oops). I've reuploaded it with freqtb.inc included.
Another thing I'm considering is adding support for NeoGeo Pocket VGMs. Afaik the sound chip in the NGP is nearly identical to the SN76489, except that it has separate volume controls for right and left, and is controlled through two ports instead of one. The problem is that NGP songs wouldn't pack as well with the compression scheme I'm using.
Quick thought on timing, after I skimmed the source and noticed you were using timers but were considering cycle-timing. At first I was like, yeah, cycle-time it, duh, but then you'd have to cycle-time everything. Since you can have the third timer increment T2OUT every 16 cycles, you can get 131072 per second. Then just divide by 3 to get a tick almost every 1/44100 second (about 1% less). Maybe you already do this.
Quote:
Since you can have the third timer increment T2OUT every 16 cycles, you can get 131072 per second.
I do use timer2. But I thought it ran at 64kHz.
Yeah, sorry, you're right. I messed up in my calculations. And duh, anything above 32 kHz is wasted anyway since the DSP only runs at 32 kHz. So it would make sense to just run timer 2 with a period of 2 (*16), giving you 32 kHz, and keep track of the fraction (which has to be what you're doing already). I'll refrain from further ignorance and wait until I have actually tried the source out and read it more closely.
Quote:
So it would make sense to just run timer 2 with a period of 2 (*16), giving you 32 kHz, and keep track of the fraction (which has to be what you're doing already).
Actually I keep a table of floor(n*64/44.1), where n=[1..16].
But I altered some timer values because it turned out that with the overhead from all the other code I needed to run the timers for slightly shorter periods than what would've been the case under optimal conditions.
Any chance on getting the source to vgm2spc.exe?
The scheme I want to try involves having several square wave samples, and storing in the sample index and pitch to play it at in the converted VGM data. The samples have periods of 16, 32, 48, 64, etc. perhaps more inbetween if necessary. I compared a 48 played at pitch 1.5 to a 32, and the timbre hardly changes, so this seems a reasonable granularity.
BTW, the syntax for BBS and other bit instructions (clr1, set1, etc.) in wla-dx is bbs addr .bit,branch_label. The space must be present. wla-dx is charming, isn't it?
Hmm.. Doesn't a change to SRCNx require a new KON to take effect? Then how do you plan to get around the distortion that would cause?
EDIT: Nevermind, I had an INC too many so I ended up writing to one of the ADSR regs instead of SRCN.
I've actually implemented your idea in a more limited form (or maybe it's not what you had in mind at all..). For values of P < 0x2000 I use a sample twice as wide and double the value of P. Something like this:
Code:
mov SAMPLE,#0
mov y,#$20
mov a,#$00
cmpw ya,PVAL
bcc +
asl PVAL
rol PVAL+1
mov SAMPLE,#2
+:
This makes low frequency square waves more square.
It could easily be extended one step further to use a sample with 4x the sample rate if P < 0x1000.
Cool, that's the idea. I'm not that interested in messing with SNES stuff right now, so guess I'll leave it to you. BTW, the reason I used more samples than one per octave is that pitches going above 1.5 or so caused some audible aliasing. Octaves are easier to handle the calculations for, though. I had concluded that the host-side program would have to calculate the pitches if you were using more than one sample per octave.
I've made an updated version available
here.
It takes care of most of the TODOs, adds dynamic switching to samples with higher sample rates when possible, has an adjusted volume table, and possibly other things I've forgotten. On the converter side I've added slightly better compression and partial GD3->ID666 conversion.
The source for the converter is now also included in the zip.
Quote:
Hmm.. Doesn't a change to SRCNx require a new KON to take effect? Then how do you plan to get around the distortion that would cause?
Surprisingly enough, it doesn't. It takes effect when the sample loops, where the "new" loop value from the new instrument is used instead.
I think Dragon Quest 5 does this for the "sap" spell - it simulate a NES sound effect cycling through duty cycles (but sounds incredibly muffled).
BTW it's also possible to dynamically change BRR sample data - Chrono Trigger does this for sample #16 to constantly change the duty cycle - resulting in a cool effect. Doing something like that (by changing the position of the END flag in the BRR sample) could be another way to change the frequency of the playing sample dynamically.
Awesome work on vgm2spc. I have a bug report for the current version.
Code:
if ((outData.size() + extraDataBlock.size()) > 0xEEC0)
{
printf("Error: the vgm data is too large to fit. the maximum size after packing is 61120 bytes\n");
delete [] vgmData;
outData.clear();
return;
}
That should actually be the following code, based upon the addition of the second frequency table.
Code:
if ((outData.size() + extraDataBlock.size()) > 0xE8C0)
{
printf("Error: the vgm data is too large to fit. the maximum size after packing is 59584 bytes\n");
delete [] vgmData;
outData.clear();
return;
}
Ideally though, we should never hard code this value, and instead, get the file size of s-smp_player.bin every time, to determine max size of the output vgm.
Ah, yeah, my bad. That value was left over from the first version.
Btw, does anyone know if the S-DSP volume control is linear or if it follows a logarithmic curve?
I decided that it'd be a good idea to use as much of the dynamic range of P as possible. So I now calculate my LUT the following way:
Code:
p = floor(f * 16 / 7.8125)
if p < #1000 then
p = floor(f * 64 / 7.8125) + #8000
elsif p >= #4000 then
p = #3FFF
end if
All I need to do then when my player code loads the high byte of a value from the table is to check the negative flag. If set, it picks a sample 4 times as wide and clears bit 15.
The results can be quite noticable, as can be heard in this song from Alex Kidd in Miracle World:
.SPC (old frequency table)
.SPC (new frequency table)
.VGM for reference
With the new table it obviously sounds much more like the original.
I found that using a rate of more than around 1.5 results in noticeable aliasing. Surely 3.5 results in lots.
Actually it's 4.0. Or even 8.0 for really low frequency waves (like "periodic noise" driven by tone2).
But the increased accuracy in the frequency table seems to more than make up for it, since it sounds better now (IMO).
Here's my little continuation of this project.
Use a real SNES or BSNES to run it, other emulators will likely mess the sound up.
As usual, Had to rip an SPC and try to figure out what commands drive it.
Command goes on Port 1, and issuing that command, goes onto port 0.
If a command is valid, what is written to port 0, is echoed back, otherwise it is not echoed back. Regardless of whether the value is echoed back, next command to be issued has to have a new value written to port 0.
Valid Commands are
0x21 - Restart Playback of current song
0x22 - Pause / Resume playback
0x23 - Stop Playback, and start standard IPL loader. New song is loaded using standard IPL protocol.
0x24 - Unknown command
0x30-0x3F - Set master Volume
0x40 - Toggle Tone Channel 1
0x41 - Toggle Tone Channel 2
0x43 - Toggle Tone Channel 3
0x44 - Toggle Noise Channel
0x45-0x4F - no known effect, but is treated as valid.
Port 2 and 3 contains the 4 channel visualizers, one channel per nybble. In terms of the driver, Port 2-3 are never written to, only read from, except when using the IPL loader.
When a new song is to be loaded, IPL load song to 0x1700, max size 0xE8C0. When done, IPL jump to 0x0300. Issue command 0x21, new song starts playing.
Right.
Quote:
0x24 - Unknown command
When repeat mode is disabled in the GUI I want to know from the playback driver when the song has looped (which it does by modifying the value on port0), so that I can stop it and load the next song. When repeat is enabled I tell the playback driver to not send any such notifications. Command 0x24 toggles a flag in the playback driver which determines if it should send loop notifications or not.
Made a SPC set of all of the included tunes. Trackcount is 32 tunes... that should be the correct number of tracks. Nice work! ^_^
Download collection
Edit: Oops! I missed one track. The file has been updated to reflect this.
That's really cool. I've only tried it on ZSNES so far, though.
I remember a long time ago I had looked at .YM files (from Atari ST) and was curious about playing them on the SPC. But it uses some kind of compression, and bigger than SNES RAM without it, so I didn't try it.
Memblers wrote:
I remember a long time ago I had looked at .YM files (from Atari ST) and was curious about playing them on the SPC. But it uses some kind of compression, and bigger than SNES RAM without it, so I didn't try it.
Yeah, I've thought about YM too. They use LZH IIRC. Perhaps they can be compressed in some other way that's easier to unpack, like I did with the VGM files in my player. And/or you could stream the data from ROM if you don't need to do anything else on the S-CPU.
Another possibility would be SAP (Atari XL/XE music, for the POKEY chip). It's an executable format, but it's a 6502-based system. And the files are usually tiny.
I could be remembering wrong, but I think YM format had saved all the registers for each update, instead of doing the time-delta/address/data type thing that I imagine most logged formats would do. In that case, a simple RLE stream for each register would probably work out pretty well.
POKEY with SAP format is a cool idea too. Actually I've been wondering lately how I can make some looped POKEY and 2600 waveforms for my wavetable synth.
Memblers wrote:
I could be remembering wrong, but I think YM format had saved all the registers for each update, instead of doing the time-delta/address/data type thing that I imagine most logged formats would do. In that case, a simple RLE stream for each register would probably work out pretty well.
Sounds reasonable. I haven't looked at any YM files to see how often they typically change the register values. The fact that you've got 10 different envelope shapes and an adjustable envelope speed means that you might have to waste a bit of memory on samples if you want them to sound good. And I don't know how the "special effects" in v6 of the YM format works (e.g. "SID-voice" and "Sync-buzzer") or how common their use is.
Quote:
POKEY with SAP format is a cool idea too. Actually I've been wondering lately how I can make some looped POKEY and 2600 waveforms for my wavetable synth.
I started writing a music playback engine for the Atari XE/XL earlier this year, but I haven't finished it yet. POKEY has some quirky features, like the ability to combine channels to get higher precision, and the whole distortion control which I haven't looked too closely on.
mic_ wrote:
Here's my little continuation of this project.
Use a real SNES or BSNES to run it, other emulators will likely mess the sound up.
Works great! Enabling/disabling channels a lot will cause graphical glitches, however. Screenshots made with bsnes (on the real hardware w/ PowerPak, the glitches were worse).
Huh.. is that PAL or NTSC? I've tested it mainly on a PAL SNES / BSNES with PAL emulation. Never noticed any graphical glitches.
I tried it under 60 Hz only (NTSC SNES & bsnes) but will try again in PAL mode.
I just tried it in BSNES.. in NTSC mode I can replicate it if I press X & Y repeatedly in fairly quick succession. In PAL mode I wasn't able to replicate it.
I guess I'm pretty tight on vblank time in NTSC mode. Perhaps I should do the volume bar update to RAM instead and DMA the new data to VRAM during the next vblank, like I do with the song list.
I've released the source code for my music disk (you can download it
here). Included is the UI code, as well as the VGM playback code for the SPC. The VGM packer is also included in the archive, so it's possible to replace the songs.
This code wasn't originally meant to be released, so it's not pretty to look at. At least I've cleaned it up a little bit.
The graphical glitches that orwannon reported have been fixed AFAICT, as well as a couple of other minor UI bugs.
I've made some updates on the musicdisk. The new ROM is found
here (and
here's a video of it running on my SNES).
The compression scheme used for the VGM data has been improved, so I decided to include some more songs. 18 more to be precise. This also left me with some spare RAM on the SPC side, which is decided to give to the DSP's echo processor to implement a sort-of poor man's delayed bass boost (you can toggle this on/off with Select+A).
I've also improved the timing, so the playback rate should be more even now.
Delayed? As I recall, EDL=0 gives you only one sample delayed echo, and the FIR still works normally (since the history buffer is in internal registers, rather than read from RAM each sample). Or maybe you want it delayed...
Seems like you could do treble and bass adjustment by feeding everything into the echo buffer, turning main volume to zero so that only the echo plays through its FIR, and having feedback volume=0 so you don't get any feedback. Then set the center FIR coefficient to 64 (0.5) and you can then increase/decrease treble and bass. Just going by memory of the SPC here.
It's mainly because I started out with a standard echo with a flat frequency response filter, which I thought sounded better with a fairly long delay. Then I tried out different types of filters, but kept the delay and feedback pretty much unchanged.
Could be nice to have several different filters that the user can select from. I still have some spare RAM on the SPC, so it should be possible to shoehorn in somewhere.
That would make a nice feature for a SNES sound library btw; low/high/band pass filters with adjustable cutoff frequency, so you can do SID-style filter sweeps.
Here's yet another
updated ROM (
youtube video).
The latest source code is available
here. For the filter-related stuff, look for the update_filter routine in vgmplay_spc700_ext.asm, which is triggered by the NextFilter routine in t700c.asm.
This version has the following filter settings that you can switch between:
* Off (no filtering)
* Standard echo, 80ms delay
* Bass boost (low pass filter mixed with an attenuated version of the original signal)
* Bass boost, 80ms delay
* Treble boost (high pass filter mixed with an attenuated version of the original signal)
Use Right on the D-Pad to switch filter. I also made it so that the song isn't restarted when a new filter setting is applied.
Not sure if it really matters anymore or if it's worth the bump, but I'm getting some graphical glitches on real (2-1-3) NTSC hardware using an sd2snes.
EDIT: Turns out this was an sd2snes issue, not an actual hardware issue. The latest firmware update seems to have fixed things.
There is actually a GUI bug even in the latest version of my player where the volume bars can get messed up. I've triggered it myself a couple of times, but I don't have a sure-fire way of reproducing it.