Skip to content

Commit 36d116d

Browse files
committed
Improving Windows playback, sounddevice is default
1 parent 26de274 commit 36d116d

File tree

6 files changed

+115
-42
lines changed

6 files changed

+115
-42
lines changed

mediadecoder/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.1.2"
1+
__version__ = "0.1.3"
22
__author__ = "Daniel Schreij"
33
__license__ = "MIT"
44

mediadecoder/decoder.py

+38-35
Original file line numberDiff line numberDiff line change
@@ -415,41 +415,44 @@ def __audiorender_thread(self):
415415

416416
while self.status in [PLAYING,PAUSED]:
417417
# Retrieve audiochunk
418-
if self.status == PLAYING and new_audioframe is None:
419-
# Get a new frame from the audiostream, skip to the next one
420-
# if the current one gives a problem
421-
try:
422-
start = self.audio_times.pop(0)
423-
stop = self.audio_times[0]
424-
except IndexError:
425-
logger.debug("Audio times could not be obtained")
426-
time.sleep(0.02)
427-
continue
428-
429-
# Get the frame numbers to extract from the audio stream.
430-
chunk = (1.0/self.audioformat['fps'])*np.arange(start, stop)
431-
432-
try:
433-
# Extract the frames from the audio stream. Does not always,
434-
# succeed (e.g. with bad streams missing frames), so make
435-
# sure this doesn't crash the whole program.
436-
new_audioframe = self.clip.audio.to_soundarray(
437-
tt = chunk,
438-
buffersize = self.frame_interval*self.clip.audio.fps,
439-
quantize=True
440-
)
441-
except OSError as e:
442-
logger.warning("Sound decoding error: {}".format(e))
443-
new_audioframe = None
444-
# Put audioframe in buffer/queue for soundrenderer to pick up. If
445-
# the queue is full, try again after a timeout (this allows to check
446-
# if the status is still PLAYING after a pause.)
447-
if not new_audioframe is None:
448-
try:
449-
self.audioqueue.put(new_audioframe, timeout=.05)
450-
new_audioframe = None
451-
except Full:
452-
pass
418+
if self.status == PLAYING:
419+
if new_audioframe is None:
420+
# Get a new frame from the audiostream, skip to the next one
421+
# if the current one gives a problem
422+
try:
423+
start = self.audio_times.pop(0)
424+
stop = self.audio_times[0]
425+
except IndexError:
426+
logger.debug("Audio times could not be obtained")
427+
time.sleep(0.02)
428+
continue
429+
430+
# Get the frame numbers to extract from the audio stream.
431+
chunk = (1.0/self.audioformat['fps'])*np.arange(start, stop)
432+
433+
try:
434+
# Extract the frames from the audio stream. Does not always,
435+
# succeed (e.g. with bad streams missing frames), so make
436+
# sure this doesn't crash the whole program.
437+
new_audioframe = self.clip.audio.to_soundarray(
438+
tt = chunk,
439+
buffersize = self.frame_interval*self.clip.audio.fps,
440+
quantize=True
441+
)
442+
except OSError as e:
443+
logger.warning("Sound decoding error: {}".format(e))
444+
new_audioframe = None
445+
# Put audioframe in buffer/queue for soundrenderer to pick up. If
446+
# the queue is full, try again after a timeout (this allows to check
447+
# if the status is still PLAYING after a pause.)
448+
if not new_audioframe is None:
449+
try:
450+
self.audioqueue.put(new_audioframe, timeout=.05)
451+
new_audioframe = None
452+
except Full:
453+
pass
454+
455+
time.sleep(0.005)
453456

454457
logger.debug("Stopped audio rendering thread.")
455458

mediadecoder/soundrenderers/pyaudiorenderer.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ def get_frame(self, in_data, frame_count, time_info, status):
5050
frame = self.queue.get(False, timeout=queue_timeout)
5151
return (frame, pyaudio.paContinue)
5252
except Empty:
53-
time.sleep(queue_timeout)
54-
continue
53+
pass
5554
return (None, pyaudio.paComplete)
5655

5756
def start(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import time
2+
import pyaudio
3+
import threading
4+
5+
try:
6+
# Python 3
7+
from queue import Queue, Empty
8+
except:
9+
# Python 2
10+
from Queue import Queue, Empty
11+
12+
from mediadecoder.soundrenderers._base import SoundRenderer
13+
14+
queue_timeout=0.01
15+
16+
class SoundrendererPyAudio(threading.Thread, SoundRenderer):
17+
""" Uses pyaudio to play sound """
18+
def __init__(self, audioformat, queue=None):
19+
"""Constructor.
20+
Creates a pyaudio sound renderer.
21+
22+
Parameters
23+
----------
24+
audioformat : dict
25+
A dictionary containing the properties of the audiostream
26+
queue : Queue.queue
27+
A queue object which serves as a buffer on which the individual
28+
audio frames are placed by the decoder.
29+
"""
30+
if pyaudio is None:
31+
raise RuntimeError("Pyaudio sound renderer is not available")
32+
33+
super(SoundrendererPyAudio, self).__init__()
34+
35+
if not queue is None:
36+
self.queue = queue
37+
38+
self.pa = pyaudio.PyAudio()
39+
self.stream = self.pa.open(
40+
channels = audioformat["nchannels"],
41+
rate = audioformat["fps"],
42+
# frames_per_buffer = audioformat['buffersize'],
43+
format = pyaudio.get_format_from_width(audioformat["nbytes"]),
44+
output = True,
45+
)
46+
47+
def run(self):
48+
""" Initializes the stream. """
49+
if not hasattr(self, 'queue'):
50+
raise RuntimeError("Audio queue is not intialized.")
51+
52+
self.stream.start_stream()
53+
self.keep_listening = True
54+
55+
while self.keep_listening:
56+
try:
57+
frame = self.queue.get(False, timeout=queue_timeout)
58+
self.stream.write(frame)
59+
except Empty:
60+
continue
61+
time.sleep(0.01)
62+
63+
self.stream.stop_stream()
64+
self.stream.close()
65+
self.pa.terminate()
66+
67+
def close_stream(self):
68+
""" Closes the stream. Performs cleanup. """
69+
self.keep_listening = False

mediadecoder/soundrenderers/pygamerenderer.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import time
12
import threading
23
import pygame
34

@@ -68,6 +69,7 @@ def run(self):
6869
if not channel.get_queue():
6970
channel.queue(chunk)
7071
chunk = None
72+
time.sleep(0.005)
7173

7274
if not channel is None:
7375
channel.stop()

play.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ def load_media(self, vidSource):
134134
from mediadecoder.soundrenderers import SoundrendererPygame
135135
self.audio = SoundrendererPygame(self.decoder.audioformat)
136136
elif self.soundrenderer == "pyaudio":
137-
from mediadecoder.soundrenderers import SoundrendererPyAudio
137+
from mediadecoder.soundrenderers.pyaudiorenderer import SoundrendererPyAudio
138138
self.audio = SoundrendererPyAudio(self.decoder.audioformat)
139139
elif self.soundrenderer == "sounddevice":
140-
from mediadecoder.soundrenderers import SoundrendererSounddevice
140+
from mediadecoder.soundrenderers.sounddevicerenderer import SoundrendererSounddevice
141141
self.audio = SoundrendererSounddevice(self.decoder.audioformat)
142142
self.decoder.set_audiorenderer(self.audio)
143143

@@ -293,8 +293,8 @@ def pause(self):
293293
parser.add_argument("-l", "--loop", help="loop the video",
294294
action="store_true", default=False)
295295
parser.add_argument("-s", "--soundrenderer", help="the backend that should "
296-
" render the sound (default: pyaudio)", choices=["pygame","pyaudio",
297-
"sounddevice"], default="pyaudio")
296+
" render the sound (default: sounddevice)", choices=["pygame","pyaudio",
297+
"sounddevice"], default="sounddevice")
298298
parser.add_argument("-r", "--resolution", help="The resolution of the video."
299299
"\nSpecify as <width>x<height> (default: 1024x768)", default="1024x768")
300300

0 commit comments

Comments
 (0)