Skip to content

Commit b2a311d

Browse files
committed
DOC: Use NumPy in play_long_file.py, move non-NumPy version to play_long_file_raw.py
1 parent f063cd1 commit b2a311d

File tree

3 files changed

+118
-11
lines changed

3 files changed

+118
-11
lines changed

doc/examples.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ Play a Very Long Sound File
1414

1515
.. literalinclude:: ../examples/play_long_file.py
1616

17+
Play a Very Long Sound File without Using NumPy
18+
-----------------------------------------------
19+
20+
:gh-example:`play_long_file_raw.py`
21+
22+
.. literalinclude:: ../examples/play_long_file_raw.py
23+
1724
Play a Web Stream
1825
-----------------
1926

examples/play_long_file.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
#!/usr/bin/env python3
22
"""Play an audio file using a limited amount of memory.
33
4-
The soundfile module (https://PySoundFile.readthedocs.io/) must be
5-
installed for this to work. NumPy is not needed.
4+
The soundfile module (https://python-soundfile.readthedocs.io/) must be
5+
installed for this to work.
66
77
In contrast to play_file.py, which loads the whole file into memory
88
before starting playback, this example program only holds a given number
99
of audio blocks in memory and is therefore able to play files that are
1010
larger than the available RAM.
1111
12-
A similar example could of course be implemented using NumPy,
13-
but this example shows what can be done when NumPy is not available.
12+
This example is implemented using NumPy, see play_long_file_raw.py
13+
for a version that doesn't need NumPy.
1414
1515
"""
1616
import argparse
@@ -77,7 +77,7 @@ def callback(outdata, frames, time, status):
7777
raise sd.CallbackAbort from e
7878
if len(data) < len(outdata):
7979
outdata[:len(data)] = data
80-
outdata[len(data):] = b'\x00' * (len(outdata) - len(data))
80+
outdata[len(data):].fill(0)
8181
raise sd.CallbackStop
8282
else:
8383
outdata[:] = data
@@ -86,18 +86,18 @@ def callback(outdata, frames, time, status):
8686
try:
8787
with sf.SoundFile(args.filename) as f:
8888
for _ in range(args.buffersize):
89-
data = f.buffer_read(args.blocksize, dtype='float32')
90-
if not data:
89+
data = f.read(args.blocksize)
90+
if not len(data):
9191
break
9292
q.put_nowait(data) # Pre-fill queue
93-
stream = sd.RawOutputStream(
93+
stream = sd.OutputStream(
9494
samplerate=f.samplerate, blocksize=args.blocksize,
95-
device=args.device, channels=f.channels, dtype='float32',
95+
device=args.device, channels=f.channels,
9696
callback=callback, finished_callback=event.set)
9797
with stream:
9898
timeout = args.blocksize * args.buffersize / f.samplerate
99-
while data:
100-
data = f.buffer_read(args.blocksize, dtype='float32')
99+
while len(data):
100+
data = f.read(args.blocksize)
101101
q.put(data, timeout=timeout)
102102
event.wait() # Wait until playback is finished
103103
except KeyboardInterrupt:

examples/play_long_file_raw.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python3
2+
"""Play an audio file using a limited amount of memory.
3+
4+
This is the same as play_long_file.py, but implemented without using NumPy.
5+
6+
"""
7+
import argparse
8+
import queue
9+
import sys
10+
import threading
11+
12+
import sounddevice as sd
13+
import soundfile as sf
14+
15+
16+
def int_or_str(text):
17+
"""Helper function for argument parsing."""
18+
try:
19+
return int(text)
20+
except ValueError:
21+
return text
22+
23+
24+
parser = argparse.ArgumentParser(add_help=False)
25+
parser.add_argument(
26+
'-l', '--list-devices', action='store_true',
27+
help='show list of audio devices and exit')
28+
args, remaining = parser.parse_known_args()
29+
if args.list_devices:
30+
print(sd.query_devices())
31+
parser.exit(0)
32+
parser = argparse.ArgumentParser(
33+
description=__doc__,
34+
formatter_class=argparse.RawDescriptionHelpFormatter,
35+
parents=[parser])
36+
parser.add_argument(
37+
'filename', metavar='FILENAME',
38+
help='audio file to be played back')
39+
parser.add_argument(
40+
'-d', '--device', type=int_or_str,
41+
help='output device (numeric ID or substring)')
42+
parser.add_argument(
43+
'-b', '--blocksize', type=int, default=2048,
44+
help='block size (default: %(default)s)')
45+
parser.add_argument(
46+
'-q', '--buffersize', type=int, default=20,
47+
help='number of blocks used for buffering (default: %(default)s)')
48+
args = parser.parse_args(remaining)
49+
if args.blocksize == 0:
50+
parser.error('blocksize must not be zero')
51+
if args.buffersize < 1:
52+
parser.error('buffersize must be at least 1')
53+
54+
q = queue.Queue(maxsize=args.buffersize)
55+
event = threading.Event()
56+
57+
58+
def callback(outdata, frames, time, status):
59+
assert frames == args.blocksize
60+
if status.output_underflow:
61+
print('Output underflow: increase blocksize?', file=sys.stderr)
62+
raise sd.CallbackAbort
63+
assert not status
64+
try:
65+
data = q.get_nowait()
66+
except queue.Empty as e:
67+
print('Buffer is empty: increase buffersize?', file=sys.stderr)
68+
raise sd.CallbackAbort from e
69+
if len(data) < len(outdata):
70+
outdata[:len(data)] = data
71+
outdata[len(data):] = b'\x00' * (len(outdata) - len(data))
72+
raise sd.CallbackStop
73+
else:
74+
outdata[:] = data
75+
76+
77+
try:
78+
with sf.SoundFile(args.filename) as f:
79+
for _ in range(args.buffersize):
80+
data = f.buffer_read(args.blocksize, dtype='float32')
81+
if not data:
82+
break
83+
q.put_nowait(data) # Pre-fill queue
84+
stream = sd.RawOutputStream(
85+
samplerate=f.samplerate, blocksize=args.blocksize,
86+
device=args.device, channels=f.channels, dtype='float32',
87+
callback=callback, finished_callback=event.set)
88+
with stream:
89+
timeout = args.blocksize * args.buffersize / f.samplerate
90+
while data:
91+
data = f.buffer_read(args.blocksize, dtype='float32')
92+
q.put(data, timeout=timeout)
93+
event.wait() # Wait until playback is finished
94+
except KeyboardInterrupt:
95+
parser.exit('\nInterrupted by user')
96+
except queue.Full:
97+
# A timeout occurred, i.e. there was an error in the callback
98+
parser.exit(1)
99+
except Exception as e:
100+
parser.exit(type(e).__name__ + ': ' + str(e))

0 commit comments

Comments
 (0)