Konami VRC6 Sound Engine (Nerdy Nights Fork)

Discuss NSF files, FamiTracker, MML tools, or anything else related to NES music.

Moderator: Moderators

Post Reply
Posts: 160
Joined: Sat Apr 24, 2021 2:18 pm

Konami VRC6 Sound Engine (Nerdy Nights Fork)

Post by puppydrum64 » Sat May 15, 2021 3:32 pm

I've been working for a while on the Nerdy Nights NES audio tutorial, and I've finally added VRC6 support that works up to the tempo section of the tutorial. Here's the source code:

Code: Select all


NOISE 	  EQU $03
DPCM	  EQU $04
PULSE_1	  EQU $05
PULSE_2	  EQU $06

SFX_1	  EQU $08
SFX_2	  EQU $09

    .rsset $0300 ;sound engine variables will be on the $0300 page of RAM
sound_disable_flag  .rs 1   ;a flag variable that keeps track of whether the sound engine is disabled or not. 
sound_temp1 .rs 1           ;temporary variables
sound_temp2 .rs 1
sound_sq1_old .rs 1  ;the last value written to $4003
sound_sq2_old .rs 1  ;the last value written to $4007
sound_pu1_old .rs 1	 ;the last value written to $9002
sound_pu2_old .rs 1	 ;the last value written to $A002
soft_apu_ports .rs 32	;0-3	2A03 SQUARE 1
						;4-7	2A03 SQUARE 2
						;8-11   2A03 TRIANGLE
						;12-15  2A03 NOISE
						;16-19  2A03 DPCM
						;20-23  VRC6 PULSE 1
						;24-27  VRC6 PULSE 2
						;28-31  VRC6 SAWTOOTH

;reserve 6 bytes, one for each stream
stream_curr_sound .rs 10     ;current song/sfx loaded
stream_status .rs 10         ;status byte.   bit0: (1: stream enabled; 0: stream disabled)
stream_channel .rs 10        ;what channel is this stream playing on?
stream_ptr_LO .rs 10         ;low byte of pointer to data stream
stream_ptr_HI .rs 10         ;high byte of pointer to data stream
stream_vol_duty .rs 10       ;stream volume/duty settings
stream_note_LO .rs 10        ;low 8 bits of period for the current note on a stream
stream_note_HI .rs 10        ;high 3 bits of period for the current note on a stream 
stream_tempo .rs 10          ;the value to add to our ticker total each frame
stream_ticker_total .rs 10   ;our running ticker total.
stream_note_length_counter .rs 10
stream_note_length .rs 10
    lda #$0F
    sta $4015   ;enable Square 1, Square 2, Triangle and Noise channels
    lda #$00
	sta $9003	;enable VRC6 audio
    sta sound_disable_flag  ;clear disable flag
    ;later, if we have other variables we want to initialize, we will do that here.
    lda #$FF
    sta sound_sq1_old   ;initializing these to $FF ensures that the first notes of the first song isn't skipped
    sta sound_sq2_old
	sta sound_pu1_old
	sta sound_pu2_old
    lda #$30
    sta soft_apu_ports      ;set Square 1 volume to 0
    sta soft_apu_ports+4    ;set Square 2 volume to 0
    sta soft_apu_ports+12   ;set Noise volume to 0
    lda #$80
    sta soft_apu_ports+8     ;silence Triangle
	lda soft_apu_ports+16
	and #$7F
	sta soft_apu_ports+16
	lda soft_apu_ports+20
	and #$7F
	sta soft_apu_ports+20
	lda soft_apu_ports+24
	and #$7F
	sta soft_apu_ports+24

    lda #$00
    sta $4015   ;disable all channels
	lda #0
	sta $9000
	sta $A000
	sta $B000
	lda #$01
    sta sound_disable_flag  ;set disable flag
; load_sound will prepare the sound engine to play a song or sfx.
;   input:
;       A: song/sfx number to play
    sta sound_temp1         ;save song number
    asl a                   ;multiply by 2.  We are indexing into a table of pointers (words)
    lda song_headers, y     ;setup the pointer to our song header
    sta sound_ptr
    lda song_headers+1, y
    sta sound_ptr+1
    ldy #$00
    lda [sound_ptr], y      ;read the first byte: # streams
    sta sound_temp2         ;store in a temp variable.  We will use this as a loop counter: how many streams to read stream headers for
.loop:						;y = which type of data we are loading (status, channel, volume, etc.)
    lda [sound_ptr], y      ;stream number
    tax                     ;stream number acts as our variable index
    lda [sound_ptr], y      ;status byte.  1= enable, 0=disable
    sta stream_status, x
    beq .next_stream        ;if status byte is 0, stream disabled, so we are done
    lda [sound_ptr], y      ;channel number
    sta stream_channel, x
    lda [sound_ptr], y      ;initial duty and volume settings
    sta stream_vol_duty, x
    lda [sound_ptr], y      ;pointer to stream data.  Little endian, so low byte first
    sta stream_ptr_LO, x
    lda [sound_ptr], y
    sta stream_ptr_HI, x
    lda [sound_ptr], y
    sta stream_tempo, x
    lda #$A0
    sta stream_ticker_total, x
    lda #$01
    sta stream_note_length_counter,x
    lda sound_temp1         ;song number
    sta stream_curr_sound, x
    dec sound_temp2         ;our loop counter
    bne .loop

; sound_play_frame advances the sound engine by one frame
    lda sound_disable_flag
    bne .done   ;if disable flag is set, don't advance a frame

    jsr se_silence  ;silence all channels.  se_set_apu will set volume later for all channels that are enabled.
                    ;the purpose of this subroutine call is to silence channels that aren't used by any streams.
    ldx #$00
    lda stream_status, x
    and #$01    ;check whether the stream is active
    beq .endloop  ;if the stream isn't active, skip it
    ;add the tempo to the ticker total.  If there is a FF-> 0 transition, there is a tick
    lda stream_ticker_total, x
    adc stream_tempo, x
    sta stream_ticker_total, x
    bcc .set_buffer    ;carry clear = no tick.  if no tick, we are done with this stream
    dec stream_note_length_counter, x   ;else there is a tick. decrement the note length counter
    bne .set_buffer    ;if counter is non-zero, our note isn't finished playing yet
    lda stream_note_length, x   ;else our note is finished. reload the note length counter
    sta stream_note_length_counter, x
    jsr se_fetch_byte   ;read the next byte from the data stream
    jsr se_set_temp_ports   ;copy the current stream's sound data for the current frame into our temporary APU vars (soft_apu_ports)
    cpx #$0A
    bne .loop
    jsr se_set_apu      ;copy the temporary APU variables (soft_apu_ports) to the real APU ports ($4000, $4001, etc)

; se_fetch_byte reads one byte from a sound data stream and handles it
;   input: 
;       X: stream number    
    lda stream_ptr_LO, x
    sta sound_ptr
    lda stream_ptr_HI, x
    sta sound_ptr+1
    ldy #$00
    lda [sound_ptr], y
    bpl .note                ;if < #$80, it's a Note
    cmp #$A0
    bcc .note_length         ;else if < #$A0, it's a Note Length
.opcode:                     ;else it's an opcode
    ;do Opcode stuff
    cmp #$FF
    bne .endthis
	lda #0
	sta lockUserInput
    lda stream_status, x    ;if $FF, end of stream, so disable it and silence
    and #%11111110
    sta stream_status, x    ;clear enable flag in status byte
    lda stream_channel, x
	cmp #PULSE_1
	bcs .silence_vrc6
    cmp #TRIANGLE
    beq .silence_tri        ;triangle is silenced differently from squares and noise
    lda #$30                ;squares and noise silenced with #$30
    bne .silence
    lda #$80                ;triangle silenced with #$80
    sta stream_vol_duty, x  ;store silence value in the stream's volume variable.
    jmp update_pointer     ;done
	lda #$00
	sta stream_vol_duty, x
	jmp update_pointer
    ;do note length stuff
    and #%01111111          ;chop off bit7
    sty sound_temp1         ;save Y because we are about to destroy it
    lda note_length_table, y    ;get the note length count value
    sta stream_note_length, x
    sta stream_note_length_counter, x   ;stick it in our note length counter
    ldy sound_temp1         ;restore Y
    iny                     ;set index to next byte in the stream
    jmp .fetch              ;fetch another byte
    ;do Note stuff
    sty sound_temp1     ;save our index into the data stream
    asl a
	; lda stream_channel,x
	; cmp #SAWTOOTH
	; beq lookupSaw
    lda note_table, y
    sta stream_note_LO, x
    lda note_table+1, y
    sta stream_note_HI, x
    ldy sound_temp1     ;restore data stream index
	jmp skipSaw
	lda note_table_saw, y
	sta stream_note_LO, x
	lda note_table_saw+1, y
	sta stream_note_HI, x
	ldy sound_temp1
    ;check if it's a rest and modify the status flag appropriately
    jsr se_check_rest    
    adc stream_ptr_LO, x
    sta stream_ptr_LO, x
    bcc .end
    inc stream_ptr_HI, x

; se_check_rest will read a byte from the data stream and
;       determine if it is a rest or not.  It will set or clear the current
;       stream's rest flag accordingly.
;       input:
;           X: stream number
;           Y: data stream index
    lda [sound_ptr], y  ;read the note byte again
    cmp #rest
    bne .not_rest
    lda stream_status, x
    ora #%00000010  ;set the rest bit in the status byte
    bne .store  ;this will always branch.  bne is cheaper than a jmp.
    lda stream_status, x
    and #%11111101  ;clear the rest bit in the status byte
    sta stream_status, x
; se_set_temp_ports will copy a stream's sound data to the temporary apu variables
;      input:
;           X: stream number
    lda stream_channel, x
    asl a
    asl a
    lda stream_vol_duty, x
    sta soft_apu_ports, y       ;vol
    lda #$08
    sta soft_apu_ports+1, y     ;sweep
    lda stream_note_LO, x
    sta soft_apu_ports+2, y     ;period LO
    lda stream_note_HI, x
    sta soft_apu_ports+3, y     ;period HI
    ;check the rest flag. if set, overwrite volume with silence value 
    lda stream_status, x
    and #%00000010
    beq .done       ;if clear, no rest, so quit
    lda stream_channel, x
	cmp #PULSE_1
	bcs .silence_vrc6		;if vrc6, silence by AND #$7F
    cmp #TRIANGLE   ;if triangle, silence with #$80
    beq .tri        ;else, silence with #$30
    lda #$30        
    bne .store
    lda #$80
    sta soft_apu_ports, y
	lda stream_channel,x
	cmp #PULSE_1
	BCS correctVRC6
	lda stream_note_HI,x
	and #$7f
	sta soft_apu_ports+3, y
	lda stream_note_HI,x
	ora #$80
	sta soft_apu_ports+3,y
; se_set_apu copies the temporary RAM ports to the APU ports
    lda soft_apu_ports+0
    sta $4000
    lda soft_apu_ports+1
    sta $4001
    lda soft_apu_ports+2
    sta $4002
    lda soft_apu_ports+3
    cmp sound_sq1_old       ;compare to last write
    beq .square2            ;don't write this frame if they were equal
    sta $4003
    sta sound_sq1_old       ;save the value we just wrote to $4003
    lda soft_apu_ports+4
    sta $4004
    lda soft_apu_ports+5
    sta $4005
    lda soft_apu_ports+6
    sta $4006
    lda soft_apu_ports+7
    cmp sound_sq2_old
    beq .triangle
    sta $4007
    sta sound_sq2_old       ;save the value we just wrote to $4007
    lda soft_apu_ports+8
    sta $4008
    lda soft_apu_ports+10   ;there is no $4009, so we skip it
    sta $400A
    lda soft_apu_ports+11
    sta $400B
    lda soft_apu_ports+12
    sta $400C
    lda soft_apu_ports+14   ;there is no $400D, so we skip it
    sta $400E
    lda soft_apu_ports+15
    sta $400F
	; lda soft_apu_ports+16
	; sta $4010
	; lda soft_apu_ports+17
	; sta $4011
	; lda soft_apu_ports+18
	; sta $4012
	; lda soft_apu_ports+19
	; sta $4013
	lda soft_apu_ports+20
	sta $9000
	lda soft_apu_ports+22
	sta $9001
	lda soft_apu_ports+23
	sta sound_pu1_old
	sta $9002
	lda soft_apu_ports+24
	sta $A000
	lda soft_apu_ports+26
	sta $A001
	lda soft_apu_ports+27
	sta sound_pu2_old
	sta $A002
	lda soft_apu_ports+28
	sta $B000
	lda soft_apu_ports+30
	sta $B001
	lda soft_apu_ports+31
	sta $B002
NUM_SONGS = $07 ;if you add a new song, change this number.    
                ;headers.asm checks this number in its song_up and song_down subroutines
                ;to determine when to wrap around.
;this is our pointer table.  Each entry is a pointer to a song header                
    .word song0_header  ;this is a silence song.  See song0.i for more details
    .word song1_header  ;evil, demented notes
    .word song2_header  ;a sound effect.  Try playing it over the other songs.
    .word song3_header  ;a little chord progression.
    .word song4_header  ;a new song taking advantage of note lengths and rests
    .word song5_header  ;another sound effect played at a very fast tempo.
	.word song6_header	;Kiss From A Rose intro
    .include "note_table.i" ;period lookup table for notes
    .include "note_length_table.i"
    .include "song0.i"  ;holds the data for song 0 (header and data streams)
    .include "song1.i"  ;holds the data for song 1
    .include "song2.i"
    .include "song3.i"
    .include "song4.i"
    .include "song5.i"
    .include "song6.i"
This works pretty well, but it doesn't sound quite the same as Famitracker, regardless of which emulator I'm running it on. (I've tested it about equally on FCEUX and Mesen, a little bit on Nestopia but not as much as the other two.) Making readable code isn't my strong suit and I feel that this implementation is a bit messy. Hopefully I can get some advice for making this a little bit easier to understand. I was a bit wasteful with my RAM (there are a few soft apu ports that serve no purpose other than taking up space to make the looping easier.) There's a lot of misalignment going on in the formatting but it only appeared after pasting from Notepad++.
(48.02 KiB) Downloaded 38 times
Current Projects: Aquarium Simulator (MMC1), Astro Birds (VRC6)

Post Reply