|
|
在钢琴曲mp3文件中添加10hz的双耳节拍
钢琴 MP3
│
├── 左声道钢琴 ──┐
│ ├── + 200 Hz
│ │
└── 右声道钢琴 ──┤
└── + 210 Hz
↓
最终立体声音乐
这样左右耳仍然分别包含:
左耳:钢琴 + 200 Hz
右耳:钢琴 + 210 Hz
两者的频率差为:
210 - 200 = 10 Hz
Python 程序。它可以直接读取 piano.mp3,添加 10 Hz 双耳节拍,再输出 piano_binaural_10hz.wav。
import subprocess
import numpy as np
import soundfile as sf
import os
import tempfile
# ============================================================
# 1. 基本参数
# ============================================================
# ------------------------------------------------------------
# 输入的钢琴 MP3 文件
# ------------------------------------------------------------
input_mp3 = "piano.mp3"
# ------------------------------------------------------------
# 输出文件
# ------------------------------------------------------------
# 第一版建议输出 WAV。
#
# 原因:
# WAV 是无损格式,更适合后续进行 EEG 实验和音频分析。
output_wav = "piano_binaural_10hz.wav"
# ============================================================
# 2. 双耳节拍参数
# ============================================================
# 左耳载频
#
# 左耳最终会增加一个 200 Hz 的正弦波。
left_freq = 200
# 右耳载频
#
# 右耳最终会增加一个 210 Hz 的正弦波。
right_freq = 210
# 计算双耳节拍频率
#
# 210 - 200 = 10 Hz
beat_frequency = abs(
right_freq - left_freq
)
# ------------------------------------------------------------
# 双耳节拍的音量
# ------------------------------------------------------------
#
# 这是非常重要的参数。
#
# 正弦波本身的范围是:
#
# -1 ~ +1
#
# 如果直接:
#
# music + sine_wave
#
# 那么双耳纯音可能非常明显,而且容易造成削波。
#
# 所以这里给它一个比较小的增益。
#
# 例如:
#
# 0.01 = 1%
# 0.02 = 2%
# 0.03 = 3%
# 0.05 = 5%
#
# 第一版建议从 0.02 或 0.03 开始测试。
binaural_volume = 0.03
# ============================================================
# 3. 音量淡入/淡出
# ============================================================
#
# 如果双耳纯音从 0 秒突然出现,可能会产生明显的“啪”的瞬态。
#
# 所以我们让双耳纯音:
#
# 0 秒 → 逐渐增加
# 最后几秒 → 逐渐降低
#
# 单位:秒
fade_in_seconds = 3.0
fade_out_seconds = 3.0
# ============================================================
# 4. 使用 FFmpeg 将 MP3 转成 WAV
# ============================================================
# NumPy 和 soundfile 对 MP3 的支持不如 WAV 直接。
#
# 所以这里使用:
#
# MP3
# ↓
# FFmpeg
# ↓
# WAV
# ↓
# NumPy
#
# 最后再进行处理。
# 创建临时 WAV 文件
temp_dir = tempfile.gettempdir()
temp_wav = os.path.join(
temp_dir,
"piano_binaural_temp.wav"
)
# ------------------------------------------------------------
# FFmpeg 命令
# ------------------------------------------------------------
#
# -y
# 如果文件存在,直接覆盖
#
# -i
# 输入文件
#
# -ar 44100
# 音频采样率 44100 Hz
#
# -ac 2
# 保持双声道
#
# -sample_fmt s16
# 使用 16 bit PCM
#
# 最后是输出 WAV 文件。
ffmpeg_command = [
"ffmpeg",
"-y",
"-i",
input_mp3,
"-ar",
"44100",
"-ac",
"2",
"-sample_fmt",
"s16",
temp_wav
]
print("====================================")
print("开始解码 MP3")
print("====================================")
result = subprocess.run(
ffmpeg_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# ------------------------------------------------------------
# 检查 FFmpeg 是否执行成功
# ------------------------------------------------------------
if result.returncode != 0:
print("FFmpeg 执行失败:")
print(result.stderr)
raise RuntimeError(
"MP3 转 WAV 失败,请检查 FFmpeg 是否正确安装。"
)
print("MP3 解码完成")
print()
# ============================================================
# 5. 读取 WAV
# ============================================================
audio, sample_rate = sf.read(
temp_wav
)
print("====================================")
print("音频信息")
print("====================================")
print(
"采样率:",
sample_rate,
"Hz"
)
print(
"音频形状:",
audio.shape
)
# ============================================================
# 6. 确保音频是立体声
# ============================================================
# 我们必须保证:
#
# audio[:, 0] → 左声道
# audio[:, 1] → 右声道
#
# 因为双耳节拍要求:
#
# 左耳 ≠ 右耳
#
# 如果原来的钢琴 MP3 是单声道,
# 我们需要把它复制成两个声道。
if audio.ndim == 1:
print("原始 MP3 是单声道")
print("正在复制为立体声")
audio = np.column_stack(
(
audio,
audio
)
)
# 如果不是两个声道,直接报错。
if audio.shape[1] != 2:
raise ValueError(
"输入音频必须是单声道或双声道。"
)
# ============================================================
# 7. 获取音频长度
# ============================================================
total_samples = len(audio)
duration = (
total_samples
/ sample_rate
)
print(
f"音频长度: {duration:.2f} 秒"
)
print(
f"总采样点: {total_samples}"
)
# ============================================================
# 8. 创建时间轴
# ============================================================
# 假设:
#
# sample_rate = 44100
#
# 那么:
#
# 第一个采样点:
#
# 0 / 44100
#
# 第二个:
#
# 1 / 44100
#
# 第三个:
#
# 2 / 44100
#
# ...
#
# 最终得到以“秒”为单位的时间轴。
t = (
np.arange(total_samples)
/ sample_rate
)
# ============================================================
# 9. 生成左耳 200 Hz 正弦波
# ============================================================
# 正弦波公式:
#
# sin(2πft)
#
# 其中:
#
# f = 200 Hz
# t = 时间
#
# 所以:
#
# left_binaural =
# sin(2π × 200 × t)
left_binaural = np.sin(
2
* np.pi
* left_freq
* t
)
# ============================================================
# 10. 生成右耳 210 Hz 正弦波
# ============================================================
# 同理:
#
# right_binaural =
# sin(2π × 210 × t)
right_binaural = np.sin(
2
* np.pi
* right_freq
* t
)
# ============================================================
# 11. 创建淡入淡出包络
# ============================================================
# 我们不希望 200/210 Hz 在第一帧突然出现。
#
# 因此创建一个 envelope:
#
# 0
# ↓
# 慢慢增加
# ↓
# 1
# ↓
# 保持
# ↓
# 慢慢降低
# ↓
# 0
envelope = np.ones(
total_samples
)
# ------------------------------------------------------------
# 11.1 淡入
# ------------------------------------------------------------
fade_in_samples = int(
fade_in_seconds
* sample_rate
)
# 防止淡入时间超过音频长度。
fade_in_samples = min(
fade_in_samples,
total_samples
)
if fade_in_samples > 0:
envelope[
:fade_in_samples
] = np.linspace(
0,
1,
fade_in_samples
)
# ------------------------------------------------------------
# 11.2 淡出
# ------------------------------------------------------------
fade_out_samples = int(
fade_out_seconds
* sample_rate
)
fade_out_samples = min(
fade_out_samples,
total_samples
)
if fade_out_samples > 0:
envelope[
-fade_out_samples:
] = np.linspace(
1,
0,
fade_out_samples
)
# ============================================================
# 12. 将淡入淡出应用到双耳信号
# ============================================================
left_binaural *= envelope
right_binaural *= envelope
# ============================================================
# 13. 调整双耳节拍音量
# ============================================================
# 例如:
#
# binaural_volume = 0.03
#
# 表示把正弦波振幅缩小到原来的 3%。
left_binaural *= binaural_volume
right_binaural *= binaural_volume
# ============================================================
# 14. 将双耳信号加入钢琴左声道
# ============================================================
# 原来的钢琴:
#
# audio[:, 0]
#
# 是左声道。
#
# 我们把:
#
# 200 Hz
#
# 加进去。
audio[:, 0] = (
audio[:, 0]
+ left_binaural
)
# ============================================================
# 15. 将双耳信号加入钢琴右声道
# ============================================================
# 原来的钢琴:
#
# audio[:, 1]
#
# 是右声道。
#
# 我们加入:
#
# 210 Hz
#
# 得到最终右声道。
audio[:, 1] = (
audio[:, 1]
+ right_binaural
)
# ============================================================
# 16. 防止音频削波
# ============================================================
# 正常 WAV 浮点音频一般应该保持在:
#
# -1.0 ~ +1.0
#
# 但是:
#
# 钢琴 + 双耳纯音
#
# 叠加之后可能超过这个范围。
#
# 例如:
#
# 1.02
#
# 就超过了最大值。
#
# 如果直接保存,可能产生严重失真。
#
# 所以这里进行限制。
audio = np.clip(
audio,
-1.0,
1.0
)
# ============================================================
# 17. 保存最终 WAV
# ============================================================
print()
print("====================================")
print("正在生成最终 WAV")
print("====================================")
sf.write(
output_wav,
audio,
sample_rate
)
print()
print("====================================")
print("生成完成")
print("====================================")
print(
"输出文件:",
output_wav
)
print(
"左耳钢琴 +",
left_freq,
"Hz"
)
print(
"右耳钢琴 +",
right_freq,
"Hz"
)
print(
"双耳节拍:",
beat_frequency,
"Hz"
)
print(
"双耳信号音量:",
binaural_volume
)
# ============================================================
# 18. 删除临时 WAV
# ============================================================
try:
os.remove(
temp_wav
)
print(
"临时文件已删除"
)
except OSError:
pass |
|