Add reference_pitch to Score and playback pipeline

Score(reference_pitch=415.0, temperament="meantone") renders an
entire piece at Baroque pitch with historical tuning. Flows through
to all .pitch() calls in both normal and legato renderers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 00:32:11 -04:00
parent 9850a8016e
commit 9887b59cfb
2 changed files with 13 additions and 9 deletions
+11 -8
View File
@@ -1971,7 +1971,8 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
filter_attack=0.01, filter_decay=0.3,
filter_sustain=0.0, filter_amount=0.0,
vel_to_filter=0.0, filter_q=0.707,
synth_kwargs=None, temperament="equal"):
synth_kwargs=None, temperament="equal",
reference_pitch=440.0):
"""Render a list of Notes into an existing buffer at the correct positions."""
import random as _rnd
@@ -2000,9 +2001,9 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
if n_samples > 0 and start >= 0:
# Get pitches
if hasattr(note.tone, 'tones'):
pitches = [t.pitch(temperament=temperament) for t in note.tone.tones]
pitches = [t.pitch(temperament=temperament, reference_pitch=reference_pitch) for t in note.tone.tones]
else:
pitches = [note.tone.pitch(temperament=temperament)]
pitches = [note.tone.pitch(temperament=temperament, reference_pitch=reference_pitch)]
# Render oscillators (pass synth_kwargs for FM etc.)
waves = [synth_fn(hz, n_samples=n_samples, **_skw)
for hz in pitches]
@@ -2088,7 +2089,7 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
def _render_legato_to_buf(notes, buf, samples_per_beat, total_samples,
synth_fn, envelope_tuple, volume, bpm,
glide_time=0.0, swing=0.0, tempo_map=None,
temperament="equal"):
temperament="equal", reference_pitch=440.0):
"""Render notes as one continuous waveform with pitch glide.
Instead of rendering each note separately with its own envelope,
@@ -2118,9 +2119,9 @@ def _render_legato_to_buf(notes, buf, samples_per_beat, total_samples,
vel = getattr(note, 'velocity', 100)
if note.tone is not None:
if hasattr(note.tone, 'tones'):
hz = note.tone.tones[0].pitch(temperament=temperament)
hz = note.tone.tones[0].pitch(temperament=temperament, reference_pitch=reference_pitch)
else:
hz = note.tone.pitch(temperament=temperament)
hz = note.tone.pitch(temperament=temperament, reference_pitch=reference_pitch)
events.append((start, end, hz, vel))
else:
events.append((start, end, 0, vel)) # rest
@@ -2239,13 +2240,14 @@ def render_score(score):
synth_kwargs["mod_ratio"] = part.fm_ratio
synth_kwargs["mod_index"] = part.fm_index
_temperament = getattr(score, 'temperament', 'equal')
_ref_pitch = getattr(score, 'reference_pitch', 440.0)
if part.legato:
_render_legato_to_buf(
part.notes, part_buf, samples_per_beat, total_samples,
synth_fn, env_tuple, part.volume, score.bpm,
glide_time=part.glide, swing=effective_swing,
tempo_map=tempo_map if has_tempo_changes else None,
temperament=_temperament)
temperament=_temperament, reference_pitch=_ref_pitch)
else:
_render_notes_to_buf(
part.notes, part_buf, samples_per_beat, total_samples,
@@ -2265,7 +2267,8 @@ def render_score(score):
vel_to_filter=part.vel_to_filter,
filter_q=part.lowpass_q,
synth_kwargs=synth_kwargs,
temperament=_temperament)
temperament=_temperament,
reference_pitch=_ref_pitch)
# Apply effects — segmented if automation exists
auto_points = part._get_automation_points()
+2 -1
View File
@@ -2074,7 +2074,7 @@ class Score:
def __init__(self, time_signature="4/4", bpm=120, swing: float = 0.0,
drum_humanize: float = 0.15, system: str = "western",
temperament: str = "equal"):
temperament: str = "equal", reference_pitch: float = 440.0):
if isinstance(time_signature, str):
self.time_signature = TimeSignature.from_string(time_signature)
else:
@@ -2083,6 +2083,7 @@ class Score:
self.swing = swing
self.system = system
self.temperament = temperament
self.reference_pitch = reference_pitch
self._drum_humanize = drum_humanize
self.notes: list[Note] = []
self.parts: dict[str, Part] = {}