在Python中播放时改变音频的音调(和速度)

4 投票
4 回答
8669 浏览
提问于 2025-04-16 05:49

我正在做一个用Python编写的音乐播放器。这个程序有一个功能,就是用户可以拖动一个滑块来实时改变音乐的音调。

举个例子,如果把音调设置为2,那么音乐就会听起来高一个八度,播放速度会加快一倍,持续时间也会缩短一半。其实我主要是改变播放速度,但需要做到实时互动。

一个很好的示例可以在这个链接中找到(加载可能需要一点时间,请耐心等候)。

我查阅了很多Python的音频库,但还没有找到一个可以改变正在播放的声音音调的库。我有多个版本的Python,所以对库支持哪个版本没有要求。我是在Windows 7上开发这个程序。

有没有什么建议呢?

4 个回答

0

你可以考虑使用 wxPython制作一个媒体播放器,并了解一下 SetPlaybackRate() 这个函数。详细的 wxWidget 文档在这里

不过要注意,SetPlaybackRate() 这个函数并不是在所有平台上都能用,我自己也没有试过,不知道它是否能完全满足你的需求,以及效果如何。

3

Craig McQueen的帮助下,我创建了一个概念验证程序。

这个程序可以播放一个叫做“music.wav”的单声道音频文件(这个文件和程序在同一个文件夹里),并且会显示一个短而宽的窗口。当你在窗口里点击并拖动时,音乐的音调会发生变化。窗口的左边音调比正常低两个八度,右边则高两个八度。

这里有一些奇怪的现象,我不太确定怎么解决。如果音调比较低,音调变化会有大约2秒的延迟。不过对于高音调,音调变化是实时的。(随着音调变低,延迟会逐渐增加)。我只在soundOutput.getLeft() < 0.2的情况下才会往缓冲区里添加更多的声音。也就是说,如果缓冲区里剩下的声音少于0.2秒,就会添加声音。因此应该没有延迟。为了排查问题,我加了一段代码,把soundOutput.getLeft()的值写入一个文件。这个值通常会保持在0或者非常接近0。

把读取的帧数减少到waveRead.readframes(100)可以减少延迟,但声音会变得很卡。增加读取的帧数则会显著增加延迟。

import os, sys, wave, pygame, numpy, pymedia.audio.sound, scikits.samplerate

class Window:
    def __init__(self, width, height, minOctave, maxOctave):
        """
        width, height: the width and height of the screen.
        minOctave, maxOctave: the highest and lowest pitch changes. 0 is no change.
        """
        self.minOctave = minOctave
        self.maxOctave = maxOctave
        self.width = width
        self.mouseDown = False
        self.ratio = 1.0 # The resampling ratio
        waveRead = wave.open(os.path.join(sys.path[0], "music.wav"), 'rb')
        sampleRate = waveRead.getframerate()
        channels = waveRead.getnchannels()
        soundFormat = pymedia.audio.sound.AFMT_S16_LE
        soundOutput = pymedia.audio.sound.Output(sampleRate, channels, soundFormat)
        pygame.init()
        screen = pygame.display.set_mode((width, height), 0)
        screen.fill((255, 255, 255))
        pygame.display.flip()
        fout = open(os.path.join(sys.path[0], "musicdata.txt"), 'w') # For troubleshooting
        byteString = waveRead.readframes(1000) # Read at most 1000 samples from the file.
        while len(byteString) != 0:
            self.handleEvent(pygame.event.poll()) # This does not wait for an event.
            fout.write(str(soundOutput.getLeft()) + "\n") # For troubleshooting
            if soundOutput.getLeft() < 0.2: # If there is less than 0.2 seconds left in the sound buffer.
                array = numpy.fromstring(byteString, dtype=numpy.int16)
                byteString = scikits.samplerate.resample(array, self.ratio, "sinc_fastest").astype(numpy.int16).tostring()
                soundOutput.play(byteString)
                byteString = waveRead.readframes(500) # Read at most 500 samples from the file.
        waveRead.close()
        return

    def handleEvent(self, event):
        if event.type == pygame.QUIT or (event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE):
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            self.mouseDown = True
            self.setRatio(event.pos)
        if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
            self.mouseDown = False
        if event.type == pygame.MOUSEMOTION and self.mouseDown:
            self.setRatio(event.pos)
        return None

    def setRatio(self, point):
        self.ratio = 2 ** -(self.minOctave + point[0] * (self.maxOctave - self.minOctave) / float(self.width))
        print(self.ratio)

def main():
    Window(768, 100, -2.0, 2.0)

if __name__ == '__main__':
    main()

想要让我用的所有包都能很好地配合在一起真是太麻烦了。我使用的是Python 2.6.6适用于Python 2.6的PyGame 1.9.1适用于Python 2.6的NumPy 1.3.0适用于Python 2.6的PyMedia 1.3.7.3,以及适用于Python 2.6的scikits.samplerate 0.3.1。需要注意的是,scikits.samplerate和NumPy 1.4或更高版本有冲突,而且其中一个包(我忘了是哪个)需要setuptools

2

听起来你想要实时地重新采样音频。

你可以试试使用 scikits.samplerate 这个模块。它是基于 Secret Rabbit Code库 的。

撰写回答