v1
下面给你一个系统、可操作的“EEG(BrainVision 文件)→ 声音”转换(脑电声化 / Sonification)方案。内容包括:总体思路、准备环境、基础预处理、三类常见声化策略(直接时间压缩法 / 载波调制法 / 频带映射合成法)、进阶增强、完整示例代码、常见问题与优化建议。你可以先跑“最简方案”快速得到声音,再逐步优化艺术性或分析价值。
====================
一、总体思路(为什么 EEG 可变成音频)
EEG 原始频段主要 0.5–40 Hz(有时到 80+ Hz),远低于人耳 20–20000 Hz。要“听到”其结构,需要把这些慢信号映射到可听频段或通过调制让其影响可听载波。典型做法:
- 时间压缩 / 加速(Resample):将采样率从 ~500 Hz 拉升到 48 kHz,相当于把所有频率整体乘以 ~96,使 10 Hz → 960 Hz。
- 频率搬移(Heterodyning / Frequency Shift):用希尔伯特变换 + 乘以 e^(j2πft) 将频谱整体上移。
- 载波调制(AM/FM):低频 EEG 作为一个或多个可听载波 (例如 220/440/660 Hz) 的振幅或频率包络。
- 多频带映射:将 δ, θ, α, β, γ 各带功率映射到不同乐音/通道或音色。
====================
二、环境准备
建议 Python:
pip install mne numpy scipy librosa soundfile matplotlib
主要库用途:
- mne:读取 BrainVision (.vhdr) + 预处理
- numpy/scipy:信号处理(滤波、Hilbert、Welch)
- librosa:重采样与音频辅助
- soundfile:写 wav
====================
三、读取 BrainVision 数据
BrainVision 有三文件:.vhdr(头信息)、.eeg(二进制)、.vmrk(标记)。只需给 read_raw_brainvision() 传 .vhdr。
示例:
import mne
raw = mne.io.read_raw_brainvision(“your_file.vhdr”, preload=True)
print(raw.info[‘sfreq’], raw.ch_names)
====================
四、基础预处理(建议)
- 选择 EEG 通道(去除 EOG/EMG)
- 重参考:raw.set_eeg_reference(‘average’) 或选择双乳突/特定参考
- 滤波:常用带通 1–40 Hz(去 DC 漂移 & 高频噪声)
- 可选:ICA 去眼动(对音频美感影响小,可先跳过)
- 标度:MNE 默认单位为 Volt,脑电微伏级,导出音频前需要归一化
示例:
raw.pick_types(eeg=True)
raw.set_eeg_reference(‘average’)
raw.filter(1., 40.)
====================
五、三类常见声化策略
策略 A:直接时间压缩(最直观,信息保真)
- 将原始采样率 fs(例如 500 Hz)重采样到音频标准 48000 Hz。
- 相当于播放速度加快 96 倍 → 所有低频变成可听中频。
优点:简单,保留原始相对形状;缺点:整体“加速”,语感不一定好听。
核心步骤:
factor = int(48000 / fs)(若不整除用 librosa.resample)
data = raw.get_data() # shape: (n_ch, n_samples)
选一个通道或进行混合:signal = data[ch_idx]
使用 librosa.resample(signal, orig_sr=fs, target_sr=48000)
归一化后写 wav。
策略 B:幅度调制 (AM) / 频率调制 (FM)
- 选择一个载波频率 f_c(如 440 Hz),用 EEG 作为包络: audio(t) = (1 + k norm_EEG(t)) sin(2πf_c t)
- 或多个 EEG 通道 → 不同载波 → 混合成立体声/多层。
- FM:freq(t) = f_c + Δf * norm_EEG(t), audio(t)=sin(2π∫freq(t)dt)。
优点:声音悦耳、结构清晰;缺点:相比原始频谱失真较大。
策略 C:频带功率 → 多音合成(“脑电乐器”)
- 将 EEG 分解成 c(1–4), θ(4–8), α(8–13), β(13–30), γ(30–45) 五段。
- 用滑动窗口 (e.g., 0.5 s 步长) 计算每段功率 (Welch)。
- 将每段功率标准化后映射到对应音高/振幅:
- δ → 220 Hz (A3)
- θ → 261.6 Hz (C4)
- α → 329.6 Hz (E4)
- β → 392.0 Hz (G4)
- γ → 523.3 Hz (C5)
- 在窗口内合成正弦或包络平滑的波,最后拼接 & overlap-add。
优点:突出脑节律结构;缺点:非原始波形,更多“信息抽象”。
====================
六、最简可运行代码(策略 A:时间压缩)
import mne, numpy as np, librosa, soundfile as sf
# 1. 读取
raw = mne.io.read_raw_brainvision("your_file.vhdr", preload=True)
raw.pick_types(eeg=True)
raw.set_eeg_reference('average')
raw.filter(1., 40.)
# 2. 获取数据
data = raw.get_data() # shape: (n_channels, n_samples)
fs = raw.info['sfreq'] # 原采样率,如 500
ch_idx = 0 # 选一个通道先体验
signal = data[ch_idx]
# 3. 去均值(防止慢漂移影响幅度)
signal = signal - np.mean(signal)
# 4. 重采样到 48000 Hz (时间压缩)
target_sr = 48000
resampled = librosa.resample(signal, orig_sr=fs, target_sr=target_sr)
# 5. 归一化 (避免爆音)
resampled /= np.max(np.abs(resampled) + 1e-12)
audio = 0.9 * resampled # 留点 headroom
# 6. 保存
sf.write("eeg_time_compress.wav", audio.astype('float32'), target_sr)
print("导出完成:eeg_time_compress.wav")
====================
七、策略 B:AM 调制示例(听“波动”)
import mne, numpy as np, soundfile as sf
raw = mne.io.read_raw_brainvision("your_file.vhdr", preload=True)
raw.pick_types(eeg=True)
raw.set_eeg_reference('average')
raw.filter(1., 40.)
data = raw.get_data()
fs = raw.info['sfreq']
signal = data[0]
signal = signal - np.mean(signal)
# 低频包络需要放大;标准化
env = signal / (np.std(signal) + 1e-12)
env = np.tanh(env) # 压缩极值
env = (env - env.min()) / (env.max() - env.min()) # 0~1
env = 2*env - 1 # -1~1
duration = len(env)/fs
t = np.linspace(0, duration, len(env), endpoint=False)
carrier_freq = 440.0
k = 0.6 # 调制深度(0~1)
carrier = np.sin(2*np.pi*carrier_freq*t)
audio = (1 + k*env) * carrier
audio /= np.max(np.abs(audio)+1e-12)
sf.write("eeg_am.wav", audio.astype('float32'), fs)
听感:你会听到一个 440Hz 声音,其响度随 EEG 波形变化。
====================
八、策略 C:频带功率映射(节律“和弦”风格)
import mne, numpy as np, scipy.signal as sig, soundfile as sf
raw = mne.io.read_raw_brainvision("your_file.vhdr", preload=True)
raw.pick_types(eeg=True)
raw.set_eeg_reference('average')
raw.filter(1., 45.)
data = raw.get_data()
fs = raw.info['sfreq']
signal = data[0] - np.mean(data[0])
# 定义频带与对应音高
bands = {
"delta": (1,4, 220.0),
"theta": (4,8, 261.6),
"alpha": (8,13,329.6),
"beta": (13,30,392.0),
"gamma": (30,45,523.3)
}
win_sec = 0.5
step_sec = 0.25
win_samples = int(win_sec * fs)
step_samples = int(step_sec * fs)
segments_audio = []
t_global = 0.0
for start in range(0, len(signal)-win_samples, step_samples):
seg = signal[start:start+win_samples]
# 计算各频带功率
synth = np.zeros(win_samples)
for name,(f1,f2,freq) in bands.items():
# 带通
sos = sig.butter(4, [f1, f2], btype='bandpass', fs=fs, output='sos')
band_sig = sig.sosfilt(sos, seg)
power = np.sqrt(np.mean(band_sig**2)) # RMS
# 归一化映射
amp = power
# 后面统一标准化,所以这里先缓存
phase = 2*np.pi*freq*np.linspace(0, win_sec, win_samples, endpoint=False)
synth += amp * np.sin(phase)
segments_audio.append(synth)
# 拼接与归一化
audio = np.zeros(step_samples*(len(segments_audio)-1)+win_samples)
for i,seg in enumerate(segments_audio):
start = i*step_samples
audio[start:start+win_samples] += seg # overlap-add
audio /= (np.max(np.abs(audio))+1e-12)
target_sr = 48000
# 重采样提升音质
import librosa
audio_hi = librosa.resample(audio, orig_sr=fs, target_sr=target_sr)
audio_hi /= np.max(np.abs(audio_hi)+1e-12)
sf.write("eeg_band_chords.wav", (0.9*audio_hi).astype('float32'), target_sr)
====================
九、进阶增强点
- 立体声/空间化:
- 左半球通道平均 → 左声道;右半球 → 右声道。
- 或不同频带放不同声道,增强可分辨性。
- 动态范围控制:使用对数/双曲正切压缩抑制异常尖峰。
- 去伪迹:利用 ICA 去除眨眼 (EOG) 前再声化,声音更平滑。
- 频率搬移(替代时间加速):
- analytic = hilbert(signal)
- shift = np.real(analytic np.exp(1j2np.pishift_hz*t))
- shift_hz 例如 300 Hz,把 1–40 Hz 搬到 301–340 Hz;再放大。
- 多通道合成:为每个选定通道分配一个音阶(例如左到右映射到一个五声音阶),做一个“脑电合唱”。
- 与事件标记同步:读取 .vmrk 中的刺激事件,在音频中插入短促“点击”或打击音做参考。
- MIDI 输出:将 α 功率阈值上升触发 MIDI Note On/Off,用于交互艺术。
====================
十、质量与听感调校技巧
- 如果“时间压缩”声音太尖锐,可在重采样后加温和低通(如 8 kHz)。
- 去 DC 与慢漂:高通 0.5 或 1 Hz;或直接减均值。
- 避免剪切:输出前 max_abs>1 就再缩放。
- 试听工具:Audacity 或 DAW(可视化波形 + 频谱)。
====================
十一、常见问题 (FAQ)
Q: 原始采样率不是整除 48000?
A: 用 librosa.resample(orig_sr=fs, target_sr=48000) 自动处理。
Q: 声音太小或太大?
A: 归一化后乘以 0.8~0.95;不要直接放大微伏数据到 1,先标准化再调。
Q: 多个通道叠加出现失真?
A: 叠加后统一归一化;或在叠加前每通道乘以 1/N。
Q: 要实时声化?
A: 可用 lab streaming layer (LSL) + PyAudio:窗口收集→即时调制→播放。
Q: 会泄露隐私吗?
A: 纯波形一般难以识别个人,但仍建议获得被试知情同意,不附带元数据(姓名、实验条件)。
====================
十二、快速决策:我应该先用哪种?
- 想“立刻听到”脑波形状:策略 A(时间压缩)。
- 想更悦耳、带“呼吸”感:策略 B(AM)。
- 想突出频段节律、做科普或艺术装置:策略 C(频带功率和弦)。
====================
十三、下一步你可以尝试的实验
- 比较闭眼 vs 睁眼 α 波:取两段 EEG → 频带功率声化 → 听 α 功率差异(闭眼 α 强时某个音更响)。
- 冥想前后:声音平稳度(包络方差)对比。
- 给不同实验事件打 Mark,在音频中混入提示音监听事件相关脑反应。
====================
十四、简化的一行式(只做最初体验)
(假设已预处理 raw)
import librosa, soundfile as sf; sf.write(“fast.wav”, librosa.resample(raw.get_data()[0], raw.info[‘sfreq’], 48000)/10, 48000)
====================
十五、注意与免责声明
- 这些声化输出仅用于探索 / 艺术 / 教学,不能作临床诊断。
- 若数据来自被试,请确保有伦理审批与同意书。
- 避免在公共传播中做未经验证的“健康功效”宣称。
如果你接下来想:
A. 做实时声化 (LSL + PyAudio)
B. 输出 MIDI 控制灯光或视觉
C. 使用深度学习提取特征后再声化
D. 基于多通道做空间化(Ambisonics)
继续告诉我,我会给定制方案。祝你玩得开心,生出有趣的“脑电音乐”!
V2
下面给你一个“从当前声化到真正‘有起伏、有结构、有多种音色的音乐’”的系统化路线。尽量分层、给思路、关键映射策略、结构/张力设计、以及可直接扩展到你现有脚本的模块化改造建议,并附上一些伪代码/示例。
======================================== 总体目标:把“直接映射的连续声”→“分层的音乐语言”
核心思想:不要直接把 EEG → 音符,而是两级映射:
- EEG/特征层(低层数据):多频段功率、功率比、瞬态事件、复杂度指标、同步性、熵、微状态……
- 中间语义层(Musical Descriptors):能量 Energy、紧张度 Tension、稳定度 Stability、复杂度 Complexity、情绪向量(明亮 Brightness / 情感 Valence / 唤醒 Arousal)
- 作曲引擎:根据这些描述选择(或转移)调式、和声、节奏密度、乐器编制、主题变体、动态。
- 渲染层:音色合成(你现有的 guqin/xiao + 新增采样/合成器)、MIDI 序列、自动混音。
======================================== 第一层:扩展 EEG 特征
除了各频带包络,建议新增:
- Band Ratios: alpha/theta, beta/(alpha+theta), (gamma)/(beta) —— 控制明亮度、复杂度。
- 瞬态能量突增:每个频带包络的一阶差分 + 峰值检测 → 触发事件(打击点、拨弦事件、鼓 fill)。
- 低频节律性指标:对 delta/theta 滑动自相关峰值的强度 → 决定全局 BPM 微调或节奏稳定度。
- 复杂度/熵:样本熵、近似熵、Permutation Entropy(滑窗)→ 控制和弦张力(加 7,9,11 扩展)。
- 相位同步/相干度(如 alpha 频道之间 PLV 平均)→ 决定“合奏是否同时进入/淡出”。
- 微状态(可选):对 EEG 进行微状态聚类(A/B/C/D),不同微状态映射不同段落主色彩。
- Frontal Asymmetry (F4-F3 alpha power diff) → 调式 (大调/小调) 或和声色彩(加入 ♭3、♭7)。
这些特征统一归一化 (0~1) 或标准化,送往“描述层”。
======================================== 第二层:中间“音乐描述”变量
定义几个连续变量(0~1):
- Energy:总功率(或加权:beta+gamma*0.7)
- Tension:熵/复杂度 + 高频占比 + 突发事件密度
- Brightness:alpha/beta 比、Frontal Asymmetry、谱重心
- Stability:低频节律性强度(自相关峰)
- Novelty:当前特征向量与过去窗口余弦距离
- Calm vs Active:Energy 与 Tension 的组合
再根据这些变量用逻辑/查表/规则机生成音乐参数:
- 调式 / Key:Brightness 高 & Asymmetry 正 → 大调;否则小调或多利亚。
- 和弦延伸:Tension 高 → 使用 7/9/11;Tension 低 → 三和弦。
- 节奏密度:Energy, Stability(高 → 均匀律动;低/不稳定 → 加 syncopation 或休止)。
- 乐器编制:Energy 上升 → 增加层;Energy 下降 → 减少层。
- 动态(音量曲线):Energy 平滑映射。
- Orchestration Color:Brightness 控制高频乐器(笛、弦泛音)是否出现。
======================================== 第三层:结构 / Form 设计
- 分段:对特征向量做滑动窗口 k-means / HDBSCAN / 突变检测(KL divergence 或 Cosine 距离 > 阈值),得出 Section 边界。
- 给段落贴标签(Cluster ID → A,B,C…)。相同类别的段落可重复主题。
- 规划宏观结构:Intro (低 Energy), Build (Energy / Tension 递增), Climax (峰值), Release (快速下降), Outro (平静)。
- 过渡策略:
- 在 Section 临界前 n 秒插入过渡和弦(如 V → I 或 IV → V → I)。
- 采用 ramp:把 event density 与 和弦张力线性插值到新段落 target 值。
- 张力曲线:用 Energy + Tension 生成全局目标曲线;如不足,可引入“虚拟调控”轻微人工引导(sculpting),保证音乐性。
======================================== 第四层:和声 / Chord Engine
流程:
- 选择调:key_center, scale_mode
- 生成基础和弦走向(progression):
- 根据 Stability:稳定高时用常见循环 (I – IV – V – I, I – vi – IV – V),稳定低时用模态交换 (bVII, bIII)。
- 根据 Tension:Tension 上升 → 向二级属(secondary dominant)过渡;高峰 → 加 ♯11、9、13。
- 即时调整:如果 Energy 突增事件 -> 插入一个“借用和弦”或“延迟和弦”制造亮点。
- 输出节拍级别 chord timeline,比如每 2 小节更新一次,或当 Section 变化时强制刷新。
示例伪代码(和弦选择):
def select_key(brightness, asym):
if brightness > 0.6 and asym > 0.55:
return ("D", "major")
elif brightness < 0.35:
return ("A", "dorian")
else:
return ("C", "minor")
def chord_extension(tension):
if tension < 0.3: return "triad"
elif tension 0.65:
return [["I","vi","IV","V"], ["I","IV","ii","V"]]
else:
return [["i","bVI","bIII","bVII"], ["i","iv","ii°","V"]]
def choose_progression(state):
base = progression_template(state["stability"])
block = base[int((state["novelty"]*len(base))%len(base))]
ext = chord_extension(state["tension"])
return [c + ("("+ext+")" if ext!="triad" else "") for c in block]
======================================== 第五层:节奏 / Rhythm Engine
- 全局 BPM:基于低频节律指标(Stability 高 → 稳定 BPM;低 → 轻微随机摆动 ±3 BPM)
- 分层节奏:
- 基础脉冲:四分或八分(驱动由 delta/theta)。
- 装饰事件:来自 gamma 突增 → 加击掌、轻击、泛音闪光。
- 事件对齐:所有触发对齐到最近 16th note(避免凌乱)。
- 节奏密度控制: density = clamp( 0.4Energy + 0.6Tension , 0.1, 1.0 ) 根据 density 选用一个 pattern 集合(如 drum pattern 变体)。
======================================== 第六层:旋律 / Motif Engine
- 建立一个动机库(motif):短的音程模式(如 [0,2,3,5], [0, -2, -3, 1])。
- 依据 Novelty/Entropy 决定是否引入新动机或变体(逆行、移调、节奏扩展)。
- Melodic register(音域)随 Brightness / Energy 上升。
- Pitch quantization:先根据 pitch_mod 的连续频率 → 最近音阶音,保持“EEG → melody”痕迹但在调内。
- 装饰音:高频事件(gamma spike)触发 grace note 或回旋。
示例(将连续 pitch_env 转为 quantized melody):
scale_pitches = [0,2,3,5,7,10] # e.g. Dorian
def quantize_pitch(p_cont, scale):
# p_cont in MIDI float
base = int(round(p_cont))
# 寻找最近的 scale 度数
for offset in range(12):
up = base + offset
down = base - offset
if (up % 12) in scale: return up
if (down % 12) in scale: return down
return base
melody = []
for frame in frames: # 每个节拍或 16 分
raw_midi = 60 + pitch_variation_from_alpha(frame) # 中央 C 基准
q = quantize_pitch(raw_midi, scale_pitches)
melody.append(q)
======================================== 第七层:音色 / Orchestration Engine
在你已有 guqin / xiao 基础上,扩展为“乐器槽”:
- Sustained Pad(可由平滑 alpha/theta 合成)
- Plucked Layer(guqin 事件)
- Breath/Wind(xiao)
- Percussion(触发自突增)
- Sparkles (高频 gamma → 钟琴/铃声/泛音)
决策:
- Energy 上升:添加 Sparkles + Percussion layer
- Tension 低:移除尖锐,高保留 pad 与 xiao
- Brightness 高:提升泛音/滤波开度
- Section 切换:淡入淡出特定组(避免突变)
可以设计一个 Orchestration State:
orch = {
"pad_level": f(Energy, Stability),
"pluck_density": f(Tension),
"wind_level": f(Brightness),
"perc_enable": gamma_spike_density > 0.2,
"sparkle_prob": Tension > 0.6
}
======================================== 第八层:事件总线 / 模块化架构
建议把系统重构为一个“时间驱动循环”:
- 建立全局 musical clock(以 ticks 或小节为单位)
- 每个 tick:
- 更新特征缓冲(滑动窗口特征)
- 生成/更新 descriptor
- 事件检测 & 发出事件 (Event objects)
- 和声/节奏/旋律引擎读取 descriptors & 事件 → 输出 note/chord/pattern
- Orchestration 根据 descriptors 更新层级电平
- 渲染:写入 MIDI 序列或实时合成 buffer
伪代码骨架:
class FeatureExtractor:
def update(eeg_chunk): ...
class DescriptorLayer:
def compute(features): return descriptors
class EventDetector:
def detect(features, prev_features): return events
class HarmonyEngine:
def next_chords(descriptors, events, bar_index): ...
class RhythmEngine:
def pattern(descriptors, events): ...
class MelodyEngine:
def melody(descriptors, chords, events): ...
class Orchestrator:
def mix_instruments(descriptors, events): ...
while not end:
eeg_chunk = read_next()
features = feat.update(eeg_chunk)
descriptors = desc.compute(features)
events = ev.detect(features, prev_features)
if new_bar:
chords = harm.next_chords(descriptors, events, bar_idx)
rhythm = rhyth.pattern(descriptors, events)
melody_notes = mel.melody(descriptors, chords, events)
audio = orch.render(chords, melody_notes, rhythm, descriptors)
write_audio(audio)
======================================== 第九层:MIDI + 后期
建议把音乐结构部分先输出为 MIDI,再用 DAW 或音源渲染更真实的古琴、笛、打击、Pad。
- Python 库:mido / pretty_midi
- 优点:快速换音色、编曲、混音
- Work Flow:
- EEG → MIDI(节奏、旋律、和声、控制器)
- DAW 导入 → 选音色(软音源/采样)
- 自动加混响、EQ、轻压缩
- 进行人工微调
======================================== 第十层:张力模型 (可选进阶)
用一个“目标张力曲线”T_target(t),实时张力 T_real(t) 来驱动“纠偏”(比如当前 EEG 太平时,稍扶起一些和声变化,防止音乐无聊):
- error = T_target – T_real
- 如果 error > ε,允许注入“虚拟事件”或强制使用非主和弦(如次属)。
- 这样保证作品整体宏观叙事,而不是被 EEG 完全牵制。
======================================== 实际落地的 3 个阶段(建议步骤)
阶段 1(结构化):
- 提取 features + descriptors
- 做 section segmentation + 简单 chord progression (I-IV-V / i-bVII-bVI-V)
- 旋律:量化 pitch_env 到音阶
阶段 2(多层编配):
- 引入 Orchestrator 状态机 + Percussion 事件
- 事件拨弦 + 节奏 quantize
- MIDI 输出 + DAW 测试音色
阶段 3(细化与情感):
- 熵/复杂度驱动 tension
- 动机库 + 变体
- 张力曲线引导
- 自动混音(简单 EQ + Reverb send)
======================================== 示例:将你的当前包络映射到 descriptors(简化)
def descriptors_from_env(envs):
# envs: dict band_name -> np.array
alpha = envs["alpha"]
beta = envs["beta"]
gamma = envs["gamma"]
theta = envs["theta"]
delta = envs["delta"]
# 取末尾窗口平均(或滚动)
def avg(x): return float(np.mean(x[-512:])) # 假设高采样
A=avg(alpha); B=avg(beta); G=avg(gamma); T=avg(theta); D=avg(delta)
total = A+B+G+T+D + 1e-9
energy = (B+G)/total
brightness = (A+B)/(total)
tension = (G + 0.5*B)/(total)
stability = (D+T)/(total)
# 归一裁剪
return {
"energy": float(np.clip(energy,0,1)),
"brightness": float(np.clip(brightness,0,1)),
"tension": float(np.clip(tension,0,1)),
"stability": float(np.clip(stability,0,1))
}
======================================== 防止“随机散乱”的关键技巧
- 时间分辨率层次:快速层(毫秒)只给微调参数(滤波开度等),中层(beat)决定旋律音符,慢层(bar/section)决定和声与编制。
- 滞后 / Hysteresis:改变调式、音色编制时设两个阈值(进入与退出)避免频繁抖动。
- 指数平滑 + 限速:descriptor(t) = descriptor(t-1) + α*(raw – descriptor(t-1)),限制每秒最大变化量。
- 稀疏事件:用最小间隔、概率衰减防止“事件雪崩”。
- 动态范围压缩:对高频突增进行概率采样而非全部映射。
======================================== 音色扩展建议
- 加入“Pad”:基于多个宽带噪声 + 共振峰(formant) → 氛围层
- 加入“击乐”:
- 简化 drum:kick (低频瞬态), snare (beta 突增), hat (gamma 间隔)
- Sparkle/Bell:短脉冲 + 频率包络 + 长尾混响
======================================== 若你要我下一步提供的内容(可选)
A. 一个“最小作曲引擎”Python 模块:输入 envs → 输出简易 MIDI(旋律+和弦) B. 具体 tension→和弦扩展映射表 C. 采样加载(pretty_midi + soundfont)示例 D. 微状态分段代码(如果你提供 EEG 通道数)
告诉我你优先需要哪一个,我再写“可直接运行的升级版”。
======================================== 总结
把“科学声化”升级为“音乐”需要:抽象层(descriptors)、节奏/和声/动机结构、时间尺度分离、事件稀疏化与情绪张力控制。先实现 Section + 和弦 + quantized melody,再逐步加 orchestration、事件拨弦、MIDI 输出与后期即可。
======================================== 请反馈
- 你最想先做哪一步?(和声 / 节奏 / MIDI 输出 / 动机系统)
- 是否需要我先给一个“feature→chord + melody”的最小完整脚本?
- 是否计划用 DAW 后期(可以决定我给你的输出形式:WAV 还是 MIDI)?
回复你的优先项,我下一条直接给你相应的代码与进一步细化。
v3
下面给出一个“单文件”示例,把多模块整合:从 EEG(BrainVision .vhdr)读取 → 提取频带特征 → 生成描述变量(energy / tension / brightness / stability / novelty)→ 分段(Section)→ 生成和弦进程 → 旋律(动机+量化到音阶)→ 节奏/打击层 → 编配 → 输出 MIDI(使用 pretty_midi 或 mido)→ 可选快速合成一个参考 WAV(简单合成+古琴/萧示意,不追求真实混音)。
特点(精简/教学用骨架):
- 各功能用类封装但仍全部处于一个文件里,便于一次运行。
- 和声:基于描述变量选择调式、和弦扩展(7/9)与进行模板。
- 旋律:使用动机库 + 描述调制音域 + EEG pitch 映射 + 量化到音阶。
- 打击:简单鼓组(Kick / Snare / Hat)按 Energy / Tension / Gamma 事件密度生成。
- Event 检测:gamma 突增、包络差分。
- 可调参数通过命令行。
- 参考音频合成只为预听:guqin/xiao/pad/perc 简化物理+加法/噪声模型(与上一版风格一致但大幅精简)。
- 代码尽量自注释;真实项目可再拆分、优化、加错误处理。
依赖(请先 pip 安装缺失库): pip install mne numpy scipy pretty_midi mido soundfile
运行示例: python eeg_music_pipeline.py –file your.vhdr –channel 0 –outdir output –minutes 2 –bpm_auto (若没有 pretty_midi 会自动 fallback 到 mido 简易 MIDI 写出)
注意:
- 这里只生成一个基础音乐骨架;结果音乐性取决于 EEG 特征波动。若 EEG 单调,你可尝试缩短窗口、增加突增灵敏度或人工扰动 tension。
- 真实制作请导出 MIDI 到 DAW 做音色替换、混响、压缩等后期。
完整代码(保存为 eeg_music_pipeline.py)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
EEG → 多层描述 → 和声 / 旋律 / 节奏 / 编配 → MIDI + 参考音频 (单文件示例)
说明:
这是一个教学/原型性质的“一体化脚本”,展示如何把 EEG 频带特征转为更“像音乐”的结构。
为保持紧凑,算法做了大量简化与启发式设计。
你可以基于此继续扩展:更复杂的和弦语法、概率模型、神经网络辅助、真实采样渲染等。
主要流程:
1. 读取 BrainVision EEG
2. 频带滤波 -> 窗口功率/包络 -> 特征 (band powers, ratios, entropy 近似, spike events)
3. 条件平滑 + 描述变量 (energy, tension, brightness, stability, novelty)
4. Section 分段 (基于描述向量余弦突变 & 最小长度)
5. 和声引擎: 选调式 -> 进行模板 -> 和弦扩展
6. 旋律引擎: motif + 描述调制音高范围 + EEG pitch base + 量化
7. 节奏/打击: Energy / tension / gamma spikes 决定密度
8. Orchestrator: 分配乐器轨 (Pad / Guqin / Xiao / Perc / Sparkle)
9. MIDI 序列化
10. 简易音频合成 (可关闭) 仅供快速试听
命令行参数: python eeg_music_pipeline.py -h
作者: 你可以自行署名
"""
import argparse, os, sys, math, json, warnings
import numpy as np
import scipy.signal as sig
import soundfile as sf
# 第三方依赖
try:
import mne
except ImportError:
print("需要安装 mne: pip install mne")
sys.exit(1)
# MIDI 库 (优先 pretty_midi)
try:
import pretty_midi
HAS_PRETTY = True
except ImportError:
HAS_PRETTY = False
try:
import mido
except ImportError:
print("需要安装 pretty_midi 或 mido 以输出 MIDI。")
sys.exit(1)
# ========================= 参数与常量 =========================
BANDS = [
("delta", 1, 4),
("theta", 4, 8),
("alpha", 8, 13),
("beta", 13, 30),
("gamma", 30, 45)
]
# 可用音阶 (简化): mode -> semitone set
SCALE_MODES = {
"major": [0,2,4,5,7,9,11],
"minor": [0,2,3,5,7,8,10],
"dorian": [0,2,3,5,7,9,10],
"mixolydian":[0,2,4,5,7,9,10],
"phrygian":[0,1,3,5,7,8,10]
}
MOTIFS = [
[0,2,3,5],
[0,2,4,7],
[0,-2,-3,1],
[0,3,5,7],
[0,4,5,2]
]
DRUM_MAP = { # General MIDI number
"kick": 36,
"snare": 38,
"hat": 42
}
# ========================= 工具函数 =========================
def butter_band(f1, f2, fs, order=4):
return sig.butter(order, [f1, f2], btype='band', fs=fs, output='sos')
def rms(x):
return math.sqrt(np.mean(x**2) + 1e-20)
def moving_average(x, k):
if k <= 1: return x
return np.convolve(x, np.ones(k)/k, mode='same')
def cosine_distance(a, b):
na = np.linalg.norm(a) + 1e-12
nb = np.linalg.norm(b) + 1e-12
return 1 - np.dot(a, b) / (na * nb)
def sample_entropy_approx(x):
# 极简近似:用标准差+自相关峰替代 (正式请用真正样本熵)
if len(x) 3:
peak = np.max(ac[1:3]) / (ac[0] + 1e-12)
else:
peak = 0
# 低自相关 -> 高熵
ent = s * (1 - peak)
return float(ent)
def normalize01(x):
if np.max(x) - np.min(x) self.novelty_thresh and (i - last_change) >= self.min_bars:
sections.append((start, i-1))
start = i
last_change = i
sections.append((start, len(bar_descs)-1))
# 标记标签 A,B,C...
labeled = []
label_seq = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for idx,(s,e) in enumerate(sections):
label = label_seq[idx % len(label_seq)]
labeled.append({"label":label,"start_bar":s,"end_bar":e})
return labeled
class HarmonyEngine:
def __init__(self, base_key="C"):
self.base_key = base_key
def choose_mode(self, brightness, tension):
if brightness > 0.65 and tension 0.65 and brightness < 0.55:
return "phrygian"
if 0.45 < brightness < 0.7 and tension 0.6 and brightness > 0.6:
return "mixolydian"
return "minor"
def progression_template(self, stability, tension):
if stability > 0.6 and tension 0.65:
return [["i","bVI","bIII","bVII"], ["i","iv","ii°","V"], ["i","bVII","bVI","V"]]
# 混合
return [["i","bVII","IV","V"], ["I","vi","IV","V"]]
def chord_extension(self, tension):
if tension < 0.35: return ""
if tension 小三和弦 (i)
intervals = [0,3,7]
else:
intervals = [0,4,7]
if ext == "7":
intervals.append(10 if qual.lower()==qual else 11)
elif ext == "9":
intervals.extend([10 if qual.lower()==qual else 11, 14])
pitches = [root_midi + iv for iv in intervals]
return pitches
def roman_to_root(self, roman, key_root, mode_scale):
# roman: I, vi, bVII, ii°, etc.
accidental = 0
main = roman
if roman.startswith("b"):
accidental = -1
main = roman[1:]
if roman.startswith("#"):
accidental = 1
main = roman[1:]
degree_map = {"I":0,"II":1,"III":2,"IV":3,"V":4,"VI":5,"VII":6,
"i":0,"ii":1,"iii":2,"iv":3,"v":4,"vi":5,"vii":6}
deg = degree_map.get(main,0)
scale = SCALE_MODES[mode_scale]
# degrees beyond scale length wrap
sc_degree = scale[deg % len(scale)]
return key_root + sc_degree + accidental
def build_chords(self, bar_descs):
# 为每个 bar 生成 chord (根音+音符)
chords = []
# 选全局调式? 或每 Section 重新选。简化:按 bar 动态
for i, d in enumerate(bar_descs):
mode = self.choose_mode(d["brightness"], d["tension"])
templates = self.progression_template(d["stability"], d["tension"])
template = templates[i % len(templates)]
roman = template[i % len(template)]
ext = self.chord_extension(d["tension"])
key_root = 60 # 中央 C 做根 (C4)
root = self.roman_to_root(roman, key_root, mode)
pitches = self.chord_to_pitches(root, roman, ext, mode)
chords.append({
"bar": i,
"mode": mode,
"roman": roman + ext,
"root": root,
"pitches": pitches
})
return chords
class MelodyEngine:
def __init__(self, base_octave=5):
self.base_oct = base_octave
self.motif_index = 0
def choose_motif(self, novelty, tension):
# 简单映射
idx = int((novelty*0.5 + tension*0.5) * (len(MOTIFS)-1))
idx = np.clip(idx,0,len(MOTIFS)-1)
return MOTIFS[idx]
def gen_bar_melody(self, bar_desc, chord, steps_per_bar=16):
# 生成 16 分音序列
motif = self.choose_motif(bar_desc["novelty"], bar_desc["tension"])
scale = SCALE_MODES[chord["mode"]]
energy = bar_desc["energy"]
tension = bar_desc["tension"]
brightness = bar_desc["brightness"]
# 音域范围: base_oct 调节
base_midi = 60 + 12*(self.base_oct-4) # 例如 base_oct=5 -> 72
span = 5 + int(tension*3) # 可用级数
notes = []
motif_pos = 0
prob_rest = max(0.05, 0.35 - energy*0.3) # 能量高 -> 少休止
for step in range(steps_per_bar):
if np.random.rand() 0.7 and np.random.rand() list of drum hits
pattern = {i:[] for i in range(self.steps_per_bar)}
energy = bar_desc["energy"]
tension = bar_desc["tension"]
# Kick: on 0, (8) 可选 + 随机
pattern[0].append("kick")
if energy > 0.5:
pattern[8].append("kick")
if energy > 0.7 and np.random.rand() 0.3:
pattern[4].append("snare")
pattern[12].append("snare")
# Hat: 密度取决于 gamma_spike_density
hat_density = 0.3 + 0.5*gamma_spike_density + 0.2*energy
for s in range(self.steps_per_bar):
if np.random.rand() < hat_density:
pattern[s].append("hat")
return pattern
class EventDetector:
def __init__(self, thresh=0.6):
self.thresh = thresh
def gamma_spikes(self, gamma_env, fs, segment_samples):
# 以 segment_samples 为窗口大小统计局部峰值
spikes_per_window = []
for start in range(0, len(gamma_env), segment_samples):
seg = gamma_env[start:start+segment_samples]
if len(seg)<3: break
# 简单阈值 = 均值 + 1 std
mu = np.mean(seg)
sd = np.std(seg)
thr = mu + 1.0*sd
peaks, _ = sig.find_peaks(seg, height=thr)
spikes_per_window.append(len(peaks))
if not spikes_per_window:
return 0.0
return float(np.mean(spikes_per_window)) / (segment_samples/fs + 1e-9)
# ========================= 音频合成 (极简示例) =========================
class SimpleSynth:
def __init__(self, sr=44100):
self.sr = sr
def gen_pad(self, freqs, dur, amp=0.3):
n = int(dur*self.sr)
t = np.linspace(0,dur,n,endpoint=False)
out = np.zeros(n)
for f in freqs:
ph = 2*np.pi*f*t
out += np.sin(ph)*0.5 + 0.5*np.sin(ph*0.5)
env = np.clip(np.linspace(0,1,int(0.3*n)),0,1)
tail = np.clip(np.linspace(1,0,int(0.2*n)),0,1)
e = np.ones(n)
e[:len(env)] *= env
e[-len(tail):] *= tail
out = out * e
out = sig.lfilter([1],[1,-0.995], out) # 轻度谐波堆积
out = out / (np.max(np.abs(out))+1e-9) * amp
return out
def gen_pluck(self, f0, dur, amp=0.4):
n = int(dur*self.sr)
if n < 8: return np.zeros(n)
# Karplus-Strong 简化
length = max(2, int(self.sr / max(f0,40)))
buf = np.random.randn(length)
out = np.zeros(n)
idx = 0
for i in range(n):
avg = 0.5*(buf[idx] + buf[(idx+1)%length])
out[i] = buf[idx] = avg * 0.996
idx = (idx+1)%length
env = np.exp(-np.linspace(0,1,n)*4)
out *= env
out = out / (np.max(np.abs(out))+1e-9) * amp
return out
def gen_flute(self, f0, dur, amp=0.3):
n = int(dur*self.sr)
t = np.linspace(0,dur,n,endpoint=False)
sigf = np.sin(2*np.pi*f0*t)
breath = np.random.randn(n)
# 低通 breath
b,a = sig.butter(2, 3000/(self.sr/2), btype='low')
breath = sig.filtfilt(b,a,breath)
breath *= 0.15
vib = 0.005*np.sin(2*np.pi*5.5*t)
sigf = np.sin(2*np.pi*f0*(1+vib)*t)
out = sigf + breath
env = np.sin(np.linspace(0, np.pi, n))**1.2
out *= env
out = out/(np.max(np.abs(out))+1e-9)*amp
return out
def mix_tracks(self, tracks):
length = max([len(tr) for tr in tracks]) if tracks else 0
mix = np.zeros(length)
for tr in tracks:
mix[:len(tr)] += tr
mix = soft_limiter(mix, 1.2)
mix /= (np.max(np.abs(mix))+1e-9)
return mix
# ========================= MIDI 渲染 =========================
class MidiRenderer:
def __init__(self, bpm=100, steps_per_bar=16, bars=0):
self.bpm = bpm
self.spb = steps_per_bar
self.seconds_per_beat = 60.0 / bpm
self.seconds_per_bar = self.seconds_per_beat * 4
self.seconds_per_step = self.seconds_per_bar / steps_per_bar
self.bars = bars
def render(self, chords, melodies, percussions, out_path):
if HAS_PRETTY:
pm = pretty_midi.PrettyMIDI(initial_tempo=self.bpm)
inst_pad = pretty_midi.Instrument(program=89, name="Pad") # Pad
inst_mel = pretty_midi.Instrument(program=74, name="Lead") # Flute-like
inst_plk = pretty_midi.Instrument(program=24, name="Guitar") # Pluck
inst_spr = pretty_midi.Instrument(program=11, name="Sparkle") # Vibraphone
drum_inst = pretty_midi.Instrument(program=0, is_drum=True, name="Drums")
# Chords (Pad) & Pluck accent on bar start
for ch in chords:
start = ch["bar"] * self.seconds_per_bar
end = start + self.seconds_per_bar
for p in ch["pitches"]:
note = pretty_midi.Note(velocity=55, pitch=int(p), start=start, end=end)
inst_pad.notes.append(note)
# Pluck root accent
note = pretty_midi.Note(velocity=72, pitch=int(ch["pitches"][0]), start=start, end=start+0.4)
inst_plk.notes.append(note)
# Melody
for bar_idx, seq in enumerate(melodies):
for step, pitch in enumerate(seq):
if pitch is None: continue
st = bar_idx*self.seconds_per_bar + step*self.seconds_per_step
dur = self.seconds_per_step * 0.9
vel = 60
note = pretty_midi.Note(velocity=vel, pitch=int(pitch), start=st, end=st+dur)
inst_mel.notes.append(note)
# Sparkle (octave up occasional)
if np.random.rand() Music 单文件原型 (和声/旋律/节奏/MIDI)")
parser.add_argument("--file", required=True, help=".vhdr 文件路径")
parser.add_argument("--channel", type=int, default=0, help="使用的 EEG 通道索引")
parser.add_argument("--minutes", type=float, default=2.0, help="截取前多少分钟 (0=全部)")
parser.add_argument("--win", type=float, default=1.0, help="特征窗口秒")
parser.add_argument("--step", type=float, default=0.5, help="特征步进秒")
parser.add_argument("--bars_per_chunk", type=int, default=1, help="1 bar = 4 拍")
parser.add_argument("--bpm", type=int, default=96, help="固定 BPM (若不用自动)")
parser.add_argument("--bpm_auto", action="store_true", help="根据 stability 自动 BPM")
parser.add_argument("--steps_per_bar", type=int, default=16, help="旋律/节奏步分辨率 (16 -> 十六分音符)")
parser.add_argument("--outdir", type=str, default="out_music")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--no_audio", action="store_true", help="不生成参考 WAV")
parser.add_argument("--audio_sr", type=int, default=44100)
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
np.random.seed(args.seed)
if not os.path.isfile(args.file):
print("文件不存在:", args.file); sys.exit(1)
print("读取 EEG ...")
raw = mne.io.read_raw_brainvision(args.file, preload=True, verbose=False)
raw.pick_types(eeg=True)
data = raw.get_data()
if args.channel >= data.shape[0]:
print("通道索引超范围")
sys.exit(1)
eeg = data[args.channel]
fs = int(raw.info["sfreq"])
# 截取
if args.minutes > 0:
max_samples = int(args.minutes * 60 * fs)
eeg = eeg[:max_samples]
eeg = eeg - np.mean(eeg)
# 特征提取
print("提取特征...")
fe = EEGFeatureExtractor(fs, win_sec=args.win, step_sec=args.step)
feat_list, band_envs = fe.extract(eeg)
if len(feat_list) < 4:
print("数据太短或参数过大,无法继续。")
sys.exit(1)
# 描述
dl = DescriptorLayer()
desc_list, vecs = dl.compute_batch(feat_list)
# 将窗口映射到 bar: 用窗口中心时间对齐
# 先确定 BPM
mean_stability = np.mean([d["stability"] for d in desc_list])
if args.bpm_auto:
# 在 80~120 范围波动
bpm = int(80 + mean_stability * 40)
else:
bpm = args.bpm
print(f"确定 BPM = {bpm}")
seconds_per_bar = 60.0 / bpm * 4
total_time = desc_list[-1]["time"]
total_bars = int(total_time / seconds_per_bar)
if total_bars < 4:
total_bars = max(4, total_bars)
# 聚合为 bar 描述
bar_descs = []
for b in range(total_bars):
t0 = b * seconds_per_bar
t1 = (b+1) * seconds_per_bar
seg = [d for d in desc_list if t0 <= d["time"] 用于节奏
ed = EventDetector()
# 估算 gamma spike density (用整个 gamma_env 分段)
gamma_env = band_envs["gamma"]
seg_len = int(fs * args.win)
gamma_spike_density = ed.gamma_spikes(gamma_env, fs, seg_len)
if args.debug:
print("Gamma spike density ~", gamma_spike_density)
rhythm = RhythmEngine(steps_per_bar=args.steps_per_bar)
percussions = []
for bdesc in bar_descs:
patt = rhythm.gen_percussion(bdesc, gamma_spike_density)
percussions.append(patt)
# 输出目录
os.makedirs(args.outdir, exist_ok=True)
# MIDI 渲染
midi_path = os.path.join(args.outdir, "eeg_music_output.mid")
mr = MidiRenderer(bpm=bpm, steps_per_bar=args.steps_per_bar, bars=total_bars)
mr.render(chords, melodies, percussions, midi_path)
print("MIDI 已输出:", midi_path)
# 元数据保存
meta = {
"file": args.file,
"channel": args.channel,
"bpm": bpm,
"total_bars": total_bars,
"sections": sections,
"chords": chords[:min(16,len(chords))], # 截取一部分预览
"params": {
"win": args.win,
"step": args.step,
"steps_per_bar": args.steps_per_bar
}
}
with open(os.path.join(args.outdir,"metadata.json"),"w",encoding="utf-8") as f:
json.dump(meta,f,ensure_ascii=False,indent=2)
if args.no_audio:
print("跳过参考音频合成 (--no_audio)")
return
print("合成参考音频(简化示例)...")
synth = SimpleSynth(sr=args.audio_sr)
tracks = []
# 基础 Pad (用和弦)
for ch in chords:
bar_start = ch["bar"] * seconds_per_bar
freqs = [440 * 2**((p-69)/12) for p in ch["pitches"][:3]]
pad = synth.gen_pad(freqs, seconds_per_bar, amp=0.20)
# 放到时间线上
n_bar = len(pad)
# 确保混合长度
if len(tracks)==0:
tracks.append(np.zeros(int((total_bars+1)*seconds_per_bar*synth.sr)))
tracks[0][int(bar_start*synth.sr):int(bar_start*synth.sr)+n_bar] += pad
# Pluck: 每小节根音 + 中间一次
pluck_track = np.zeros_like(tracks[0])
for ch in chords:
bar_start = ch["bar"] * seconds_per_bar
root_f = 440*2**((ch["pitches"][0]-69)/12)
plk = synth.gen_pluck(root_f, 0.6, amp=0.35)
idx = int(bar_start*synth.sr)
pluck_track[idx:idx+len(plk)] += plk
# 中间点
mid_t = bar_start + seconds_per_bar/2
plk2 = synth.gen_pluck(root_f*2, 0.4, amp=0.25)
idx2 = int(mid_t*synth.sr)
pluck_track[idx2:idx2+len(plk2)] += plk2
tracks.append(pluck_track)
# Melody (flute)
mel_track = np.zeros_like(tracks[0])
step_dur = seconds_per_bar / args.steps_per_bar
for bar_idx, seq in enumerate(melodies):
for step, pitch in enumerate(seq):
if pitch is None: continue
start = (bar_idx*seconds_per_bar + step*step_dur)
dur = step_dur*0.9
f0 = 440*2**((pitch-69)/12)
note = synth.gen_flute(f0, dur, amp=0.25)
idx = int(start*synth.sr)
mel_track[idx:idx+len(note)] += note
tracks.append(mel_track)
# Perc (hat: noise bursts, kick: low sin, snare: noise)
perc_track = np.zeros_like(tracks[0])
for bar_idx, patt in enumerate(percussions):
for step, hits in patt.items():
start = (bar_idx*seconds_per_bar + step*step_dur)
idx = int(start*synth.sr)
for h in hits:
if h == "kick":
dur = 0.25
n = int(dur*synth.sr)
t = np.linspace(0,dur,n,endpoint=False)
w = np.sin(2*np.pi*60*t)*np.exp(-t*8)
perc_track[idx:idx+n] += w*0.8
elif h == "snare":
dur = 0.2
n = int(dur*synth.sr)
noise = np.random.randn(n)
noise = sig.lfilter([1],[1,-0.6],noise)
env = np.exp(-np.linspace(0,1,n)*10)
perc_track[idx:idx+n] += noise*env*0.5
elif h == "hat":
dur = 0.08
n = int(dur*synth.sr)
noise = np.random.randn(n)
env = np.exp(-np.linspace(0,1,n)*25)
perc_track[idx:idx+n] += noise*env*0.25
tracks.append(perc_track)
mix = synth.mix_tracks(tracks)
wav_path = os.path.join(args.outdir,"eeg_music_demo.wav")
sf.write(wav_path, mix.astype(np.float32), synth.sr)
print("参考音频已输出:", wav_path)
print("完成。请将 MIDI 导入 DAW,替换真实音色后再混音。")
if __name__ == "__main__":
with warnings.catch_warnings():
warnings.simplefilter("ignore")
main()
后续可扩展方向(可选):
- 用真实乐器采样(SoundFont)通过 fluidsynth 或 sf2 loader 渲染。
- 改 SectionSegmenter 为聚类(k-means / HDBSCAN)得到结构标签。
- HarmonyEngine 增加调中心迁移(转调 / 借用和弦表)。
- MelodyEngine 添加动机变形(逆行、转位、节奏拉伸)。
- 节奏引擎使用模式库 + VAE / Markov。
- 引入张力目标曲线对 chord tension 做纠偏。
需要我下一步提供某一块“增强版”(比如更智能和弦或更真实音色)就告诉我。祝你实验顺利!
发表回复