Audio psuedo-code

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Audio psuedo-code
by on (#163071)
A few people might have seen my other thread about struggling with generating some NES-audio. I'm trying to cleanup this mess and thought about trying to generate some psuedo-code to hopefully make things more clear. I'm not sure this is a good idea or not, we'll see. I don't understand all of the documentation that's available but I'm hoping some talented people (I know there are many around here) perhaps wants to help out?
Feel free to make comments about adjustements and I'll update this post. Or if you have a better idea, feel free to let me know. :)
Currently I'm focusing on understanding the squarewave channel 0 (below) but it's obviously not complete at the moment.

EDIT: Ignore this psuedo-code below, check the posts from Disch instead. :)

Code:
$4000:
   Set DutyType to (bit 6-7)

   if bit 5 = 1 (?????)
      if VolumeMode=Decay
         if bit 4 = 1
            DecayLooping=true, restart at 0x0F;
         else
            DecayLooping=false, stay at 0
      LenghtCounterClock = Disabled

   if bit 4 = 0
      VolumeMode=Decay
      Set Volume to 0x0F
      Set EnvelopeReloadValue to (bit 3-0)
   else
      VolumeMode=Fixed
      Set volume to (bit 3-0)


$4001: SWEEP
   

$4002
   WaveLength (lowest 8 bits) = value

$4003
   WaveLength (upper 3 bits) = bit 2-0
   CurrentWaveLength=WaveLength

   LengtCounter=Bit 7-3 (5 bits)
   LenghtCounter=DurationTable[LengthCounter]
   LenghtCounterClock = enabled (if LengthCounter>0)


$Vertical blank:
if LengthCounterClock is enabled
   LengthCounter--
   if LenghtCounter==0
      SilenceChannel()


Re: Audio psuedo-code
by on (#163098)
1) Nothing happens during VBlank. VBlank is tied to the PPU and is in no way related to the APU. The APU is driven entirely by CPU/APU clocks.

2) Things like the sweep/decay/length units are clocked by the frame counter, but don't let the "frame" in the name fool you, it has nothing to do with an actual frame. it's just another clock divider.

3) The length counter will never* be 0 after a write to $4003 because every entry in the duration table is > 0. Whether or not the length counter is enabled depends on $4000.5 (* unless the channel is disabled via $4015, in which case the length counter is always 0 until the channel is re-enabled)


I'll whip up some pseudo-code for you and post it. Give me a few mins
Re: Audio psuedo-code
by on (#163102)
In doing this pseudo-code writeup... I realized just how much of a mess the wiki is for APU details. Some pages have only brief descriptions and link to other places for further details, some places leave out entire registers, etc. I had to flip through like 5 different pages just to get all the information here. This could really stand to be cleaned up.


I'm throwing in EVERYTHING you need to make a 100% functional pulse wave. Note that sweep/length/decay can probably be omitted if you just want to generate basic tones, and it should be fine to play music in most games (though sound effects will likely screw up).

There might be screwey edge-case conditions not accounted for here that you might need to pass hypertechnical test ROMs, but I'll assume you don't care about that.


Code:
========================================================

$4000 write:
    duty_table =        dutytables[ v.76 ]
    decay_loop =        v.5
    length_enabled =    !v.5
    decay_enabled =     !v.4
    decay_V =           v.3210
   
========================================================
   
$4001 write:
    sweep_timer =       v.654
    sweep_negate =      v.3
    sweep_shift =       v.210
    sweep_reload =      true
    sweep_enabled =     v.7  &&  sweep_shift != 0
   
========================================================
   
$4002 write:
    freq_timer =        v           (low 8 bits)
   
========================================================
   
$4003 write:
    freq_timer =        v.210       (high 3 bits)
   
    if( channel_enabled )
        length_counter =    lengthtable[ v.76543 ]
       
    ; phase is also reset here  (important for games like SMB)
    freq_counter =      freq_timer
    duty_counter =      0
   
    ; decay is also flagged for reset here
    decay_reset_flag =  true
   
========================================================
   
$4015 write:
    channel_enabled =   v.0
    if( !channel_enabled )
        length_counter = 0
       
    ; ... other channels and DMC here ...
   
========================================================
   
$4017 write:
    sequencer_mode =    v.7     ; switch between 5-step (1) and 4-step (0) mode
    irq_enabled =       !v.6
    next_seq_phase =    0
    sequencer_counter = ClocksToNextSequence()  ; see: http://wiki.nesdev.com/w/index.php/APU_Frame_Counter
                                                ; for example, this will be 3728.5 APU cycles, or 7457 CPU cycles.
                                                ; It might be easier to work in CPU cycles so you don't have to deal with
                                                ;  half cycles.
   
    if(sequencer_mode)
    {
        Clock_QuarterFrame()                    ; see below
        Clock_HalfFrame()
    }
    if(!irq_enabled)
        irq_pending = false             ; acknowledge Frame IRQ
       
========================================================

$4015 read:
    output = 0
   
    if( length_counter != 0 )       output |= 0x01
    ; ... other channels length counters here
   
    if( irq_pending )
        output |= 0x40
   
    ; ... DMC IRQ state read back here
   
    irq_pending = false                 ; IRQ acknowledged on $4015 read
   
    return output
   

========================================================

Every APU Cycle:
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; clock pulse wave
   
    if( freq_counter > 0 )
        --freq_counter
    else
    {
        freq_counter = freq_timer
        duty_counter = (duty_counter + 1) & 7
    }
   
    ; ... clock other channels here
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; clock frame sequencer
    if( sequencer_counter > 0 )
        --sequencer_counter
    else
    {
        ; see http://wiki.nesdev.com/w/index.php/APU_Frame_Counter for more details on here
        ;  I'm just giving the basic idea here to conceptualize it
       
        if( next_seq_phase causes a Quarter Frame Clock )
            Clock_QuarterFrame();
        if( next_seq_phase causes a Half Frame Clock )
            Clock_HalfFrame();
        if( irq_enabled && next_seq_phase causes an IRQ )
            irq_pending = true          ; raise IRQ
           
        ++next_seq_phase
        if( next_seq_phase > max phases for this mode )
            next_seq_phase = 0
           
        sequencer_counter = ClocksToNextSequence()
    }
   
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; determine audio output
    if(    duty_table[ duty_counter ]   ; current duty phase is high
        && length_counter != 0          ; length counter is nonzero (channel active)
        && !IsSweepForcingSilence()     ; sweep unit is not forcing channel to be silent
        )
    {
        ; output current volume
        if(decay_enabled)       output = decay_hidden_vol
        else                    output = decay_V
    }
    else            ; low duty, or channel is silent
        output = 0
       
    ; ... mix other channels with output here
   
   
========================================================

Clock_QuarterFrame:
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; quarter frame clocks Decay
    if( decay_reset_flag )
    {
        decay_reset_flag =  false
        decay_hidden_vol =  0xF
        decay_counter =     decay_V
    }
    else
    {
        if( decay_counter > 0 )
            --decay_counter
        else
        {
            decay_counter = decay_V
            if( decay_hidden_vol > 0 )
                --decay_hidden_vol
            else if( decay_loop )
                decay_hidden_vol = 0xF
        }
    }
   
   
========================================================

Clock_HalfFrame:
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; half frame clocks Sweep
    if( sweep_reload )
    {
        sweep_counter = sweep_timer
        ; note there's an edge case here -- see http://wiki.nesdev.com/w/index.php/APU_Sweep
        ;   for details.  You can probably ignore it for now
       
        sweep_reload = false
    }
    else if( sweep_counter > 0 )
        --sweep_counter
    else
    {
        sweep_counter = sweep_timer
        if( sweep_enabled && !IsSweepForcingSilence() )
        {
            if(sweep_negate)
                freq_timer -= (freq_timer >> sweep_shift) + 1   ; note: +1 for Pulse1 only.  Pulse2 has no +1
            else
                freq_timer += (freq_timer >> sweep_shift)
        }
    }
   
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; half frame also clocks length
    if( length_enabled && length_counter > 0 )
        --length_counter
       
     
======================================================== 
       
IsSweepForcingSilence:

    if( freq_timer < 8 )
        return true
       
    else if(    !sweep_negate
                &&
                freq_timer + (freq_timer >> sweep_shift) >= 0x800    )
        return true
       
    else
        return false
       
Re: Audio psuedo-code
by on (#163108)
Huge thanks once again Disch for your help! Great work!
And I'm glad I am not the only one that gets confused by the wiki. :)

Here's a sample-wave where I've implemented the "psuedo-code" in my little work-in-progress emulator (square wave 0 only, for "Mario Bros."). I'm doing this "get every 40:th byte-way" you described in our other thread..
I think it sounds a bit "high pitched" or something.. FCEUX' sounds a bit "softer" (but perhaps that has something to do with resampling), but of course, I might have bugs aswell. ;)
Re: Audio psuedo-code
by on (#163109)
@Disch
Where did you take such notation "v.3210" instead of "v & 0x0F" (or v & 15)??? Never seen such notation. :shock: :shock:
Re: Audio psuedo-code
by on (#163114)
@ oRBIT2002:

Yeah it sounds like you're playing an octave too high, which means you are clocking twice as fast as you should be. It's weird because there are 2 CPU cycles to every 1 APU cycle.... so the channels* clock at half the CPU rate.

So you can either divide the CPU cycles by 2 before clocking your APU... or you can effectively double the width of the wave by having a 16-step duty instead of an 8-step. Note that doing the latter will result in slight inaccuracies and might cause you to fail some test ROMs, but it would mostly work.


* Also note that not ALL the APU channels clock at 0.5 the CPU rate (the triangle clocks at the CPU rate... and the frame sequencer seems to operate on half cycles, implying that it's really using the CPU clock and not the APU clock), so I really question whether or not using APU cycles is the best way to go about this. This is kind of a mess and I don't really have a good answer for the best way to approach this problem yet.


Side note: That 'tinny' or 'buzzing' effect that you hear is due to the nearest-neighbor approach to downsampling. When you employ a better downsampling method, that will go away. But for now don't worry about it ;)


@ Zepper:

It's pseudocode. I made it up ;)
Re: Audio psuedo-code
by on (#163116)
Ah, I did the APU-clocking once for every CPU-clock. I fixed that (runs once every 2:th CPU-clock now) but now the audio plays twice as fast (at 44.1Khz) so I'm probably doing something else wrong aswell.
Re: Audio psuedo-code
by on (#163118)
If you halved the APU clock rate but are still outputting one sample every ~40 APU cycles, you have effectively cut the sample rate in half.

You'll want to output 1 sample every ~20 APU cycles now (which is ~40 CPU cycles)
Re: Audio psuedo-code
by on (#163119)
Of course. Genius.. :) Thanks!
Re: Audio psuedo-code
by on (#163152)
I think your psuedocode would fit into the wiki!
Are you interested in doing this for the other channels aswell? Let me know if you need a bribe. :)
Would be a great addition for everyone that finds the documentation confusing (well, parts of it anyway).
Re: Audio psuedo-code
by on (#163155)
Pulse 2: Identical to Pulse 1 with the following changes:

- use $4004-4007 instead of $4000-4003
- $4015 reads/writes use bit 1 instead of bit 0
- no '+ 1' when doing sweep negate




Triangle:
A few things to note about the triangle:

- It's clocked at twice the rate of other channels (use CPU clock instead of APU clock)
- To silence it, you stop clocking the tri-step unit, but do not change its output. This is in contrast to other channels where you silence them by forcing output to zero.
- There is no volume control, but Tri might appear quieter sometimes due to interference from the DMC. See http://wiki.nesdev.com/w/index.php/APU_Mixer for details
- When the freq timer is < 2, it goes "ultrasonic" and is effectively silenced by forcing output to "7.5" (this causes a pop).


Code:
$4015 read / write:  Same as Pulse1, only use bit 2 instead of bit 0
        Note 4015 touches length counter only, it does not do anything with linear counter
                     
========================================================

$4008 write:
    linear_control = v.7
    length_enabled = !v.7
    linear_load = v.6543210
   
========================================================

$400A write:
    freq_timer = v                  (low 8 bits)
   
========================================================

$400B write:
    freq_timer = v.210              (high 3 bits)
   
    if( channel_enabled )
        length_counter = lengthtable[ v.76543 ]
       
    linear_reload = true
   
   
========================================================

Every **CPU** Cycle:
    ; Note the Triangle is clocked at twice the rate of other channels!
    ; It is clocked by CPU cycle and not by APU cycle!
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;; clock tri wave
   
    ultrasonic = false
    if( freq_timer < 2 && freq_counter == 0 )
        ultrasonic = true
   
    clock_triunit = true
    if( length_counter == 0 )       clock_triunit = false
    if( linear_counter == 0 )       clock_triunit = false
    if( ultrasonic )                clock_triunit = false
   
    if( clock_triunit )
    {
        if( freq_counter > 0 )
            --freq_counter
        else
        {
            freq_counter = freq_timer
            tri_step = (tri_step + 1) & 0x1F    ; tri-step bound to 00..1F range
        }
    }
   
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;; determine audio output
   
    ; the xor here creates the 'triangle' shape
    if( ultrasonic )                output = 7.5
    else if( tri_step & 0x10 )      output = tri_step ^ 0x1F
    else                            output = tri_step
   
   
========================================================

Clock_QuarterFrame:
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; quarter frame clocks Linear
   
    if( linear_reload )
        linear_counter = linear_load
    else if( linear_counter > 0 )
        --linear_counter
       
    if( !linear_control )
        linear_reload = false
       
       
       
========================================================

Clock_HalfFrame:

    ; clock Length counter, same as Pulse
Re: Audio psuedo-code
by on (#163157)
Noise:

Notes:
- noise_shift must never be zero or the noise channel will never produce any output. Initialize it with 1 at bootup / hard reset.
- with below implementation, noise_shift must not be signed-16 bit (unsigned is OK, or something larget than 16 bit is OK). If signed, the right-shift will feed in unwanted 1s.

Code:
$4015 read / write:  Same as Pulse1, only use bit 3 instead of bit 0
                     
========================================================

$400C write:
    ; same as $4000, only ignore bits 6 and 7 because noise has no duty
   
========================================================

$400E write:
    freq_timer = noise_freq_table[ v.3210 ]  ; see http://wiki.nesdev.com/w/index.php/APU_Noise for freq table
    shift_mode = v.7
   
========================================================

$400F write:
    if( channel_enabled )
        length_counter = lengthtable[ v.76543 ]
       
    decay_reset_flag = true
   
========================================================

Every APU Cycle:

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; clock noise shift
   
    if( freq_counter > 0 )
        --freq_counter
    else
    {
        freq_counter = freq_timer
       
                            ; note, set bit fifteen here, not bits 1 and 5
        if( shift_mode )    noise_shift.15 = noise_shift.6 ^ noise_shift.0
        else                noise_shift.15 = noise_shift.1 ^ noise_shift.0
       
        noise_shift >>= 1
    }
   
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; determine audio output
    if(    noise_shift.0 == 0           ; current noise output is low (output vol when low -- opposite of pulse)
        && length_counter != 0          ; length counter is nonzero (channel active)
        )
    {
        ; output current volume
        if(decay_enabled)       output = decay_hidden_vol
        else                    output = decay_V
    }
    else            ; high shift output, or channel is silent
        output = 0
   
   
========================================================

Clock_QuarterFrame:
    ; clock Decay, same as Pulse
       
       
       
========================================================

Clock_HalfFrame:
    ; clock Length counter, same as Pulse
   




I might do the DMC later
Re: Audio psuedo-code
by on (#163160)
Awesome, thanks again! :)

Is volume of the Noise-channel working the same way as the square/trianglewaves? In case I haven't missed any obvious bugs, my output seems very loud compared to FCEUX..
Or if it's always starting at volume 0x0F for some reason...
Re: Audio psuedo-code
by on (#163162)
Noise period values 0-2 will sound louder with point sampling than with proper low-pass filtering. And noise itself might be mixed quieter.
Re: Audio psuedo-code
by on (#163187)
DMC boy-eeeeee


Code:
$4010 write:
    dmcirq_enabled =    v.7
    dmc_loop =          v.6
    freq_timer =        dmc_freq_table[ v.3210 ]   ; see http://wiki.nesdev.com/w/index.php/APU_DMC for freq table
   
    if( !dmcirq_enabled )
        dmcirq_pending = false  ; acknowledge IRQ if disabled
     
   
========================================================

$4011 write:
    output = v.6543210    ; note there is some edge case weirdness here, see wiki for details
       
========================================================
   
$4012 write:
    addrload = $C000 | v<<6

========================================================
   
$4013 write:
    lengthload = v<<4 + 1
   
   
========================================================

$4015 write:
    if( v.4 )
    {
        if( length == 0 )
        {
            length = lengthload
            addr = addrload
        }
    }
    else
        length = 0
       
    dmcirq_pending = false      ; acknowledge DMC IRQ on write
       
========================================================

$4015 read:
    v.4 = (length > 0)
    v.7 = dmcirq_pending
   
    ; ... other channels and frame IRQ set other bits

   
========================================================

Every ?CPU? cycle????
( not sure if DMC runs on APU cycles or CPU cycles.  It doesn't really matter because all the frequencies
are even.  The wiki lists freqs in CPU cycles, so.... *shrug* )



    ;;;;;;;;;;;;;;;;;;;;;;;;
    ;  Clock DMC unit

    if( freq_counter > 0 )
        --freq_counter
    else
    {
        freq_counter = freq_timer
       
        if( !output_unit_silent )
        {
            if( (output_shift & 1) && output < $7E )    output += 2
            if(!(output_shift & 1) && output > $01 )    output -= 2
        }
        --bits_in_output_unit
        output_shift >>= 1
           
        if( bits_in_output_unit == 0 )
        {
            bits_in_output_unit = 8
            output_shift = sample_buffer
            output_unit_silent = is_sample_buffer_empty
            is_sample_buffer_empty = true
        }
    }
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;  Perform DMA if necessary
   
    if( length > 0 && is_sample_buffer_empty )
    {
        sample_buffer = DMAReadFromCPU( addr )   ; note:  this DMA halts the CPU for up to 4 cycles.
                                                 ;  See wiki for timing details.  Note that all commercial games will work
                                                 ;  fine if you ignore these stolen cycles, but some tech
                                                 ;  demos and test ROMs will glitch/fail.  So getting these stolen cycles
                                                 ;  correct is not super important unless you're putting a lot of emphasis
                                                 ;  on accuracy.
        is_sample_buffer_empty = false
        addr = (addr + 1) | $8000     ; <- wrap $FFFF to $8000
        --length
       
        if(length == 0)
        {
            if( dmc_loop )
            {
                length = lengthload
                addr = addrload
            }
            else if( dmcirq_enabled )
                dmcirq_pending = true       ; raise IRQ
        }
    }
   
   
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;; Determine channel output
   
   
    ; output is always 'output' ... the 7 bit value written to $4011 and modified by the DMC unit
Re: Audio psuedo-code
by on (#163204)
Thanks again for your great help.

Triangle-channel is causing me a little headache. I attach a sample from "Mario Bros". Zero-volume doesn't seem to be zero if you check the wav-file. I've checked your psuedo-code and compared it to my code and it should be the same, so...
Or should it behave like that?
Re: Audio psuedo-code
by on (#163205)
It's that way, but there's a fix. When stopped, the triangle always outputs the last sample. Use a volume decay method, like... output -= output >> shf_factor (usually, a number between 5 and 8 - for 16bit output mode).
Re: Audio psuedo-code
by on (#163208)
Zepper is correct. Such a filter will cause output to "drift" towards zero. Though his pseudo-code is a little off... you'd want to keep track of a previous sample... otherwise you're just reducing the volume without actually introducing drift.

Blargg posted a close approximation of the filters used by the actual NES in another thread... here:
viewtopic.php?p=44255#p44255


Specifically there are 3 filters:

1 lowpass:
out[i]=(in[i]-out[i-1])*0.815686

and 2 highpass:
out[i]=out[i-1]*0.996039+in[i]-in[i-1]
out[i]=out[i-1]*0.999835+in[i]-in[i-1]


(note the 0.8... and 0.99... numbers are balanced for 44100 Hz audio output and will need adjusting for different samplerates)

If the above isn't clear, I'll provide pseudocode for it when I get some free time. Right now I gotta run.



EDIT:


Pseudo-code:
Code:
// 'sample' is your output sample as generated by your APU
// 'output' is what you will actually output
//
// initialize all intermediate vars to 0.0

// low pass
LP_In = sample
LP_Out = (LP_In - LP_Out) * 0.815686


// high pass A
HPA_Out = HPA_Out*0.996039 + LP_Out - HPA_Prev
HPA_Prev = LP_Out

// high pass B
HPB_Out = HPB_Out*0.999835 + HPA_Out - HPB_Prev
HPB_Prev = HPA_Out


output = HPB_Out

// scale output to be within min/max bounds
Re: Audio psuedo-code
by on (#163219)
Argh... :?
At every triangle clock: output += sample[ phase++ ]
At every APU update: output -= output >> sft
Re: Audio psuedo-code
by on (#163276)
Disch, I suspect there's something wrong the DMC psuedocode. All I get is zero-output..
I reach the
"sample_buffer = DMAReadFromCPU( addr )" part but it doesn't ever seems to reach "output".
Re: Audio psuedo-code
by on (#163287)
The sample buffer is moved to the output unit in the 'clock DMC unit' section.... after the channel has received 8 clocks (one for each bit in the sample buffer). bits_in_output_unit should count down, and once it hits zero, sample buffer is moved into 'output_shift'.


One problem I can see is that if you are initializing bits_in_output_unit to zero, then it will go negative and that "bits_in_output_unit == 0" check would take REALLY long to catch because the var will have to loop all the way around. Maybe that should have been <= 0. Other than that I don't see any problems with the pseudo-code.
Re: Audio psuedo-code
by on (#163291)
You're right. Initializing the bit to anything else bit zero did the trick, thanks. :)
I also discovered a length-problem with $4013 (it was playing too much). Changing it to:
lengthload = v<<3 + 1
instead of
lengthload = v<<4 + 1
worked better. I am not entirely sure why though... :)
Re: Audio psuedo-code
by on (#163293)
Hmm... no the length should definitely be << 4

See: http://wiki.nesdev.com/w/index.php/APU_DMC

Can you post your code?


EDIT:

Wait a minute....

+ has a higher precedent than <<.
So: val<<4 + 1 is being interpreted as val << (4+1)

So you'll need parenthesis here:

lengthload = (val << 4) + 1;
Re: Audio psuedo-code
by on (#163327)
Ah, that did the trick obviously.
Re: Audio psuedo-code
by on (#163934)
I suspect there might be something off with the triangle-lengthcounter. There are times when the triangle "wears off" too slowly. For example, the time counter on "Mario Bros" bonuslevels, it's almost a constant tone, it should be ticking, sort of.
Of course, it might be problems with my code. :) But there isn't that much code to check so and I haven't found anything that differs from your psuedocode (yet).

Other than that, other channels are working great! :)
Re: Audio psuedo-code
by on (#163956)
oRBIT2002 wrote:
I suspect there might be something off with the triangle-lengthcounter. There are times when the triangle "wears off" too slowly. For example, the time counter on "Mario Bros" bonuslevels, it's almost a constant tone, it should be ticking, sort of.
Of course, it might be problems with my code. :) But there isn't that much code to check so and I haven't found anything that differs from your psuedocode (yet).

Other than that, other channels are working great! :)


Writes to $4003/$4007 should set the duty cycle to zero. Don't take the example code as "fully functional".
Re: Audio psuedo-code
by on (#163982)
Zepper wrote:
Writes to $4003/$4007 should set the lenght counter to zero. Don't take the example code as "fully functional".


???

AFAIK 4003/4007/400B sets the length counter to whatever value you specify -- and that is covered in the example code. If it set the length to zero, your channel would be silent all the time.


oRBIT2002 wrote:
I suspect there might be something off with the triangle-lengthcounter.


Tri is probably using the Linear counter, not the Length.
And the culprit is probably questionable Frame Counter timing.

Check to see if Mario Bros is writing to 4017 with the high bit set every frame. I suspect it is. Remember that such a write will clock audio subunits (like length/linear) immediately in addition to resetting the Frame Counter and changing the mode.
Re: Audio psuedo-code
by on (#164014)
Argh... :lol: :lol: The duty cycle is set to zero on $4003/$4007 writes. :? :?
Re: Audio psuedo-code
by on (#164024)
I've changed a few things related to the APU Frame Counter but the triangle lengthproblem hasn't changed in the scenario with Mario Bros I explained. Back to the drawingboard.

What's the purpose of the "frame interrupt flag"?
Re: Audio psuedo-code
by on (#164027)
APU Frame IRQ isn't very useful in the NES. It's mostly for other applications of the 2A03, which may have no NMI source, to keep time.
Re: Audio psuedo-code
by on (#164030)
tepples wrote:
APU Frame IRQ isn't very useful in the NES. It's mostly for other applications of the 2A03, which may have no NMI source, to keep time.

e.g. arcade games like Punch Out!! that use 2A03 for music/sound but a separate CPU for the game
Re: Audio psuedo-code
by on (#164033)
oRBIT2002 wrote:
I've changed a few things related to the APU Frame Counter but the triangle lengthproblem hasn't changed in the scenario with Mario Bros I explained. Back to the drawingboard.


Tracelogs mofo! Do you make them? ;D

A quick trace of the game shows that it does the following with the triangle every 8 frames:

Code:
- write $08 to $4008
- write $34 to $400A
- write $08 to $400B
[Frame]
- write $C0 to $4017
[Repeat only $4017 writes for 8 frames]


This will do the following:

1) the 4008 write sets the linear load value to 8
2) the 400B write sets the reload flag
3) the 4017 write will clock linear. Since the reload flag is set, the counter is loaded with 8, and the reload flag is cleared
4) During the following frame, the frame counter will clock the linear counter 4 times, knocking it down to 4 by frame end
5) Another 4017 write will clock linear again, knocking it down to 3
6) During the following frame, linear gets clocked 4 more times, knocking it down to 0
7) Further clocks are irrelevant, and the linear will now silence the channel until 400B is written to again (won't happen for another 6 frames)


So the triangle should have ~2 frames of output, followed by ~6 frames of silence.

So start dumping trace logs to see if you're catching these writes correctly. And to see how many times your linear counter is being clocked, and if it's behaving appropriately.


EDIT:

Also I think there's only like 2 NES games that actually use the frame IRQs. Dragon Quest is one... and I think Shin 4 Nin Uchi Mahjong (MMC5) is the other. They may be others but those are the only two I know about.
Re: Audio psuedo-code
by on (#164039)
@oRBIT2002
You mentioned SMB' time summing sfx effect, clearing the level. It's produced by squares, NOT triangle. The only effect I know of triangle channel is SMB3' sliding sound effect.
Re: Audio psuedo-code
by on (#164046)
@Zepper: I think he's talking about the music during the bonus stages of Mario Bros (not "Super" Mario Bros... but the other one -- the one with the POW blocks). Which uses the Triangle.
Re: Audio psuedo-code
by on (#164103)
Yes, I mean "Mario Bros". I've fixed a few errors but it hasn't affected the triangle length yet so probably I've got a bug elsewhere with my audiocode..
Re: Audio psuedo-code
by on (#164112)
Regarding the triangle code...
Code:
Clock_QuarterFrame:
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ORIGINAL
    ; quarter frame clocks Linear
   
    if( linear_reload )
        linear_counter = linear_load
    else if( linear_counter > 0 )
        --linear_counter
       
    if( !linear_control )
        linear_reload = false

Something's different here in my emu... :roll: :wink: :wink:
Code:
Clock_QuarterFrame:
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; quarter frame clocks Linear
   
    if( linear_reload ) {
        linear_counter = linear_load

       if( !linear_control )
           linear_reload = false
   }  else if( linear_counter > 0 )
        --linear_counter
Re: Audio psuedo-code
by on (#164120)
@Zepper:

Both of those should have the same result.

@oRBIT:

Tracelogs tracelogs tracelogs.

Spit out a line of text at each of the following events:

- 4008-400B writes
- 4017 writes
- linear clocks
- Frame start

On each line, include the contents of the Linear Counter so you can see how it's being updated. If you can see where your counting is going wrong, you can see where the problem in your code is.
Re: Audio psuedo-code
by on (#164267)
I detected the error, you mentioned
"3) the 4017 write will clock linear. Since the reload flag is set, the counter is loaded with 8, and the reload flag is cleared"
I didn't see that in your psuedocode that the reloadflag is cleared during that condition. This is mentioned concerning "linear_reload=false":
if( !linear_control )
linear_reload = false
Perhaps I've missed something else..

I am slightly confused about $4017.. When bit 7 is set, should all channels immediately clock (quarter/half-frames)? Because when I do that, most sounds are bit "cut off" so I'm probably doing it wrong. :)
Re: Audio psuedo-code
by on (#164276)
Quote:
I am slightly confused about $4017.. When bit 7 is set, should all channels immediately clock (quarter/half-frames)? Because when I do that, most sounds are bit "cut off" so I'm probably doing it wrong.


The frame counter is probably the most confusing and hardest part of the APU.

But yeah -- a $4017 write immediately triggers a quarter+half frame clock. Though I may have been wrong about the channel being clocked 4 times during the frame -- looking again at the wiki it should only be clocked 3 times + 1 for the $4017 write resulting in a total of 4 clocks per frame instead of the previously mentioned 5.

See this page for ref:
http://wiki.nesdev.com/w/index.php/APU_Frame_Counter


Note how there's a "blank step" in before that final half-frame clock in the 5-step sequence. That blank step should prevent the final clock from occurring because the $4017 write will happen first, which will restart the sequence from the top.
Re: Audio psuedo-code
by on (#164752)
This was a godsend. I had a lot of trouble understanding the APU, but reading / implementing the pseudocode made me understand it better. I also got pulse output! Thanks a lot.
Re: Audio psuedo-code
by on (#164753)
Yeah this helped things alot. Disch is my number one hero at the moment. :)
Re: Audio psuedo-code
by on (#165954)
I've discovered something strange in my APUcode. The triangle-related code is sometimes causing my emulator to slow down more or less randomly for a few secs, and then it continues at full frame rate again.

It looks something like this in psuedo:
...
Code:
TriangleOutput=RenderTriangleData();
TriangleOutput=DoFiltering(TriangleOutput);

..
MixAllChannels();


If I comment away the filtering part (filtering algorithm described earlier in this thread), the problem goes away. If I let RenderTriangleData() always return zero (=0), problem goes away.

My first guess is that I had some loop that's not working correctly (since framerate drops from 60 to 20-30) but no, it doesn't seem to be related to a buggy loop or something similar..
There doesn't seem to be any exceptions going on either that might be causing this..

The puzzlepieces doesn't fit together here so I'm scratching my head a little, so if anyone got some nice ideas that you'd like to share, feel free.. :)
Re: Audio psuedo-code
by on (#166661)
Just discovered that the triangle filter-algorithm "killed" my CPU more or less. The problem was floating point calculations, obviously the c#/x86 code wasn't up for the task, but converting it to "fixed point" saved the day. A friend of mine helped me discover this nasty one...
Re: Audio psuedo-code
by on (#166662)
oRBIT2002 wrote:
Just discovered that the triangle filter-algorithm "killed" my CPU more or less. The problem was floating point calculations, obviously the c#/x86 code wasn't up for the task, but converting it to "fixed point" saved the day. A friend of mine helped me discover this nasty one...


We wary of denormal numbers. Set very tiny floating point values to 0 to avoid performance issues.