Skip to content

feat(audio): add --ntsc_audio_rate for NTSC-locked PCM output#1046

Merged
happycube merged 1 commit into
happycube:mainfrom
mpownby:feat/ntsc-locked-audio-rate
Jun 13, 2026
Merged

feat(audio): add --ntsc_audio_rate for NTSC-locked PCM output#1046
happycube merged 1 commit into
happycube:mainfrom
mpownby:feat/ntsc-locked-audio-rate

Conversation

@mpownby

@mpownby mpownby commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Full disclosure: Claude did all the work; I reviewed code changes, they looked minimal/safe. We (Claude and me!) did manual testing to confirm that the .pcm output file size changes as expected when this flag is passed.

Add an opt-in --ntsc_audio_rate flag that outputs analog audio locked to NTSC line timing (2.8 samples/line = 1470 samples/frame, ~44055.944 Hz) instead of the default 44100 Hz, which drifts at 1471.47 samples/frame against NTSC video.

This reuses the existing negative-freq convention in downscale_audio (a negative value is a multiple of the horizontal line frequency). The multiple 2.8 is fractional, so two plumbing fixes were needed:

  • core.py: stop truncating analog_audio with int() for negative values, so the fractional HSYNC multiple survives.
  • core.py: resolve the negative rate to its real nominal value in build_json so metadata reports the true sample rate (44055.944...) rather than the -2.8 sentinel.

PAL is already frame-locked at 44100 Hz (1764 samples/frame), so the flag is NTSC-only and warns + no-ops on PAL. Default behavior is unchanged for backward compatibility.

Add tests/test_audio_rate.py covering the negative-freq mapping, and document the new flag in docs/user-guide/command.md.

Checklist

  • I have searched the open pull requests to confirm this change has not already been submitted.
  • My branch is up to date with the target branch.
  • I have tested my changes and all existing tests pass.
  • I have updated documentation where necessary.
  • My code follows the project's coding standards (see CONTRIBUTING.md).

Description

Adds an opt-in --ntsc_audio_rate flag that outputs analog audio locked to NTSC
line timing — exactly 2.8 samples/line (1470 samples/frame, 735/field), i.e.
~44055.944 Hz — instead of the default 44100 Hz. Off by default; existing
behavior is unchanged.

Motivation

Primary motivation is to improve disc audio stacking.

The default 44100 Hz PCM rate does not divide evenly into NTSC frame timing:
44100 / (30000/1001 fps) ≈ 1471.47 samples/frame, so the audio sample grid
slowly drifts relative to the video. 44055.944 Hz = 2.8 × the NTSC horizontal
line frequency (4.5 MHz / 286 = 15734.2657 Hz), giving exactly 1470 samples per
frame and perfect, drift-free A/V alignment. PAL already divides evenly
(44100 / 25 fps = 1764 samples/frame), so it needs no change.

Related Issues

N/A

Changes Made

  • Add --ntsc_audio_rate CLI flag (default off, backward compatible). NTSC-only;
    warns and no-ops on PAL.
  • Reuse the existing negative-freq convention in downscale_audio (a negative
    value is a multiple of the horizontal line frequency) by mapping the flag to
    freq = -2.8.
  • core.py: preserve fractional negative rates (no longer int()-truncate
    analog_audio), so -2.8 survives end-to-end.
  • core.py build_json: report the resolved nominal sample rate
    (44055.944055944055) in the .tbc.json instead of the -2.8 sentinel.
  • Add tests/test_audio_rate.py and document the flag in
    docs/user-guide/command.md.

Testing

  • All existing tests pass (pytest) — 5 passed.
  • Tested manually with: a CAV NTSC capture (-s 2000 -l 30). Default →
    44144 stereo samples (1471.47/frame); --ntsc_audio_rate → exactly 44100
    stereo samples (1470.000/frame). JSON sampleRate reported 44100 vs
    44055.944055944055.
  • New tests added for: the negative-freq → samples-per-line mapping
    (-2.8 → 1470/frame, -2.8 → 44055.944 Hz, default 44100 → 1471/frame).

Screenshots (if applicable)

N/A — no UI changes.

Additional Notes

  • The .tbc.json sampleRate can now be a non-integer float when this flag is
    used. Downstream ld-decode-tools should accept a float sampleRate (handled
    separately).
  • PAL output is intentionally unaffected.

Add an opt-in --ntsc_audio_rate flag that outputs analog audio locked
to NTSC line timing (2.8 samples/line = 1470 samples/frame, ~44055.944
Hz) instead of the default 44100 Hz, which drifts at 1471.47
samples/frame against NTSC video.

This reuses the existing negative-freq convention in downscale_audio
(a negative value is a multiple of the horizontal line frequency).
The multiple 2.8 is fractional, so two plumbing fixes were needed:

- core.py: stop truncating analog_audio with int() for negative values,
  so the fractional HSYNC multiple survives.
- core.py: resolve the negative rate to its real nominal value in
  build_json so metadata reports the true sample rate (44055.944...)
  rather than the -2.8 sentinel.

PAL is already frame-locked at 44100 Hz (1764 samples/frame), so the
flag is NTSC-only and warns + no-ops on PAL. Default behavior is
unchanged for backward compatibility.

Add tests/test_audio_rate.py covering the negative-freq mapping, and
document the new flag in docs/user-guide/command.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@happycube
happycube merged commit 75f9c0a into happycube:main Jun 13, 2026
6 checks passed
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.

2 participants