Skip to content

Commit 06853c9

Browse files
author
Simon Holliday
committed
- Add tuner tone script
1 parent 4a222ed commit 06853c9

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

scripts/tuner_tone.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
"""
3+
This script plays a pulsed E3 note on all 16 MIDI channels of a selected output device.
4+
5+
It is utility is used to assess whether instruments are in tune with each
6+
other and allow tuning against a reference. This is particularly useful
7+
for analogue instruments which may drift in pitch over time.
8+
9+
The note is triggered once per second to accommodate instruments with
10+
short non-sustaining envelopes.
11+
"""
12+
13+
import logging
14+
import sys
15+
import time
16+
import typing
17+
18+
import mido
19+
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
def _select_output_device () -> typing.Tuple[str, typing.Any]:
25+
26+
"""Prompt the user to select a MIDI output device and open it."""
27+
28+
try:
29+
outputs = mido.get_output_names()
30+
logger.info("Available MIDI outputs: %s", outputs)
31+
32+
if not outputs:
33+
raise RuntimeError("No MIDI output devices found.")
34+
35+
if len(outputs) == 1:
36+
selected_name = outputs[0]
37+
midi_out = mido.open_output(selected_name)
38+
logger.info("One MIDI output found - using '%s'", selected_name)
39+
return selected_name, midi_out
40+
41+
print("\nAvailable MIDI output devices:\n")
42+
for i, name in enumerate(outputs, 1):
43+
print(f" {i}. {name}")
44+
print()
45+
46+
while True:
47+
try:
48+
choice = int(input(f"Select a device (1-{len(outputs)}): "))
49+
if 1 <= choice <= len(outputs):
50+
break
51+
except (ValueError, EOFError):
52+
pass
53+
print(f"Enter a number between 1 and {len(outputs)}.")
54+
55+
selected_name = outputs[choice - 1]
56+
midi_out = mido.open_output(selected_name)
57+
logger.info("Opened MIDI output: %s", selected_name)
58+
return selected_name, midi_out
59+
except Exception as exc:
60+
raise RuntimeError(f"Failed to open MIDI output: {exc}") from exc
61+
62+
63+
def main () -> None:
64+
65+
"""Run the tuner tone script."""
66+
67+
logging.basicConfig(level=logging.WARNING, format='%(message)s')
68+
69+
print("Subsequence Tuner Tone\n")
70+
71+
try:
72+
device_name, midi_out = _select_output_device()
73+
except RuntimeError as exc:
74+
print(exc)
75+
sys.exit(1)
76+
77+
# E3 is MIDI note 52 (assuming C4 = 60).
78+
pitch = 52
79+
velocity = 100
80+
81+
print(f"\nSending E3 (note {pitch}) to all 16 channels on '{device_name}'...")
82+
print("Press CTRL-C to stop.")
83+
84+
try:
85+
# Keep running until interrupted, repeatedly triggering the notes
86+
while True:
87+
for channel in range(16):
88+
msg_on = mido.Message('note_on', note=pitch, velocity=velocity, channel=channel)
89+
midi_out.send(msg_on)
90+
91+
time.sleep(1.0)
92+
93+
for channel in range(16):
94+
msg_off = mido.Message('note_off', note=pitch, velocity=0, channel=channel)
95+
midi_out.send(msg_off)
96+
97+
except KeyboardInterrupt:
98+
print("\nStopping notes...")
99+
finally:
100+
# Send note_off to all channels
101+
for channel in range(16):
102+
msg_off = mido.Message('note_off', note=pitch, velocity=0, channel=channel)
103+
midi_out.send(msg_off)
104+
midi_out.close()
105+
print("Done.")
106+
107+
108+
if __name__ == '__main__':
109+
main()

0 commit comments

Comments
 (0)