Commits

spencercw committed f54c749

#25 Move the sound generation back into the main thread.

This brings back the skipping problem, but is required for accurate emulation when games rely on precise timing.

  • Participants
  • Parent commits 0d48a35

Comments (0)

Files changed (7)

File gb_emulator/gb.pb.cc

       sizeof(GbMemoryData));
   GbSoundData_descriptor_ = file->message_type(2);
   static const int GbSoundData_offsets_[5] = {
-    GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(GbSoundData, excess_samples_),
+    GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(GbSoundData, excess_cycles_),
     GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(GbSoundData, sound_1_),
     GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(GbSoundData, sound_2_),
     GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(GbSoundData, sound_3_),
     "(\r\022\014\n\004cart\030\010 \002(\r\022\026\n\016ext_ram_enable\030\t \002(\010"
     "\022\020\n\010rom_bank\030\n \002(\r\022\020\n\010ram_bank\030\013 \002(\r\022\024\n\014"
     "ext_ram_bank\030\014 \002(\r\022\021\n\tvram_bank\030\r \002(\r\022\013\n"
-    "\003mbc\030\016 \002(\014\022\013\n\003rtc\030\017 \002(\014\"\324\007\n\013GbSoundData\022"
-    "\026\n\016excess_samples\030\001 \002(\001\022$\n\007sound_1\030\002 \002(\013"
-    "2\023.GbSoundData.Sound1\022$\n\007sound_2\030\003 \002(\0132\023"
-    ".GbSoundData.Sound2\022$\n\007sound_3\030\004 \002(\0132\023.G"
-    "bSoundData.Sound3\022$\n\007sound_4\030\005 \002(\0132\023.GbS"
-    "oundData.Sound4\032\376\001\n\006Sound1\022\024\n\014gb_frequen"
-    "cy\030\001 \002(\001\022\025\n\ract_frequency\030\002 \002(\001\022\n\n\002hi\030\003 "
-    "\002(\010\022\014\n\004duty\030\004 \002(\001\022\021\n\tcountdown\030\005 \002(\001\022\024\n\014"
-    "sweep_shifts\030\006 \002(\r\022\022\n\nsweep_type\030\007 \002(\010\022\022"
-    "\n\nsweep_step\030\010 \002(\001\022\027\n\017sweep_countdown\030\t "
-    "\002(\001\022\020\n\010envelope\030\n \002(\r\022\025\n\renvelope_step\030\013"
-    " \002(\001\022\032\n\022envelope_countdown\030\014 \002(\001\032\247\001\n\006Sou"
-    "nd2\022\024\n\014gb_frequency\030\001 \002(\001\022\025\n\ract_frequen"
-    "cy\030\002 \002(\001\022\n\n\002hi\030\003 \002(\010\022\014\n\004duty\030\004 \002(\001\022\021\n\tco"
-    "untdown\030\005 \002(\001\022\020\n\010envelope\030\006 \002(\r\022\025\n\renvel"
-    "ope_step\030\007 \002(\001\022\032\n\022envelope_countdown\030\010 \002"
-    "(\001\032\227\001\n\006Sound3\022\024\n\014gb_frequency\030\001 \002(\001\022\025\n\ra"
-    "ct_frequency\030\002 \002(\001\022\020\n\010duration\030\003 \002(\001\022\021\n\t"
-    "countdown\030\004 \002(\001\022\022\n\nwave_index\030\005 \002(\r\022\021\n\to"
-    "ut_level\030\006 \002(\r\022\024\n\014wave_pattern\030\007 \002(\014\032\317\001\n"
-    "\006Sound4\022\021\n\tfrequency\030\001 \002(\001\022\020\n\010duration\030\002"
-    " \002(\001\022\021\n\tcountdown\030\003 \002(\001\022\020\n\010envelope\030\004 \002("
-    "\r\022\032\n\022envelope_direction\030\005 \002(\010\022\025\n\renvelop"
-    "e_step\030\006 \002(\001\022\032\n\022envelope_countdown\030\007 \002(\001"
-    "\022\025\n\rcounter_steps\030\010 \002(\010\022\025\n\rcounter_index"
-    "\030\t \002(\r\"\272\001\n\013GbVideoData\022\024\n\014priority_map\030\001"
-    " \002(\014\022\026\n\016gbc_bg_palette\030\002 \002(\014\022\032\n\022gbc_spri"
-    "te_palette\030\003 \002(\014\022\025\n\rhblank_cycles\030\004 \002(\r\022"
-    "\027\n\017oam_read_cycles\030\005 \002(\r\022\034\n\024oam_vram_rea"
-    "d_cycles\030\006 \002(\r\022\023\n\013line_cycles\030\007 \002(\r\"\274\001\n\006"
-    "GbData\022\013\n\003rom\030\001 \002(\t\022\020\n\010rom_size\030\002 \002(\r\022\024\n"
-    "\014rom_checksum\030\003 \002(\r\022\013\n\003gbc\030\004 \002(\010\022\027\n\003cpu\030"
-    "\005 \002(\0132\n.GbCpuData\022\035\n\006memory\030\006 \002(\0132\r.GbMe"
-    "moryData\022\033\n\005sound\030\007 \002(\0132\014.GbSoundData\022\033\n"
-    "\005video\030\010 \002(\0132\014.GbVideoData", 1746);
+    "\003mbc\030\016 \002(\014\022\013\n\003rtc\030\017 \002(\014\"\323\007\n\013GbSoundData\022"
+    "\025\n\rexcess_cycles\030\001 \002(\001\022$\n\007sound_1\030\002 \002(\0132"
+    "\023.GbSoundData.Sound1\022$\n\007sound_2\030\003 \002(\0132\023."
+    "GbSoundData.Sound2\022$\n\007sound_3\030\004 \002(\0132\023.Gb"
+    "SoundData.Sound3\022$\n\007sound_4\030\005 \002(\0132\023.GbSo"
+    "undData.Sound4\032\376\001\n\006Sound1\022\024\n\014gb_frequenc"
+    "y\030\001 \002(\001\022\025\n\ract_frequency\030\002 \002(\001\022\n\n\002hi\030\003 \002"
+    "(\010\022\014\n\004duty\030\004 \002(\001\022\021\n\tcountdown\030\005 \002(\001\022\024\n\014s"
+    "weep_shifts\030\006 \002(\r\022\022\n\nsweep_type\030\007 \002(\010\022\022\n"
+    "\nsweep_step\030\010 \002(\001\022\027\n\017sweep_countdown\030\t \002"
+    "(\001\022\020\n\010envelope\030\n \002(\r\022\025\n\renvelope_step\030\013 "
+    "\002(\001\022\032\n\022envelope_countdown\030\014 \002(\001\032\247\001\n\006Soun"
+    "d2\022\024\n\014gb_frequency\030\001 \002(\001\022\025\n\ract_frequenc"
+    "y\030\002 \002(\001\022\n\n\002hi\030\003 \002(\010\022\014\n\004duty\030\004 \002(\001\022\021\n\tcou"
+    "ntdown\030\005 \002(\001\022\020\n\010envelope\030\006 \002(\r\022\025\n\renvelo"
+    "pe_step\030\007 \002(\001\022\032\n\022envelope_countdown\030\010 \002("
+    "\001\032\227\001\n\006Sound3\022\024\n\014gb_frequency\030\001 \002(\001\022\025\n\rac"
+    "t_frequency\030\002 \002(\001\022\020\n\010duration\030\003 \002(\001\022\021\n\tc"
+    "ountdown\030\004 \002(\001\022\022\n\nwave_index\030\005 \002(\r\022\021\n\tou"
+    "t_level\030\006 \002(\r\022\024\n\014wave_pattern\030\007 \002(\014\032\317\001\n\006"
+    "Sound4\022\021\n\tfrequency\030\001 \002(\001\022\020\n\010duration\030\002 "
+    "\002(\001\022\021\n\tcountdown\030\003 \002(\001\022\020\n\010envelope\030\004 \002(\r"
+    "\022\032\n\022envelope_direction\030\005 \002(\010\022\025\n\renvelope"
+    "_step\030\006 \002(\001\022\032\n\022envelope_countdown\030\007 \002(\001\022"
+    "\025\n\rcounter_steps\030\010 \002(\010\022\025\n\rcounter_index\030"
+    "\t \002(\r\"\272\001\n\013GbVideoData\022\024\n\014priority_map\030\001 "
+    "\002(\014\022\026\n\016gbc_bg_palette\030\002 \002(\014\022\032\n\022gbc_sprit"
+    "e_palette\030\003 \002(\014\022\025\n\rhblank_cycles\030\004 \002(\r\022\027"
+    "\n\017oam_read_cycles\030\005 \002(\r\022\034\n\024oam_vram_read"
+    "_cycles\030\006 \002(\r\022\023\n\013line_cycles\030\007 \002(\r\"\274\001\n\006G"
+    "bData\022\013\n\003rom\030\001 \002(\t\022\020\n\010rom_size\030\002 \002(\r\022\024\n\014"
+    "rom_checksum\030\003 \002(\r\022\013\n\003gbc\030\004 \002(\010\022\027\n\003cpu\030\005"
+    " \002(\0132\n.GbCpuData\022\035\n\006memory\030\006 \002(\0132\r.GbMem"
+    "oryData\022\033\n\005sound\030\007 \002(\0132\014.GbSoundData\022\033\n\005"
+    "video\030\010 \002(\0132\014.GbVideoData", 1745);
   ::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(
     "gb.proto", &protobuf_RegisterTypes);
   GbCpuData::default_instance_ = new GbCpuData();
 // -------------------------------------------------------------------
 
 #ifndef _MSC_VER
-const int GbSoundData::kExcessSamplesFieldNumber;
+const int GbSoundData::kExcessCyclesFieldNumber;
 const int GbSoundData::kSound1FieldNumber;
 const int GbSoundData::kSound2FieldNumber;
 const int GbSoundData::kSound3FieldNumber;
 
 void GbSoundData::SharedCtor() {
   _cached_size_ = 0;
-  excess_samples_ = 0;
+  excess_cycles_ = 0;
   sound_1_ = NULL;
   sound_2_ = NULL;
   sound_3_ = NULL;
 
 void GbSoundData::Clear() {
   if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
-    excess_samples_ = 0;
+    excess_cycles_ = 0;
     if (has_sound_1()) {
       if (sound_1_ != NULL) sound_1_->::GbSoundData_Sound1::Clear();
     }
   ::google::protobuf::uint32 tag;
   while ((tag = input->ReadTag()) != 0) {
     switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
-      // required double excess_samples = 1;
+      // required double excess_cycles = 1;
       case 1: {
         if (::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
             ::google::protobuf::internal::WireFormatLite::WIRETYPE_FIXED64) {
           DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
                    double, ::google::protobuf::internal::WireFormatLite::TYPE_DOUBLE>(
-                 input, &excess_samples_)));
-          set_has_excess_samples();
+                 input, &excess_cycles_)));
+          set_has_excess_cycles();
         } else {
           goto handle_uninterpreted;
         }
 
 void GbSoundData::SerializeWithCachedSizes(
     ::google::protobuf::io::CodedOutputStream* output) const {
-  // required double excess_samples = 1;
-  if (has_excess_samples()) {
-    ::google::protobuf::internal::WireFormatLite::WriteDouble(1, this->excess_samples(), output);
+  // required double excess_cycles = 1;
+  if (has_excess_cycles()) {
+    ::google::protobuf::internal::WireFormatLite::WriteDouble(1, this->excess_cycles(), output);
   }
   
   // required .GbSoundData.Sound1 sound_1 = 2;
 
 ::google::protobuf::uint8* GbSoundData::SerializeWithCachedSizesToArray(
     ::google::protobuf::uint8* target) const {
-  // required double excess_samples = 1;
-  if (has_excess_samples()) {
-    target = ::google::protobuf::internal::WireFormatLite::WriteDoubleToArray(1, this->excess_samples(), target);
+  // required double excess_cycles = 1;
+  if (has_excess_cycles()) {
+    target = ::google::protobuf::internal::WireFormatLite::WriteDoubleToArray(1, this->excess_cycles(), target);
   }
   
   // required .GbSoundData.Sound1 sound_1 = 2;
   int total_size = 0;
   
   if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
-    // required double excess_samples = 1;
-    if (has_excess_samples()) {
+    // required double excess_cycles = 1;
+    if (has_excess_cycles()) {
       total_size += 1 + 8;
     }
     
 void GbSoundData::MergeFrom(const GbSoundData& from) {
   GOOGLE_CHECK_NE(&from, this);
   if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
-    if (from.has_excess_samples()) {
-      set_excess_samples(from.excess_samples());
+    if (from.has_excess_cycles()) {
+      set_excess_cycles(from.excess_cycles());
     }
     if (from.has_sound_1()) {
       mutable_sound_1()->::GbSoundData_Sound1::MergeFrom(from.sound_1());
 
 void GbSoundData::Swap(GbSoundData* other) {
   if (other != this) {
-    std::swap(excess_samples_, other->excess_samples_);
+    std::swap(excess_cycles_, other->excess_cycles_);
     std::swap(sound_1_, other->sound_1_);
     std::swap(sound_2_, other->sound_2_);
     std::swap(sound_3_, other->sound_3_);

File gb_emulator/gb.pb.h

   
   // accessors -------------------------------------------------------
   
-  // required double excess_samples = 1;
-  inline bool has_excess_samples() const;
-  inline void clear_excess_samples();
-  static const int kExcessSamplesFieldNumber = 1;
-  inline double excess_samples() const;
-  inline void set_excess_samples(double value);
+  // required double excess_cycles = 1;
+  inline bool has_excess_cycles() const;
+  inline void clear_excess_cycles();
+  static const int kExcessCyclesFieldNumber = 1;
+  inline double excess_cycles() const;
+  inline void set_excess_cycles(double value);
   
   // required .GbSoundData.Sound1 sound_1 = 2;
   inline bool has_sound_1() const;
   
   // @@protoc_insertion_point(class_scope:GbSoundData)
  private:
-  inline void set_has_excess_samples();
-  inline void clear_has_excess_samples();
+  inline void set_has_excess_cycles();
+  inline void clear_has_excess_cycles();
   inline void set_has_sound_1();
   inline void clear_has_sound_1();
   inline void set_has_sound_2();
   
   ::google::protobuf::UnknownFieldSet _unknown_fields_;
   
-  double excess_samples_;
+  double excess_cycles_;
   ::GbSoundData_Sound1* sound_1_;
   ::GbSoundData_Sound2* sound_2_;
   ::GbSoundData_Sound3* sound_3_;
 
 // GbSoundData
 
-// required double excess_samples = 1;
-inline bool GbSoundData::has_excess_samples() const {
+// required double excess_cycles = 1;
+inline bool GbSoundData::has_excess_cycles() const {
   return (_has_bits_[0] & 0x00000001u) != 0;
 }
-inline void GbSoundData::set_has_excess_samples() {
+inline void GbSoundData::set_has_excess_cycles() {
   _has_bits_[0] |= 0x00000001u;
 }
-inline void GbSoundData::clear_has_excess_samples() {
+inline void GbSoundData::clear_has_excess_cycles() {
   _has_bits_[0] &= ~0x00000001u;
 }
-inline void GbSoundData::clear_excess_samples() {
-  excess_samples_ = 0;
-  clear_has_excess_samples();
-}
-inline double GbSoundData::excess_samples() const {
-  return excess_samples_;
-}
-inline void GbSoundData::set_excess_samples(double value) {
-  set_has_excess_samples();
-  excess_samples_ = value;
+inline void GbSoundData::clear_excess_cycles() {
+  excess_cycles_ = 0;
+  clear_has_excess_cycles();
+}
+inline double GbSoundData::excess_cycles() const {
+  return excess_cycles_;
+}
+inline void GbSoundData::set_excess_cycles(double value) {
+  set_has_excess_cycles();
+  excess_cycles_ = value;
 }
 
 // required .GbSoundData.Sound1 sound_1 = 2;

File gb_emulator/gb.proto

 		required uint32 counter_index       = 9;
 	}
 
-	required double excess_samples  = 1;
+	required double excess_cycles   = 1;
 	required Sound1 sound_1         = 2;
 	required Sound2 sound_2         = 3;
 	required Sound3 sound_3         = 4;

File gb_emulator/include/gb_emulator/constants.hpp

 const uint8_t  HUC3                     = 0xfe;
 const uint8_t  HUC1_RAM_BAT             = 0xff;
 
-/* Frame duration in nanoseconds */
-const unsigned FRAME_DURATION           = 16742006;
+/* Interesting durations */
+const unsigned FRAME_DURATION           = 16742006;  /* Frame duration in nanoseconds */
+const unsigned CPU_CLOCK                = 4194304;   /* CPU clock frequency */
+const unsigned SAMPLE_RATE              = 48000;     /* Audio sample rate */
+                                                     /* Number of CPU cycles per audio sample */
+const double SAMPLE_CYCLES              = static_cast<double>(CPU_CLOCK) / 4 / SAMPLE_RATE; 
 
 /* Memory map */
 const uint16_t BIOS1_END                = 0x0100;

File gb_emulator/include/gb_emulator/gb_sound.hpp

 
 #include <stdint.h>
 
-#include <vector>
-
+#include <boost/circular_buffer.hpp>
 #include <boost/filesystem/path.hpp>
 #include <boost/shared_ptr.hpp>
 
 	//! Constructor; sets the associated emulator container.
 	GbSound(Gb &gb);
 
+	//! Generates a CPU cycles's worth of sound.
+	int poll();
+
 	//! Starts recording to the given file.
 	/**
 	 * This does nothing if already recording.
 
 	// Sample rate of the audio device
 	unsigned sampleRate_;
-	// The precise number of samples per video frame. The fractional remainder is carried in
-	// excessSamples_ so the timing is correct
-	double frameSamples_;
-	// Fractional number of samples left after an operation. Since only an integral number of
-	// samples can be generated for a given duration, if a fractional number of samples is required,
-	// it will be rounded to the nearest integral value and the excess stored in this variable. It
-	// will then be added to the number of samples required on the next operation. This is to
-	// prevent timing errors
-	double excessSamples_;
+	// Fractional number of CPU cycles left after a sample has been generated. The sample rate does
+	// not divide evenly into the CPU clock frequency so the number of cycles between samples will
+	// be rounded to the nearest integral value and the excess stored in this variable. It will then
+	// be added to the number of cycles required on the next operation. This is to prevent timing
+	// errors.
+	double excessCycles_;
 	// Current frequency in GameBoy format
 	double gbFrequency1_, gbFrequency2_, gbFrequency3_;
 	// Current frequency in Hz
 	unsigned counterDataSize4_;
 	// Current index into the LFSR counter array
 	unsigned counterIndex4_;
+
+	// Buffer of samples used by the sound callback
+	boost::circular_buffer<int16_t> soundBuf_;
 
 	// Functions for each of the sound modes. The output is mixed in the main poll() function
 	double sound1();

File gb_emulator/src/gb.cpp

 	// Enter the main loop
 	int videoCycles = HBLANK_CYCLES;
 	int timerCycles = DIVIDER_CYCLES;
+	int soundCycles = static_cast<int>(SAMPLE_CYCLES);
 	for (;;)
 	{
 		// Update the debugger
 		}
 
 		// Execute an emulation tick
-		int cyclesExecuted = cpu_.poll((min)(videoCycles, timerCycles));
+		int cycles = (min)(videoCycles, timerCycles);
+		cycles = (min)(cycles, soundCycles);
+		int cyclesExecuted = cpu_.poll(cycles);
+
+		timerCycles -= cyclesExecuted;
 		videoCycles -= cyclesExecuted;
-		timerCycles -= cyclesExecuted;
+		soundCycles -= cyclesExecuted;
 
 		if (timerCycles <= 0)
 		{
 			timerCycles += timers_.poll();
 		}
-		
 		if (videoCycles <= 0)
 		{
 			videoCycles += video_.poll();
 		}
+		if (soundCycles <= 0)
+		{
+			soundCycles += sound_.poll();
+		}
 
 		input_.poll();
 	}

File gb_emulator/src/gb_sound.cpp

 #include "gb_sound_tables.h"
 
 namespace fs = boost::filesystem;
+using boost::circular_buffer;
 using std::clog;
 using std::ios_base;
 using std::min;
 using std::vector;
 
 static const double PI = 3.14159265358979323846264338327950288;
+static const unsigned SOUND_BUF_SIZE = 4096;
 
 struct SdlAudioLock
 {
 
 GbSound::GbSound(Gb &gb):
 	gb_(gb),
-	excessSamples_(0),
+	excessCycles_(SAMPLE_CYCLES - static_cast<unsigned>(SAMPLE_CYCLES)),
 	gbFrequency1_(0),
 	gbFrequency2_(0),
 	gbFrequency3_(0),
 	outLevel3_(0),
 	counterData4_(lfsr15),
 	counterDataSize4_(32768),
-	counterIndex4_(0)
+	counterIndex4_(0),
+	soundBuf_(SOUND_BUF_SIZE)
 {
 	memset(wavePattern3_, 0, 0x10);
 
 	}
 
 	SDL_AudioSpec fmt, actFmt;
-	fmt.freq = 48000;
+	fmt.freq = SAMPLE_RATE;
 	fmt.format = AUDIO_S16;
 	fmt.channels = 2;
-	fmt.samples = 1024;
+	fmt.samples = 512;
 	fmt.callback = soundCallback;
 	fmt.userdata = this;
 
 		throw runtime_error(string("failed to open audio device: ") + SDL_GetError());
 	}
 
-	// Save the values
+	// Save the sample rate
 	sampleRate_ = actFmt.freq;
-	frameSamples_ = (FRAME_DURATION * 1e-9) * sampleRate_;
+}
 
-	// Begin playback
-	SDL_PauseAudio(0);
+int GbSound::poll()
+{
+	// Generate the sample
+	double left = 0, right = 0;
+	doChannel(sound1(), 1, left, right);
+	doChannel(sound2(), 2, left, right);
+	doChannel(sound3(), 4, left, right);
+	doChannel(sound4(), 8, left, right);
+		
+	left  += left  *  (gb_.mem_.ioPorts[NR50] & 0x07);
+	right += right * ((gb_.mem_.ioPorts[NR50] & 0x70) >> 4);
+
+	// Add the sample to the buffer to be played back
+	{
+		SdlAudioLock lock;
+		soundBuf_.push_back(static_cast<int16_t>(left));
+		soundBuf_.push_back(static_cast<int16_t>(right));
+
+		SDL_audiostatus status = SDL_GetAudioStatus();
+		if (status != SDL_AUDIO_PLAYING && soundBuf_.size() >= SOUND_BUF_SIZE / 2)
+		{
+			// Start playback if the buffer is half full
+			SDL_PauseAudio(0);
+		}
+		else if (status == SDL_AUDIO_PLAYING && soundBuf_.full())
+		{
+			// Overflow; discard the oldest data so we don't get repeated overflows
+			OutputDebugString(L"overflow\n");
+			soundBuf_.erase_begin(SOUND_BUF_SIZE / 2);
+		}
+	}
+
+	// Return the number of cycles to execute before being called again
+	double realCycles = SAMPLE_CYCLES;
+	if (gb_.mem_.ioPorts[KEY1] & 0x80)
+	{
+		realCycles *= 2;
+	}
+	realCycles += excessCycles_;
+
+	int cycles = static_cast<int>(realCycles);
+	excessCycles_ = realCycles - cycles;
+	return cycles;
 }
 
 void GbSound::record(const fs::path &path)
 {
 	GbSound *parent = static_cast<GbSound *>(userData);
 
-	// Generate the audio
-	int16_t *actStream = reinterpret_cast<int16_t *>(stream);
-	for (int i = 0; i != len / 4; ++i)
-	{
-		double left = 0, right = 0;
-		parent->doChannel(parent->sound1(), 1, left, right);
-		parent->doChannel(parent->sound2(), 2, left, right);
-		parent->doChannel(parent->sound3(), 4, left, right);
-		parent->doChannel(parent->sound4(), 8, left, right);
-		
-		left  += left  *  (parent->gb_.mem_.ioPorts[NR50] & 0x07);
-		right += right * ((parent->gb_.mem_.ioPorts[NR50] & 0x70) >> 4);
-
-		actStream[2 * i] =     static_cast<int16_t>(left);
-		actStream[2 * i + 1] = static_cast<int16_t>(right);
-	}
+	// Copy the sound into the SDL buffer
+	size_t remaining = len / 2;
+	size_t consumed = 0;
+	circular_buffer<int16_t>::array_range range1 = parent->soundBuf_.array_one();
+	if (range1.second)
+ 	{
+		size_t samples = (min)(remaining, range1.second);
+		memcpy(stream, range1.first, samples * 2);
+		remaining -= samples;
+		consumed  += samples;
+
+		if (remaining)
+		{
+			circular_buffer<int16_t>::array_range range2 = parent->soundBuf_.array_two();
+			if (range2.second)
+			{
+				samples = (min)(remaining, range2.second);
+				memcpy(&stream[consumed * 2], range2.first, samples * 2);
+				remaining -= samples;
+				consumed  += samples;
+			}
+		}
+
+		parent->soundBuf_.erase_begin(consumed);
+	}
+
+	if (remaining)
+	{
+		// Underflow; stop playback. It will be restarted when the internal buffer has enough data
+		// to avoid repeated underflows
+		OutputDebugString(L"underflow\n");
+		SDL_PauseAudio(1);
+ 	}
 
 	// Dump the audio if recording
 	if (parent->soundFile_)
 	{
+		int16_t *actStream = reinterpret_cast<int16_t *>(stream);
 		parent->soundFile_->write(actStream, len / 2);
 	}
 }
 {
 	assert(data.sound_3().wave_pattern().length() == 0x10);
 
-	excessSamples_ = data.excess_samples();
+	excessCycles_ = data.excess_cycles();
 
 	const GbSoundData::Sound1 &sound1 = data.sound_1();
 	gbFrequency1_ = sound1.gb_frequency();
 
 void GbSound::save(GbSoundData &data) const
 {
-	data.set_excess_samples(excessSamples_);
+	data.set_excess_cycles(excessCycles_);
 
 	GbSoundData::Sound1 &sound1 = *data.mutable_sound_1();
 	sound1.set_gb_frequency(gbFrequency1_);