Search code examples
python-3.xqt5pyqt5qtmultimedia

QtMultimedia - QAudioDecoder - Python - state changes, but buffer never made available


I hate to say how long it took me to get to this point but I had real trouble completely understanding PyQt5 and how it relates to the C++ code I was seeing on the Qt website but I think .. I get it, or... at least I thought I did until this completely failed to work. I'll start with the output I'm getting, that tells me I have a file that really exists. I've tried mp3 and ogg version in case for some reason AudioDecoder can't decode the MP3 even though other parts of QtMultimedia have been able to play it (I'm trying to get lower level so I can apply panning to the audio and shift the left/right balance, and maybe other fun things once I figure that out).

Debug output:

MP3 exists:True
Decoder stopped:True <- expected at this point, just confirming state works 
Decoder state changed? <- this means state change signal is being sent
Decoder stopped?:False <- ok, state did actually change, that's expected
Decoder decoding?:True <- expected, confirming there are only 2 states as documentation indicates 
Init finished, Decoder started? <- after this, i expect to see position changes, buffer availability changes, or errors ... but I get nothing and it just exits the script.

Code:

from PyQt5 import QtCore, QtMultimedia
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QByteArray, QIODevice, QFileInfo
from PyQt5.QtMultimedia import QAudioDecoder, QAudioFormat, QMediaObject, QAudioBuffer, QAudioOutput, QAudio

class AudioDecoder(QObject):  
  def __init__(self):
    super(AudioDecoder,self).__init__()
    self.desiredFormat = QAudioFormat()
    self.desiredFormat.setChannelCount(2)
    self.desiredFormat.setCodec('audio/pcm')
    self.desiredFormat.setSampleType(QAudioFormat.UnSignedInt)
    self.desiredFormat.setSampleRate(48000)
    self.desiredFormat.setSampleSize(16)

    self.decoder = QAudioDecoder()
    self.decoder.setAudioFormat(self.desiredFormat)
    self.decoder.setSourceFilename('D:\\python\\sounds\\30.mp3')
    fs = QFileInfo()
    print('MP3 exists:' + str(fs.exists('D:\\python\\sounds\\30.mp3')))

    #self.connect(decoder,bufferReady(),None,readBuffer())
    self.decoder.bufferReady.connect(self.readBuffer)
    self.decoder.finished.connect(self.play)
    self.decoder.error.connect(self.error)
    self.decoder.stateChanged.connect(self.stateChanged)
    self.decoder.positionChanged.connect(self.positionChanged)
    self.decoder.bufferAvailableChanged.connect(self.bufferAvailableChanged)

    #using this to determine if we need to start byte array or append to it
    self.readamount = 0 

    #Expect this to be true since we haven't started yet 
    print('Decoder stopped:' + str(self.decoder.state() == QAudioDecoder.StoppedState))    
    self.decoder.start()
    print('Init finished, Decoder started?')
  def bufferAvailableChanged(self):
   print(str(decoder.available))
  def positionChanged(self):
    print(str(decoder.position())+'/'+str(decoder.duration))
  def stateChanged(self):
    #Confirm state is what we expect
    print('Decoder state changed?')
    print('Decoder stopped?:' + str(self.decoder.state() == QAudioDecoder.StoppedState))
    print('Decoder decoding?:' + str(self.decoder.state() == QAudioDecoder.DecodingState))
  def error(self):
    print('Decoder error?')
    print(self.decoder.errorString())
  def readBuffer(self):
    print('Decoder ready for reading?')
    buffer = self.decoder.read()
    print('Bytecount in buffer:' + str(buffer.byteCount))
    if self.readamount == 0:
      self.ba = QByteArray()
      self.ba.fromRawData(buffer.data(),buffer.byteCount())
    else:
      self.ba.append(buffer.data(),buffer.byteCount())
    print('Bytearray size:' + str(self.ba.length()))  
  def play(self):
    print('Decoding finished, ready to play')

ad = AudioDecoder()

Revised code, attempting WAV, still not working though:

from PyQt5 import QtCore, QtMultimedia
from PyQt5.QtTest import QSignalSpy
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QByteArray, QIODevice, QFileInfo
from PyQt5.QtMultimedia import QAudioDecoder, QAudioFormat, QMediaObject, QAudioBuffer, QAudioOutput, QAudio

class AudioDecoder(QObject):  
  def __init__(self):
    super(AudioDecoder,self).__init__()
    self.desiredFormat = QAudioFormat()
    self.desiredFormat.setChannelCount(2)
    self.desiredFormat.setCodec('audio/pcm')
    self.desiredFormat.setSampleType(QAudioFormat.UnSignedInt)
    self.desiredFormat.setSampleRate(48000)
    self.desiredFormat.setSampleSize(16)

    self.decoder = QAudioDecoder()

    self.decoder.bufferReady.connect(self.readBuffer)
    self.decoder.finished.connect(self.play)
    self.decoder.error.connect(lambda: self.error(self.decoder.error()))
    self.decoder.stateChanged.connect(lambda: self.stateChanged(self.decoder.state()))
    self.decoder.positionChanged.connect(lambda: self.positionChanged(self.decoder.position(),self.decoder.duration()))
    self.decoder.bufferAvailableChanged.connect(lambda: self.bufferAvailableChanged(self.decoder.available()))

    self.decoder.setAudioFormat(self.desiredFormat)
    self.decoder.setSourceFilename('D:\\python\\sounds\\piano2.wav')
    fs = QFileInfo()
    print('File exists:' + str(fs.exists('D:\\python\\sounds\\piano2.wav')))

    #using this to determine if we need to start byte array or append to it
    self.readamount = 0 

    #Expect this to be true since we haven't started yet 
    print('Decoder stopped?:' + str(self.decoder.state() == QAudioDecoder.StoppedState))    
    self.decoder.start()      
    print('Init finished, Decoder started on file:' + self.decoder.sourceFilename())    
  @pyqtSlot()
  def bufferAvailableChanged(self,available):
   print('Available:' + str(available))
  @pyqtSlot() 
  def positionChanged(self,position,duration):
    print('Position:' + str(position())+'/'+str(duration()))  
  @pyqtSlot()   
  def stateChanged(self,state):
    #Confirm state is what we expect
    print('Decoder state changed')
    if state == QAudioDecoder.StoppedState:
      print('Decoder stopped?:' + str(state == QAudioDecoder.StoppedState))
    else:
      print('Decoder decoding?:' + str(state == QAudioDecoder.DecodingState))    
  @pyqtSlot()  
  def error(self,err):
    print('Decoder error')
    print(self.decoder.errorString())  
  def readBuffer(self):
    print('Decoder ready for reading?')
    buffer = self.decoder.read()
    print('Bytecount in buffer:' + str(buffer.byteCount))
    if self.readamount == 0:
      self.ba = QByteArray()
      self.ba.fromRawData(buffer.data(),buffer.byteCount())
    else:
      self.ba.append(buffer.data(),buffer.byteCount())
    self.readamount = self.readamount + 1
    print('Bytearray size:' + str(self.ba.length()))      
  def play(self):
    print('Decoding finished, ready to play')

ad = AudioDecoder()

My update code post answer from below, and it works with mp3 :)

from PyQt5 import QtCore, QtMultimedia, QtWidgets
from PyQt5.QtTest import QSignalSpy
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QByteArray, QIODevice, QFileInfo
from PyQt5.QtMultimedia import QAudioDecoder, QAudioFormat, QMediaObject, QAudioBuffer, QAudioOutput, QAudio
import signal

class AudioDecoder(QObject):  
  def __init__(self):
    super(AudioDecoder,self).__init__()
    self.desiredFormat = QAudioFormat()
    self.desiredFormat.setChannelCount(2)
    self.desiredFormat.setCodec('audio/pcm')
    self.desiredFormat.setSampleType(QAudioFormat.UnSignedInt)
    self.desiredFormat.setSampleRate(48000)
    self.desiredFormat.setSampleSize(16)

    self.decoder = QAudioDecoder()

    self.decoder.bufferReady.connect(self.readBuffer)
    self.decoder.finished.connect(self.play)
    self.decoder.error.connect(self.error)
    self.decoder.stateChanged.connect(self.stateChanged)
    self.decoder.positionChanged.connect(self.positionChanged)
    self.decoder.bufferAvailableChanged.connect(self.bufferAvailableChanged)

    self.decoder.setAudioFormat(self.desiredFormat)
    self.decoder.setSourceFilename('D:\\python\\sounds\\30.mp3')
    fs = QFileInfo()
    print('File exists:' + str(fs.exists('D:\\python\\sounds\\30.mp3')))

    #using this to determine if we need to start byte array or append to it
    self.readamount = 0 

    #Expect this to be true since we haven't started yet 
    print('Decoder stopped?:' + str(self.decoder.state() == QAudioDecoder.StoppedState))    
    self.decoder.start()      
    print('Init finished, Decoder started on file:' + self.decoder.sourceFilename())      
  def bufferAvailableChanged(self,available):
   print('Available:' + str(available))  
  def positionChanged(self,position):
    print('Position:' + str(position)+'/'+str(self.decoder.duration))     
  def stateChanged(self,state):
    #Confirm state is what we expect
    print('Decoder state changed')
    if state == QAudioDecoder.StoppedState:
      print('Decoder stopped?:' + str(state == QAudioDecoder.StoppedState))
    else:
      print('Decoder decoding?:' + str(state == QAudioDecoder.DecodingState))      
  def error(self,err):
    print('Decoder error')
    print(self.decoder.errorString())  
  def readBuffer(self):
    print('Decoder ready for reading?')
    buffer = self.decoder.read()
    byteCount = buffer.byteCount()
    print('Bytecount in buffer:' + str(byteCount))
    if self.readamount == 0:
      self.ba = QByteArray()
      self.ba.fromRawData(buffer.constData().asstring(byteCount))
    else:
      self.ba.append(buffer.constData().asstring(byteCount))
    self.readamount = self.readamount + 1
    print('Bytearray size:' + str(self.ba.length()))      
  def play(self):
    print('Decoding finished, ready to play')

app = QtWidgets.QApplication([''])    
ad = AudioDecoder()

signal.signal(signal.SIGINT,signal.SIG_DFL)
app.exec_()

Solution

  • Here is my (Linux) working version of your original script:

    from PyQt5 import QtWidgets
    from PyQt5 import QtCore, QtMultimedia
    from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QByteArray, QIODevice, QFileInfo
    from PyQt5.QtMultimedia import QAudioDecoder, QAudioFormat, QMediaObject, QAudioBuffer, QAudioOutput, QAudio
    
    class AudioDecoder(QObject):
      def __init__(self):
        super(AudioDecoder,self).__init__()
        self.desiredFormat = QAudioFormat()
        self.desiredFormat.setChannelCount(2)
        self.desiredFormat.setCodec('audio/pcm')
        self.desiredFormat.setSampleType(QAudioFormat.UnSignedInt)
        self.desiredFormat.setSampleRate(48000)
        self.desiredFormat.setSampleSize(16)
    
        self.decoder = QAudioDecoder()
        self.decoder.setAudioFormat(self.desiredFormat)
        fs = QFileInfo('test.wav')
        self.decoder.setSourceFilename(fs.absoluteFilePath())
        print('File exists:' + str(fs.exists()))
    
        #self.connect(decoder,bufferReady(),None,readBuffer())
        self.decoder.bufferReady.connect(self.readBuffer)
        self.decoder.finished.connect(self.play)
        self.decoder.error.connect(self.error)
        self.decoder.stateChanged.connect(self.stateChanged)
        self.decoder.positionChanged.connect(self.positionChanged)
        self.decoder.bufferAvailableChanged.connect(self.bufferAvailableChanged)
    
        #using this to determine if we need to start byte array or append to it
        self.readamount = 0
    
        #Expect this to be true since we haven't started yet
        print('Decoder stopped:' + str(self.decoder.state() == QAudioDecoder.StoppedState))
        self.decoder.start()
        print('Init finished, Decoder started?')
      def bufferAvailableChanged(self):
       print(str(self.decoder.bufferAvailable()))
      def positionChanged(self):
        print(str(self.decoder.position())+'/'+str(self.decoder.duration()))
      def stateChanged(self):
        #Confirm state is what we expect
        print('Decoder state changed?')
        print('Decoder stopped?:' + str(self.decoder.state() == QAudioDecoder.StoppedState))
        print('Decoder decoding?:' + str(self.decoder.state() == QAudioDecoder.DecodingState))
      def error(self):
        print('Decoder error?')
        print(self.decoder.errorString())
      def readBuffer(self):
        print('Decoder ready for reading?')
        buffer = self.decoder.read()
        count = buffer.byteCount()
        print('Bytecount in buffer:' + str(count))
        if self.readamount == 0:
          self.ba = QByteArray()
          self.ba.fromRawData(buffer.constData().asstring(count))
          self.readamount = count
        else:
          self.ba.append(buffer.constData().asstring(count))
        print('Bytearray size:' + str(self.ba.length()))
      def play(self):
        print('Decoding finished, ready to play')
    
    app = QtWidgets.QApplication([''])
    ad = AudioDecoder()
    
    import signal
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    # press Ctrl+C to exit
    
    app.exec_()
    

    Diff:

    --- yours
    +++ mine
    @@ -1,3 +1,4 @@
    +from PyQt5 import QtWidgets
     from PyQt5 import QtCore, QtMultimedia
     from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QByteArray, QIODevice, QFileInfo
     from PyQt5.QtMultimedia import QAudioDecoder, QAudioFormat, QMediaObject, QAudioBuffer, QAudioOutput, QAudio
    @@ -14,9 +15,9 @@
    
         self.decoder = QAudioDecoder()
         self.decoder.setAudioFormat(self.desiredFormat)
    -    self.decoder.setSourceFilename('D:\\python\\sounds\\30.mp3')
    -    fs = QFileInfo()
    -    print('MP3 exists:' + str(fs.exists('D:\\python\\sounds\\30.mp3')))
    +    fs = QFileInfo('test.wav')
    +    self.decoder.setSourceFilename(fs.absoluteFilePath())
    +    print('File exists:' + str(fs.exists()))
    
         #self.connect(decoder,bufferReady(),None,readBuffer())
         self.decoder.bufferReady.connect(self.readBuffer)
    @@ -34,9 +35,9 @@
         self.decoder.start()
         print('Init finished, Decoder started?')
       def bufferAvailableChanged(self):
    -   print(str(decoder.available))
    +   print(str(self.decoder.bufferAvailable()))
       def positionChanged(self):
    -    print(str(decoder.position())+'/'+str(decoder.duration))
    +    print(str(self.decoder.position())+'/'+str(self.decoder.duration()))
       def stateChanged(self):
         #Confirm state is what we expect
         print('Decoder state changed?')
    @@ -48,14 +49,23 @@
       def readBuffer(self):
         print('Decoder ready for reading?')
         buffer = self.decoder.read()
    -    print('Bytecount in buffer:' + str(buffer.byteCount))
    +    count = buffer.byteCount()
    +    print('Bytecount in buffer:' + str(count))
         if self.readamount == 0:
           self.ba = QByteArray()
    -      self.ba.fromRawData(buffer.data(),buffer.byteCount())
    +      self.ba.fromRawData(buffer.constData().asstring(count))
    +      self.readamount = count
         else:
    -      self.ba.append(buffer.data(),buffer.byteCount())
    +      self.ba.append(buffer.constData().asstring(count))
         print('Bytearray size:' + str(self.ba.length()))
       def play(self):
         print('Decoding finished, ready to play')
    
    +app = QtWidgets.QApplication([''])
     ad = AudioDecoder()
    +
    +import signal
    +signal.signal(signal.SIGINT, signal.SIG_DFL)
    +# press Ctrl+C to exit
    +
    +app.exec_()
    

    Output:

    File exists:True
    Decoder stopped:True
    Init finished, Decoder started?
    Decoder state changed?
    Decoder stopped?:False
    Decoder decoding?:True
    True
    Decoder ready for reading?
    0/196238
    Bytecount in buffer:7680
    Bytearray size:0
    Decoder ready for reading?
    40/196238
    Bytecount in buffer:7680
    Bytearray size:7680
    Decoder ready for reading?
    80/196238
    Bytecount in buffer:7680
    Bytearray size:15360
    Decoder ready for reading?
    120/196238
    Bytecount in buffer:7680
    Bytearray size:23040
    Decoder ready for reading?
    False
    160/196238
    Bytecount in buffer:7680
    Bytearray size:30720
    ...
    
    Bytecount in buffer:7680
    Bytearray size:37662720
    Decoder ready for reading?
    False
    196200/196238
    Bytecount in buffer:7364
    Bytearray size:37670084
    Decoding finished, ready to play
    Decoder state changed?
    Decoder stopped?:True
    Decoder decoding?:False