Git Product home page Git Product logo

nes_snd_emu's Introduction

Nes_Snd_Emu: NES Sound Emulator

Actions Status

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).

Build Requirements

  • 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.

Technical Overview

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.

Using the APU

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.

Basic APU support

#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 );
}

Full APU support

#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;
}

Emulation Accuracy

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.

Solving Problems

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.

Error handling

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.

nes_snd_emu's People

Contributors

jamesathey avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

nes_snd_emu's Issues

Update and refine MMC5 audio

The MMC5 implementation currently has a few minor inaccuracies, mostly due to assuming it behaves just like the pulse and DPCM channels of the internal APU.

  • Frequency values less than 8 do not silence the MMC5 pulse channels; they can output ultrasonic frequencies.
  • Length counter operates twice as fast as the APU length counter (might be clocked at the envelope rate).
  • The polarity of all MMC5 channels is reversed compared to the APU.
  • The raw PCM channel uses all 8 bits, not 7.
  • The IRQ register $5010 is unimplemented - it has the flag for Read/Write mode for the PCM channel and an IRQ enable bit.

Most likely, the Nes_Mmc5_Apu class needs to stop being a subclass of Nes_Apu, and instead have its own PCM implementation, and use composition (instead of inheritance) to share code for the square wave channels.

Restore save state functionality

Somewhere in the evolution of Nes_Snd_Emu into its inclusion in Game_Music_Emu, the save state functionality was lost. The apu_snapshot code from 0.1.7 does not fit at all with the current conventions.

VRC6 has an implementation of save_state(), as does Blip_Buffer, but the other expansion audio chips as well as the main APU do not have implementations.

Using the old apu_snapshot code as a reference and the VRC6 code as a current example, implement the save_state() function for the 2A03, followed by the remaining expansion audio chips.

Utilize digital-sound-antiques / emu2413 as submodule

It appears that Mitsutaka Okazaki is still maintaining the emu2413 code in 2020, making it much more recent than the 2004 era code currently in Nes_Snd_Emu.

Bring the emu2413 repo in as a submodule to replace the imported code. Test with Lagrange Point music and some VRC7 NSFs.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.