It is currently Sun Dec 17, 2017 10:19 pm

All times are UTC - 7 hours





Post new topic Reply to topic  [ 44 posts ]  Go to page 1, 2, 3  Next
Author Message
 Post subject: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 4:54 am 
Offline
User avatar

Joined: Sun Mar 19, 2006 3:06 am
Posts: 584
Location: Gothenburg/Sweden
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()



_________________
http://nes.goondocks.se/


Last edited by oRBIT2002 on Fri Jan 22, 2016 1:19 pm, edited 1 time in total.

Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 9:56 am 
Offline
User avatar

Joined: Wed Nov 10, 2004 6:47 pm
Posts: 1845
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


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 10:50 am 
Offline
User avatar

Joined: Wed Nov 10, 2004 6:47 pm
Posts: 1845
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
       


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 1:17 pm 
Offline
User avatar

Joined: Sun Mar 19, 2006 3:06 am
Posts: 584
Location: Gothenburg/Sweden
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. ;)


Attachments:
File comment: Squarewave 0 from titlescreen, Mario Bros
MarioBros_sq1.zip [9.03 KiB]
Downloaded 74 times

_________________
http://nes.goondocks.se/
Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 1:20 pm 
Offline
Formerly Fx3
User avatar

Joined: Fri Nov 12, 2004 4:59 pm
Posts: 3076
Location: Brazil
@Disch
Where did you take such notation "v.3210" instead of "v & 0x0F" (or v & 15)??? Never seen such notation. :shock: :shock:


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 1:47 pm 
Offline
User avatar

Joined: Wed Nov 10, 2004 6:47 pm
Posts: 1845
@ 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 ;)


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 1:59 pm 
Offline
User avatar

Joined: Sun Mar 19, 2006 3:06 am
Posts: 584
Location: Gothenburg/Sweden
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.

_________________
http://nes.goondocks.se/


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 2:05 pm 
Offline
User avatar

Joined: Wed Nov 10, 2004 6:47 pm
Posts: 1845
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)


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Fri Jan 22, 2016 2:18 pm 
Offline
User avatar

Joined: Sun Mar 19, 2006 3:06 am
Posts: 584
Location: Gothenburg/Sweden
Of course. Genius.. :) Thanks!

_________________
http://nes.goondocks.se/


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Sat Jan 23, 2016 6:39 am 
Offline
User avatar

Joined: Sun Mar 19, 2006 3:06 am
Posts: 584
Location: Gothenburg/Sweden
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).

_________________
http://nes.goondocks.se/


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Sat Jan 23, 2016 11:28 am 
Offline
User avatar

Joined: Wed Nov 10, 2004 6:47 pm
Posts: 1845
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


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Sat Jan 23, 2016 11:43 am 
Offline
User avatar

Joined: Wed Nov 10, 2004 6:47 pm
Posts: 1845
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


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Sat Jan 23, 2016 2:02 pm 
Offline
User avatar

Joined: Sun Mar 19, 2006 3:06 am
Posts: 584
Location: Gothenburg/Sweden
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...

_________________
http://nes.goondocks.se/


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Sat Jan 23, 2016 2:10 pm 
Offline

Joined: Sun Sep 19, 2004 11:12 pm
Posts: 19355
Location: NE Indiana, USA (NTSC)
Noise period values 0-2 will sound louder with point sampling than with proper low-pass filtering. And noise itself might be mixed quieter.


Top
 Profile  
 
 Post subject: Re: Audio psuedo-code
PostPosted: Sun Jan 24, 2016 4:14 pm 
Offline
User avatar

Joined: Wed Nov 10, 2004 6:47 pm
Posts: 1845
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


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 44 posts ]  Go to page 1, 2, 3  Next

All times are UTC - 7 hours


Who is online

Users browsing this forum: No registered users and 3 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group