70 lines
2.6 KiB
Python
70 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Give a fitted calibration table the reference instrument's touch.
|
|
|
|
The fitter matches the native Salamander layers' spectra, but the layers are
|
|
recorded without their SFZ amp_veltrack (73): played through its SFZ, the
|
|
reference is quieter at pianissimo and louder at fortissimo than its layer
|
|
files. This shifts every partial's pp and ff gain of a key by one constant
|
|
each, so the calibrated level at velocity 28 and 112 (stereo RMS, 0-2 s
|
|
after onset) re velocity 68 equals the reference's: the native layer's level
|
|
times the SFZ velocity gain (1 - 0.73) + 0.73 (v/127)^2. The mf gains and
|
|
every spectral shape are unchanged. Render the result and repeat until the
|
|
shifts are ~0 (two or three rounds).
|
|
|
|
preserve_touch.py CORPUS CALIBRATED_RENDERS TABLE.csv OUT.csv
|
|
"""
|
|
import csv
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import acoustic # noqa: E402
|
|
import fit_voicing # noqa: E402
|
|
|
|
KNOTS = (28, 68, 112)
|
|
VELTRACK = 0.73
|
|
|
|
|
|
def level_db(x, rate):
|
|
onset = acoustic.onset_index(x, rate)
|
|
return 20 * math.log10(float(np.sqrt(np.mean(x[onset:onset + 2 * rate] ** 2))))
|
|
|
|
|
|
def reference_db(regions, key, velocity):
|
|
region = fit_voicing.select_region(regions, key, velocity)
|
|
rate, x = acoustic.read_wav(region.sample)
|
|
gain = (1 - VELTRACK) + VELTRACK * (velocity / 127) ** 2
|
|
return level_db(x, rate) + 20 * math.log10(gain)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 5:
|
|
raise SystemExit(__doc__)
|
|
corpus, calibrated, table, out = sys.argv[1:]
|
|
_, regions = fit_voicing.parse_sfz(corpus)
|
|
rows = list(csv.reader(open(table)))
|
|
if rows[0] != ["key", "partial", "pp_db", "mf_db", "ff_db", "decay_scale"]:
|
|
raise SystemExit("unexpected calibration CSV header")
|
|
shift = {}
|
|
for key in sorted({int(r[0]) for r in rows[1:]}):
|
|
ref = {v: reference_db(regions, key, v) for v in KNOTS}
|
|
cal = {}
|
|
for v in KNOTS:
|
|
rate, x = acoustic.read_wav(Path(calibrated) / f"note_{key:03d}_vel_{v:03d}.wav")
|
|
cal[v] = level_db(x, rate)
|
|
shift[key] = {v: (ref[v] - ref[68]) - (cal[v] - cal[68]) for v in (28, 112)}
|
|
print(f"{key}: pp {shift[key][28]:+.2f} dB, ff {shift[key][112]:+.2f} dB")
|
|
with open(out, "x") as f:
|
|
f.write(",".join(rows[0]) + "\n")
|
|
for r in rows[1:]:
|
|
key = int(r[0])
|
|
pp = min(24.0, max(-36.0, float(r[2]) + shift[key][28]))
|
|
ff = min(24.0, max(-36.0, float(r[4]) + shift[key][112]))
|
|
f.write(f"{r[0]},{r[1]},{pp:.6f},{r[3]},{ff:.6f},{r[5]}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|