Playing .mp3 files with PyAudio

2019-02-11 01:35发布

问题:

Can pyaudio play .mp3 files? If yes, may I ask to write an example please. If no, what is the simplest way to convert .mp3 to .wav?

I have tried to use PyDub, could get my .wav file, but when I try to play it with PyAudio I get following error:

  File "C:\Python33\lib\wave.py", line 130, in initfp
    raise Error('file does not start with RIFF id')
wave.Error: file does not start with RIFF id

With other .wav samples (which where not converted from mp3) if works well.

I am using gTTS library to convert text to speech for my application. It creates short .mp3 files which I need to play then. Right now I am using just

os.system("start english.mp3")

I want to find a better way to do this. First of all I don't want to be restricted about the platform. Secondly, I don't like that the player pops-up when the file begins to play, I'd like it to stay on the background.

I try to find the minimalist solution for this, as I don't need anything more than simple playing.

UPD: I managed to play it with pyglet. Seems fine, except it takes sooo long... I have about 10 seconds delay before I hear the sound. And it doesn't work proper with threading (I want to play the .mp3 while the program is still running). Is there the way just to make the player stay on the background and not pop-up over all other windows?

回答1:

Here's the short answer:

ffmpeg -i song.mp3 -acodec pcm_u8 -ar 22050 song.wav

TL;DR: I'm assuming you want to play a audio file, without a front-end.

There's a library for that, called The Snack Sound Toolkit which does this beautifully:

player = Sound() 
player.read('song.wav') 
player.play()

I know I used this with both streams and I think mp3 files, can't remember how or in which project, might have to look in to this tho. Think it was mumble related.. anyhow..

If you're completely fine with using a front-end code such as pyglet (which is my pick of the heard), you need some options and some code for this to work as best as possible.

import pyglet
from pyglet.gl import *
pyglet.options['audio'] = ('openal', 'directsound', 'silent')

music = pyglet.resource.media('music.mp3')
music.play()

pyglet.app.run()

Dependencies: * OpenAL (for cross-platform compatibility)

Your problem with threading, is that Pyglet is a OpenGL library. Which doesn't take to kindly to Threading at all. Unless you let Pyglet fetch the data you need. Also you will most likely bump into the problem of "pyglet blocks my code" (all graphic libraries do. so here's a workaround)

import pyglet, os
from time import sleep
from threading import *
from pyglet.gl import *

pyglet.options['audio'] = ('openal', 'directsound', 'silent')

class worker(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.audio_frames = []
        self.run()

    def add_frame(self, filename):
        self.audio_frames.append(filename)

    def get_frame(self):
         if len(self.audio_frames) > 0:
             return self.audio_frames.pop(0)
         return None

    def run(self):
        while 1:
            for root, folders, files in os.walk('./audio_files/'):
                for f in file:
                    self.add_frame(f)
            sleep(1)

class AudioWindow(pyglet.window.Window):
    def __init__(self):
        self.audio_worker = worker()

    def render(self):
        frame = self.audio_frames.get_frame()
        if frame:
            music = pyglet.resource.media('music.mp3')
            music.play()
    def run(self):
        while self.alive == 1:
            self.render()

        # -----------> This is key <----------
        # This is what replaces pyglet.app.run()
        # but is required for the GUI to not freeze
        #
        event = self.dispatch_events()

This is totally fine, since you're not trying to update THE graphics from another thread.
Instead you're fetching data on the graphics terms. You can however from worker() update a variable/list/array/w/e inside AudioWindow() without any problems, the only thing you can't do is call any graphical function from outside the graphic-class.

Without front-end the fun approach:

The most ideal way however, would to be going old-school as hell and use pyaudio and fiddle with audioframes manually. This way you can read literally any audio-file as long as you decode the data properly. I use this one(Tread lightly, cause it ain't pritty) for transporting audio myself:

import pyaudio
import wave

CHUNK_SIZE = 1024
FORMAT = pyaudio.paInt16
RATE = 44100

p = pyaudio.PyAudio()
output = p.open(format=FORMAT,
                        channels=1,
                        rate=RATE,
                        output=True) # frames_per_buffer=CHUNK_SIZE
with open('audio.wav', 'rb') as fh:
    while fh.tell() != FILE_SIZE: # get the file-size from the os module
        AUDIO_FRAME = fh.read(CHUNK_SIZE)
        output.write(AUDIO_FRAME)

This should produce something close to audio :)
The reason why wav is so overutelized in examples etc, is because it's basically a unencoded stream of sound, not encoding in any way. mp3 however is a heavily compressed audio format, format being the key-word here. You need some way of read the mp3 data and reverse it from a compact state into a stream of data which you can squeeze into the speakers.

I'm no audio expert, but this is a rough explanation of how audio works from someone fiddling about with it for a bit and got it working.

Last note:

If you're expecting to play compressed audio-files with Pyglet, you can use AVbin. A library used for compressed files.



回答2:

My gist is here, enjoy

#!/usr/bin/env python3
"""
Play a file continously, and exit gracefully on signal

Based on https://github.com/steveway/papagayo-ng/blob/working_vol/SoundPlayer.py

@author Guy Sheffer (GuySoft) <guysoft at gmail dot com>
"""
import signal
import time
import os
import threading
import pyaudio
from pydub import AudioSegment
from pydub.utils import make_chunks

class GracefulKiller:
    kill_now = False
    def __init__(self):
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)

    def exit_gracefully(self,signum, frame):
        self.kill_now = True

    def play(self):
        """
        Just another name for self.start()
        """
        self.start()

    def stop(self):
        """
        Stop playback. 
        """
        self.loop = False


class PlayerLoop(threading.Thread):
    """
    A simple class based on PyAudio and pydub to play in a loop in the backgound
    """

    def __init__(self, filepath, loop=True):
        """
        Initialize `PlayerLoop` class.

        PARAM:
            -- filepath (String) : File Path to wave file.
            -- loop (boolean)    : True if you want loop playback. 
                                False otherwise.
        """
        super(PlayerLoop, self).__init__()
        self.filepath = os.path.abspath(filepath)
        self.loop = loop

    def run(self):
        # Open an audio segment
        sound = AudioSegment.from_file(self.filepath)
        player = pyaudio.PyAudio()

        stream = player.open(format = player.get_format_from_width(sound.sample_width),
            channels = sound.channels,
            rate = sound.frame_rate,
            output = True)

        # PLAYBACK LOOP
        start = 0
        length = sound.duration_seconds
        volume = 100.0
        playchunk = sound[start*1000.0:(start+length)*1000.0] - (60 - (60 * (volume/100.0)))
        millisecondchunk = 50 / 1000.0


        while self.loop:
            self.time = start
            for chunks in make_chunks(playchunk, millisecondchunk*1000):
                self.time += millisecondchunk
                stream.write(chunks._data)
                if not self.loop:
                    break
                if self.time >= start+length:
                    break

        stream.close()
        player.terminate()


    def play(self):
        """
        Just another name for self.start()
        """
        self.start()

    def stop(self):
        """
        Stop playback. 
        """
        self.loop = False


def play_audio_background(audio_file):
    """
    Play audio file in the background, accept a SIGINT or SIGTERM to stop
    """
    killer = GracefulKiller()
    player = PlayerLoop(audio_file)
    player.play()
    print(os.getpid())
    while True:      
        time.sleep(0.5)
        # print("doing something in a loop ...")
        if killer.kill_now:
            break
    player.stop()
    print("End of the program. I was killed gracefully :)")
    return



if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(add_help=True, description="Play a file continously, and exit gracefully on signal")
    parser.add_argument('audio_file', type=str, help='The Path to the audio file (mp3, wav and more supported)')
    args = parser.parse_args()

    play_audio_background(args.audio_file)


回答3:

If you're using Windows with Windows Media Player installed here's a simple solution:

wmp = Dispatch('WMPlayer.OCX')
wmp.settings.autoStart = True
wmp.settings.volume = 50
wmp.URL = r"tts_cache\gtts.mp3" #auto-start
while wmp.PlayState != 1: #wait until stopped
    pythoncom.PumpWaitingMessages()
    time.sleep(0.1)
wmp.URL = ""

Unfortunately when i try to replace mp3 file with a new one it sometimes cannot be written. I think WMP blocks it or something. So i decided to create a new file every time and call it "caching".)