Skip to content

Commit 0898941

Browse files
committed
sounddevice renderer working
1 parent 96ba10a commit 0898941

File tree

6 files changed

+112
-12
lines changed

6 files changed

+112
-12
lines changed

mediadecoder/decoder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def __audiorender_thread(self):
420420
# if the current one gives a problem
421421
try:
422422
start = self.audio_times.pop(0)
423-
stop = self.audio_times[1]
423+
stop = self.audio_times[0]
424424
except IndexError:
425425
logger.debug("Audio times could not be obtained")
426426
time.sleep(0.02)
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
from mediadecoder.soundrenderers.pyaudiorenderer import SoundrendererPyAudio
2-
from mediadecoder.soundrenderers.pygamerenderer import SoundrendererPygame
3-
from mediadecoder.soundrenderers.sounddevicerenderer import SoundrendererSounddevice
1+
import warnings
2+
3+
try:
4+
from mediadecoder.soundrenderers.pyaudiorenderer import SoundrendererPyAudio
5+
except Exception as e:
6+
warnings.warn("Could not import pyaudio sound renderer: {}".format(e))
7+
8+
try:
9+
from mediadecoder.soundrenderers.pygamerenderer import SoundrendererPygame
10+
except Exception as e:
11+
warnings.warn("Could not import pygame sound renderer: {}".format(e))
12+
13+
try:
14+
from mediadecoder.soundrenderers.sounddevicerenderer import SoundrendererSounddevice
15+
except Exception as e:
16+
warnings.warn("Could not import sounddevice sound renderer: {}".format(e))
417

518
__all__ = ['SoundrendererPygame', 'SoundrendererPyAudio','SoundrendererSounddevice']
619

mediadecoder/soundrenderers/pygamerenderer.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,18 @@ def __init__(self, audioformat, queue=None):
4141
fps = audioformat["fps"]
4242
nchannels = audioformat["nchannels"]
4343
nbytes = audioformat["nbytes"]
44+
buffersize = audioformat["buffersize"]
4445

4546
pygame.mixer.quit()
46-
pygame.mixer.init(fps, -8 * nbytes, nchannels)
47+
pygame.mixer.init(fps, -8 * nbytes, nchannels, buffersize)
4748

4849
def run(self):
4950
""" Main thread function. """
5051
if not hasattr(self, 'queue'):
5152
raise RuntimeError("Audio queue is not intialized.")
5253

5354
chunk = None
55+
channel = None
5456
self.keep_listening = True
5557
while self.keep_listening:
5658
if chunk is None:
@@ -60,12 +62,16 @@ def run(self):
6062
except Empty:
6163
continue
6264

63-
if not hasattr(self,"channel"):
64-
self.channel = chunk.play()
65+
if channel is None:
66+
channel = chunk.play()
6567
else:
66-
if not pygame.mixer.get_busy():
67-
self.channel.queue(chunk)
68+
if not channel.get_queue():
69+
channel.queue(chunk)
6870
chunk = None
71+
72+
if not channel is None:
73+
channel.stop()
74+
pygame.mixer.quit()
6975

7076
def close_stream(self):
7177
""" Cleanup (done by pygame.quit() in main loop) """

mediadecoder/soundrenderers/sounddevicerenderer.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def __init__(self, audioformat, queue=None):
3232
self.stream = sd.OutputStream(
3333
channels = audioformat["nchannels"],
3434
samplerate = audioformat["fps"],
35-
blocksize = audioformat["buffersize"]*2,
35+
dtype = 'int{}'.format(audioformat['nbytes']*8),
36+
blocksize = audioformat["buffersize"],
3637
callback = self.get_frame
3738
)
3839
self.keep_listening = True
@@ -41,7 +42,12 @@ def get_frame(self, outdata, frames, timedata, status):
4142
""" Callback function for the audio stream. Don't use directly. """
4243
try:
4344
chunk = self.queue.get_nowait()
44-
outdata[:] = chunk
45+
# Check if the chunk contains the expected number of frames
46+
# The callback function raises a ValueError otherwise.
47+
if chunk.shape[0] == frames:
48+
outdata[:] = chunk
49+
else:
50+
outdata.fill(0)
4551
except Empty:
4652
outdata.fill(0)
4753

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
""" This is an alternative implementation of sounddevicerenderer, that doesn't use
3+
the callback functionality of sounddevice's OutputStream. The threading is done by
4+
python, instead of C (under the hood) by sounddevice. I haven't determined yet
5+
which method is better, so I am leaving them both in for now. """
6+
7+
8+
import threading
9+
import sounddevice as sd
10+
import logging
11+
logger = logging.getLogger(__name__)
12+
13+
try:
14+
# Python 3
15+
from queue import Queue, Empty
16+
except:
17+
# Python 2
18+
from Queue import Queue, Empty
19+
20+
from mediadecoder.soundrenderers._base import SoundRenderer
21+
22+
queue_timeout=0.01
23+
24+
class SoundrendererSounddevice(threading.Thread, SoundRenderer):
25+
""" Uses pyaudio to play sound """
26+
def __init__(self, audioformat, queue=None):
27+
"""Constructor.
28+
Creates a pyaudio sound renderer.
29+
30+
Parameters
31+
----------
32+
audioformat : dict
33+
A dictionary containing the properties of the audiostream
34+
queue : Queue.queue
35+
A queue object which serves as a buffer on which the individual
36+
audio frames are placed by the decoder.
37+
"""
38+
super(SoundrendererSounddevice, self).__init__()
39+
40+
if not queue is None:
41+
self.queue = queue
42+
43+
self.stream = sd.OutputStream(
44+
channels = audioformat["nchannels"],
45+
samplerate = audioformat["fps"],
46+
dtype = 'int{}'.format(audioformat['nbytes']*8),
47+
blocksize = audioformat["buffersize"],
48+
)
49+
50+
51+
def run(self):
52+
""" Initializes the stream. """
53+
if not hasattr(self, 'queue'):
54+
raise RuntimeError("Audio queue is not intialized.")
55+
self.keep_listening = True
56+
self.stream.start()
57+
58+
while self.keep_listening:
59+
try:
60+
chunk = self.queue.get(timeout=queue_timeout)
61+
underflowed = self.stream.write(chunk)
62+
if underflowed:
63+
logger.debug("Buffer underrun")
64+
except Empty:
65+
pass
66+
67+
self.stream.stop()
68+
self.stream.close()
69+
70+
71+
def close_stream(self):
72+
""" Closes the stream. Performs cleanup. """
73+
self.keep_listening = False

play.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import mediadecoder # For the state constants
1414
from mediadecoder.decoder import Decoder
15-
from mediadecoder.soundrenderers import *
1615

1716
class VideoPlayer():
1817
""" This is an example videoplayer that uses pygame+pyopengl to render a video.
@@ -132,10 +131,13 @@ def load_media(self, vidSource):
132131

133132
if(self.decoder.audioformat):
134133
if self.soundrenderer == "pygame":
134+
from mediadecoder.soundrenderers import SoundrendererPygame
135135
self.audio = SoundrendererPygame(self.decoder.audioformat)
136136
elif self.soundrenderer == "pyaudio":
137+
from mediadecoder.soundrenderers import SoundrendererPyAudio
137138
self.audio = SoundrendererPyAudio(self.decoder.audioformat)
138139
elif self.soundrenderer == "sounddevice":
140+
from mediadecoder.soundrenderers import SoundrendererSounddevice
139141
self.audio = SoundrendererSounddevice(self.decoder.audioformat)
140142
self.decoder.set_audiorenderer(self.audio)
141143

0 commit comments

Comments
 (0)