Skip to content

Commit 6ba1b37

Browse files
SpotlightKidmgeier
authored andcommitted
Add examples for for JACK transport and timebase master
Has been squashed from and closes #74. Closes #19.
1 parent 6e67d9b commit 6ba1b37

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

examples/timebase_master.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+
"""A simple JACK timebase master."""
3+
4+
import argparse
5+
import sys
6+
from threading import Event
7+
8+
import jack
9+
10+
11+
class TimebaseMasterClient(jack.Client):
12+
def __init__(self, name, *, bpm=120.0, beats_per_bar=4, beat_type=4,
13+
ticks_per_beat=1920, conditional=False, debug=False, **kw):
14+
super().__init__(name, **kw)
15+
self.beats_per_bar = int(beats_per_bar)
16+
self.beat_type = int(beat_type)
17+
self.bpm = bpm
18+
self.conditional = conditional
19+
self.debug = debug
20+
self.ticks_per_beat = int(ticks_per_beat)
21+
self.stop_event = Event()
22+
self.set_shutdown_callback(self.shutdown)
23+
24+
def shutdown(self, status, reason):
25+
print('JACK shutdown:', reason, status)
26+
self.stop_event.set()
27+
28+
def _tb_callback(self, state, nframes, pos, new_pos):
29+
if self.debug and new_pos:
30+
print("New pos:", jack.position2dict(pos))
31+
32+
# Adapted from:
33+
# https://github.com/jackaudio/jack2/blob/develop/example-clients/transport.c#L66
34+
if new_pos:
35+
pos.beats_per_bar = float(self.beats_per_bar)
36+
pos.beats_per_minute = self.bpm
37+
pos.beat_type = float(self.beat_type)
38+
pos.ticks_per_beat = float(self.ticks_per_beat)
39+
pos.valid = jack._lib.JackPositionBBT
40+
41+
minutes = pos.frame / (pos.frame_rate * 60.0)
42+
abs_tick = minutes * self.bpm * self.ticks_per_beat
43+
abs_beat = abs_tick / self.ticks_per_beat
44+
45+
pos.bar = int(abs_beat / self.beats_per_bar)
46+
pos.beat = int(abs_beat - (pos.bar * self.beats_per_bar) + 1)
47+
pos.tick = int(abs_tick - (abs_beat * self.ticks_per_beat))
48+
pos.bar_start_tick = pos.bar * self.beats_per_bar * self.ticks_per_beat
49+
pos.bar += 1 # adjust start to bar 1
50+
else:
51+
# Compute BBT info based on previous period.
52+
pos.tick += int(nframes * pos.ticks_per_beat *
53+
pos.beats_per_minute / (pos.frame_rate * 60))
54+
55+
while pos.tick >= pos.ticks_per_beat:
56+
pos.tick -= int(pos.ticks_per_beat)
57+
pos.beat += 1
58+
59+
if pos.beat > pos.beats_per_bar:
60+
pos.beat = 1
61+
pos.bar += 1
62+
pos.bar_start_tick += pos.beats_per_bar * pos.ticks_per_beat
63+
64+
if self.debug:
65+
print("Pos:", jack.position2dict(pos))
66+
67+
def become_timebase_master(self, conditional=None):
68+
return self.set_timebase_callback(self._tb_callback, conditional
69+
if conditional is not None
70+
else self.conditional)
71+
72+
73+
def main(args=None):
74+
ap = argparse.ArgumentParser(description=__doc__)
75+
ap.add_argument(
76+
'-d', '--debug',
77+
action='store_true',
78+
help="Enable debug messages")
79+
ap.add_argument(
80+
'-c', '--conditional',
81+
action='store_true',
82+
help="Exit if another timebase master is already active")
83+
ap.add_argument(
84+
'-n', '--client-name',
85+
metavar='NAME',
86+
default='timebase',
87+
help="JACK client name (default: %(default)s)")
88+
ap.add_argument(
89+
'-m', '--meter',
90+
default='4/4',
91+
help="Meter as <beats-per-bar>/<beat-type> (default: %(default)s)")
92+
ap.add_argument(
93+
'-t', '--ticks-per-beat',
94+
type=int,
95+
metavar='NUM',
96+
default=1920,
97+
help="Ticks per beat (default: %(default)s)")
98+
ap.add_argument(
99+
'tempo',
100+
nargs='?',
101+
type=float,
102+
default=120.0,
103+
help="Tempo in beats per minute (0.1-300.0, default: %(default)s)")
104+
105+
args = ap.parse_args(args)
106+
107+
try:
108+
beats_per_bar, beat_type = (int(x) for x in args.meter.split('/', 1))
109+
except (TypeError, ValueError):
110+
print("Error: invalid meter: {}\n".format(args.meter))
111+
ap.print_help()
112+
return 2
113+
114+
try:
115+
tbmaster = TimebaseMasterClient(
116+
args.client_name,
117+
bpm=max(0.1, min(300.0, args.tempo)),
118+
beats_per_bar=beats_per_bar,
119+
beat_type=beat_type,
120+
ticks_per_beat=args.ticks_per_beat,
121+
debug=args.debug)
122+
except jack.JackError as exc:
123+
return "Could not create timebase master JACK client: {}".format(exc)
124+
125+
with tbmaster:
126+
if tbmaster.become_timebase_master(args.conditional):
127+
try:
128+
print("Press Ctrl-C to quit...")
129+
tbmaster.stop_event.wait()
130+
except KeyboardInterrupt:
131+
print('')
132+
finally:
133+
try:
134+
tbmaster.release_timebase()
135+
except jack.JackError:
136+
# another JACK client might have grabbed timebase master
137+
pass
138+
else:
139+
return "Timebase master already present. Exiting..."
140+
141+
142+
if __name__ == '__main__':
143+
sys.exit(main() or 0)

examples/transporter.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
"""Query or change the JACK transport state."""
3+
4+
import argparse
5+
import string
6+
import sys
7+
8+
import jack
9+
10+
11+
STATE_LABELS = {
12+
jack.ROLLING: "rolling",
13+
jack.STOPPED: "stopped",
14+
jack.STARTING: "starting",
15+
jack.NETSTARTING: "waiting for network sync",
16+
}
17+
18+
19+
def main(args=None):
20+
ap = argparse.ArgumentParser(description=__doc__)
21+
ap.add_argument(
22+
'-c', '--client-name',
23+
metavar='NAME',
24+
default='transporter',
25+
help="JACK client name (default: %(default)s)")
26+
ap.add_argument(
27+
'command',
28+
nargs='?',
29+
default='status',
30+
choices=['query', 'rewind', 'start', 'status', 'stop', 'toggle'],
31+
help="Transport command")
32+
33+
args = ap.parse_args(args)
34+
35+
try:
36+
client = jack.Client(args.client_name)
37+
except jack.JackError as exc:
38+
return "Could not create JACK client: {}".format(exc)
39+
40+
state = client.transport_state
41+
result = 0
42+
43+
if args.command == 'status':
44+
print("JACK transport is {}.".format(STATE_LABELS[state]))
45+
result = 1 if state == jack.STOPPED else 0
46+
elif args.command == 'query':
47+
print("State: {}".format(STATE_LABELS[state]))
48+
info = client.transport_query()[1]
49+
50+
for field in sorted(info):
51+
label = string.capwords(field.replace('_', ' '))
52+
print("{}: {}".format(label, info[field]))
53+
54+
result = 1 if state == jack.STOPPED else 0
55+
elif args.command == 'start':
56+
if state == jack.STOPPED:
57+
client.transport_start()
58+
elif args.command == 'stop':
59+
if state != jack.STOPPED:
60+
client.transport_stop()
61+
elif args.command == 'toggle':
62+
if state == jack.STOPPED:
63+
client.transport_start()
64+
else:
65+
client.transport_stop()
66+
elif args.command == 'rewind':
67+
client.transport_frame = 0
68+
69+
client.close()
70+
return result
71+
72+
73+
if __name__ == '__main__':
74+
sys.exit(main() or 0)

0 commit comments

Comments
 (0)