脑电数据转为音乐

v1

下面给你一个系统、可操作的“EEG(BrainVision 文件)→ 声音”转换(脑电声化 / Sonification)方案。内容包括:总体思路、准备环境、基础预处理、三类常见声化策略(直接时间压缩法 / 载波调制法 / 频带映射合成法)、进阶增强、完整示例代码、常见问题与优化建议。你可以先跑“最简方案”快速得到声音,再逐步优化艺术性或分析价值。

====================
一、总体思路(为什么 EEG 可变成音频)
EEG 原始频段主要 0.5–40 Hz(有时到 80+ Hz),远低于人耳 20–20000 Hz。要“听到”其结构,需要把这些慢信号映射到可听频段或通过调制让其影响可听载波。典型做法:

  1. 时间压缩 / 加速(Resample):将采样率从 ~500 Hz 拉升到 48 kHz,相当于把所有频率整体乘以 ~96,使 10 Hz → 960 Hz。
  2. 频率搬移(Heterodyning / Frequency Shift):用希尔伯特变换 + 乘以 e^(j2πft) 将频谱整体上移。
  3. 载波调制(AM/FM):低频 EEG 作为一个或多个可听载波 (例如 220/440/660 Hz) 的振幅或频率包络。
  4. 多频带映射:将 δ, θ, α, β, γ 各带功率映射到不同乐音/通道或音色。

====================
二、环境准备
建议 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)

====================
四、基础预处理(建议)

  1. 选择 EEG 通道(去除 EOG/EMG)
  2. 重参考:raw.set_eeg_reference(‘average’) 或选择双乳突/特定参考
  3. 滤波:常用带通 1–40 Hz(去 DC 漂移 & 高频噪声)
  4. 可选:ICA 去眼动(对音频美感影响小,可先跳过)
  5. 标度: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:频带功率 → 多音合成(“脑电乐器”)

  1. 将 EEG 分解成 c(1–4), θ(4–8), α(8–13), β(13–30), γ(30–45) 五段。
  2. 用滑动窗口 (e.g., 0.5 s 步长) 计算每段功率 (Welch)。
  3. 将每段功率标准化后映射到对应音高/振幅:
    • δ → 220 Hz (A3)
    • θ → 261.6 Hz (C4)
    • α → 329.6 Hz (E4)
    • β → 392.0 Hz (G4)
    • γ → 523.3 Hz (C5)
  4. 在窗口内合成正弦或包络平滑的波,最后拼接 & 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)

====================
九、进阶增强点

  1. 立体声/空间化:
    • 左半球通道平均 → 左声道;右半球 → 右声道。
    • 或不同频带放不同声道,增强可分辨性。
  2. 动态范围控制:使用对数/双曲正切压缩抑制异常尖峰。
  3. 去伪迹:利用 ICA 去除眨眼 (EOG) 前再声化,声音更平滑。
  4. 频率搬移(替代时间加速):
    • analytic = hilbert(signal)
    • shift = np.real(analytic np.exp(1j2np.pishift_hz*t))
    • shift_hz 例如 300 Hz,把 1–40 Hz 搬到 301–340 Hz;再放大。
  5. 多通道合成:为每个选定通道分配一个音阶(例如左到右映射到一个五声音阶),做一个“脑电合唱”。
  6. 与事件标记同步:读取 .vmrk 中的刺激事件,在音频中插入短促“点击”或打击音做参考。
  7. 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(频带功率和弦)。

====================
十三、下一步你可以尝试的实验

  1. 比较闭眼 vs 睁眼 α 波:取两段 EEG → 频带功率声化 → 听 α 功率差异(闭眼 α 强时某个音更响)。
  2. 冥想前后:声音平稳度(包络方差)对比。
  3. 给不同实验事件打 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 → 音符,而是两级映射:

  1. EEG/特征层(低层数据):多频段功率、功率比、瞬态事件、复杂度指标、同步性、熵、微状态……
  2. 中间语义层(Musical Descriptors):能量 Energy、紧张度 Tension、稳定度 Stability、复杂度 Complexity、情绪向量(明亮 Brightness / 情感 Valence / 唤醒 Arousal)
  3. 作曲引擎:根据这些描述选择(或转移)调式、和声、节奏密度、乐器编制、主题变体、动态。
  4. 渲染层:音色合成(你现有的 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 设计

  1. 分段:对特征向量做滑动窗口 k-means / HDBSCAN / 突变检测(KL divergence 或 Cosine 距离 > 阈值),得出 Section 边界。
  2. 给段落贴标签(Cluster ID → A,B,C…)。相同类别的段落可重复主题。
  3. 规划宏观结构:Intro (低 Energy), Build (Energy / Tension 递增), Climax (峰值), Release (快速下降), Outro (平静)。
  4. 过渡策略:
    • 在 Section 临界前 n 秒插入过渡和弦(如 V → I 或 IV → V → I)。
    • 采用 ramp:把 event density 与 和弦张力线性插值到新段落 target 值。
  5. 张力曲线:用 Energy + Tension 生成全局目标曲线;如不足,可引入“虚拟调控”轻微人工引导(sculpting),保证音乐性。

======================================== 第四层:和声 / Chord Engine

流程:

  1. 选择调:key_center, scale_mode
  2. 生成基础和弦走向(progression):
    • 根据 Stability:稳定高时用常见循环 (I – IV – V – I, I – vi – IV – V),稳定低时用模态交换 (bVII, bIII)。
    • 根据 Tension:Tension 上升 → 向二级属(secondary dominant)过渡;高峰 → 加 ♯11、9、13。
  3. 即时调整:如果 Energy 突增事件 -> 插入一个“借用和弦”或“延迟和弦”制造亮点。
  4. 输出节拍级别 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

  1. 建立一个动机库(motif):短的音程模式(如 [0,2,3,5], [0, -2, -3, 1])。
  2. 依据 Novelty/Entropy 决定是否引入新动机或变体(逆行、移调、节奏扩展)。
  3. Melodic register(音域)随 Brightness / Energy 上升。
  4. Pitch quantization:先根据 pitch_mod 的连续频率 → 最近音阶音,保持“EEG → melody”痕迹但在调内。
  5. 装饰音:高频事件(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
}

======================================== 第八层:事件总线 / 模块化架构

建议把系统重构为一个“时间驱动循环”:

  1. 建立全局 musical clock(以 ticks 或小节为单位)
  2. 每个 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:
    1. EEG → MIDI(节奏、旋律、和声、控制器)
    2. DAW 导入 → 选音色(软音源/采样)
    3. 自动加混响、EQ、轻压缩
    4. 进行人工微调

======================================== 第十层:张力模型 (可选进阶)

用一个“目标张力曲线”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))
    }

======================================== 防止“随机散乱”的关键技巧

  1. 时间分辨率层次:快速层(毫秒)只给微调参数(滤波开度等),中层(beat)决定旋律音符,慢层(bar/section)决定和声与编制。
  2. 滞后 / Hysteresis:改变调式、音色编制时设两个阈值(进入与退出)避免频繁抖动。
  3. 指数平滑 + 限速:descriptor(t) = descriptor(t-1) + α*(raw – descriptor(t-1)),限制每秒最大变化量。
  4. 稀疏事件:用最小间隔、概率衰减防止“事件雪崩”。
  5. 动态范围压缩:对高频突增进行概率采样而非全部映射。

======================================== 音色扩展建议

  • 加入“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 输出与后期即可。

======================================== 请反馈

  1. 你最想先做哪一步?(和声 / 节奏 / MIDI 输出 / 动机系统)
  2. 是否需要我先给一个“feature→chord + melody”的最小完整脚本?
  3. 是否计划用 DAW 后期(可以决定我给你的输出形式:WAV 还是 MIDI)?

回复你的优先项,我下一条直接给你相应的代码与进一步细化。

v3

下面给出一个“单文件”示例,把多模块整合:从 EEG(BrainVision .vhdr)读取 → 提取频带特征 → 生成描述变量(energy / tension / brightness / stability / novelty)→ 分段(Section)→ 生成和弦进程 → 旋律(动机+量化到音阶)→ 节奏/打击层 → 编配 → 输出 MIDI(使用 pretty_midi 或 mido)→ 可选快速合成一个参考 WAV(简单合成+古琴/萧示意,不追求真实混音)。

特点(精简/教学用骨架):

  1. 各功能用类封装但仍全部处于一个文件里,便于一次运行。
  2. 和声:基于描述变量选择调式、和弦扩展(7/9)与进行模板。
  3. 旋律:使用动机库 + 描述调制音域 + EEG pitch 映射 + 量化到音阶。
  4. 打击:简单鼓组(Kick / Snare / Hat)按 Energy / Tension / Gamma 事件密度生成。
  5. Event 检测:gamma 突增、包络差分。
  6. 可调参数通过命令行。
  7. 参考音频合成只为预听:guqin/xiao/pad/perc 简化物理+加法/噪声模型(与上一版风格一致但大幅精简)。
  8. 代码尽量自注释;真实项目可再拆分、优化、加错误处理。

依赖(请先 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()

后续可扩展方向(可选):

  1. 用真实乐器采样(SoundFont)通过 fluidsynth 或 sf2 loader 渲染。
  2. 改 SectionSegmenter 为聚类(k-means / HDBSCAN)得到结构标签。
  3. HarmonyEngine 增加调中心迁移(转调 / 借用和弦表)。
  4. MelodyEngine 添加动机变形(逆行、转位、节奏拉伸)。
  5. 节奏引擎使用模式库 + VAE / Markov。
  6. 引入张力目标曲线对 chord tension 做纠偏。

需要我下一步提供某一块“增强版”(比如更智能和弦或更真实音色)就告诉我。祝你实验顺利!


评论

《“脑电数据转为音乐”》 有 1 条评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注