声音也可以写成数学。 Sound can be written in mathematics too.

弦长决定音高,共振峰决定元音,几毫秒的错位把一段噪声变成拍手。 把这些关系写成函数,代码就能直接算出波形。下面的曲子没有使用采样包,它们都从这些公式开始。 String length sets pitch, resonances shape a vowel, and a few milliseconds of separation turn noise into a clap. Written as functions, those relationships produce the waveform directly in code. The pieces below use no sample packs; each one begins with these equations.

曲目Tracks 从公式算出波形,再离线渲染成完整曲子;频谱图也可以直接拖动 waveforms calculated from equations, then rendered offline; drag the spectrogram to seek
C 大调前奏曲Prelude in C Major, BWV 846 Prelude in C Major, BWV 846 60 BPM · C 大调C majorC 大调 / C major · 2:25

按公版原谱重建完整的 35 小节。和声始终被拆成流动的十六分音符,直到最后两小节才停下来收束。音符来自乐谱,钢琴音色和录音则由代码重新生成。 All 35 bars are rebuilt from the public-domain score. The harmony keeps moving as sixteenth notes until the final two bars bring it to rest. The notes come from the score; the piano and recording are generated anew in code.

C 大调前奏曲 / Prelude in C Major, BWV 846 的频谱图
0:00 / 2:25 开篇 · 展开 · 属音持续 · 终止 opening · expansion · dominant pedal · cadence 开篇 · 展开 · 属音持续 · 终止 自然动态natural dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
致爱丽丝Für Elise, WoO 59 Für Elise, WoO 59 72 BPM · A 小调A minorA 小调 / A minor · 2:19

按公版原谱重建完整的 A–B–A–C–A 回旋结构,并不只截取开头主题。A 小调主题两次离开,也两次回来;钢琴音色和录音由代码重新生成。 The complete A–B–A–C–A rondo is rebuilt from the public-domain score, not just the opening theme. The A-minor refrain leaves twice and returns twice; the piano and recording are generated anew in code.

致爱丽丝 / Für Elise, WoO 59 的频谱图
0:00 / 2:19 A 主题 · B 段 · A 再现 · C 段 · A 尾声 A refrain · B turn · A return · C storm · A coda A 主题 · B 段 · A 再现 · C 段 · A 尾声 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
郊游Day Trip Day Trip 96 BPM · C 大调C majorC 大调 / C major · 1:44

C 大调钢琴小品,左手一路走分解和弦,没有鼓可以躲。写它是为了检验引擎的钢琴:每个音都露在外面。 A C major piano piece with a left hand that never stops walking and no drums to hide behind. Written to test the engine’s piano, where every note is exposed.

郊游 / Day Trip 的频谱图
0:00 / 1:44 前奏 · 主题 · 加弦乐 · 桥段 · 再现 · 尾声 intro · a1 · a2 · b · a3 · outro 前奏 · 主题 · 加弦乐 · 桥段 · 再现 · 尾声 自然动态natural dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
D 大调卡农Canon in D Canon in D 72 BPM · D 大调D majorD 大调 / D major · 1:58

三把小提琴演奏同一条旋律,彼此相差两小节。织体自己变厚,没有人写过更厚的声部。 Three violins playing one line two bars apart. The texture thickens without anybody writing a thicker part.

D 大调卡农 / Canon in D 的频谱图
0:00 / 1:58 固定低音 · 第一声部 · 第二声部 · 三声部 · 收束 ground · first voice · second voice · three voices · close 固定低音 · 第一声部 · 第二声部 · 三声部 · 收束 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
沿海公路Coast Road Coast Road 112 BPM · D 大调 / B 小调D major / B minorD 大调 / B 小调 / D major / B minor · 1:12

明亮钢琴、拨弦和木琴般的高音,用 3+3+2 的切分向前推。主题再次出现时换了落点,也换了结尾。 Bright piano, plucked strings, and bell-like highlights ride a 3+3+2 pulse. The returning theme changes both its landing points and its ending.

沿海公路 / Coast Road 的频谱图
0:00 / 1:12 轮廓 · 上路 · 晴空 · 回归 · 余晖 sketch · road · open sky · return · last light 轮廓 · 上路 · 晴空 · 回归 · 余晖 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
风过窗边Wind Past the Window Wind Past the Window 72 BPM · C 大调 / A 小调C major / A minorC 大调 / A 小调 / C major / A minor · 1:24

钢琴与弦乐。从八小节主题出发,第二次出现时打开音域,最后只留下最初的两个音程。 Piano and strings built from an eight-bar theme. Its second statement opens the register; the ending keeps only the first two intervals.

风过窗边 / Wind Past the Window 的频谱图
0:00 / 1:24 开场 · 主题 · 展开 · 回归 opening · first theme · lift · return 开场 · 主题 · 展开 · 回归 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
廊下Verandah Verandah 128 BPM · F 大调F majorF 大调 / F major · 1:34

波萨诺瓦。尼龙吉他落在拍间,贝斯落在拍上,萨克斯不着急。合成库最难的风格,因为它安静。 Bossa nova: nylon guitar between the beats, bass on them, and a saxophone taking its time. The hardest style for a synthesis library, because it is quiet.

廊下 / Verandah 的频谱图
0:00 / 1:34 前奏 · 主题 · 桥段 · 再现 · 钢琴独奏 · 末次 · 尾声 intro · a1 · b · a2 · solo · a3 · outro 前奏 · 主题 · 桥段 · 再现 · 钢琴独奏 · 末次 · 尾声 自然动态natural dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
查拉图斯特拉如是说 · 日出Also sprach Zarathustra — Sunrise Also sprach Zarathustra — Sunrise 66 BPM · C 大调C majorC 大调 / C major · 1:41

一个永不移动的低音 C,小号在它上面爬三次,管风琴最后到。什么都不转调,所以唯一能制造运动的是规模。 One low C that never moves, a trumpet climbing over it three times, and the organ arriving last. Nothing modulates, so the only thing that can create motion is scale.

查拉图斯特拉如是说 · 日出 / Also sprach Zarathustra — Sunrise 的频谱图
0:00 / 1:41 持续低音 · 第一次 · 第二次 · 第三次 · 余响 pedal · first · second · third · hold 持续低音 · 第一次 · 第二次 · 第三次 · 余响 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
圣徒进行曲When the Saints Go Marching In When the Saints Go Marching In 104 BPM · C 大调C majorC 大调 / C major · 1:54

新奥尔良街头铜管。出力最多的不是任何一个音色,是 0.56 的摇摆——直着弹这就是首赞美诗。 A New Orleans street band. The hardest-working thing in the file is a swing of 0.56 — played straight, this is a hymn.

圣徒进行曲 / When the Saints Go Marching In 的频谱图
0:00 / 1:54 前奏 · 主题 · 间奏 · 三度和声 · 尾声 intro · head · break · head2 · outro 前奏 · 主题 · 间奏 · 三度和声 · 尾声 自然动态natural dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
青花Porcelain Blue Porcelain Blue 84 BPM · D 大调五声音阶 / B 小调D major pentatonic / B minorD 大调五声音阶 / B 小调 / D major pentatonic / B minor · 1:23

国风 R&B,古筝和笛子落在十六分摇摆上。 Chinese-style R&B, with guzheng and flute over a swung sixteenth-note groove.

青花 / Porcelain Blue 的频谱图
0:00 / 1:23 前奏 · 主歌 · 过渡 · 副歌 · 尾声 intro · verse · pre · chorus · outro 前奏 · 主歌 · 过渡 · 副歌 · 尾声 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
霓虹时刻Neon Hour Neon Hour 104 BPM · A 大调A majorA 大调 / A major · 1:45

舞曲流行,锯齿合成器、人声钩子和明显的侧链起伏。 Dance pop with saw synths, a vocal hook, and an audible sidechain pulse.

霓虹时刻 / Neon Hour 的频谱图
0:00 / 1:45 前奏 · 主歌一 · 过渡一 · 副歌一 · 主歌二 · 过渡二 · 副歌二 · 尾声 intro · verse1 · pre1 · chorus1 · verse2 · pre2 · chorus2 · outro 前奏 · 主歌一 · 过渡一 · 副歌一 · 主歌二 · 过渡二 · 副歌二 · 尾声 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
欢乐颂Ode to Joy Ode to Joy 126 BPM · D 大调D majorD 大调 / D major · 2:05

贝多芬的赞美诗放到 126 BPM 的舞池上。这调子方得没有一个切分音,所以让周围动——你听到的律动大半是 sidechain。 Beethoven’s hymn on a dance floor at 126 BPM. The tune is too square to syncopate, so the space around it moves instead — most of what you hear as groove is the sidechain.

欢乐颂 / Ode to Joy 的频谱图
0:00 / 2:05 前奏 · 铺垫 · 主段 · 间歇 · 推进 · 高潮 · 尾声 intro · build · drop · break · rise · drop2 · outro 前奏 · 铺垫 · 主段 · 间歇 · 推进 · 高潮 · 尾声 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
卡农 · 驱动版Canon in D, driven Canon in D, driven 118 BPM · D 大调D majorD 大调 / D major · 1:49

同一条固定低音配上强拍鼓组。那条著名旋律仍然在追自己,只是这次晚两小节的是失真吉他。 The same ground bass under a backbeat. The famous line still chases itself two bars behind, this time on a distorted guitar.

卡农 · 驱动版 / Canon in D, driven 的频谱图
0:00 / 1:49 前奏 · 铺垫 · 主歌 · 副歌 · 间歇 · 副歌二 · 尾声 intro · build · verse · chorus · break · chorus2 · outro 前奏 · 铺垫 · 主歌 · 副歌 · 间歇 · 副歌二 · 尾声 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗
起飞Launch Launch 120 BPM · A 小调A minorA 小调 / A minor · 0:57

一段留白很多的配乐,节拍之间有足够空间。 A sparse score with plenty of room between the beats.

起飞 / Launch 的频谱图
0:00 / 0:57 开场 · 推进 · 标题 · 蒙太奇 · 尾声 open · build · title · montage · outro 开场 · 推进 · 标题 · 蒙太奇 · 尾声 宽动态wide dynamics 在 GitHub 查看 HitoneHitone on GitHub ↗

这些声音从哪里来 Where the sounds come from

下面 7 段源码,每一段都是某件乐器之所以是它自己的那个物理事实。 7 excerpts, each one the physical fact that makes an instrument itself rather than a preset of another one.

把滤波器写成闭式The filters, in closed form The filters, in closed form

core.py 第 1–13 行lines 1–13
"""Sample rate, envelopes, and every filter in the library.

The one idea worth knowing before reading anything else: **no filter in Hitone is
iterated per sample.** A one-pole lowpass and a two-pole resonator both have impulse
responses you can write down in closed form, so we write them down and convolve.

    one-pole:   h[n] = (1-a) a^n,                 a = e^(-2*pi*f/SR)
    resonator:  h[n] = r^n sin((n+1)th) / sin th, r = e^(-pi*(f/q)/SR), th = 2*pi*f/SR

Both die to -80 dB inside a few hundred samples at musical cutoffs, so the truncated
FIR is exact to well below the noise floor of a 16-bit file. This is the single reason
a full arrangement renders in seconds instead of minutes; the first version of this
library used a python `for` loop and a 100-second track took four minutes.

一阶低通和二阶共振器的冲激响应都能直接写出来。响应截到 −80 dB 以下再交给卷积函数,Python 就不用逐个采样点迭代。误差低于 16 位音频的噪声底,105 秒的曲子 8 秒就能渲完;最早那版要跑四分钟。 The impulse responses of a one-pole lowpass and a two-pole resonator can both be written directly. Truncating them below −80 dB before convolution removes the Python loop over individual samples. The error stays below the noise floor of 16-bit audio, and a 105-second track renders in 8 seconds. The first version took four minutes.

一根弦,是一条延迟线A string is a delay line A string is a delay line

osc.py 第 130–140 行lines 130–140
p = max(2, int(round(SR / freq)))
st = np.random.RandomState(int(freq * 13) % 99991)
buf = lp(st.randn(p), 400 + 7000 * bright)
buf = buf - buf.mean()
out = np.empty(n)
i = 0
for k in range(n):
    j = (i + 1) % p
    out[k] = buf[i]
    buf[i] = ((1 - damp) * buf[i] + damp * buf[j]) * decay
    i = j

先用噪声填满一条恰好一个周期长的延迟线,每循环一圈,就把相邻采样平均一次。这个平均动作就是反馈回路里的低通,所以高频比低频掉得快。拨弦的质感来自这种不均匀的衰减,普通包络做不到,因为它会把所有频率一起缩小。 Start with a delay line exactly one period long and fill it with noise. On every pass, average neighbouring samples. That average is the lowpass in the feedback loop, so high frequencies die faster than low ones. A plucked string gets its character from this uneven decay; an ordinary envelope scales every frequency together.

一个元音,是三个共振峰A vowel is three resonances A vowel is three resonances

voice.py 第 19–43 行lines 19–43
# F1/F2/F3 in Hz with relative level. Measured vowel resonances, not tuned by ear.
VOWELS = {
    'aa': [(730, 1.00), (1090, 0.50), (2440, 0.22)],   # father
    'eh': [(530, 1.00), (1840, 0.45), (2480, 0.30)],   # bed
    'ee': [(270, 1.00), (2290, 0.40), (3010, 0.28)],   # see
    'oh': [(570, 1.00), (840, 0.42), (2410, 0.14)],    # go
    'oo': [(300, 1.00), (870, 0.28), (2240, 0.10)],    # boot
    'uh': [(640, 1.00), (1190, 0.48), (2390, 0.20)],   # but
}


def voice(freq, n, vowel='aa', glide=0.0, vib=5.2, breath=0.05, q=11.0):
    """Sung vowel. Returns a mono buffer."""
    freq = nf(freq)
    fr = vibrato(bend(freq, n, glide, 0.05), rate=vib, depth=0.009, onset=0.14)
    ph = (np.cumsum(fr) / SR) % 1.0
    # A soft asymmetric pulse, not a saw. The asymmetry is what stops it sounding like
    # a filtered oscillator: a glottis snaps shut faster than it opens.
    src = np.exp(-ph * 5.5) - 0.28
    src = src - src.mean()
    out = np.zeros(n)
    for hz, lvl in VOWELS[vowel]:
        out += reso(src, hz, q, lvl)
    out += hp(lp(noise(n), 5200), 1600) * breath
    return lp(out, 6500) * env(n, 0.022, n / SR * 0.45, 1.9) * 0.5

声门脉冲先经过三个固定在元音频率上的共振器。音高变化时,共振峰留在原处,因为声道长度没有变。这样听起来更像人声,也不会变成跟着音高跑的滤波器。声源本身故意做成不对称,因为声门闭合比张开快。 A glottal pulse passes through three resonators fixed at vowel frequencies. The pitch can move while the formants stay put, because the vocal tract has not changed length. That makes the result read as a voice instead of a filter following the note. The source is asymmetric because the glottis closes faster than it opens.

一次拍手,只是时间A clap is only timing A clap is only timing

drums.py 第 32–44 行lines 32–44
def clap():
    """Four noise bursts at 0/8/17/25 ms, then a tail.

    That stagger IS the clap. One burst is a snare; a smear of them is a room full of
    hands, because no two people in a room hit on the same millisecond.
    """
    n = at(0.34)
    s = np.zeros(n)
    for d, a in ((0.000, 1.00), (0.008, 0.85), (0.017, 0.70), (0.025, 0.55)):
        k, m = at(d), at(0.02)
        s[k:k + m] += hp(lp(noise(m), 7500), 1100)[:m] * np.linspace(1, 0.2, m) * a
    tail = hp(lp(noise(n), 6200), 1300) * env(n, 0.026, 0.30, 2.6) * 0.5
    return (s + tail) * 0.9

这里用了四段噪声,分别在 0、8、17 和 25 毫秒到达。材料和军鼓相同,差别只在时间。把四个时间压到同一点,听起来更像军鼓;稍微错开,才像一群人同时拍手。 Four noise bursts arrive at 0, 8, 17, and 25 ms. The material is the same as a snare; only the timing changes. Collapse the four arrivals into one and it sounds like a snare. Stagger them and it becomes a group of people clapping together.

分音不必是整数倍Partials need not be whole multiples Partials need not be whole multiples

core.py 第 173–186 行lines 173–186
f0 = float(f0)
t = np.arange(n) / sr
out = np.zeros(n)
rng = np.random.RandomState(int(f0 * 31) % 99991)
for k, (r, lv, dc) in enumerate(zip(ratios, levels, decays)):
    f = f0 * r
    if f >= sr * 0.47 or lv <= 0:
        continue
    ph = 2 * np.pi * rng.rand() if phase0 is None else phase0
    out += np.sin(2 * np.pi * f * t + ph) * lv * np.exp(-t / max(dc, 1e-4))
if not norm:
    return out
m = np.abs(out).max()
return out / m if m > 0 else out

加法合成,但每个分音有自己的频率比和自己的衰减速率。这两件事是滤波过的振荡器永远做不到的:被敲响的木条,它的第一泛音落在基频的 3.99 倍上;一根有劲度的钢弦,它的第十分音比十倍频偏高。马林巴之于钟琴、钢琴之于锯齿波,差别大半就在这里。 Additive synthesis, but with a frequency ratio and a decay rate per partial. Neither is reachable by filtering an oscillator: a struck bar's first overtone sits at 3.99 times its fundamental, and a stiff piano string's tenth partial is sharp of ten. That is most of what separates a marimba from a bell, and a piano from a sawtooth.

一根弦,两个衰减阶段One string, two decay stages One string, two decay stages

keys.py 第 53–66 行lines 53–66
def piano_partials(f0, vel, strike, inharm, tau, bright, slow_frac=SLOW_FRACTION):
    """Where the partials are, how loud, and how fast each of them dies.

    Split out so that anything drawing a picture of this note reads the same numbers
    the samples were made from. A visualisation derived by eye from the docstring is
    an illustration; one that calls this is evidence.
    """
    ks = np.arange(1, 41)
    ratios = ks * np.sqrt(1 + inharm * ks ** 2)
    levels = ks ** -(1.9 - 0.9 * vel) * np.abs(np.sin(ks * np.pi * strike))
    levels *= np.exp(-ks / (6.0 + 26.0 * bright))
    fast = tau / (1 + ks * 0.55)
    slow = tau * SLOW_RATIO / (1 + ks * 0.18)
    return ks, ratios, levels, fast, slow, slow_frac

被敲响的弦在两个平面上振动:垂直于音板的那个通过琴桥强耦合、掉得快,平行的那个耦合很弱、能响好几秒。你听到的是两者之和——一次快速下坠接一条很长很浅的尾巴。每个分音只给一条指数衰减,无论谐波调得多准,那都是合成器。 A struck string vibrates in two planes. The one perpendicular to the soundboard couples hard through the bridge and dies fast; the one parallel to it couples weakly and rings for seconds. What you hear is their sum — a quick drop into a long shallow tail. One exponential per partial is a synthesiser, however carefully its harmonics are tuned.

两行常数,两件乐器Two lists, two instruments Two lists, two instruments

mallets.py 第 35–39 行lines 35–39
# Undercut until the overtones are consonant. Two octaves, then three octaves and a
# major third — the intervals a marimba maker actually tunes to.
BAR_TUNED = (1.0, 3.99, 9.2, 16.8, 24.1)
# What a bar does when nobody carves it.
BAR_FREE = (1.0, 2.758, 5.404, 8.933, 13.34)

自由振动的木条,泛音落在 1 : 2.76 : 5.40 上,那在任何调里都不是和弦。马林巴的琴板被削到第一泛音正好是两个八度;钟琴的基本不削,泛音就停在物理给的位置。互换这两行常数就互换了乐器——没有任何低通设置能做到这件事。 A free bar's partials land at roughly 1 : 2.76 : 5.40, which is not a chord in any key. A marimba's bars are carved until the first overtone is two octaves up; a glockenspiel's are barely carved at all, so the partials stay where physics put them. Swapping these two lists swaps the instruments, and no lowpass setting can do that.

声音 sound 声音 / sound waveforms generated in code, without sample packs