SPC700 sample streaming

Discussion of hardware and software development for Super NES and Super Famicom. See the SNESdev wiki for more information.

Moderator: Moderators

Forum rules
  • For making cartridges of your Super NES games, see Reproduction.
Post Reply
lex
Posts: 14
Joined: Sat Dec 31, 2022 9:31 pm

SPC700 sample streaming

Post by lex »

Hey, Lex here. Recently I've been trying to learn about programming the SPC700. After a few weeks of studying the
SPC700 and studying the sound drivers of commercial SNES games; I've learned enough to implement a sample swapping
engine.

How does it work? Well on the SNES side, there is code for clearing the SNES CPU's ram, preparing the SPC buffer and uploading
data to the SPC700. What is the SPC buffer? It is a 64KB area at 7F:0000 in the SNES CPU's ram where you can prepare the data that
will be sent to SPC700's ram before sending it to the SPC700's ram. The source directory data, sound program and sample data
are written to this buffer by the SNES CPU. It is basically a mirror of what will end up in the SPC700's ram.

Then there's some code for uploading the SPC buffer to the SPC700.

This basically does the same thing as the code at the link below does:
https://snes.nesdev.org/wiki/Booting_the_SPC700

Then later when 0 is written to $2141 this causes the SPC700 to go to the code that jumps to the main sound program.

On the SPC700 side there is some code that clears the DSP registers.

Code: Select all


arch spc700


macro writeToDSP()		;This is a macro for writing values to the DSP.

mov $F2,Y				;Move the value in Y to the DSPAddr register.
						;The value in Y holds the address of the DSP register to write to.
mov $F3,A				;The A register is what we want to write to the current DSP registeer.

endmacro

ORG $018200

clear_DSP_registers:		;Clear the direct page to zero.


clear_A:
mov A,#$00					;Clear A.
							;The value in A, 0x00 is the value we will be writing to
							;the DSP registers to clear them.
							
clear_Y:					;Clear Y.
							;Y will store the address of the current DSP register.
mov Y,#$00


clear_DSP_register:		;Clear the current DSP register.
%writeToDSP()			;Write values to the DSP registers.
INC Y						;Increment Y so that it contains the address of the next DSP register.
CMP Y,#$79
BNE clear_DSP_register	;Is Y equal to 0x79 the maximum number of DSP registers? If not loop.


RET							;Return to the main SPC program code.


There is some code that sets up the DSP registers. It does things such as setting up the main volume, disabling echo,
setting the source directory and setting up the parameters for voice 0.

Code: Select all


arch spc700

!pitchLValue = $02
!pitchHValue = $03

macro writeToDSP2()		;This is a macro for writing values to the DSP.

mov $F2,Y				;Move the value in Y to the DSPAddr register.
						;The value in Y holds the address of the DSP register to write to.
mov $F3,A				;The A register we want to write to the current DSP registeer.

endmacro

ORG $018400

setup_DSP_registers:		;Clear the direct page to zero.

disable_echo:				;Disable echo.
mov Y,#$6C					;Move the value of the FLG DSP register to Y.
							;To disable echo we will write a value to the FLG DSP register.
mov A,#$20					;Set bit 5 of the FLG register.
							;0x20 in binary is 00100000
							;bit 5 of FLG is the echo enable bit.
							
%writeToDSP2()				;Write a value to the FLG DSP register.

left_main_volume:			;Set the left main volume.
mov Y,#$0C
mov A,#$7F					;Write 0x7F to the Main Volume Left DSP register.
%writeToDSP2()

right_main_volume:			;Set the right main volume.
mov Y,#$1C
mov A,#$7F					;Write 0x7F to the Main Volume Right DSP register.
%writeToDSP2()

set_source_directory:
mov Y,#$5D
mov A,#$10					;Set the source directory to 0x1000, 0x10*0x100.
%writeToDSP2()	

voice0_volume:				;Set the volume for voice 0.
mov Y,#$00
mov A,#$7F					
%writeToDSP2()				;Voice 0 left volume.
mov Y,#$01					
%writeToDSP2()				;Voice 0 right volume.

load_note_value:			;Load the value for the note we want to play.
mov A,#$4f					;Move 0x50 the index for note F5 to A.
							;0x50 gives you a note value of 
ASL A						;Multiply it by two since each entry in the pitch table is two bytes.
mov X,A						;Move the noteIndex*2 to X.
							;The value in X will be used as an index in the pitch table.
MOV A,$2000+X			    ;Move the 16-bit index for this note to the YA register.
MOV	!pitchLValue,A		    ;Store the Pitch (L) value to ram.
MOV A,$2001+X				;Store the Pitch(H) value to ram.
MOV !pitchHValue,A

load_pitchL_value:
MOV A,!pitchLValue			;Move the pitchLValue to Y.
MOV Y,#$02					;Load the address of the Pitch(L) DSP register for voice 0 into A.
%writeToDSP2()

load_pitchH_value:
MOV A,!pitchHValue			;Move the pitchLValue to Y.
MOV Y,#$03					;Load the address of the Pitch(L) DSP register for voice 0 into A.
%writeToDSP2()

load_source_value:			;Load the source value.
MOV Y,#$04					;Set the source instrument to 0.
MOV A,#$00					;Write to the SRC register for voice 0.
%writeToDSP2()

write_ASDR:
MOV Y,#$05					;Key on voice zero, 00000001.
MOV A,#$8F
%writeToDSP2()
MOV Y,#$06					;Key on voice zero, 00000001.
MOV A,#$CA
%writeToDSP2()

write_gain:
MOV Y,#$07					;Key on voice zero, 00000001.
MOV A,#$7F
%writeToDSP2()


write_key_on:
MOV Y,#$4C					;Key on voice zero, 00000001.
MOV A,#$01
%writeToDSP2()




RET							;Return to the main SPC program code.
After all the DSP registers have been set up, voice 0 is keyed on and the first sample plays.

After that, there is some code to wait and after a certain amount of time has passed voice 0 will be keyed off.

Then there is a jump back to $FFC9, this is where the IPL ROM communicates with the SNES CPU.

On the SNES side I set the rom address where the new sample is as the source address. Then I set the address where
the sample is in the SPC700's ram 0x3000 as the destination address. I basically use the same code that I used
to upload to the SPC700 before, to upload a new sample.

Then once the new sample has been completely uploaded to the SPC700, I key on voice 0 and the newly swapped in sample plays.

With a few modifications this can be used during gameplay as well. The idea is that on a certain scanline, say in the middle of the
screen or something; you would make the SNES CPU check if the SPC700 needs more data. Before that certain scanline, the main code
for the game could run.

One frame however is too short to swap an entire sample, so you would have to send say, 128 bytes to the SPC700 every frame. Until
the entire sample is uploaded, the voice for that sample would have to be muted.

Below you can download a ROM showing off this sample swapping code. It's not much; just a simple rom where you can hear a brass sample
on voice 0 and then later a string sample on the same voice.

Update:
I've now successfully figured out how to do sample streaming on the SPC700. You can see a rom demonstrating this in the attachments.

How I did sample streaming is after the initial SPC buffer is sent to the SPC700's ram; I overwrite it with a 64KB sample. So the 7F bank in work ram stores the 64KB sample.

Then I wrote some code that writes a chunk of data from the sample to a buffer in the SNES CPU's ram. In this streaming code there are two buffers, buffer 0 and buffer 1. Buffer 0 is at 0x3000 in the SNES CPU's ram and buffer 1 is at 0x3100. The "sampleOffset" variable tells you the position in the sample data.

So a chunk of "bufferSize" size is written to one of the buffers. After the chunk is written to the buffer, I OR the first byte in the last BRR block of the buffer chunk with #%00000001. This makes it the DSP see the last block of the chunk as the end of a sample.

The first time a chunk is written it is written to buffer 0. Chunks alternate between writing to buffer 0 and buffer 1.

Then the chunk is written to one of the buffers in the SPC700's memory. Just like in the SNES CPU ram, the buffers are at 0x3000 and 0x3100 in the SPC700's ram. Then the sample at buffer 0 is keyed on and the SPC700 program writes a byte that tells the SNES CPU to write the next chunk to buffer 1. If it writes 0xE0 to $2143, that means to write to buffer 0. If it writes 0xE1 that means write to buffer 1.

Once the SNES CPU receives the byte from $2143 it uses that byte to get the "currentBufferAddress", the address in the buffer the next chunk will be written to.

So after that the next "bufferSize" chunk will be written to buffer 1 at 0x7E3100. Then that buffer chunk is uploaded to buffer 1 in the SPC700's ram.

Then it just repeats. The whole idea behind this is; while a chunk in one buffer is playing, the next chunk is being uploaded. Once the chunk in buffer 0 stops playing, you will play the sample at buffer 1 and start uploading new data to buffer 0.
Attachments
sampleStreamingSuccess.sfc
(256 KiB) Downloaded 390 times
sampleSwappingSuccess.sfc
(256 KiB) Downloaded 396 times
Post Reply