Nes_Snd_Emu is a portable Nintendo Entertainment System (NES) 2A03/2A07 APU sound chip emulator library. Its main features are high accuracy, sound quality, and efficiency. Also included are emulators for the following Famicom expansion sound chips:
- Konami VRC6
- Konami VRC7
- Namco 163
- Nintendo Famicom Disk System (FDS)
- Nintendo MMC5
- Sunsoft 5B
The library also includes a sound sample buffer, support for state snapshots, and a nonlinear sound buffer.
Licensed under the GNU Lesser General Public License (LGPL).
- CMake 3.0+
- A C++11 compiler
- The optional
Sound_Queue
class uses libSDL.
Previous versions of Nes_Snd_Emu went to great lengths to support obsolete platforms and compilers. The current maintainer does not have these obsolete targets to test against, and quality C++ compilers are available for free on every modern platform. Therefore, support for obsolete targets has been removed.
The sound chip emulators handle reads and writes to their registers and generate samples into one or more sound buffers. Register accesses take a CPU clock count, relative to the current time frame. When a time frame is ended all samples from it are added to the sound buffer.
The sound buffer accumulates samples generated by the sound chips and allows them to be read out at any time. The sample rate can be adjusted freely.
Simple_Apu
is recommended instead of Nes_Apu when using the library for the first time (see Simple_Apu.h
for reference). Its source code demonstrates basic use of the APU.
To use Nes_Apu
(or the other sound chips), its output must be routed to a Blip_Buffer
. Then pass CPU reads and writes to the sound chip, end its time frame periodically and read samples from the sound buffer.
Two code skeletons are shown below. The first shows how to add basic APU support to an emulator that doesn't keep track of overall CPU time. The second shows how to add full APU support (including IRQs) to a framework which keeps track of overall CPU time.
#include "Nes_Apu.h"
Blip_Buffer buf;
Nes_Apu apu;
void output_samples( const blip_sample_t*, size_t count );
const size_t out_size = 4096;
blip_sample_t out_buf [out_size];
int total_cycles;
int cycles_remain;
int elapsed()
{
return total_cycles - cycles_remain;
}
const int apu_addr = 0x4000;
void cpu_write_memory( cpu_addr_t addr, int data )
{
// ...
if ( addr >= apu.start_addr && addr <= apu.end_addr )
apu.write_register( elapsed(), addr, data );
}
int cpu_read_memory( cpu_addr_t addr )
{
// ...
if ( addr == apu.status_addr )
return apu.read_status( elapsed() );
}
int dmc_read( void*, cpu_addr_t addr )
{
return cpu_read_memory( addr );
}
void emulate_cpu( int cycle_count )
{
total_cycles += cycle_count;
cycles_remain += cycle_count;
while ( cycles_remain > 0 )
{
// emulate opcode
// ...
cycles_remain -= cycle_table [opcode];
}
}
void end_time_frame( int length )
{
apu.end_frame( length );
buf.end_frame( length );
total_cycles -= length;
// Read some samples out of Blip_Buffer if there are enough to fill our output buffer
if ( buf.samples_avail() >= out_size )
{
size_t count = buf.read_samples( out_buf, out_size );
output_samples( out_buf, count );
}
}
void render_frame()
{
// ...
end_time_frame( elapsed() );
}
void init()
{
std::error_condition error = buf.sample_rate( 44100 );
if ( error )
report_error( error );
buf.clock_rate( 1789773 );
apu.output( &buf );
apu.dmc_reader( dmc_read );
}
#include "Nes_Apu.h"
Blip_Buffer buf;
Nes_Apu apu;
void output_samples( const blip_sample_t*, size_t count );
const size_t out_size = 4096;
blip_sample_t out_buf [out_size];
cpu_time_t cpu_end_time; // Time for CPU to stop at
cpu_time_t cpu_time; // Current CPU time relative to current time frame
unsigned apu_addr = 0x4000;
void cpu_write_memory( cpu_addr_t addr, int data )
{
// ...
if ( addr >= apu.start_addr && addr <= apu.end_addr )
apu.write_register( cpu_time, addr, data );
}
int cpu_read_memory( cpu_addr_t addr )
{
// ...
if ( addr == apu.status_addr )
return apu.read_status( cpu_time );
}
int dmc_read( cpu_addr_t addr )
{
return cpu_read_memory( addr );
}
void emulate_cpu()
{
while ( cpu_time < cpu_end_time )
{
// Decode instruction
// ...
cpu_time += cycle_table [opcode];
switch ( opcode )
{
// ...
case 0x58: // CLI
if ( cpu_status & i_flag )
{
cpu_status &= ~i_flag;
return; // I flag cleared; stop CPU immediately
}
}
}
}
// Time of next IRQ if before end_time, otherwise end_time
cpu_time_t earliest_irq_before( cpu_time_t end_time )
{
if ( !(cpu_status & i_flag) )
{
cpu_time_t irq_time = apu.earliest_irq();
if ( irq_time < end_time )
end_time = irq_time;
}
return end_time;
}
// IRQ time may have changed, so update CPU end time
void irq_changed()
{
cpu_end_time = earliest_irq_before( cpu_end_time );
}
// Run CPU to 'end_time' (possibly a few cycles over depending on instruction)
void run_cpu_until( cpu_time_t end_time )
{
while ( cpu_time < end_time )
{
cpu_end_time = earliest_irq_before( end_time );
if ( cpu_end_time <= cpu_time )
{
// Save PC and status, load IRQ vector, set I flag, etc.
cpu_trigger_irq();
// I flag is now set, so CPU can be run for full time
cpu_end_time = end_time;
}
emulate_cpu();
}
}
// Run CPU for at least 'cycle_count'
void run_cpu( int cycle_count )
{
run_cpu_until( cpu_time + cycle_count );
}
// End a time frame and make its samples available for reading
void end_time_frame( cpu_time_t length )
{
apu.end_frame( length );
buf.end_frame( length );
cpu_time -= length;
// Read some samples out of Blip_Buffer if there are enough to fill our output buffer
if ( buf.samples_avail() >= out_size )
{
size_t count = buf.read_samples( out_buf, out_size );
output_samples( out_buf, count );
}
}
// Emulator probably has a function which renders a video frame
void render_video_frame()
{
for ( int n = scanline_count; n--; )
{
run_cpu( 113 ); // or whatever would be done here
// ...
}
// ...
end_time_frame( cpu_time );
}
void init()
{
std::error_condition error = buf.sample_rate( 44100 );
if ( error )
report_error( error );
buf.clock_rate( 1789773 );
apu.output( &buf );
apu.dmc_reader = dmc_read;
apu.irq_notifier = irq_changed;
}
Nes_Apu
accuracy has some room for improvement, especially regarding IRQ handling.
Much of the expansion audio emulation code is based on potentially outdated documentation. A thorough review of these chips using the latest discoveries and documentation is pending.
If you're having problems, check the following:
- If multiple threads are being used, ensure that only one at a time is accessing objects from the library. This library is not thread-safe.
- Enable debugging support. This enables assertions and other run-time checks.
- See if the demo works.
Functions which can fail have a return type of std::error_condition
, which is a portable error construct included with C++11 and later.
Significant violations of the documented interface are flagged with debug-only assertions. Failure of these usually indicates a caller error rather than a defect in the library.