File size: 2,410 Bytes
4aac74e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
"""Did the Ableton renders pass through a limiter / normaliser?

This decides whether a Python linear crossfade can stand in for an Ableton
render at a lower send level.

If the chain is purely linear (sum of dry + reverb return, no dynamics), then
  0.7*dry + 0.3*(dry+reverb) = dry + 0.3*reverb
is EXACTLY an Ableton render at 30% send, and Python is equivalent.

A limiter breaks that. It is level-dependent and non-linear, so a render at 30%
send hits it differently than the 100% render did -- and crossfading afterwards
cannot reproduce that. The tell is a hard ceiling: many files peaking at the
same value rather than scattered.
"""
import wave
from collections import Counter
from pathlib import Path

import numpy as np

DATA = Path("/workspace/Demos/data/acoustic-space-ableton/audio")


def peak_db(p):
    with wave.open(str(p), "rb") as w:
        n, ch, sw = w.getnframes(), w.getnchannels(), w.getsampwidth()
        raw = w.readframes(n)
    if sw == 3:  # 24-bit
        a = np.frombuffer(raw, dtype=np.uint8).reshape(-1, 3).astype(np.int32)
        x = (a[:, 0] | (a[:, 1] << 8) | (a[:, 2] << 16))
        x = np.where(x & 0x800000, x - 0x1000000, x).astype(float) / 8388608.0
    else:
        x = np.frombuffer(raw, dtype=np.int16).astype(float) / 32768.0
    if x.size == 0:
        return None
    return 20 * np.log10(np.abs(x).max() + 1e-12)


for label in ("references", "targets"):
    peaks = []
    for p in sorted((DATA / label).glob("*.wav")):
        d = peak_db(p)
        if d is not None:
            peaks.append(round(d, 2))
    if not peaks:
        continue
    print(f"=== {label}: {len(peaks)} files ===")
    print(f"  peak range: {min(peaks):.2f} .. {max(peaks):.2f} dBFS")
    common = Counter(peaks).most_common(5)
    print("  most common peak values:")
    for v, c in common:
        bar = "#" * min(c, 40)
        print(f"    {v:>7.2f} dBFS  x{c:<3} {bar}")
    ceiling = [p for p in peaks if p > -0.5]
    print(f"  files peaking above -0.5 dBFS: {len(ceiling)} of {len(peaks)} "
          f"({100.0*len(ceiling)/len(peaks):.0f}%)")
    print()

print("A tight cluster at one value just below 0 => a ceiling was applied")
print("(limiter or normaliser), so the render chain is NOT purely linear and an")
print("Ableton render at lower send will differ from a Python crossfade.")
print("A broad scatter => linear sum, and Python is mathematically equivalent.")