From time to time, when nobody sees me, I like to play the guitar and every time I face a challenge – how to tune it properly. And like in any other case Ruby comes to the rescue!

Run-up

Tuned classic guitar has known frequencies – 329,63Hz for the open first string and so on. So to tune the guitar we need to know frequency shift for any string and simply fix it.

Let’s start by recording the sound of the first open string.

In .wav file we have “signal vs time” data and to look at the wave form we can use some software, like Audacity:

Open string recording in Audacity

or we can plot it with Ruby and Gnuplot.

To obtain sound vs time data from .wav file let’s use SoX:

sox guitar_first_string.wav guitar_first_string.dat

guitar_first_string.dat will consist of several columns – time and volumes of every channel:

You can download .dat file from here.

Plotter code:

require 'open3' class GNUPlotter < Struct . new ( :data , :params ) def plot image , s = Open3 . capture2 ( "gnuplot" , stdin_data: gnuplot_commands , binmode: true ) system "open #{ params [ :image_name ] } " end private def gnuplot_commands commands = %{ set terminal png font "/Library/Fonts/Arial.ttf" 14 set title "#{params[:title]}" set xlabel "#{params[:x_axis_title]}" set ylabel "#{params[:y_axis_title]}" set output "#{params[:image_name]}" set key off plot "-" with points } data . each do | x , y | commands << " #{ x } #{ y }

" end commands << "e

" end end sound_data = File . read ( "guitar_first_string.dat" ). split ( "

" )[ 2 ..- 1 ]. map { | row | row . split . map ( & :to_f ) }. map { | r | r . first ( 2 ) } plot_params = { image_name: "plot.png" , title: "Guitar first string sound" , x_axis_title: "Time, s" , y_axis_title: ".wav signal" } plotter = GNUPlotter . new ( sound_data , plot_params ) plotter . plot

Fast Fourier Transform

Mathematical transformation employed to transform signals between time domain and frequency domain by Wikipedia

We will use http://www.fftw.org/ library for converting .wav file data (sound vs time) to its spectrum (magnitude vs frequency).

Now we have everything to calculate spectrum.

require "fftw3" require_relative "plotter" def read_channel_data ( filename , channel_number ) data = File . read ( filename ). split ( "

" )[ 2 ..- 1 ]. map { | row | row . split . map ( & :to_f ) } duration = data . last [ 0 ] signal = data . map { | r | r [ channel_number ] } return signal , duration end def calculate_fft ( signal , duration , max_points = 3000 ) na = NArray [ signal ] fc = FFTW3 . fft ( na ) spectrum = fc . real . to_a . flatten . first ( na . length / 2 ). first ( max_points ). each_with_index . map do | val , index | [ index / duration , val . abs ] end end signal , duration = read_channel_data ( "guitar_first_string.dat" , 1 ) spectrum = calculate_fft ( signal , duration ) max_frequency = spectrum . sort_by ( & :last ). last . first . round ( 2 ) spectrum_plot_params = { image_name: "spectrum.png" , title: "First guitar string spectrum #{ max_frequency } Hz" , x_axis_title: "Frequency, Hz" , y_axis_title: "Magnitude" } plotter = GNUPlotter . new ( spectrum , spectrum_plot_params ) plotter . plot

And a final plot:

As you can see we need to pull the string for 77.09Hz :)