使用 Python 探索音频合成和数字信号处理 (DSP) 的世界。 学习生成波形、应用滤波器并从头开始创建声音。
释放声音:深入探索用于音频合成和数字信号处理的 Python
从您耳机中的音乐流媒体到视频游戏的沉浸式音景以及我们设备上的语音助手,数字音频是现代生活中不可或缺的一部分。但您有没有想过这些声音是如何产生的? 这不是魔法; 它是数学、物理和计算机科学的迷人融合,被称为数字信号处理 (DSP)。 今天,我们将揭开神秘面纱,向您展示如何利用 Python 的强大功能从头开始生成、操纵和合成声音。
本指南适用于开发人员、数据科学家、音乐家、艺术家以及任何对代码和创造力之间的交叉点感到好奇的人。 您无需成为 DSP 专家或经验丰富的音频工程师。 只要对 Python 有基本的了解,您很快就能创作出自己独特的音景。 我们将探索数字音频的基本构建块,生成经典波形,使用包络和滤波器对它们进行整形,甚至构建一个迷你合成器。 让我们开始我们进入充满活力的计算音频世界的旅程。
理解数字音频的构建块
在我们编写任何一行代码之前,我们必须了解声音如何在计算机中表示。 在物理世界中,声音是连续的模拟压力波。 计算机是数字的,无法存储连续波。 相反,它们每秒拍摄数千个波的样本。 这个过程称为采样。
采样率
采样率决定了每秒采集多少个样本。 它以赫兹 (Hz) 为单位测量。 较高的采样率会更准确地表示原始声波,从而产生更高保真度的音频。 常见的采样率包括:
- 44100 Hz (44.1 kHz):音频 CD 的标准。 它的选择基于奈奎斯特-香农采样定理,该定理指出采样率必须至少是您要捕获的最高频率的两倍。 由于人类听觉的范围在 20,000 Hz 左右达到顶峰,因此 44.1 kHz 提供了足够的缓冲。
- 48000 Hz (48 kHz):专业视频和数字音频工作站 (DAW) 的标准。
- 96000 Hz (96 kHz):用于高分辨率音频制作,以获得更高的精度。
就我们的目的而言,我们将主要使用 44100 Hz,因为它在质量和计算效率之间提供了极好的平衡。
位深度
如果采样率决定了时间分辨率,那么位深度决定了幅度(响度)的分辨率。 每个样本都是一个数字,表示该特定时刻波的幅度。 位深度是用于存储该数字的位数。 较高的位深度允许更多的可能幅度值,从而产生更大的动态范围(最安静和最响亮的声音之间的差异)和更低的本底噪声。
- 16 位:CD 的标准,提供 65,536 个可能的幅度级别。
- 24 位:专业音频制作的标准,提供超过 1670 万个级别。
当我们使用 NumPy 等库在 Python 中生成音频时,我们通常使用浮点数(例如,介于 -1.0 和 1.0 之间)以获得最大的精度。 然后,在保存到文件或通过硬件播放时,会将它们转换为特定的位深度(如 16 位整数)。
声道
这只是指音频流的数量。 单声道音频有一个声道,而立体声音频有两个声道(左声道和右声道),从而产生空间感和方向感。
设置您的 Python 环境
要开始,我们需要一些基本的 Python 库。 它们构成了我们用于数值计算、信号处理、可视化和音频播放的工具包。
您可以使用 pip 安装它们:
pip install numpy scipy matplotlib sounddevice
让我们简要回顾一下它们的作用:
- NumPy:Python 中科学计算的基石。 我们将使用它来创建和操作数字数组,这将代表我们的音频信号。
- SciPy:建立在 NumPy 之上,它为信号处理提供了大量的算法集合,包括波形生成和滤波。
- Matplotlib:Python 中的主要绘图库。 对于可视化我们的波形和理解我们处理的效果,它是非常宝贵的。
- SoundDevice:一个方便的库,用于通过您计算机的扬声器将我们的 NumPy 数组作为音频播放。 它提供了一个简单且跨平台的接口。
波形生成:合成的核心
所有声音,无论多么复杂,都可以分解为简单、基本波形的组合。 这些是我们声音调色板上的原色。 让我们学习如何生成它们。
正弦波:最纯粹的音调
正弦波是所有声音的绝对构建块。 它表示一个单一频率,没有泛音或谐波。 它听起来非常流畅、干净,通常被描述为“长笛般”。 数学公式是:
y(t) = 幅度 * sin(2 * π * 频率 * t)
其中“t”是时间。 让我们将其转换为 Python 代码。
import numpy as np
import sounddevice as sd
import matplotlib.pyplot as plt
# --- Global Parameters ---
SAMPLE_RATE = 44100 # samples per second
DURATION = 3.0 # seconds
# --- Waveform Generation ---
def generate_sine_wave(frequency, duration, sample_rate, amplitude=0.5):
"""Generate a sine wave.
Args:
frequency (float): The frequency of the sine wave in Hz.
duration (float): The duration of the wave in seconds.
sample_rate (int): The sample rate in Hz.
amplitude (float): The amplitude of the wave (0.0 to 1.0).
Returns:
np.ndarray: The generated sine wave as a NumPy array.
"""
# Create an array of time points
t = np.linspace(0, duration, int(sample_rate * duration), False)
# Generate the sine wave
# 2 * pi * frequency is the angular frequency
wave = amplitude * np.sin(2 * np.pi * frequency * t)
return wave
# --- Example Usage ---
if __name__ == "__main__":
# Generate a 440 Hz (A4 note) sine wave
frequency_a4 = 440.0
sine_wave = generate_sine_wave(frequency_a4, DURATION, SAMPLE_RATE)
print("Playing 440 Hz sine wave...")
# Play the sound
sd.play(sine_wave, SAMPLE_RATE)
sd.wait() # Wait for the sound to finish playing
print("Playback finished.")
# --- Visualization ---
# Plot a small portion of the wave to see its shape
plt.figure(figsize=(12, 4))
plt.plot(sine_wave[:500])
plt.title("Sine Wave (440 Hz)")
plt.xlabel("Sample")
plt.ylabel("Amplitude")
plt.grid(True)
plt.show()
在此代码中,np.linspace 创建一个表示时间轴的数组。 然后,我们将正弦函数应用于此时间数组,并按所需的频率进行缩放。 结果是一个 NumPy 数组,其中每个元素都是我们声波的一个样本。 然后,我们可以使用 sounddevice 播放它,并使用 matplotlib 可视化它。
探索其他基本波形
虽然正弦波是纯粹的,但它并不总是最有趣的。 其他基本波形富含谐波,赋予它们更复杂和明亮的特性(音色)。 scipy.signal 模块提供了方便的函数来生成它们。
方波
方波在其最大和最小幅度之间立即跳跃。 它仅包含奇数谐波。 它具有明亮、芦苇般和有点“空心”或“数字”的声音,通常与早期的视频游戏音乐相关联。
from scipy import signal
# Generate a square wave
square_wave = 0.5 * signal.square(2 * np.pi * 440 * np.linspace(0, DURATION, int(SAMPLE_RATE * DURATION), False))
# sd.play(square_wave, SAMPLE_RATE)
# sd.wait()
锯齿波
锯齿波线性上升,然后立即下降到其最小值(或反之亦然)。 它非常丰富,包含所有整数谐波(包括偶数和奇数)。 这使得它听起来非常明亮、嗡嗡作响,并且是减法合成的绝佳起点,我们稍后将介绍。
# Generate a sawtooth wave
sawtooth_wave = 0.5 * signal.sawtooth(2 * np.pi * 440 * np.linspace(0, DURATION, int(SAMPLE_RATE * DURATION), False))
# sd.play(sawtooth_wave, SAMPLE_RATE)
# sd.wait()
三角波
三角波线性上升和下降。 像方波一样,它仅包含奇数谐波,但它们的幅度下降得更快。 这使得它的声音比方波更柔和和更柔和,更接近正弦波,但具有更多的“主体”。
# Generate a triangle wave (a sawtooth with 0.5 width)
triangle_wave = 0.5 * signal.sawtooth(2 * np.pi * 440 * np.linspace(0, DURATION, int(SAMPLE_RATE * DURATION), False), width=0.5)
# sd.play(triangle_wave, SAMPLE_RATE)
# sd.wait()
白噪声:随机的声音
白噪声是一种在每个频率上都包含相等能量的信号。 它听起来像静电或瀑布的“嘘”声。 它在声音设计中非常有用,可以创建打击乐声音(如踩镲和小军鼓)和大气效果。 生成它非常简单。
# Generate white noise
num_samples = int(SAMPLE_RATE * DURATION)
white_noise = np.random.uniform(-1, 1, num_samples)
# sd.play(white_noise, SAMPLE_RATE)
# sd.wait()
加法合成:构建复杂性
法国数学家约瑟夫·傅里叶发现,任何复杂的周期性波形都可以分解为简单正弦波的总和。 这是加法合成的基础。 通过添加不同频率(谐波)和幅度的正弦波,我们可以构建新的、更丰富的音色。
让我们通过添加基频的前几个谐波来创建一个更复杂的音调。
def generate_complex_tone(fundamental_freq, duration, sample_rate):
t = np.linspace(0, duration, int(sample_rate * duration), False)
# Start with the fundamental frequency
tone = 0.5 * np.sin(2 * np.pi * fundamental_freq * t)
# Add harmonics (overtones)
# 2nd harmonic (octave higher), lower amplitude
tone += 0.25 * np.sin(2 * np.pi * (2 * fundamental_freq) * t)
# 3rd harmonic, even lower amplitude
tone += 0.12 * np.sin(2 * np.pi * (3 * fundamental_freq) * t)
# 5th harmonic
tone += 0.08 * np.sin(2 * np.pi * (5 * fundamental_freq) * t)
# Normalize the waveform to be between -1 and 1
tone = tone / np.max(np.abs(tone))
return tone
# --- Example Usage ---
complex_tone = generate_complex_tone(220, DURATION, SAMPLE_RATE)
sd.play(complex_tone, SAMPLE_RATE)
sd.wait()
通过仔细选择要添加哪些谐波以及在什么幅度下添加,您可以开始模仿真实乐器的声音。 这个简单的例子听起来已经比纯正弦波丰富和有趣得多。
使用包络整形声音 (ADSR)
到目前为止,我们的声音突然开始和停止。 它们的整个持续时间内音量恒定,这听起来非常不自然和机器人化。 在现实世界中,声音会随着时间的推移而演变。 钢琴音符有一个尖锐、响亮的开始,会迅速消退,而小提琴上弹奏的音符会逐渐增大音量。 我们使用幅度包络来控制这种动态演变。
ADSR 模型
最常见的包络类型是 ADSR 包络,它有四个阶段:
- Attack:声音从无声到最大幅度所需的时间。 快速起音会产生打击乐器般的尖锐声音(如鼓点)。 缓慢的起音会产生柔和、膨胀的声音(如弦垫)。
- Decay:声音从最大起音水平降低到持续水平所需的时间。
- Sustain:在保持音符期间声音保持的幅度水平。 这是一个级别,而不是时间。
- Release:在释放音符后声音从持续水平消退到无声所需的时间。 较长的释放会使声音持续存在,就像按下延音踏板的钢琴音符一样。
在 Python 中实现 ADSR 包络
我们可以实现一个函数来生成作为 NumPy 数组的 ADSR 包络。 然后,我们通过简单的逐元素乘法将其应用于我们的波形。
def adsr_envelope(duration, sample_rate, attack_time, decay_time, sustain_level, release_time):
num_samples = int(duration * sample_rate)
attack_samples = int(attack_time * sample_rate)
decay_samples = int(decay_time * sample_rate)
release_samples = int(release_time * sample_rate)
sustain_samples = num_samples - attack_samples - decay_samples - release_samples
if sustain_samples < 0:
# If times are too long, adjust them proportionally
total_time = attack_time + decay_time + release_time
attack_time, decay_time, release_time = \
attack_time/total_time*duration, decay_time/total_time*duration, release_time/total_time*duration
attack_samples = int(attack_time * sample_rate)
decay_samples = int(decay_time * sample_rate)
release_samples = int(release_time * sample_rate)
sustain_samples = num_samples - attack_samples - decay_samples - release_samples
# Generate each part of the envelope
attack = np.linspace(0, 1, attack_samples)
decay = np.linspace(1, sustain_level, decay_samples)
sustain = np.full(sustain_samples, sustain_level)
release = np.linspace(sustain_level, 0, release_samples)
return np.concatenate([attack, decay, sustain, release])
# --- Example Usage: Plucky vs. Pad Sound ---
# Pluck sound (fast attack, quick decay, no sustain)
pluck_envelope = adsr_envelope(DURATION, SAMPLE_RATE, 0.01, 0.2, 0.0, 0.5)
# Pad sound (slow attack, long release)
pad_envelope = adsr_envelope(DURATION, SAMPLE_RATE, 0.5, 0.2, 0.7, 1.0)
# Generate a harmonically rich sawtooth wave to apply envelopes to
saw_wave_for_env = generate_complex_tone(220, DURATION, SAMPLE_RATE)
# Apply envelopes
plucky_sound = saw_wave_for_env * pluck_envelope
pad_sound = saw_wave_for_env * pad_envelope
print("Playing plucky sound...")
sd.play(plucky_sound, SAMPLE_RATE)
sd.wait()
print("Playing pad sound...")
sd.play(pad_sound, SAMPLE_RATE)
sd.wait()
# Visualize the envelopes
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
plt.plot(pluck_envelope)
plt.title("Pluck ADSR Envelope")
plt.subplot(2, 1, 2)
plt.plot(pad_envelope)
plt.title("Pad ADSR Envelope")
plt.tight_layout()
plt.show()
请注意,仅仅通过应用不同的包络,相同的底层波形如何戏剧性地改变其特性。 这是声音设计中的一项基本技术。
数字滤波简介(减法合成)
虽然加法合成通过添加正弦波来构建声音,但减法合成以相反的方式工作。 我们从一个谐波丰富的信号(如锯齿波或白噪声)开始,然后使用滤波器雕刻或衰减特定频率。 这类似于雕塑家从一块大理石开始,然后将其敲掉以显示形状。
关键滤波器类型
- 低通滤波器:这是合成中最常见的滤波器。 它允许低于某个“截止”点的频率通过,同时衰减高于它的频率。 它使声音更暗、更温暖或更柔和。
- 高通滤波器:与低通滤波器相反。 它允许高于截止频率的频率通过,从而消除低音和低端频率。 它使声音更薄或更锡纸。
- 带通滤波器:仅允许特定频带通过,从而切断高频和低频。 这可以产生“电话”或“无线电”效果。
- 带阻(陷波)滤波器:与带通滤波器相反。 它会消除特定频带的频率。
使用 SciPy 实现滤波器
scipy.signal 库提供了用于设计和应用数字滤波器的强大工具。 我们将使用一种称为 Butterworth 滤波器的常见类型,它以其在通带中的平坦响应而闻名。
该过程包括两个步骤:首先,设计滤波器以获得其系数,其次,将这些系数应用于我们的音频信号。
from scipy.signal import butter, lfilter, freqz
def butter_lowpass_filter(data, cutoff, fs, order=5):
"""Apply a low-pass Butterworth filter to a signal."""
nyquist = 0.5 * fs
normal_cutoff = cutoff / nyquist
# Get the filter coefficients
b, a = butter(order, normal_cutoff, btype='low', analog=False)
y = lfilter(b, a, data)
return y
# --- Example Usage ---
# Start with a rich signal: sawtooth wave
saw_wave_rich = 0.5 * signal.sawtooth(2 * np.pi * 220 * np.linspace(0, DURATION, int(SAMPLE_RATE * DURATION), False))
print("Playing original sawtooth wave...")
sd.play(saw_wave_rich, SAMPLE_RATE)
sd.wait()
# Apply a low-pass filter with a cutoff of 800 Hz
filtered_saw = butter_lowpass_filter(saw_wave_rich, cutoff=800, fs=SAMPLE_RATE, order=6)
print("Playing filtered sawtooth wave...")
sd.play(filtered_saw, SAMPLE_RATE)
sd.wait()
# --- Visualization of the filter's frequency response ---
cutoff_freq = 800
order = 6
b, a = butter(order, cutoff_freq / (0.5 * SAMPLE_RATE), btype='low')
w, h = freqz(b, a, worN=8000)
plt.figure(figsize=(10, 5))
plt.plot(0.5 * SAMPLE_RATE * w / np.pi, np.abs(h), 'b')
plt.plot(cutoff_freq, 0.5 * np.sqrt(2), 'ko')
plt.axvline(cutoff_freq, color='k', linestyle='--')
plt.xlim(0, 5000)
plt.title("Low-pass Filter Frequency Response")
plt.xlabel('Frequency [Hz]')
plt.grid()
plt.show()
听取原始波和滤波波之间的差异。 原始波明亮且嗡嗡作响; 滤波后的版本更柔和和更暗,因为高频谐波已被移除。 扫描低通滤波器的截止频率是电子音乐中最具表现力和最常见的技术之一。
调制:添加运动和生命
静态声音很无聊。 调制是创建动态、演进和有趣声音的关键。 原理很简单:使用一个信号(调制器)来控制另一个信号(载波)的参数。 常见的调制器是低频振荡器 (LFO),它只是一个频率低于人类听觉范围的振荡器(例如,0.1 Hz 到 20 Hz)。
幅度调制 (AM) 和颤音
这是当我们使用 LFO 来控制我们声音的幅度时。 结果是音量中出现有节奏的脉动,称为颤音。
# Carrier wave (the sound we hear)
carrier_freq = 300
carrier = generate_sine_wave(carrier_freq, DURATION, SAMPLE_RATE)
# Modulator LFO (controls the volume)
lfo_freq = 5 # 5 Hz LFO
modulator = generate_sine_wave(lfo_freq, DURATION, SAMPLE_RATE, amplitude=1.0)
# Create tremolo effect
# We scale the modulator to be from 0 to 1
tremolo_modulator = (modulator + 1) / 2
tremolo_sound = carrier * tremolo_modulator
print("Playing tremolo effect...")
sd.play(tremolo_sound, SAMPLE_RATE)
sd.wait()
频率调制 (FM) 和颤音
这是当我们使用 LFO 来控制我们声音的频率时。 缓慢、微妙的频率调制会产生颤音,即歌手和小提琴手用来添加表现力的音高轻微摆动。
# Create vibrato effect
t = np.linspace(0, DURATION, int(SAMPLE_RATE * DURATION), False)
carrier_freq = 300
lfo_freq = 7
modulation_depth = 10 # How much the frequency will vary
# The LFO will be added to the carrier frequency
modulator_vibrato = modulation_depth * np.sin(2 * np.pi * lfo_freq * t)
# The instantaneous frequency changes over time
instantaneous_freq = carrier_freq + modulator_vibrato
# We need to integrate the frequency to get the phase
phase = np.cumsum(2 * np.pi * instantaneous_freq / SAMPLE_RATE)
vibrato_sound = 0.5 * np.sin(phase)
print("Playing vibrato effect...")
sd.play(vibrato_sound, SAMPLE_RATE)
sd.wait()
这是 FM 合成的简化版本。 当 LFO 频率增加到可听范围内时,它会创建复杂的边带频率,从而产生丰富、钟声般和金属般的音调。 这是 Yamaha DX7 等合成器标志性声音的基础。
将所有内容放在一起:迷你合成器项目
让我们将我们学到的所有内容组合成一个简单的、功能齐全的合成器类。 这会将我们的振荡器、包络和滤波器封装到一个可重用的对象中。
class MiniSynth:
def __init__(self, sample_rate=44100):
self.sample_rate = sample_rate
def generate_note(self, frequency, duration, waveform='sine',
adsr_params=(0.05, 0.2, 0.5, 0.3),
filter_params=None):
"""Generate a single synthesized note."""
num_samples = int(duration * self.sample_rate)
t = np.linspace(0, duration, num_samples, False)
# 1. Oscillator
if waveform == 'sine':
wave = np.sin(2 * np.pi * frequency * t)
elif waveform == 'square':
wave = signal.square(2 * np.pi * frequency * t)
elif waveform == 'sawtooth':
wave = signal.sawtooth(2 * np.pi * frequency * t)
elif waveform == 'triangle':
wave = signal.sawtooth(2 * np.pi * frequency * t, width=0.5)
else:
raise ValueError("Unsupported waveform")
# 2. Envelope
attack, decay, sustain, release = adsr_params
envelope = adsr_envelope(duration, self.sample_rate, attack, decay, sustain, release)
# Ensure envelope and wave are the same length
min_len = min(len(wave), len(envelope))
wave = wave[:min_len] * envelope[:min_len]
# 3. Filter (optional)
if filter_params:
cutoff = filter_params.get('cutoff', 1000)
order = filter_params.get('order', 5)
filter_type = filter_params.get('type', 'low')
if filter_type == 'low':
wave = butter_lowpass_filter(wave, cutoff, self.sample_rate, order)
# ... could add high-pass etc. here
# Normalize to 0.5 amplitude
return wave * 0.5
# --- Example Usage of the Synth ---
synth = MiniSynth()
# A bright, plucky bass sound
bass_note = synth.generate_note(
frequency=110, # A2 note
duration=1.5,
waveform='sawtooth',
adsr_params=(0.01, 0.3, 0.0, 0.2),
filter_params={'cutoff': 600, 'order': 6}
)
print("Playing synth bass note...")
sd.play(bass_note, SAMPLE_RATE)
sd.wait()
# A soft, atmospheric pad sound
pad_note = synth.generate_note(
frequency=440, # A4 note
duration=5.0,
waveform='triangle',
adsr_params=(1.0, 0.5, 0.7, 1.5)
)
print("Playing synth pad note...")
sd.play(pad_note, SAMPLE_RATE)
sd.wait()
# A simple melody
melody = [
('C4', 261.63, 0.4),
('D4', 293.66, 0.4),
('E4', 329.63, 0.4),
('C4', 261.63, 0.8)
]
final_melody = []
for note, freq, dur in melody:
sound = synth.generate_note(freq, dur, 'square', adsr_params=(0.01, 0.1, 0.2, 0.1), filter_params={'cutoff': 1500})
final_melody.append(sound)
full_melody_wave = np.concatenate(final_melody)
print("Playing a short melody...")
sd.play(full_melody_wave, SAMPLE_RATE)
sd.wait()
这个简单的类是对我们所涵盖原则的有力证明。 我鼓励您尝试使用它。 尝试不同的波形,调整 ADSR 参数,并更改滤波器截止频率以查看您可以如何彻底改变声音。
超越基础:接下来去哪里?
我们只是触及了音频合成和 DSP 深刻而有价值的领域的表面。 如果这激发了您的兴趣,以下是一些要探索的高级主题:
- 波表合成:此技术不是使用数学上完美的形状,而是使用预先录制的单周期波形作为振荡器源,从而实现令人难以置信的复杂且不断演变的音色。
- 颗粒合成:通过将现有音频样本解构为微小片段(颗粒),然后重新排列、拉伸和调整音高来创建新声音。 它非常适合创建大气纹理和垫子。
- 物理建模合成:一种引人入胜的方法,尝试通过对乐器的物理特性(吉他的弦、单簧管的管、鼓的膜)进行数学建模来创建声音。
- 实时音频处理:PyAudio 和 SoundCard 等库允许您实时处理来自麦克风或其他输入的音频流,从而为实时效果、交互式装置等打开了大门。
- 音频中的机器学习:人工智能和深度学习正在彻底改变音频。 模型可以生成新颖的音乐、合成逼真的人类语音,甚至可以从混合歌曲中分离出单独的乐器。
结论
我们已经从数字声音的基本性质到构建一个功能齐全的合成器。 我们学习了如何使用 Python、NumPy 和 SciPy 生成纯粹和复杂的波形。 我们发现了如何使用 ADSR 包络赋予我们的声音生命和形状,使用数字滤波器雕刻其特性,并使用调制添加动态运动。 我们编写的代码不仅仅是一种技术练习;它是一种创造性的工具。
Python 强大的科学堆栈使其成为在音频世界中学习、实验和创造的卓越平台。 无论您的目标是为项目创建自定义音效、构建乐器,还是仅仅了解您每天听到的声音背后的技术,您在这里学到的原理都是您的起点。 现在,轮到您进行实验了。 开始组合这些技术,尝试新的参数,并仔细倾听结果。 浩瀚的声音宇宙现在触手可及——您将创造什么?