[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Cycle By Cycle Emulator



On Thu, 02 Oct 2003 22:47:11 +0000, Bryan Parkoff wrote:

>     Now -- about sound and Disk II.  Michael mentioned that it has nothing
> to do with MPU's cycle that will work with sound and Disk II.  Let says, MPU
> operates at 1 MHz while sound operates at 28Hz.  Would 28Hz be stealing 1
> MHz's time?  It is very difficult to make the frequency to match the sound
> 28Hz.  MPU instructions have to spend 1 MHz to make sound works.

Let's assume you meant to say 28kHz instead of 28Hz--
The value 28kHz is a somewhat arbitrarily chosen frequency. You can choose
to output your emulated sound at any frequency supported by your system's
sound API. Real Apple II sound can have very high frequencies since you
can flip the speaker bit every few cycles, but that's not something you
really have to worry about when choosing an output frequency for your
emulator. All you have to worry about is mapping the Apple II speaker
state into an output buffer at your chosen frequency.

I suspect most Apple II emulators use cycle-based virtual time. By that,
I mean that the count of executed cycles is treated as the base of time
by everything else in the emulator. By counting cycles, you know when
everything else in the emulator has to happen, whether it's when the
speaker flips, or when the disk II has finished reading another byte
(or bit) from disk.

So, what you do is use that clock that measures virtual time. Assume that
your chosen output frequency is F. You then need to generate one
sample every 1/F seconds of *virtual* time. You can do this easily
by figuring out how many cycles there are in 1/F seconds, and sampling
the state of the speaker every time that many emulated CPU cycles have
occurred. The task of actually playing back the generated buffer full
of samples at the frequency F can then be handled by the sound API.

Since my code to do this is reasonably short, I'll just show you how
I do it:

First some defines. SOUND_INTERVAL ends up being the number
of cpu cycles per speaker sample (times 100 so that I can
keep some of the fraction)

  #define CPU_HZ         1020484  // adjusted for stretched cycles
  #define SOUND_HZ       22050    // my chosen output frequency
  #define SOUND_INTERVAL (unsigned int)(100 * CPU_HZ / SOUND_HZ)

Then some shared variables. The first group of variables are used to
implement a buffer for storing speaker samples. Every new sample goes
at sample_idx, which is then incremented. sample_idx advances ahead of
sample_pos as the buffer is filled. When it is time to play part of
the buffer, the part between sample_pos and sample_idx is played,
then sample_pos is set equal to sample_idx.

  #define SOUND_BUFLEN   524288
  unsigned char sample_buf[SOUND_BUFLEN];
  unsigned int sample_idx = 0; // where the next sample should go
  unsigned int sample_pos = 0; // indexes "new" samples

  unsigned char speaker_state = 0;

  unsigned long cycle_clock = 0;  // counts cycles

This is what happens when an Apple II program writes to the
speaker locations at $C03x. This is writing- the code for
reading is identical.

  void w_c0 (unsigned short w_addr, unsigned char w_byte)
  {
    switch (w_addr)
      {
      ...
      case 0xC030: case 0xC031: case 0xC032: case 0xC033:
      case 0xC034: case 0xC035: case 0xC036: case 0xC037:
      case 0xC038: case 0xC039: case 0xC03A: case 0xC03B:
      case 0xC03C: case 0xC03D: case 0xC03E: case 0xC03F:
        if (speaker_state)
          speaker_state = 0x00;
        else
          speaker_state = 0xFF;
        break;
      ...
      }
  }

Lastly, the speaker must be sampled at the appropriate times.
This is a slightly simplified snippet from the cpu core code:

      /* fetch, decode, and execute and instruction */
      opcode = READ(emPC++);
      EXECUTE(opcode);        // cycle_clock is updated in EXECUTE()

      /* update speaker */
      {
        static unsigned int sample_clock = 0;
        static unsigned long last_cpu_cycle_clock = 0;

        int cycles_elapsed;

        cycles_elapsed = cycle_clock - last_cpu_cycle_clock;
        sample_clock += (100 * cycles_elapsed);

        if (sample_clock >= SOUND_INTERVAL)
          {
            sample_clock -= SOUND_INTERVAL;
            sample_buf[sample_idx++] = speaker_state;

            /* wrap around if at end of buffer */
            if (sample_idx == SOUND_BUFLEN) sample_idx = 0;
          }
      }

Finally, the samples stored in sample_buf[] must be played using
the sound API. My code uses SDL, which works by calling a user-
specified callback function whenever it needs more sound data.
My callback copies the samples from sample_buf[] into SDL's
buffer. Then SDL plays the sound. Here's my callback:

  #define AUDIO_BUFLEN   1024
  #define SILENCE_LENGTH (SOUND_HZ / 20)
  /*
     more than this amount of silence will be removed. since
     samples are generated at virtual 22050 Hz and played
     back at real 22050 Hz, and the virtual clock ticks
     faster than the real one, sample generation outpaces
     sample output. I try to work around this by eliminating
     periods of silence in the generated samples. This allows
     things like arcade game sounds to be correctly played
     and then 'catching up' by eliminating most of the silence
     between them. This strategy works well but does fall down
     when some program plays sound continuously. In that case,
     the only solution is to throttle the emulator's speed.
     Note that this is also the reason for the large (512kB)
     sample buffer - it lets me buffer lots of samples for
     playback later when the Apple II program is actually
     finished flipping the speaker.
   */

  static unsigned char last_sample = 0;

  void fill_apple_audio (void * udata, Uint8 * stream, int buffer_len)
  {
    int stream_idx;
    unsigned int fill_len;
    unsigned int available_len;

    stream_idx = 0; // index into buffer 'Uint8 * stream'

  remove_silence:

    /* figure out how many samples I have to give */

    if (sample_idx >= sample_pos)
      available_len = sample_idx - sample_pos;
    else
      available_len = SOUND_BUFLEN - sample_pos + sample_idx;

    if (available_len <= buffer_len)
      {
        /*
        I have fewer samples than SDL wants, so fill the
        extra space with whatever the last sample was from
        from the last time this function was called
        */

        fill_len = buffer_len - available_len;
        while (fill_len--)
          stream[stream_idx++] = last_sample;

        buffer_len = available_len;
      }
    else if (available_len > SILENCE_LENGTH)
      {
	/* look for removable stretches of silence */

        int idx;
        unsigned int scan_pos;

        scan_pos = sample_pos;

        idx = SILENCE_LENGTH;
        while (idx)
          {
            --idx;
            if (sample_buf[scan_pos] != last_sample) break; // not silent
            if (++scan_pos == SOUND_BUFLEN) scan_pos = 0; // wrap around
          }

        if (idx == 0)
          {
            /* remove the silence by starting over */
            sample_pos = scan_pos;
            goto remove_silence;
          }
      }

    /* now actually fill SDL's buffer */
    while (buffer_len)
      {
        --buffer_len;
        stream[stream_idx++] = last_sample = sample_buf[sample_pos];
        if (++sample_pos == SOUND_BUFLEN) sample_pos = 0; // wrap around
      }
  }

> Now, compare to Disk II.  Disk II must use the same 1 MHz that Disk II must
> spend 36 cycles to create one valid byte through LDA C08x,X loop.  If MPU's
> 1 MHz is not fixed, 36 cycles will be wrong in the wrong time.

Disk II emulation is a lot more complex than speaker emulation, but the
timing can be handled entirely analagously