├── README.md ├── LICENSE └── tuner.py /README.md: -------------------------------------------------------------------------------- 1 | # python-tuner 2 | Minimal command-line guitar/ukulele tuner in Python. 3 | Writeup at 4 | 5 | To run: 6 | 7 | python tuner.py 8 | 9 | ...then pluck the strings! 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Matt Zucker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tuner.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | ###################################################################### 3 | # tuner.py - a minimal command-line guitar/ukulele tuner in Python. 4 | # Requires numpy and pyaudio. 5 | ###################################################################### 6 | # Author: Matt Zucker 7 | # Date: July 2016 8 | # License: Creative Commons Attribution-ShareAlike 3.0 9 | # https://creativecommons.org/licenses/by-sa/3.0/us/ 10 | ###################################################################### 11 | 12 | import numpy as np 13 | import pyaudio 14 | 15 | ###################################################################### 16 | # Feel free to play with these numbers. Might want to change NOTE_MIN 17 | # and NOTE_MAX especially for guitar/bass. Probably want to keep 18 | # FRAME_SIZE and FRAMES_PER_FFT to be powers of two. 19 | 20 | NOTE_MIN = 60 # C4 21 | NOTE_MAX = 69 # A4 22 | FSAMP = 22050 # Sampling frequency in Hz 23 | FRAME_SIZE = 2048 # How many samples per frame? 24 | FRAMES_PER_FFT = 16 # FFT takes average across how many frames? 25 | 26 | ###################################################################### 27 | # Derived quantities from constants above. Note that as 28 | # SAMPLES_PER_FFT goes up, the frequency step size decreases (so 29 | # resolution increases); however, it will incur more delay to process 30 | # new sounds. 31 | 32 | SAMPLES_PER_FFT = FRAME_SIZE*FRAMES_PER_FFT 33 | FREQ_STEP = float(FSAMP)/SAMPLES_PER_FFT 34 | 35 | ###################################################################### 36 | # For printing out notes 37 | 38 | NOTE_NAMES = 'C C# D D# E F F# G G# A A# B'.split() 39 | 40 | ###################################################################### 41 | # These three functions are based upon this very useful webpage: 42 | # https://newt.phys.unsw.edu.au/jw/notes.html 43 | 44 | def freq_to_number(f): return 69 + 12*np.log2(f/440.0) 45 | def number_to_freq(n): return 440 * 2.0**((n-69)/12.0) 46 | def note_name(n): return NOTE_NAMES[n % 12] + str(n/12 - 1) 47 | 48 | ###################################################################### 49 | # Ok, ready to go now. 50 | 51 | # Get min/max index within FFT of notes we care about. 52 | # See docs for numpy.rfftfreq() 53 | def note_to_fftbin(n): return number_to_freq(n)/FREQ_STEP 54 | imin = max(0, int(np.floor(note_to_fftbin(NOTE_MIN-1)))) 55 | imax = min(SAMPLES_PER_FFT, int(np.ceil(note_to_fftbin(NOTE_MAX+1)))) 56 | 57 | # Allocate space to run an FFT. 58 | buf = np.zeros(SAMPLES_PER_FFT, dtype=np.float32) 59 | num_frames = 0 60 | 61 | # Initialize audio 62 | stream = pyaudio.PyAudio().open(format=pyaudio.paInt16, 63 | channels=1, 64 | rate=FSAMP, 65 | input=True, 66 | frames_per_buffer=FRAME_SIZE) 67 | 68 | stream.start_stream() 69 | 70 | # Create Hanning window function 71 | window = 0.5 * (1 - np.cos(np.linspace(0, 2*np.pi, SAMPLES_PER_FFT, False))) 72 | 73 | # Print initial text 74 | print 'sampling at', FSAMP, 'Hz with max resolution of', FREQ_STEP, 'Hz' 75 | print 76 | 77 | # As long as we are getting data: 78 | while stream.is_active(): 79 | 80 | # Shift the buffer down and new data in 81 | buf[:-FRAME_SIZE] = buf[FRAME_SIZE:] 82 | buf[-FRAME_SIZE:] = np.fromstring(stream.read(FRAME_SIZE), np.int16) 83 | 84 | # Run the FFT on the windowed buffer 85 | fft = np.fft.rfft(buf * window) 86 | 87 | # Get frequency of maximum response in range 88 | freq = (np.abs(fft[imin:imax]).argmax() + imin) * FREQ_STEP 89 | 90 | # Get note number and nearest note 91 | n = freq_to_number(freq) 92 | n0 = int(round(n)) 93 | 94 | # Console output once we have a full buffer 95 | num_frames += 1 96 | 97 | if num_frames >= FRAMES_PER_FFT: 98 | print 'freq: {:7.2f} Hz note: {:>3s} {:+.2f}'.format( 99 | freq, note_name(n0), n-n0) 100 | --------------------------------------------------------------------------------