11 dicembre, 2006

Un modulo portabile per suoni in python

Il modulo che vi riporterò di seguito serve per poter far emettere un suono al vostro programma python (in genere suoni brevi associati a particolari eventi) senza preoccuparvi se vi trovate su piattaforma windows o su linux chiamando una semplice funzione con parametro il nome del file da suonare.
Ecco un basilare esempio di utilizzo:



import sndplay

sndplay.play("percorso_del_file.wav")

Come avrete notato è molto semplice da usare. Se vi trovate su una piattaforma diversa dalle due supportate la funzione non fa nulla. Come parametro ho inserito un file wav. In realtà su linux viene accettato come parametro un qualsiasi formato supportato dalle librerie gstreamer ma se mettiamo un file diverso dal wav perdiamo la portabilità perché (almeno per ora) su windows è supportato solo il formato wav.

Passiamo ora al codice:


import threading
import os

(
GSTPLAY,
WINPLAY,
NOENGINE
) = range(3)

try:
import gst
import gobject
ENGINE = GSTPLAY
except ImportError:
try:
import winsound
ENGINE = WINPLAY
except ImportError:
ENGINE = NOENGINE

class __GstPlayThread(threading.Thread):
def __init__(self, ply):
self.ply = ply
threading.Thread.__init__(self)
def run(self):
self.ply.set_state(gst.STATE_PLAYING)
def bus_event(bus, message):
t = message.type
if t == gst.MESSAGE_EOS:
self.ply.set_state(gst.STATE_NULL)
return True
self.ply.get_bus().add_watch(bus_event)


def __gstplay(filename):
cwd = os.getcwd()
location = os.path.join(cwd, filename)
ply = gst.element_factory_make("playbin", "player")
ply.set_property("uri", "file://" + location)
pt = __GstPlayThread(ply)
pt.start()

def __winplay(filename):
cwd = os.getcwd()
location = os.path.join(cwd, filename)
import thread
def wplay():
winsound.PlaySound(location,
winsound.SND_FILENAME)
thread.start_new_thread(wplay, ())

if ENGINE == GSTPLAY:
play = __gstplay
pass
elif ENGINE == WINPLAY:
play = __winplay
else:
def play(filename):
pass


Attenzione: su linux è supportata solo la versione 0.10 delle pygst!!!

Come si può notare l'effettivo playing del file viene eseguito da un thread in modo che la play non sia bloccante. Cio vuol dire che mentre il suono viene riprodotto in background, il nostro codice può continuare ad essere eseguito ;)

Questo modulo l'ho scritto inizialmente per planimo ma ho pensato che sarebbe stato utile postarlo a parte con qualche spiegazione a contorno


15 commenti:

  1. Il modulo è bellissimo, e funziona bene (almeno su linux).
    Purtroppo però ho un problema nel creare un programma con gui: la gui funziona bene, la thread che la separa dal codice funziona bene; non riesco ad integrarla con gstreamer, ogni volta che c'è una sequenza tipo

    [...].set_state(gst.STATE_PLAYING)
    time.sleep(1)


    o l'equivalente, utilizzando il tuo modulo,

    sndplay.play(NOTE_DIR + "/" + note)
    time.sleep(1)


    il programma va sistematicamente in segmentation fault.

    Sai darmi una mano?

    RispondiElimina
  2. Aggiornamento, se tengo sndplay.play(...) e time.sleep(1) nella stessa thread della gui il suono si sente ma la gui va in freeze;

    Se invece separo i due comandi dalla thread della gui ho il segmentation fault che dicevo prima.

    RispondiElimina
  3. Onestamente non so risponderti. A me è servito gstreamer solo per quello che fa il modulo ... :(

    RispondiElimina
  4. Nooo, dai, sei la mia ultima speranza, non so a chi chiedere altrimenti... posso passarti il codice in qualche maniera? Magari ho fatto qualche cappella esagerata e non me ne accorgo.

    RispondiElimina
  5. Se usi time.sleep(1) all'interno della gui è normale che va in freeze. Perché devi fare time.sleep(1)?
    Il modulo è fatto in modo che i suoni vengano riprodotti in modo asincrono e cioè la funzione "play" non è bloccante.

    Per quanto riguarda il codice mi devi perdonare ma non avrei il tempo di mettermi a studiarlo.

    Inoltre ricorda che il modulo è pensato per eseguire suoni associati ad eventi e quindi di breve durata e non per eseguire ad esempio un intero pezzo musicale.

    Spero di esserti stato utile

    RispondiElimina
  6. Parzialmente ci hai azzeccato.

    Uno degli sleep distanzia leggermente il controlli per il blocco della thread.

    L'altro sleep(1) non è all'interno della gui, è all'interno di un metodo che suona una serie di file lunghi due-tre secondi (fino a che qualcuno non interrompe tale metodo).

    Il programma vorrebbe essere un allenatore per l'orecchio; tra la riproduzione della nota e la riproduzione del nome, e tra due esercizi diversi, c'è una pausa impostabile dall'utente tramite due slidebar;

    In generale se tolgo le chiamate a sndplay.play il programma funziona perfettamente, rispetta le pause come impostate... ma ovviamente non suona :) Se le rimetto si presenta la situazione come descritta due post fa.

    RispondiElimina
  7. Anonimo10:52 AM

    Purtroppo detto così non riesco a capire bene quello che intendi. Comunque come ho scritto in questo post, quando usi la gui devi fare molta attenzione a come usi i threads perché possono combinare molti casini e risulta complesso trovare dove sono i problemi.

    In ogni modo per capire meglio, se puoi posta "solo" le porzioni di codice che interessano così cerchiamo di risolvere.

    RispondiElimina
  8. Ti ringrazio. Vista l'impossibilità di inserire tabulazioni raggruppo all'interno di parentesi graffe.

    while True:{
    time.sleep(1)
    if self._want_abort:{
    wx.PostEvent(self._notify_window, ResultEvent(None))
    return}
    RANDOM_INDEX = random.randint(0,len(SOUNDFILES) -1) # genero un indice casuale
    note = SOUNDFILES[RANDOM_INDEX] # recupero il file audio
    name = NOTES_DICT[note] # ...e il suo nome
    sndplay.play(NOTE_DIR + "/" + note)
    time.sleep(SLEEPTIME)
    sndplay.play(NOTE_DIR + "/" + name)
    time.sleep(SLEEPTIME2) # aspetto tra un esercizio e l'altro
    }

    SOUNDFILES è un array, NOTES_DICT è un dizionario. Se commento le due chiamate a sndplay.play() funziona perfettamente.
    Grazie ancora per l'aiuto.

    RispondiElimina
  9. per iniziare in genere per fare un operazione come quella che vuoi fare tu non si usano cicli while True. Se tu usi un ciclo di questo tipo, il programma non uscirà mai dal while e la gui non verrà aggiornata (ecco perché va in freeze, non a causa del modulo sndplay)

    Per fare l'operazione che serve a te dovresti utilizzare i timeouts. Ora tu stai usando i wxWidgets? Se si non so come aiutarti perché non li conosco.

    Con le gtk avresti dovuto utilizzare la funzione gobject.timeout_add (guardati la documentazione se non la conosci) che richiama periodicamente (allo scadere del timeout) una funzione da te specificata senza mandare in freeze la gui. Se usi i wxWidgets probabilmente esisterà qualcosa di simile.

    Inoltre ho scoperto in questi giorni un metodo più semplice per riprodurre i suoni se usi gnome:

    il codice è il seguente:

    import gnome
    gnome.sound_init('localhost')
    gnome.sound_play(percorso_al_file_da_suonare)

    Spero di esserti stato utile

    Ciao ;)

    RispondiElimina
  10. Il codice che ti ho postato non è quello che manda in freeze la gui, è quello che va in segmentation fault subito prima di suonare.

    Il "while true" è interrompibile quando si verifica la condizione self._want_abort, altrimenti deve continuare a ciclare, potenzialmente all'infinito.

    Sono riuscito forse a circoscrivere un po' il problema: arriva in questa porzione del tuo modulo sndplay

    def run(self):{
    self.ply.set_state(gst.STATE_PLAYING)
    def bus_event(bus, message):{
    t = message.type
    if t == gst.MESSAGE_EOS:{
    self.ply.set_state(gst.STATE_NULL)
    }
    return True
    }
    self.ply.get_bus().add_watch(bus_event)

    Se metto un "print" subito prima dell'if viene stampato una trentina di volte e poi va in segfault. E' normale? Se invece faccio suonare il modulo dalla python-shell non scrive nulla.

    Io sono su gnome, ma il programma serve anche ad un amico su windows, per quello il tuo modulo mi è così utile.

    Sto usando le wxwidgets, con wxglade sono riuscito a generare il codice mentre ora glade non fa altrettanto per le gtk.

    Ti ringrazio per il tempo che stai impiegando per me.

    RispondiElimina
  11. Prova a modificare il modulo e piuttosto che usare gstreamer usa la gnome play in questo modo:

    try:{
    import gnome
    gnome.sound_init("localhost")
    ENGINE = GNOMEPLAY
    }
    except ImportError:
    ...
    ...


    if ENGINE == GNOMEPLAY:{
    def play(filename):{
    gnome.sound_play(filename)
    }
    }

    In ogni caso è strano.

    Comunque o i wxWidgets per qualche motivo non vanno d'accordo con gstreamer o c'è qualcos'altro che non va nel tuo programma.

    RispondiElimina
  12. Con le modifiche il programma non va in segmentation fault ma non suona nulla.
    Non è che posso mandarti il sorgente? Sono meno di 300 righe, ma la parte importante sarà di meno di 50.

    Sto provando anche a capire come fa a suonare il player "listen", che usa le pygtk, ma senza sleep() i suoni si sovrappongono, altrimenti va in freeze la UI (come dicevi tu).

    Che casino!

    RispondiElimina
  13. Non avrei il tempo di studiarmi il tuo codice. Sono impegnato con l'università. Comunque se proprio non riesci a risolvere in alcun modo ti suggerisco di provare a chiedere sul forum di ubuntu http://forum.ubuntu-it.org/

    Mi spiace non poterti essere maggiormente d'aiuto

    RispondiElimina
  14. L'applicazione è partita.
    Sai che ho fatto? Niente. Niente, o quasi...
    Gentoo per l'importanza di portage non è ancora passata a python 2.5 (siamo ancora con python 2.4.3), così ho riavviato su $altro_os, ho installato l'ultimissimo python e ho ammirato il programma funzionare splendidamente.
    Grazie per il tempo concessomi, e complimenti per tutte le belle idee che escono dal tuo blog.

    RispondiElimina
  15. sono contento che tu abbia risolto :)

    RispondiElimina