我正在编写一个PyQt5应用程序,但我认为这个问题对PySide2和Qt同样有效。我试图将声音数据(sinuosids)写入缓冲区,然后在无缝循环中播放它。但是,当我到达缓冲区的结尾并返回到开头时,总是会有一个休息。
我想我想连续读写同一缓冲区,这可能吗?
下面是我的代码的最低版本:
import struct
import sys
from PyQt5.QtCore import QBuffer, QByteArray, QIODevice
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtMultimedia import QAudio, QAudioFormat, QAudioOutput
sample_rate = 44100
sample_size = 16
frequency = 1000
volume = 3276
class Window(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
format = QAudioFormat()
format.setChannelCount(1)
format.setSampleRate(sample_rate)
format.setSampleSize(sample_size)
format.setCodec("audio/pcm")
format.setByteOrder(QAudioFormat.LittleEndian)
format.setSampleType(QAudioFormat.SignedInt)
self.output = QAudioOutput(format, self)
self.output.stateChanged.connect(self.replay)
self.buffer = QBuffer()
self.buffer.open(QIODevice.ReadWrite)
self.createData()
self.buffer.seek(0)
self.output.start(self.buffer)
def createData(self):
print("writing")
data = QByteArray()
for i in range(round(1 * sample_rate)):
t = i / sample_rate
value = int(volume * sin(2 * pi * frequency * t))
data.append(struct.pack("<h", value))
self.buffer.write(data)
def replay(self):
print("replaying", self.output.state(), QAudio.IdleState)
if self.output.state() == QAudio.IdleState:
self.buffer.seek(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
答案 0 :(得分:1)
我认为您稍微误解了QAudioOutput(通常是音频设备对象)的行为,读取和播放音频数据的方式。
当您play()
使用QIODevice时,QAudioOutput实例将根据音频设备缓冲区设置读取一大块数据(但并非始终与bufferSize()
相同),并且将其“发送”到实际播放它的硬件设备:读取数据和“播放”是异步的。 play()
的作用是调用QIODevice.readData(maxLen),其中maxLen是音频设备需要的一些数据长度,以确保音频缓冲区被连续填充,否则,您的缓冲区将不足,这意味着该设备正在尝试播放,但没有数据可以播放。
在您的情况下,这还意味着音频设备可能会在某个特定时间点上向数据缓冲区 请求一些数据,因此您需要添加更多数据以返回。
>此外,如果您等待stateChanged信号,则意味着没有更多数据要从数据缓冲区(不是音频设备缓冲区)中读取。此时,QAudioDevice会停止音频设备并清除其缓冲区,因此,如果您“重播”,显然会听到间隙,因为该设备正在“重新启动”。
如果要循环播放某些数据,则需要实现自己的QIODevice,因为一旦达到音频设备的末端,它就必须连续地馈送音频设备。 请注意,这是一个最小的示例,您可能想进一步实现对数据缓冲区的写入操作(并更新其查找位置)
class AudioBuffer(QIODevice):
def __init__(self):
QIODevice.__init__(self)
self.bytePos = 0
self.data = QByteArray()
for i in range(round(1 * sample_rate)):
t = i / sample_rate
value = int(volume * sin(2 * pi * frequency * t))
self.data.append(struct.pack("<h", value))
def seek(self, pos):
self.bytePos = pos
return True
def readData(self, maxLen):
data = self.data[self.bytePos:self.bytePos + maxLen]
if len(data) < maxLen:
# we've reached the end of the data, restart from 0
# so the wave is continuing from its beginning
self.bytePos = maxLen - len(data)
data += self.data[:self.bytePos]
else:
self.bytePos += maxLen
return data.data()
class Window(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QHBoxLayout()
self.setLayout(layout)
self.playButton = QPushButton('Play')
self.playButton.setCheckable(True)
self.playButton.toggled.connect(self.togglePlay)
layout.addWidget(self.playButton)
format = QAudioFormat()
format.setChannelCount(1)
format.setSampleRate(sample_rate)
format.setSampleSize(sample_size)
format.setCodec("audio/pcm")
format.setByteOrder(QAudioFormat.LittleEndian)
format.setSampleType(QAudioFormat.SignedInt)
self.output = QAudioOutput(format, self)
self.output.stateChanged.connect(self.stateChanged)
self.buffer = AudioBuffer()
self.buffer.open(QIODevice.ReadWrite)
def togglePlay(self, state):
self.buffer.seek(0)
if state:
self.output.start(self.buffer)
else:
self.output.reset()
def stateChanged(self, state):
self.playButton.blockSignals(True)
self.playButton.setChecked(state == QAudio.ActiveState)
self.playButton.blockSignals(False)
那是说,我在QAudioDevice上玩了一点,恐怕它不是很可靠,至少在PyQt / PySide下是不可靠的。虽然它适用于小型示例和简单案例,但如果您需要在播放音频时做一些需要处理的事情(例如复杂的小部件/ QGraphics绘画),则变得不可靠,并且使用QThreads不会像您想的那样对您有帮助:例如,在MacOS下,您无法moveToThread()
使用QAudioOutput。
我强烈建议您使用PyAudio,其类的行为与QAudioOutput类似,但可以在不同的线程中工作。显然,如果您仍然需要连续播放,则“ readData”问题仍然存在,因为您需要一些可以循环的数据对象。
PS:该问题的标题与当前话题有些偏离,您可能会考虑对其进行更改。顺便说一句,答案是否定的,因为不能同时进行IODevice的读取和写入:读取应“锁定”写入(但不能进一步读取),反之亦然,并且这两个操作都会在内部移动IODevice的搜索pos
,但由于您根本没有处理线程,这也不重要,这还因为在您的示例中,您甚至在开始从缓冲区读取数据之前就已经完成了将数据写入缓冲区的操作,之后便不再写入任何内容。 / p>
答案 1 :(得分:0)
我目前没有设置PyQt自己进行测试,但是请尝试以下操作:
使用QAudioOutput::notify()信号。计算缓冲区音频的持续时间(以毫秒为单位)。将其用作间隔setNotifyInterval()
。将notify
而非stateChanged
连接到您的replay
方法。不要检查QAudio.IdleState
,只需倒退缓冲区。