求助:实现Python音乐脚本中父音符保持+子音符顺序播放功能
解决父音符持续播放同时子音符依次播放的方案
针对你用Python+sounddevice+numpy实现乐谱播放时,无法实现A6 + ( A4 E3 A6 )这类父音符持续播放、子音符序列依次播放的问题,核心是要实现多轨音频的并行播放,之前用Threading失败大概率是因为没有处理好音频设备的资源冲突或播放同步问题,下面给出两种可行方案:
方案1:预生成混合音频(简单易实现)
先分别生成父轨道和子轨道的音频数据,将它们混合后一次性播放,天然实现并行效果,无需线程。
基础工具函数
import numpy as np import sounddevice as sd def note_to_freq(note): # 可扩展完整的音符-频率映射,这里简化示例 note_map = {'A4': 440, 'E3': 164.81, 'A6': 1760} return note_map.get(note, 440) def generate_note_audio(freq, duration, sample_rate=44100): # 生成指定频率、时长的正弦波音频 t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False) return 0.5 * np.sin(2 * np.pi * freq * t)
实现混合播放
# 解析语法:父音符A6,子音符序列[A4, E3, A6],每个子音符持续0.5秒 parent_note = 'A6' child_notes = ['A4', 'E3', 'A6'] child_note_duration = 0.5 total_duration = len(child_notes) * child_note_duration # 生成父轨道音频:持续整个子序列的总时长 parent_audio = generate_note_audio(note_to_freq(parent_note), total_duration) # 生成子轨道音频:依次拼接每个子音符的音频片段 child_audio = np.concatenate([ generate_note_audio(note_to_freq(n), child_note_duration) for n in child_notes ]) # 混合轨道:除以轨道数防止音量溢出(幅值超过1会出现削波失真) mixed_audio = (parent_audio + child_audio) / 2 # 播放混合后的音频 sd.play(mixed_audio, samplerate=44100) sd.wait()
方案2:实时多轨回调(适合动态/长乐谱)
如果乐谱很长或需要动态调整(比如实时响应暂停、变速),可以用sounddevice的流回调机制,同时维护多个轨道的播放状态,实时生成并混合音频。
轨道播放器类
import sys import numpy as np import sounddevice as sd def note_to_freq(note): note_map = {'A4': 440, 'E3': 164.81, 'A6': 1760} return note_map.get(note, 440) class TrackPlayer: def __init__(self, sample_rate=44100): self.sample_rate = sample_rate self.current_freq = 0 self.remaining_samples = 0 self.note_queue = [] # 存储待播放的(频率, 总采样数) def queue_notes(self, notes, duration_per_note): # 将音符序列转为采样数队列 for note in notes: freq = note_to_freq(note) total_samples = int(duration_per_note * self.sample_rate) self.note_queue.append((freq, total_samples)) # 若当前无播放任务,启动第一个音符 if self.remaining_samples == 0 and self.note_queue: self.current_freq, self.remaining_samples = self.note_queue.pop(0) def get_audio_chunk(self, num_samples): # 生成指定采样数的音频片段 audio_chunk = np.zeros(num_samples) if self.remaining_samples == 0 and self.note_queue: self.current_freq, self.remaining_samples = self.note_queue.pop(0) if self.remaining_samples == 0: return audio_chunk # 计算本次要生成的采样数(不超过剩余采样数) generate_samples = min(num_samples, self.remaining_samples) t = np.linspace(0, generate_samples/self.sample_rate, generate_samples, endpoint=False) audio_chunk[:generate_samples] = 0.5 * np.sin(2 * np.pi * self.current_freq * t) self.remaining_samples -= generate_samples return audio_chunk
启动多轨播放
sample_rate = 44100 # 初始化父轨道:持续播放A6,时长等于子序列总时长(3*0.5=1.5秒) parent_track = TrackPlayer(sample_rate) parent_track.queue_notes(['A6'], 1.5) # 初始化子轨道:依次播放A4、E3、A6,每个0.5秒 child_track = TrackPlayer(sample_rate) child_track.queue_notes(['A4', 'E3', 'A6'], 0.5) def audio_callback(outdata, frames, time, status): if status: print(status, file=sys.stderr) # 获取两个轨道的音频片段 parent_chunk = parent_track.get_audio_chunk(frames) child_chunk = child_track.get_audio_chunk(frames) # 混合后输出到立体声通道 outdata[:, 0] = (parent_chunk + child_chunk) / 2 outdata[:, 1] = outdata[:, 0] # 启动输出流并等待用户停止 with sd.OutputStream(samplerate=sample_rate, callback=audio_callback): print("按回车键停止播放") input()
为什么之前Threading方案失败?
直接用线程调用sd.play()会导致多个独立的音频流同时占用设备,容易出现资源冲突、播放不同步或卡顿问题。sounddevice的播放机制更适合单流控制,多轨需求应该通过音频数据混合而非多线程独立播放来实现。
扩展建议
- 可以在
TrackPlayer中添加暂停、变速逻辑:比如暂停时停止消耗remaining_samples,变速时调整duration_per_note对应的采样数 - 完善音符解析逻辑:支持解析
A6 + (A4 E3 A6)这类语法,自动拆分父轨道和子轨道的音符序列
内容的提问来源于stack exchange,提问作者ZXCVB0T
相关产品推荐
相关产品推荐

