Skip to content

Commit dcc8e65

Browse files
committed
Add example application play_file.py
1 parent 64e1c22 commit dcc8e65

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed

doc/examples.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ Pass-Through Client
1717

1818
.. literalinclude:: ../examples/thru_client.py
1919

20+
Sound File Playback
21+
-------------------
22+
23+
:download:`play_file.py <../examples/play_file.py>`
24+
25+
.. literalinclude:: ../examples/play_file.py
26+
2027
"Showtime" Client
2128
-----------------
2229

examples/play_file.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env python3
2+
3+
"""Play a sound file.
4+
5+
This only reads a certain number of blocks at a time into memory,
6+
therefore it can handle very long files and also files with many
7+
channels.
8+
9+
NumPy and the soundfile module (http://PySoundFile.rtfd.io/) must be
10+
installed for this to work.
11+
12+
"""
13+
from __future__ import division
14+
from __future__ import print_function
15+
import argparse
16+
try:
17+
import queue # Python 3.x
18+
except ImportError:
19+
import Queue as queue # Python 2.x
20+
import sys
21+
import threading
22+
23+
parser = argparse.ArgumentParser(description=__doc__)
24+
parser.add_argument('filename', help='audio file to be played back')
25+
parser.add_argument(
26+
'-b', '--buffersize', type=int, default=10,
27+
help='number of blocks used for buffering (default: %(default)s)')
28+
parser.add_argument('-c', '--clientname', default='file player',
29+
help='JACK client name')
30+
parser.add_argument('-m', '--manual', action='store_true',
31+
help="don't connect to output ports automatically")
32+
args = parser.parse_args()
33+
if args.buffersize < 1:
34+
parser.error('buffersize must be at least 1')
35+
36+
callback2disk = queue.Queue(maxsize=1)
37+
disk2callback = queue.Queue(maxsize=1)
38+
event = threading.Event()
39+
40+
41+
def print_error(*args):
42+
print(*args, file=sys.stderr)
43+
44+
45+
def xrun(delay):
46+
print_error("An xrun occured, increase JACK's period size?")
47+
48+
49+
def shutdown(status, reason):
50+
print_error('JACK shutdown!')
51+
print_error('status:', status)
52+
print_error('reason:', reason)
53+
event.set()
54+
55+
56+
def stop_callback(msg=''):
57+
if msg:
58+
print_error(msg)
59+
for port in client.outports:
60+
port.get_array().fill(0)
61+
event.set()
62+
raise jack.CallbackExit
63+
64+
65+
def process(frames):
66+
global callback_buffer, callback_counter
67+
if frames != blocksize:
68+
stop_callback('blocksize must not be changed, I quit!')
69+
if callback_counter == 0:
70+
try:
71+
callback2disk.put_nowait(callback_buffer)
72+
callback_buffer, callback_counter = disk2callback.get_nowait()
73+
except (queue.Full, queue.Empty):
74+
stop_callback('Buffer error: increase buffersize?')
75+
if callback_counter == 0: # Playback is finished
76+
stop_callback()
77+
idx = (args.buffersize - callback_counter) * blocksize
78+
block = callback_buffer[idx:idx + blocksize]
79+
for channel, port in zip(block.T, client.outports):
80+
port.get_array()[:] = channel
81+
callback_counter -= 1
82+
83+
84+
try:
85+
import jack
86+
import numpy as np
87+
import soundfile as sf
88+
89+
client = jack.Client(args.clientname)
90+
blocksize = client.blocksize
91+
samplerate = client.samplerate
92+
client.set_xrun_callback(xrun)
93+
client.set_shutdown_callback(shutdown)
94+
client.set_process_callback(process)
95+
96+
with sf.SoundFile(args.filename) as f:
97+
block_generator = f.blocks(blocksize=blocksize, dtype='float32',
98+
always_2d=True, fill_value=0)
99+
100+
def fill_buffer(buf):
101+
nr = -1 # For the case that block_generator is already exhausted
102+
for nr, block in zip(range(args.buffersize), block_generator):
103+
idx = nr * blocksize
104+
buf[idx:idx+blocksize] = block
105+
return nr + 1 # Number of valid blocks, the rest is garbage
106+
107+
# Initialize first audio buffer to be played back
108+
buffer = np.empty([blocksize * args.buffersize, f.channels])
109+
valid_blocks = fill_buffer(buffer)
110+
disk2callback.put_nowait((buffer, valid_blocks))
111+
112+
# Initialize second buffer
113+
callback_buffer = np.empty([blocksize * args.buffersize, f.channels])
114+
callback_counter = 0
115+
116+
for ch in range(f.channels):
117+
client.outports.register('out_{0}'.format(ch + 1))
118+
119+
with client:
120+
if not args.manual:
121+
target_ports = client.get_ports(
122+
is_physical=True, is_input=True, is_audio=True)
123+
if len(client.outports) == 1 and len(target_ports) > 1:
124+
# Connect mono file to stereo output
125+
client.outports[0].connect(target_ports[0])
126+
client.outports[0].connect(target_ports[1])
127+
else:
128+
for source, target in zip(client.outports, target_ports):
129+
source.connect(target)
130+
131+
timeout = blocksize * args.buffersize / samplerate
132+
while valid_blocks:
133+
buffer = callback2disk.get(timeout=timeout)
134+
valid_blocks = fill_buffer(buffer)
135+
disk2callback.put((buffer, valid_blocks), timeout=timeout)
136+
event.wait()
137+
except KeyboardInterrupt:
138+
parser.exit('\nInterrupted by user')
139+
except (queue.Empty, queue.Full):
140+
# A timeout occured, i.e. there was an error in the callback
141+
parser.exit(1)
142+
except Exception as e:
143+
parser.exit(e)

0 commit comments

Comments
 (0)