-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathtuner.py
executable file
·99 lines (76 loc) · 3.47 KB
/
tuner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#! /usr/bin/env python
######################################################################
# tuner.py - a minimal command-line guitar/ukulele tuner in Python.
# Requires numpy and pyaudio.
######################################################################
# Author: Matt Zucker
# Date: July 2016
# License: Creative Commons Attribution-ShareAlike 3.0
# https://creativecommons.org/licenses/by-sa/3.0/us/
######################################################################
import numpy as np
import pyaudio
######################################################################
# Feel free to play with these numbers. Might want to change NOTE_MIN
# and NOTE_MAX especially for guitar/bass. Probably want to keep
# FRAME_SIZE and FRAMES_PER_FFT to be powers of two.
NOTE_MIN = 60 # C4
NOTE_MAX = 69 # A4
FSAMP = 22050 # Sampling frequency in Hz
FRAME_SIZE = 2048 # How many samples per frame?
FRAMES_PER_FFT = 16 # FFT takes average across how many frames?
######################################################################
# Derived quantities from constants above. Note that as
# SAMPLES_PER_FFT goes up, the frequency step size decreases (so
# resolution increases); however, it will incur more delay to process
# new sounds.
SAMPLES_PER_FFT = FRAME_SIZE*FRAMES_PER_FFT
FREQ_STEP = float(FSAMP)/SAMPLES_PER_FFT
######################################################################
# For printing out notes
NOTE_NAMES = 'C C# D D# E F F# G G# A A# B'.split()
######################################################################
# These three functions are based upon this very useful webpage:
# https://newt.phys.unsw.edu.au/jw/notes.html
def freq_to_number(f): return 69 + 12*np.log2(f/440.0)
def number_to_freq(n): return 440 * 2.0**((n-69)/12.0)
def note_name(n): return NOTE_NAMES[n % 12] + str(n/12 - 1)
######################################################################
# Ok, ready to go now.
# Get min/max index within FFT of notes we care about.
# See docs for numpy.rfftfreq()
def note_to_fftbin(n): return number_to_freq(n)/FREQ_STEP
imin = max(0, int(np.floor(note_to_fftbin(NOTE_MIN-1))))
imax = min(SAMPLES_PER_FFT, int(np.ceil(note_to_fftbin(NOTE_MAX+1))))
# Allocate space to run an FFT.
buf = np.zeros(SAMPLES_PER_FFT, dtype=np.float32)
num_frames = 0
# Initialize audio
stream = pyaudio.PyAudio().open(format=pyaudio.paInt16,
channels=1,
rate=FSAMP,
input=True,
frames_per_buffer=FRAME_SIZE)
stream.start_stream()
# Create Hanning window function
window = 0.5 * (1 - np.cos(np.linspace(0, 2*np.pi, SAMPLES_PER_FFT, False)))
# Print initial text
print 'sampling at', FSAMP, 'Hz with max resolution of', FREQ_STEP, 'Hz'
print
# As long as we are getting data:
while stream.is_active():
# Shift the buffer down and new data in
buf[:-FRAME_SIZE] = buf[FRAME_SIZE:]
buf[-FRAME_SIZE:] = np.fromstring(stream.read(FRAME_SIZE), np.int16)
# Run the FFT on the windowed buffer
fft = np.fft.rfft(buf * window)
# Get frequency of maximum response in range
freq = (np.abs(fft[imin:imax]).argmax() + imin) * FREQ_STEP
# Get note number and nearest note
n = freq_to_number(freq)
n0 = int(round(n))
# Console output once we have a full buffer
num_frames += 1
if num_frames >= FRAMES_PER_FFT:
print 'freq: {:7.2f} Hz note: {:>3s} {:+.2f}'.format(
freq, note_name(n0), n-n0)