Aug 16, 2022

CV3 Audio System Notes

Please refer to my CV3/AkuDen RAM map here for variable & byte references. I will eventually update my RAM map on this blog, but for now I am editing the off-site one due to how slow Blogger has become. If you are confused about some of the terms I use, Google them - it could be educational.
Channels are organized in groups of 9, 7, or 5 referenced by X-register. 
    9-Channel Layout:
        0: VRC6 Pulse 1 
        1: VRC6 Pulse 2
    2: VRC6 Sawtooth
        3: BGM Square 1
        4: BGM Square 2
        5: SFX Square 1
        6: SFX Square 2
        7: Drumkit (defines DCM and Noise playback)
        8: Noise
    7-channel layout removes Drumkit and Noise.
    5-channel layout also removes SFX channels.

When a new track is loaded via routine $E249, the track's header is loaded to initialize the required channels.

Track Initialization
    00    ROM Bank
    01    Extra Channels
Instrument Data
    00    Instrument ID
    01    First Note Address (low)
    02    First Note Address (high)

Each step, the program checks if a channel has an audio track specified. If so, it checks if the pause jingle is playing. Otherwise it checks if music is paused (byte $06DC), in which case BGM tracks are ignored. If both those checks fail, then the ritenuto flag (byte $01BF) is checked for BGM tracks, in which case the current step would be ignored.

Once all those checks are out of the way, note_duration decreases. If it hits 0, the next sequence of note data is evaluated. Specific note data can take up any number of bytes. Each note starts with a Router, read as follows.
Program behavior depends on N as follows.

0 to 11: Multiply the duration of the next note by A+1; if using an envelope, reset envelope position; multiply note duration by fade_settings & #0f, set fadeout_start to value saved in upper nybble (e.g. #1C->#01); clear alternate duty attribute unless one is specified; initialize the volume envelope if enabled; get the note-specific period from a LUT based on N, adjusted by key_change (Pulse channels range from #0D5C (C₁) up to #01C5 (B), whereas Sawtooth channel ranges from #0F44 (C₁) up to #0205 (B₂)); shift octave up by #05-octave_shift (LSR/ROR raises the octave); set the next sustained period; add pitchbend_value to the period; write period to register.

12: Multiply duration of next note by A+1; reset envelope position; initialize control register.

13: Set next note duration to A; set duty_cycle to upper nybble of next datapoint and next_volume to lower nybble; set volume envelope to next datapoint; set note_dampen to next_volume if positive, otherwise set it to #00; set fade_settings to lower nybble of next datapoint and fade_delay_next to upper nybble. Evaluate subsequent datapoint.

Set octave_shift to A if less than 6, else subtract 6 from A and goto corresponding subroutine.

0: Set next note duration to next datapoint; if disabled attribute set, restrict next note duration within #0f.
1: Set volume envelope to next datapoint.
2: Set fade_settings and fade_delay_next to next datapoint.
3: If next datapoint is #00, set alternate duty to #00; else if it's a multiple of #10, set duty_cycle to datapoint; else set alternate duty to datapoint and set attribute.
4: Shift next datapoint left and set key_change to result; if carry bit set, set key_change to negative.
5: If next datapoint is #00, clear alternate duty attribute. Otherwise subtract #50 and set as sweep_envelope; set envelope attribute; expand next note duration by upper nybble of next datapoint; set sweep_rate to lower nybble.
6: If disabled attribute set and next datapoint is a factor of #10, or attribute is cleared and next datapoint is #00, disable sustain_state; otherwise set sustain_position to 0 and set sustain_state to datapoint value -3|#50.
7 or 9: Set pitchbend_value to next datapoint. (break)
8: Set ritenuto_cutoff to next datapoint.
In each case except 7 or 9, evaluate the subsequent datapoint.

15: Terminal points.

#FB: Al segno. Add Y+1 to working address to get loop address, then set al segno state.

#FD: Volta bracket. If looping state set, goto coda_address. Otherwise set note address to next datapoint; add Y+1 to working address to get next ending address; set looping state. Evaluate subsequent datapoint.

#FE: Repeat bracket. If next datapoint is not #FF, increase loop count and compare; if equal, advance note address, otherwise jump to alsegno_address. Otherwise, set note address to next datapoint (da capo al segno). In all cases, evaluate subsequent datapoint.

#FF: End of track. Set track and channel volume to #00.

#F0 to #FC: Exept for #FB, sets note_dampen to lower nybble. Evaluate subsequent datapoint.

If note_duration does not reach 0, all that note data is processed. First, the sustain envelope is handled, assuming the track is not a sound effect or sustain is currently disabled. If note_duration modulo next_duration is 0, the note is cut off and the next sustained note is played.

If not currently in "release" state, the pitchbend envelope is handled, assuming the pitchbend attribute is set. The modified period is then written to the registers.

If not currently in "release" state, the volume envelope is handled next. If note_duration is less than fadeout_start, the channel is set to the "release" state, then the duty cycle is fetched, coupled with the volume (after dampening), and written to the registers. Note that bits 2 and 3 of the alternate duty cycle are the volume, and bit 1 tells the channel to use the alternate duty cycle.

Now the current state is processed.

0: Initialize. Not used as a state. This is the function that applies the duty cycle and volume dampening.

1: Decay. If fade_duration decreases below 0, the channel is set to the "sustain" state (fade_duration is also made negative, which is pointless because it's already negative). Otherwise, the volume is lowered by 1.

2: Sustain. The code checks if in the "release" state first, which is odd since it's never called elsewhere that I have seen. Then it exits the code if in the "release" state - that part actually makes sense in context. Anyway, this is just the volume envelope handling we already saw earlier. In other words, this state is just a waste of time.

3: Release. The next sustained note is played and volume fades out (or at least to #01). 

Let's look at an example of 2 sets of drum data.

D7 38 85 18 E2 C6
1. redirect to routine 1, set next_duration to 7
2. set duty_cycle to 3, set next_volume to 8
3. set volume_envelope to 85, set note_dampen to 0
4. set fade_settings to 8, set fade_delay_next to 1
Load next note data
5. redirect to routine 2, set pitch_sweep to 2
Load next note data
6. redirect to routine 0, set note_duration to #31 (7*(6+1))
ED 81 EA 00 E9 01 D7 40 58 1A E5 C0
1. redirect to routine 2, redirect to subroutine 7 (D-6)
2. set pitchbend_value to 81
Load next note data
3. redirect to routine 2, redirect to subroutine 4 (A-6)
4. set key_change to 00
Load next note data
5. redirect to routine 2, redirect to subroutine 3 (9-6)
6. set duty_cycle_alt to 01
Load next note data
7. redirect to routine 1, set duration_next to 7
8. set duty_cycle to 4, set next_volume to 0
9. set volume_envelope to 58
10. set fade_settings to A, set fade_delay_next to 1
Load next note data
11. redirect to routine 2, set pitch_sweep to 5
Load next note data
12. redirect to routine 0, set note_duration to 7 (7*(0+1))
Now let's look at a few notes from a Square channel.

50 A0 50 20 E3 A0 E4 90
1. set note_duration to 7 (7*(0+1)), reset pitch envelope, set fadeout_start, set fade_delay to 1, reset volume envelope, set note to F and adjust the octave
Load next note data
2. same as (1), but set note to A#
Load next note data
3. same as (1)
Load next note data
4. same as (1), but set note to D
Load next note data
5. redirect to routine 2, set octave_shift to 3
6. same as (2)
Load next note data
7. redirect to routine 2, set octave_shift to 4
8. same as (1), but set note to A
Below is a simplified chart of each router and datapoint arguments.

Router (Nx) Function Argument (xA) Datapoints (AB)
0x Play C Adjust note_duration --
1x Play C# Adjust note_duration --
2x Play D Adjust note_duration --
3x Play D# Adjust note_duration --
4x Play E Adjust note_duration --
5x Play F Adjust note_duration --
6x Play F# Adjust note_duration --
7x Play G Adjust note_duration --
8x Play G# Adjust note_duration --
9x Play A Adjust note_duration --
Ax Play A# Adjust note_duration --
Bx Play B Adjust note_duration --
Cx Routine
Reset envelopes
Adjust note_duration --
Dx Routine Set next_duration 0: A=duty_cycle; B=note_dampen
1: A=fade_delay_next,B=fade_settings
Ex Routine If <6 set octave_shift;
else subroutine=A-6
Depends on if xA>6
(see next table)
Fx Routine If <#b or =#c set note_dampen;
#b: Al segno loop;
#d: Volta bracket;
#e: Repeat bracket;
#f: End of track
For #fe only: AB=repeat count;
if AB=#ff, next address follows
Subrouter Datapoint 1 (AB) Datapoint 2 (AB)
E6 duration_next=AB --
E7 volume_envelope=AB --
E8 A=fade_delay_next,B=fade_settings --
E9 if B=0, duty_cycle=AB;
else duty_cycle_alt=AB
EA key_change=AB<<1 td=""> --
EB sweep_envelope=AB-#50 next_duration=A, sweep_rate=B
EC sustain_state=AB-3|#50 --
ED pitchbend_value=AB --
EE ritenuto_cutoff=AB --


No comments:

Post a Comment

©TheouAegis Productions™. Powered by Blogger.