You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于乐谱与乐器文件用Python生成并输出WAV波形?

如何用Python从自定义乐谱和乐器模板生成WAV波形?

我来帮你一步步搞定这个需求!咱们可以拆成几个核心环节来实现:解析乐器模板、解读乐谱、生成采样波形、导出WAV文件。下面是具体的代码和详细解释:


1. 先明确输入数据的逻辑

先把两个文件的作用理清楚:

  • score.txt:piano |*********************| 里的*数量代表要重复播放乐器波形的次数,每个*对应一个完整的乐器波形周期。
  • piano乐器文件:里面的数字是波形的关键振幅顶点,数字后的制表符分隔了顶点和过渡符号(---、/\、\/),这些符号定义了两个顶点之间的波形变化方式:
    • ---:线性平滑过渡(直线)
    • /\:先上升到峰值再下降到下一个顶点的三角波形
    • \/:先下降到谷值再上升到下一个顶点的三角波形

2. 完整实现代码

我们用Python标准库wave处理WAV文件,numpy处理波形采样的插值计算,不需要额外安装依赖:

import wave
import numpy as np

def parse_instrument_file(file_path):
    """解析乐器文件,提取波形顶点和过渡类型"""
    with open(file_path, 'r') as f:
        content = f.read().strip()
    # 按制表符分割内容,分离数字和过渡符号
    parts = content.split('\t')
    vertices = []
    transitions = []
    for i in range(len(parts)):
        # 识别数字顶点(支持负数)
        if parts[i].lstrip('-').isdigit():
            vertices.append(int(parts[i]))
            # 收集当前顶点到下一个顶点的过渡符号
            if i + 1 < len(parts):
                transition = []
                j = i + 1
                while j < len(parts) and not parts[j].lstrip('-').isdigit():
                    transition.append(parts[j])
                    j += 1
                transitions.append(''.join(transition))
    return vertices, transitions

def generate_cycle_wave(vertices, transitions, samples_per_cycle=100):
    """根据顶点和过渡类型生成单个周期的波形采样"""
    wave_samples = []
    total_segments = len(vertices) - 1
    samples_per_segment = samples_per_cycle // total_segments

    # 逐个处理每个顶点间的过渡段
    for i in range(total_segments):
        start_val = vertices[i]
        end_val = vertices[i+1]
        transition = transitions[i]

        if transition == '---':
            # 线性过渡段
            segment = np.linspace(start_val, end_val, samples_per_segment, endpoint=False)
        elif transition == '/\\':
            # 先升后降的三角段,峰值取两个顶点的最大值
            peak = max(start_val, end_val)
            up_samples = samples_per_segment // 2
            down_samples = samples_per_segment - up_samples
            up_part = np.linspace(start_val, peak, up_samples, endpoint=False)
            down_part = np.linspace(peak, end_val, down_samples)
            segment = np.concatenate([up_part, down_part])
        elif transition == '\\/':
            # 先降后升的三角段,谷值取两个顶点的最小值
            valley = min(start_val, end_val)
            down_samples = samples_per_segment // 2
            up_samples = samples_per_segment - down_samples
            down_part = np.linspace(start_val, valley, down_samples, endpoint=False)
            up_part = np.linspace(valley, end_val, up_samples)
            segment = np.concatenate([down_part, up_part])
        else:
            # 默认线性过渡,兼容未知符号
            segment = np.linspace(start_val, end_val, samples_per_segment, endpoint=False)
        
        wave_samples.extend(segment.tolist())

    # 补充剩余采样点(处理整除后的余数)
    remaining = samples_per_cycle - len(wave_samples)
    if remaining > 0:
        last_val = wave_samples[-1]
        extension = np.linspace(last_val, last_val, remaining)
        wave_samples.extend(extension.tolist())

    # 归一化到16位WAV的有效范围(-32768 到 32767)
    max_val = max(abs(np.array(wave_samples)))
    if max_val == 0:
        normalized = wave_samples
    else:
        normalized = (np.array(wave_samples) / max_val) * 32767
    return normalized.astype(np.int16)

def parse_score_file(file_path):
    """解析乐谱文件,获取乐器名和重复次数"""
    with open(file_path, 'r') as f:
        content = f.read().strip()
    # 分割出乐器名和音符段(*的数量)
    parts = content.split('|')
    instrument_name = parts[0].strip()
    note_count = len(parts[1].strip())
    return instrument_name, note_count

def generate_wav(output_path, cycle_wave, note_count, sample_rate=44100):
    """将周期波形重复指定次数,生成WAV文件"""
    # 拼接所有周期的波形
    full_wave = np.tile(cycle_wave, note_count)
    # 写入WAV文件
    with wave.open(output_path, 'w') as wav_file:
        # 设置WAV参数:单声道、16位采样、采样率、总帧数
        wav_file.setparams((1, 2, sample_rate, len(full_wave), 'NONE', 'not compressed'))
        wav_file.writeframes(full_wave.tobytes())

if __name__ == "__main__":
    # 替换成你的文件路径
    score_path = "scores/score.txt"
    # 解析乐谱
    instrument_name, note_count = parse_score_file(score_path)
    instrument_path = f"instruments/{instrument_name}"
    # 解析乐器模板
    vertices, transitions = parse_instrument_file(instrument_path)
    # 生成单个周期波形
    cycle_wave = generate_cycle_wave(vertices, transitions)
    # 导出WAV
    generate_wav("output.wav", cycle_wave, note_count)
    print(f"✅ 已成功生成WAV文件:output.wav")

3. 关键参数说明

你可以根据需求调整这些参数来改变音质和播放效果:

  • samples_per_cycle:每个周期的采样点数,数值越大波形越平滑,默认100
  • sample_rate:WAV文件的采样率,常见值为44100(CD音质)或22050,默认44100
  • 如果你的乐器文件有其他过渡符号,可以在generate_cycle_wave函数中添加对应的处理逻辑

内容的提问来源于stack exchange,提问作者DRV5

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:20:33