Quantcast
Channel: nesdev.org
Viewing all articles
Browse latest Browse all 746

HDMA streaming code

$
0
0
Hey, Lex here. Lately I've been working on an HDMA sample streaming engine for the SNES. How it works is every
frame a certain number of bytes of sample data is uploaded to an HDMA table in ram. The HDMA table is located
0x7E1000 in work ram. The "bytesPerFrame" variable controls how many bytes of sample data are uploaded to
the HDMA table every frame.


The HDMA table:
Each entry in the HDMA table is 5 bytes. One line count byte + four bytes of sample data.


However, the first entry in the HDMA table is a bit different from the following entries.
In the first entry of the HDMA table the byte after the line count byte is 0xE0. When this
entry is HDMA'd, 0xE0 will end up in the SPC700 register $F4. When the SPC700 program
receives 0xE0, that tells it to begin receiving HDMA data. So when 0xE0 is detected the
SPC700 program will start receiving HDMA data every scanline.

After byte 0xE0, the 16-bit bufferStartingAddress will be stored in the first entry. The bufferStartingAddress
is the address of the current audio buffer in the SPC700's ram that the sample data will be written to.

Every entry after the first entry has a line count of 1 followed by four bytes of sample data. The actual
sample data is begins at address 0x038000 in the rom. Every scanline this HDMA table will transfer four
bytes of sample data to the audio registers $2140,$2141,$2142 and $2143.

Here is the code for updating the HDMA table:

Code:

arch 65816;Use the SNES CPU architecture.lorom!sampleAddress = $00!sampleOffset = $0E;The current offset in the current bank of sample data.!bytesPerFrame = $08!bytesSent= $10!currentScanline = $12!sampleEndAddress = $20;The 24-bit address where the sample ends.!bufferStartingAddress = $3C!comparisonValue = $3E ;The value the bufferStartingAddress will be compared to. 144 x  some number.!hdmaTable = $7E1000macro byteToTable();This macro will transfer a byte to the HDMA table.LDA [!sampleAddress],y;Load a value from the sample data.;This will load a byte from the address stored at direct page $00+Y.;$00 holds the sampleAddress.;The sampleAddress is the 24-bit address of the sample.;To get the final address of the current byte of sample data you do this:;[$00]+YSTA !hdmaTable,x;Store the byte.INX;Increment the offset in the HDMA table.INY;Increment the offset in the sample data's current bank.;Y here represents the current offset in the sample's data.endmacroORG $009000update_HDMA_table:REP #$30;16-bit A, X and Y.LDX #$0002;Load 2 into X.;X here will be used as the current offset in the HDMA table.;$7E1002 is where the bufferStartingAddress will be stored.;So $7E1000+X gives you the address $7E1002, the address where;the bufferStartingAddress is stored in the first entry.LDA !bufferStartingAddressSTA !hdmaTable,x;Store the !bufferStartingAddress to data bytes 2 and 3 of the first entry.;The !bufferStartingAddress is the address where the current sample buffer;is in the SPC700's ram.LDY !sampleOffset;Load the sample offset into Y.;The !sampleOffset is the current offset in the current bank of the sample's data in the ROM.LDX #$0005;X will be used as the offset in the HDMA table.;Set X to 5 so that we skip the first entry.;Since each entry has a format of  line_count+4 bytes each entry in the HDMA table is 5 bytes.;So entry 1 starts at offset 5.STZ $2140;Clear $2140.update_loop:;The loop that sends data from the sample to the HDMA table.SEP #$20LDA #$01;Set the linecount byte for the current entry.;The linecount will always be 1 for every entry that is not the;first entry.;So the 4 bytes of this entry will be HDMA'd and then one line will be skipped.STA !hdmaTable,x;Store it to the HDMA table.INX;Increment the offset in the HDMA table.%byteToTable();Send data byte 1 to the table.%byteToTable();Send data byte 2 to the HDMA table.label:%byteToTable();Send data byte 3 to the HDMA table.%byteToTable();Send data byte 4 to the HDMA table.check_sampleOffset:;Check if the sample offset is greater than 0x7FFF;If it is zero, the negative flag has been set.CPY #$8000;Compare Y(currentSampleOffset) to 0x8000;;If Y is 0x8000 this means that we've transferred all the bytes;in the current 32KB bank of the sample data to the HDMA table and need to increment;the bank of the sampleAddress and reset the sampleOffset.BCC check_EndBank;Have we transferred all the bytes in the current bank of the sample data? No? Then branch.reset_sampleOffset:LDY #$0000;Reset the sampleOffset to zero, so we start reading data from the start of the next 32KB bank.STY $0EINC $02;Increment the bank in the sample data, so the program starts reading the next 32KB of sample data.;Basically we move to the next 32KB of sample data.check_EndBank:;Check if the current bank of the sample is the ending bank.SEP #$20LDA $02;Load the bank of the current sample address.CMP $22;Compare it to the bank where the sample ends.BNE check_GreaterThan;Not equal? Go to update_bytesSentcheck_EndOffset:;Compare the current sample offset to the end offset.REP #$20TYA;Transfer the sampleOffset to A.CLCADC #$8000;Add 0x8000CMP $20BCS update_bytesSentsendStopCommand:;Send the "stop" command to the SPC700 so it stops playing the sample.SEP #$20LDA #$5ESTA $2141;Write 0x5E to the SPC700 to let it know to stop the sample.LDA #$01STA $28;Set the "sampleStop" boolean to true.jmp Transfer_Y;Jump to the end of the code.endless_loop:jmp endless_loopcheck_GreaterThan:;Check if the current sample address bank is greater than the endbankLDA $02LDA $22BCS sendStopCommand;If the sample address bank is greater than the end bank branch.update_bytesSent:REP #$20;8-bit A.LDA !bytesSentCLCADC #$0004STA !bytesSent;Add 4 since we sent 4 bytes of sample data to the HDMA table.CMP !bytesPerFrame;Have we sent all of the bytes for the current frame?BNE update_loop;No? Then loop.leave:REP #$20;16-bit A.;A has to be 16-bit because we're going to add the 16-bit bytesPerFrame value;to the 16-bit bufferStartingAddress.LDA !bufferStartingAddressCLCADC !bytesPerFrame;Add the bytesPerFrame to the bufferStartingAddress.STA !bufferStartingAddressCMP !comparisonValue;Compare it to the comparison value.;The comparison value is the offset where the final audio buffer ends;in the SPC700's ram.BCC Transfer_Yresetting:LDA $80;Reset the bufferStartingAddress to the address of the first audio buffer in SPC700 ram.STA !bufferStartingAddressTransfer_Y:TYA;Transfer the new sample offset to A.STA !sampleOffsetSTZ !bytesSent;Reset the bytes sent.SEP #$20LDA #$DFSTA $2140;Store a value of 0xDF to $2140 to let the SPC700 know the SNES CPU is done updating the HDMA table.RTS
After a certain number of bytes of sample data have been transferred to the HDMA table;
the engine waits for VBlank and then transfers the HDMA table.

Once the HDMA table is transferred the byte 0xC0 is transferred to the SPC700 to tell it that
the PPU is currently in VBlank.

After the HDMA table has been transferred, the program waits until scanline 0. After waiting until
scanline 0; the SNES CPU will wait for the SPC700 code to send a byte of 0xFF. When the SPC700 code
sends a byte of 0xFF, this means that it has finished receiving the HDMA data.


SPC700 program code:
On scanline 1, the first four bytes in the first entry of the HDMA table will be sent to the audio
ports. The first byte that ends up in $F4 will be 0xE0. Byte 0xE0 tells the SPC700 to start receiving
HDMA data every scanline. The 16-bit bufferStartingAddress will end up in registers $F5 and $F6.
The 16-bit bufferStartingAddress stored after the 0xE0 byte gets stored into zero page addresses $08
and $09. Remember, the bufferStartingAddress tells the program the address of the current audio buffer.

After the bufferStartingAddress has been received, the source directory entry for sample 0 will be updated.
The sample start address and the sample loop address are the same as the bufferStartingAddress.
The source directory is at SPC700 ram offset 0x1000.


Once the source directory has been updated, there is some timed to code to wait 26 cycles. After the 26
cycles the program will be on scanline 2.

After that, there is a timed loop for receiving the bytes for the current scanline's HDMA table entry
and moving those bytes to the audio buffer. The loop for receiving bytes is always 65 cycles. There
are approximately 65 SPC cycles every scanline.

Byte-storing loop:
So the first that happens in the loop is you transfer the first byte of HDMA data to A. Then you move A
to the audio buffer in the SPC700's ram. Then you increment Y. Y is used as an offset in the audio
buffer here.

Then you do the same thing for the other 3 bytes for the current scanline's HDMA tavble entry.

After transferring all of the bytes for the current scanline's HDMA table entry to the audio buffer,
the loop checks if the negative flag has been set. Once Y overflows from 0xFF to 0x00
the negative flag will be set. If Y overflows, it will reset to zero and the low byte of the
audioBufferAddress will be incremented. If Y didn't overflow, there is some timing code and
if the scanlinesLeft value isn't zero, the code loops.

Since the SPC700 registers are only 8-bit, when the high byte of the audioBufferAddress exceeds
0xFF; the low byte has to be increased by 1 and the high byte has to be reset.


When the "scanlinesLeft" variable has a value of zero, that means all the HDMA bytes have been
transferred to the audio buffer.After that the loop bit and end it of the last BRR will be set.
The "scanlinesLeft" variable tells you how many scanlines are left in the HDMA table.
The initial "scanlinesLeft" value is calculated by taking the bytesPerFrame value and dividing it
by the "bytesPerScanline" variable.

After all the HDMA bytes have been transferred, the loop and end bits are set for the last
BRR block in the audio buffer.

Here's the code for receiving HDMA data:

Code:

arch SPC700loromORG $028800!scanlinesLeft = $0E!bytesPerScanline = $10!bytesPerFrame = $D2!sampleStopBool = $28wait_DF:;Wait for the SNES CPU to send a value of 0xDF.MOV A,$F4CMP A,#$DFBNE wait_DF;Once the SPC700 receives a value of 0xDF it knows that the SNES is finished updating the HDMA table.wait_5E:;0x5E is a "stop" command.MOV A,$F5CMP A,#$5EBNE scanlinesLeftstop:MOV !sampleStopBool,#$01;Set the "sampleStop" boolean to true.scanlinesLeft:PUSH X;Save X for later.MOV X,#$04MOVW YA,$D2;Divide the !bytesPerFrame by 4.DIV YA,X;It is divided by 4 since the HDMA table will send 4 bytes every scanline.POP XINC AMOV !scanlinesLeft,A;scanlinesLeft=!bytesPerFrame/4+1wait_C0:;Check if the PPU is in VBlank yet.MOV A,$F4CMP A,#$C0;When 0xC0 arrives in $F4 that means that the PPU is in vblank.BNE wait_C0wait_E0:;Wait until the value 0xE0 is HDMA'd to $2140.;When this happens the PPU is on scanline 0.MOV X,#$01MOV A,$F4CMP A,#$E0BNE wait_E0store_sample_offset:;Store the initial offset where the sample will be in SPC ram.MOV $08,$F5;Store sample offset high byte.MOV $09,$F6;Store sample offset low byte.MOV A,$F5MOV Y,$F6PUSH APUSH Yupdate_source_directory_hah:MOV $1000,AMOV $1001,YMOV $1002,AMOV $1003,Ywait_26_cycles: ;Wait 26 cycles, after these 26 cycles the PPU will be on scanline 2.MOV Y,#$00MOV Y,#$00MOV Y,#$00MOV Y,#$00MOV Y,#$00MOV X,#$01MOV X,#$00;3 cyclesINC YMOV Y,#$00;4 cyclesMOV Y,#$00;4 cyclesMOV Y,#$00;4 cyclesMOV Y,#$00;4 cyclesMOV Y,#$00;4 cyclesstore_bytes:;Store the HDMA'd bytes into SPC700 ram.MOV A,$F4MOV ($08)+Y,A;Move the first value.INC Y;12 cyclesMOV A,$F5MOV ($08)+Y,A;12 cyclesINC Y;Store the HDMA'd bytes into SPC700 ram.MOV A,$F6MOV ($08)+Y,A;Move the first value.INC Y;12 cyclesMOV A,$F7MOV ($08)+Y,AINC Y;12 cyclescompare_Y:BNE wait_six_cycles;4 cyclesis_FF:INCW $09;6 cyclesDBNZ !scanlinesLeft,store_byteswait_six_cycles:MOV X,#$00MOV X,#$00MOV X,#$00DBNZ !scanlinesLeft,store_byteslast_block:;Calculate where the last block begins.POP APOP YMOV $08,YMOV $09,A;Pull the offset from the stack.source_dir:MOVW YA,!bytesPerFrame;Move the !bytesPerFrame to YA.CLRCADDW YA,$08;bytesPerFrame+bufferStartingAddress = endOfBuffer.;So to get the address where the the audio buffer ends you take the;bytesPerFrame and add the bufferStartingAddress to it.MOVW !scanlinesLeft,YA;Move the endOfBuffer position to $0A.SETC;Set the carry flag.SBC A,#$09;Subtract 9.;The last block starts 9 bytes before the address where the audio buffer ends.MOVW $80,YAcheck_sampleStop:MOV A,!sampleStopBoolCMP A,#$01BNE set_loop_bitset_end_bit:MOV Y,#$00;Clear Y.MOV A,($80)+Y;Move the first byte of the last block to A.OR A,#%00000001MOV ($80)+Y,Akey_off_channels:MOV $F2,#$5CMOV $F3,#$FFloop:jmp loopset_loop_bit:MOV Y,#$00;Clear Y.MOV A,($80)+Y;Move the first byte of the last block to A.OR A,#%00000011MOV ($80)+Y,Asend_FF:MOV $F4,#$FF;0xFF is used to tell the SNES that we've fully transferred the HDMA'd bytes.wait_FE:;Wait for the SNES CPU to send FE so we can continue.CMP $F4,#$FEBNE wait_FEMOV X,#$00MOV $F4,#$00;Clear $2140.RET
Going back to the SNES CPU code; the SNES CPU then receives a byte 0xFF, which tells it that
the SPC700 is done receiving HDMA data.

Then the SNES sends a byte of 0xFE to the SPC700 to let it know that it has received the 0xFF byte.


Then the SNES program loops.

Below I've included the source code and an example rom.

Also warning, the code only works on NTSC consoles so far and won't work on PAL consoles.

Using a custom song:
To use a custom song, replace "sample.brr" with a 32KHZ BRR file. Then go into the command line, change
to the location where you installed the source code and type "asar.exe main_streaming_code.asm HDMA_streaming_code.sfc".

If you want to change the the address where the sample ends you will need to modify the sampleEndAddress which is stored at $20 in work ram. Also, the code doesn't support exLoRom currently, so your sample will need to be less than 4MB.

You can use the code or modify the code however you like; however credit would be appreciated.

Song credits:
"Adventures in Adventureland"
Kevin MacLeod (incompetech.com)
Licensed under Creative Commons: By Attribution 3.0
http://creativecommons.org/licenses/by/3.0/

Here's a youtube video showcasing the engine:
https://www.youtube.com/watch?v=lFJHG2BTIN8
HDMA_streaming_code.sfc

streaming_source_code.zip

Statistics: Posted by lex — Thu Apr 18, 2024 10:25 am — Replies 0 — Views 177



Viewing all articles
Browse latest Browse all 746

Trending Articles