Skip to content

Fix clock-coupled buzzer timing and HALT idle-cycle behavior - #40

Open
andressbarajas wants to merge 1 commit into
gyrovorbis:libgimbal-refactorfrom
andressbarajas:fix_clock_halt_audio
Open

Fix clock-coupled buzzer timing and HALT idle-cycle behavior#40
andressbarajas wants to merge 1 commit into
gyrovorbis:libgimbal-refactorfrom
andressbarajas:fix_clock_halt_audio

Conversation

@andressbarajas

Copy link
Copy Markdown
Collaborator

Buzzer PCM timing was hardcoded to quartz /6

  • File: lib/source/hw/evmu_buzzer.c
  • Issue: EvmuBuzzer_setTone() used a fixed quartz /6 cycle time and only rebuilt PCM when T1LR/T1LC changed, so Timer1 audio pitch ignored live OCR clock selection and divider changes.
  • Fix: Derive PCM playback frequency from the current system clock via EvmuClock_systemTicksPerCycle(), keep one Timer1 cycle per PCM sample, and treat OCR writes as tone-affecting so active PWM audio re-buffers immediately when the VMU clock changes.

HALT-mode timers inherited the previous instruction's cycle count

  • File: lib/source/hw/evmu_cpu.c, lib/source/hw/evmu_timers.c
  • Issue: While PCON.HALT was set, EvmuCpu_secs() correctly advanced the halted CPU loop by one system cycle at a time, but EvmuCpu_cycles() still returned the last decoded instruction's cc value. Timer0, Timer1, and the Base Timer therefore ran at an opcode-dependent speed while halted, which skewed BIOS clock updates for software that sleeps in HALT loops such as the CCSakura*.vms titles.
  • Fix: Make EvmuCpu_cycles() report a single cycle whenever the CPU is halted or held so timer advancement matches the halted CPU time step. Also add a conservative HALT-only batching path in the CPU update loop so high-clock titles do not spin one host-side iteration per emulated cycle while waiting for timer/interrupt wakeups. Without batching the game POPMUSIC*.VMS would have intense slowdown during gameplay. This was because the CF oscillator was being used and causing a lot of looping during halts.

- **File:** `lib/source/hw/evmu_buzzer.c`
- **Issue:** `EvmuBuzzer_setTone()` used a fixed quartz `/6` cycle time and only rebuilt PCM when `T1LR/T1LC` changed, so Timer1 audio pitch ignored live `OCR` clock selection and divider changes.
- **Fix:** Derive PCM playback frequency from the current system clock via `EvmuClock_systemTicksPerCycle()`, keep one Timer1 cycle per PCM sample, and treat `OCR` writes as tone-affecting so active PWM audio re-buffers immediately when the VMU clock changes.

HALT-mode timers inherited the previous instruction's cycle count
- **File:** `lib/source/hw/evmu_cpu.c`, `lib/source/hw/evmu_timers.c`
- **Issue:** While `PCON.HALT` was set, `EvmuCpu_secs()` correctly advanced the halted CPU loop by one system cycle at a time, but `EvmuCpu_cycles()` still returned the last decoded instruction's `cc` value. Timer0, Timer1, and the Base Timer therefore ran at an opcode-dependent speed while halted, which skewed BIOS clock updates for software that sleeps in `HALT` loops such as the `CCSakura*.vms` titles.
- **Fix:** Make `EvmuCpu_cycles()` report a single cycle whenever the CPU is halted or held so timer advancement matches the halted CPU time step. Also add a conservative HALT-only batching path in the CPU update loop so high-clock titles do not spin one host-side iteration per emulated cycle while waiting for timer/interrupt wakeups.
@andressbarajas

Copy link
Copy Markdown
Collaborator Author

Changing the buzzer behavior required gui frontend changes. I had AI generate a summary of those changes:

The core contract changed from:

“buzzer PCM is effectively always at one fixed rate”
to:

“buzzer PCM is a 1-cycle waveform with a dynamic source sample rate”
So any frontend/audio backend that uses this core should treat the buzzer like a tiny looping waveform plus metadata.

A good shareable pattern is:

// Open host audio once at a fixed output rate.
const int HOST_RATE = 48000;
audio_device = open_audio_device(rate=HOST_RATE, format=U8, channels=1);

// Frontend-owned playback state.
struct AudioState {
uint8_t* cycle_buffer; // one waveform cycle
size_t cycle_len; // samples in one cycle
int src_rate; // EvmuBuzzer_pcmFrequency()
int out_rate; // host/device rate
double pos; // fractional read position in cycle
double step; // src_rate / out_rate
bool active;
};

AudioState s = {0};
s.out_rate = HOST_RATE;

void refresh_step() {
if (s.active && s.src_rate > 0 && s.cycle_len > 0) {
s.step = (double)s.src_rate / (double)s.out_rate;
} else {
s.step = 0.0;
}
}

// Call this after each emulator update, or whenever the core says pcmChanged.
void sync_buzzer_from_core(EvmuDevice* dev) {
if (!dev->pBuzzer->pcmChanged)
return;

dev->pBuzzer->pcmChanged = 0;

lock_audio();

if (EvmuBuzzer_isActive(dev->pBuzzer) &&
    EvmuBuzzer_pcmSamples(dev->pBuzzer) > 0) {

    // Either point at the core buffer, or copy it into frontend-owned memory.
    // Copying is safer if threading/lifetime is unclear.
    s.cycle_buffer = EvmuBuzzer_pcmBuffer(dev->pBuzzer);
    s.cycle_len    = EvmuBuzzer_pcmSamples(dev->pBuzzer);
    s.src_rate     = (int)EvmuBuzzer_pcmFrequency(dev->pBuzzer);
    s.pos          = 0.0;
    s.active       = true;
} else {
    s.src_rate = 0;
    s.cycle_len = 0;
    s.pos = 0.0;
    s.active = false;
}

refresh_step();
unlock_audio();

}

// Audio callback fills host stream at fixed HOST_RATE.
void audio_callback(uint8_t* out, int out_len) {
if (!s.active || !s.cycle_buffer || s.cycle_len == 0 || s.step <= 0.0) {
fill_with_silence(out, out_len); // e.g. 0x7f for unsigned 8-bit
return;
}

for (int i = 0; i < out_len; ++i) {
    size_t idx = (size_t)s.pos % s.cycle_len;
    out[i] = s.cycle_buffer[idx];

    s.pos += s.step;
    while (s.pos >= (double)s.cycle_len) {
        s.pos -= (double)s.cycle_len;
    }
}

}
The important frontend rules are:

Open the host audio device at one fixed output rate.
Do not assume EvmuBuzzer_pcmFrequency() is constant.
When pcmChanged fires, refresh:
pcmBuffer
pcmSamples
pcmFrequency
isActive
Resample from src_rate to your host output rate in the callback or mixer.
If audio runs on another thread, lock or copy the cycle buffer before swapping state.
If they want the shortest possible summary to paste into another project:

libevmu buzzer output is now:

  • a looping 1-cycle PCM buffer
  • plus a dynamic source sample rate

Frontend must:

  • keep a fixed host audio rate
  • read EvmuBuzzer_pcmFrequency() whenever pcmChanged is set
  • resample/step through the cycle buffer at src_rate / out_rate
  • stop assuming the buzzer is always quartz/6
    One non-audio adjustment also mattered for us: the frontend build had to relink whenever the core library changed, otherwise it could silently keep using a stale archive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant