/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id:$ * * Copyright (C) 2008 Lechner Michael / smoking gnu * * All files in this archive are subject to the GNU General Public License. * See the file COPYING in the source tree root for full license agreement. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * * ---------------------------------------------------------------------------- * * INTRODUCTION: * OK, this is an attempt to write an instrument tuner for rockbox. * It uses a Schmitt trigger algorithm, which I copied from * tuneit [ (c) 2004 Mario Lang ], for detecting the * fundamental freqency of a sound. A FFT algorithm would be more accurate * but also much slower. * * TODO: * - Find someone who knows how recording actually works, and rewrite the * recording code to use proper, gapless recording with a callback function * that provides new buffer, instead of stopping and restarting recording * everytime the buffer is full * - Convert all floating point operations to fixed-point * - Adapt the Yin FFT algorithm, which would reduce complexity from O(n^2) * to O(nlogn), theoretically reducing latency by a factor of ~10. -David * * MAJOR CHANGES: * 08.03.2008 Started coding * 21.03.2008 Pitch detection works more or less * Button definitions for most targets added * 02.04.2008 Proper GUI added * Todo, Major Changes and Current Limitations added * 08.19.2009 Brought the code up to date with current plugin standards * Made it work more nicely with color, BW and grayscale * Changed pitch detection to use the Yin algorithm (better * detection, but slower -- would be ~4x faster with * fixed point math, I think). Code was poached from the * Aubio sound processing library (aubio.org). -David * * CURRENT LIMITATIONS: * - Everything is hard-coded: buttons, input type (mic, line), * buffer size , Yin threshold -- nothing can be changed at runtime * - No gapless recording (I think I know how to fix this but it would * only save ~1/40th of a second per sample period -David) * - Due to how the Yin algorithm works, there's high latency if there's no * pitch being recorded or if the pitch is outside the algorithm's range. * - Due to how the Yin algorithm works, latency is higher for lower * frequencies. * - In order to keep latency to a reasonable level I set the buffer * size to 1024 samples, meaning the lowest detectable frequency is * about SAMPLE_RATE / 512, or about 86Hz on devices with a 44.1k sample * rate. In reality, it should properly detect a the pitch of a * frequency of half that value if the signal includes any harmonics. * - Untested on devices other than h120 -David */ #include "plugin.h" PLUGIN_HEADER /* Button definitions and Samplerate defining*/ #if CONFIG_KEYPAD == RECORDER_PAD /* Recorder */ #define TUNER_QUIT BUTTON_OFF #elif CONFIG_KEYPAD == ARCHOS_AV300_PAD #define TUNER_QUIT BUTTON_OFF #elif CONFIG_KEYPAD == ONDIO_PAD /* Ondio */ #define TUNER_QUIT BUTTON_OFF #elif CONFIG_KEYPAD == PLAYER_PAD /* Player */ #define TUNER_QUIT BUTTON_STOP #elif (CONFIG_KEYPAD == IRIVER_H100_PAD) || \ (CONFIG_KEYPAD == IRIVER_H300_PAD) /* iRiver H1x0 && H3x0 */ #define TUNER_QUIT BUTTON_OFF #define SAMPLE_RATE 44100 #define BW #elif (CONFIG_KEYPAD == IPOD_4G_PAD) || \ (CONFIG_KEYPAD == IPOD_3G_PAD) || \ (CONFIG_KEYPAD == IPOD_1G2G_PAD) /* iPods */ #define TUNER_QUIT BUTTON_SELECT #define SAMPLE_RATE 44100 ifdecCL #elif CONFIG_KEYPAD == IAUDIO_X5M5_PAD /* iAudio X5/M5 */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 44100 #elif CONFIG_KEYPAD == GIGABEAT_PAD /* GIGABEAT */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 44100 #elif CONFIG_KEYPAD == SANSA_E200_PAD /* Sansa E200 */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 22050 #elif CONFIG_KEYPAD == SANSA_C200_PAD /* Sansa C200 */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 22050 #elif CONFIG_KEYPAD == IRIVER_H10_PAD /* iriver H10 */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 44100 #elif CONFIG_KEYPAD == MROBE500_PAD /* M:robe 500 */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 44100 #elif CONFIG_KEYPAD == GIGABEAT_S_PAD /* Gigabeat S */ #define TUNER_QUIT BUTTON_BACK #define SAMPLE_RATE 44100 #elif CONFIG_KEYPAD == MROBE100_PAD /* M:robe 100 */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 44100 #else /* Everything else */ #define TUNER_QUIT BUTTON_POWER #define SAMPLE_RATE 44100 #endif /* Some constants for tuning */ #define A_FREQ 440.0f #define D_NOTE 1.059463094359f #define LOG_D_NOTE 0.057762265047f #define D_NOTE_SQRT 1.029302236643f #define LOG_2 0.693147180559f /* The recording buffer size */ /* This is how much is sampled at a time. */ /* It also determines latency -- if BUFFER_SIZE == SAMPLE_RATE then */ /* there'll be one sample per second, or a latency of one second. */ /* Furthermore, the lowest detectable frequency will be about twice */ /* the number of reads per second */ /* For the Yin FFT algorithm this needs to be a power of 2 */ #define BUFFER_SIZE 1024 #define LCD_FACTOR ((float)LCD_WIDTH / (float)100.0f) /* The threshold for the YIN algorithm */ #define YIN_THRESHOLD 0.15f /* Change to AUDIO_SRC_LINE if you want to record from line-in */ #define INPUT_TYPE AUDIO_SRC_MIC static signed short audio_data[BUFFER_SIZE]; float yin_buffer[BUFFER_SIZE / 2]; static int recording=0; /* Frequencies of all the notes of the scale */ static const float freqs[12] = { 440.0f, /* A */ 466.1637615f, /* A# */ 493.8833013f, /* etc... */ 523.2511306f, 554.3652620f, 587.3295358f, 622.2539674f, 659.2551138f, 698.4564629f, 739.9888454f, 783.9908720f, 830.6093952f }; /* logarithm of all the notes of the scale */ static const float lfreqs[12] = { 6.086774727f, 6.144536992f, 6.202299257f, 6.260061522f, 6.317823787f, 6.375586052f, 6.433348317f, 6.491110582f, 6.548872847f, 6.606635112f, 6.664397377f, 6.722159642f }; /* GUI */ static unsigned back_color, front_color; static int xx,yy; static int bar_x_minus_50, bar_x_minus_20, bar_x_0, bar_x_20, bar_x_50; static const char *english_notes[12] = {"A","A#","B","C","C#","D","D#","E", "F","F#", "G", "G#"}; static const char gui_letters[8] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', '#'}; static const char **notes = english_notes; /* Fixed-point Natural log */ double log(double inp) { int x; long t,y; x = (int) ( inp * ( 1 << 16 ) ); y=0xa65af; if(x<0x00008000) x<<=16, y-=0xb1721; if(x<0x00800000) x<<= 8, y-=0x58b91; if(x<0x08000000) x<<= 4, y-=0x2c5c8; if(x<0x20000000) x<<= 2, y-=0x162e4; if(x<0x40000000) x<<= 1, y-=0x0b172; t=x+(x>>1); if((t&0x80000000)==0) x=t,y-=0x067cd; t=x+(x>>2); if((t&0x80000000)==0) x=t,y-=0x03920; t=x+(x>>3); if((t&0x80000000)==0) x=t,y-=0x01e27; t=x+(x>>4); if((t&0x80000000)==0) x=t,y-=0x00f85; t=x+(x>>5); if((t&0x80000000)==0) x=t,y-=0x007e1; t=x+(x>>6); if((t&0x80000000)==0) x=t,y-=0x003f8; t=x+(x>>7); if((t&0x80000000)==0) x=t,y-=0x001fe; x=0x80000000-x; y-=x>>15; return (float)y / (float)0x10000; } /* The function name is pretty self-explaining ;) */ void print_int_xy(int x, int y, int v) { char temp[20]; rb->lcd_set_foreground(front_color); rb->snprintf(temp,20,"%d",v); rb->lcd_putsxy(x,y,temp); rb->lcd_update(); } /* Print out the frequency etc - will be removed later on */ void print_str(char* s) { rb->lcd_set_foreground(front_color); rb->lcd_putsxy(0, 50, s); rb->lcd_update(); } /* What can I say? Read the function name... */ void print_char_xy(int x, int y, char c) { char temp[2]; temp[0]=c; temp[1]=0; rb->lcd_set_foreground(front_color); rb->lcd_putsxy(x, y, temp); } /* Draw the red bar and the white lines */ void draw_bar(float wrong_by_cents) { #ifdef HAVE_LCD_COLOR rb->lcd_set_foreground(LCD_RGBPACK(255,0,0)); /* Color screens */ #elif LCD_DEPTH > 1 rb->lcd_set_foreground(LCD_DARKGRAY); /* Greyscale screens */ #else rb->lcd_set_foreground(LCD_BLACK); /* Black and white screens */ #endif if (wrong_by_cents > 0) { rb->lcd_fillrect(bar_x_0,90, (int)(wrong_by_cents * LCD_FACTOR), 20); } else { rb->lcd_fillrect(bar_x_0 + (int)(wrong_by_cents * LCD_FACTOR),90, (int)(wrong_by_cents * LCD_FACTOR) * -1, 20); } #ifdef HAVE_LCD_COLOR rb->lcd_set_foreground(LCD_RGBPACK(255,255,255)); /* Color screens */ #elif LCD_DEPTH > 1 rb->lcd_set_foreground(LCD_BLACK); /* Greyscale screens */ #else rb->lcd_set_foreground(LCD_BLACK); /* Black and white screens */ #endif rb->lcd_hline(0,LCD_WIDTH-1, 85); rb->lcd_hline(0,LCD_WIDTH-1, 115); rb->lcd_vline(LCD_WIDTH / 2, 85, 115); print_int_xy(bar_x_minus_50 ,70, -50); print_int_xy(bar_x_minus_20 ,70, -20); print_int_xy(bar_x_0 ,70, 0); print_int_xy(bar_x_20 ,70, 20); print_int_xy(bar_x_50 ,70, 50); rb->lcd_update(); } /* Print the letters A-G and the # on the screen */ void draw_letters(void) { int i; rb->lcd_set_foreground(front_color); for (i=0; i<8; i++) { print_char_xy(i*(LCD_WIDTH / 8 ) + xx, 10, gui_letters[i]); } } /* Draw the yellow point(s) below the letters and the '#' */ void draw_points(const char *s) { int i; i = s[0]-'A'; #ifdef HAVE_LCD_COLOR rb->lcd_set_foreground(LCD_RGBPACK(255,255,0)); /* Color screens */ #elif LCD_DEPTH > 1 rb->lcd_set_foreground(LCD_DARKGRAY); /* Grey screens */ #else rb->lcd_set_foreground(LCD_BLACK); /* Black and White screens */ #endif rb->lcd_fillrect(i*(LCD_WIDTH / 8 ) + xx, 25, yy, yy); if (s[1] == '#') rb->lcd_fillrect(7*(LCD_WIDTH / 8 ) + xx, 25, yy, yy); rb->lcd_update(); } /* Calculate how wrong the note is and draw the GUI */ void display_frequency (double freq) { double ldf, mldf; double lfreq, nfreq; int i, note = 0; char str_buf[30]; if (freq < 1E-15) freq = 1E-15; lfreq = log(freq); while (lfreq < lfreqs[0]-LOG_D_NOTE/2.) lfreq += LOG_2; while (lfreq >= lfreqs[0]+LOG_2-LOG_D_NOTE/2.) lfreq -= LOG_2; mldf = LOG_D_NOTE; for (i=0; i<12; i++) { ldf = ((lfreq-lfreqs[i]>0) ? lfreq-lfreqs[i] : -(lfreq-lfreqs[i])); if (ldf < mldf) { mldf = ldf; note = i; } } nfreq = freqs[note]; while (nfreq/freq > D_NOTE_SQRT) nfreq /= 2.0; while (freq/nfreq > D_NOTE_SQRT) nfreq *= 2.0; ldf=1200*(log(freq/nfreq)/LOG_2); if ((int)freq == 0) /* prevents red bar at -32 cents when freq= 0*/ { ldf = 0; } rb->snprintf(str_buf,30, "%s : %d cents (%dHz)", notes[note], (int)ldf ,(int) freq); rb->lcd_clear_display(); draw_bar(ldf); /* The red bar */ draw_letters(); /* The A-G letters and the # */ draw_points(notes[note]); /* The yellow point(s) */ print_str(str_buf); } /*----------------------------------------------------------------------- * Functions for the Yin algorithm * * These were all adapted from the versions in Aubio v0.3.2 * Here's what the Aubio documentation has to say: * * This algorithm was developped by A. de Cheveigne and H. Kawahara and * published in: * * de Cheveigné, A., Kawahara, H. (2002) "YIN, a fundamental frequency * estimator for speech and music", J. Acoust. Soc. Am. 111, 1917-1930. * * see http://recherche.ircam.fr/equipes/pcm/pub/people/cheveign.html -------------------------------------------------------------------------*/ /* Find the index of the minimum element of an array of floats */ unsigned vec_min_elem(float *s, unsigned buflen) { unsigned j, pos=0.0f; float tmp = s[0]; for (j=0; j < buflen; j++) { if(tmp > s[j]) { pos = j; tmp = s[j]; } } return pos; } float aubio_quadfrac(float s0, float s1, float s2, float pf) { float tmp = s0 + (pf/2.0f) * (pf * ( s0 - 2.0f*s1 + s2 ) - 3.0f*s0 + 4.0f*s1 - s2); return tmp; } float vec_quadint_min(float *x, unsigned bufsize, unsigned pos, unsigned span) { float step = 1.0f/200.0f; /* init resold to - something (in case x[pos+-span]<0)) */ float res, frac, s0, s1, s2, exactpos = (float)pos, resold = 100000.0f; if ((pos > span) && (pos < bufsize-span)) { s0 = x[pos-span]; s1 = x[pos] ; s2 = x[pos+span]; /* increase frac */ for (frac = 0.0f; frac < 2.0f; frac = frac + step) { res = aubio_quadfrac(s0, s1, s2, frac); if (res < resold) { resold = res; } else { exactpos += (frac-step)*span - span/2.0f; break; } } } return exactpos; } /* Calculate the period of the note in the buffer using the YIN algorithm */ /* The yin pointer is just a buffer that the algorithm uses as a work space. It needs to be half the length of the input buffer. */ float pitchyin(int16_t *input, unsigned bufsize, float *yin) { unsigned j,tau = 0; unsigned yin_bufsize = bufsize / 2; int period; float tmp = 0.0f, tmp2 = 0.0f; yin[0] = 1.0f; for (tau = 1; tau < yin_bufsize; tau++) { yin[tau] = 0.0f; for (j=0;j 4 && (yin[period] < YIN_THRESHOLD) && (yin[period] < yin[period+1])) { return vec_quadint_min(yin, yin_bufsize, period, 1); } } /*return vec_quadint_min(yin, yin_bufsize, vec_min_elem(yin, yin_bufsize), 1);*/ return 0; } /*-----------------------------------------------------------------*/ /* Stop the recording when the buffer is full */ int recording_callback(int status) { (void) status; rb->pcm_stop_recording(); recording=0; return -1; } /* The main program loop */ void record_and_get_pitch(void) { int quit=0, button; long timer; char time_string[20]; float period; rb->cpu_boost(true); while(!quit) { /* Start recording */ rb->pcm_record_data(recording_callback, (void *) audio_data, (size_t) BUFFER_SIZE*sizeof(signed short)); recording=1; while (recording && !quit) /* wait for the buffer to be filled */ { rb->backlight_on(); rb->yield(); button=rb->button_get(false); switch(button) { case TUNER_QUIT: quit=true; rb->yield(); break; default: rb->yield(); break; } } /*rec_peaks(audio_data, BUFFER_SIZE);*/ /*schmitt_algorithm (BUFFER_SIZE, audio_data);*/ /* get the pitch */ /* Let's keep track of how long this takes */ timer = *(rb->current_tick); /* This returns the period of the detected pitch in samples */ period = pitchyin(audio_data, BUFFER_SIZE, yin_buffer); /* Hz = sample rate / period */ if(period != 0) { display_frequency((float)SAMPLE_RATE / period); } else { display_frequency(0); } /* Print out how long it took to find the pitch */ rb->snprintf(time_string, 20, "latency: %ld", *(rb->current_tick) - timer); rb->lcd_putsxy(0, 40, time_string); rb->lcd_update(); } rb->pcm_close_recording(); rb->cpu_boost(false); } /* Init recording, tuning, and GUI */ void init_everything(void) { /* --------- Init the audio recording ----------------- */ rb->audio_set_output_source(AUDIO_SRC_PLAYBACK); rb->audio_set_input_source(INPUT_TYPE, SRCF_RECORDING); /* This should really be wrapped in platform-specific #ifdefs */ /* set to maximum gain as in as3514.c */ rb->audio_set_recording_gain(39,39, AUDIO_GAIN_MIC); rb->pcm_set_frequency(SAMPLE_RATE); rb->pcm_apply_settings(); rb->pcm_init_recording(); back_color = rb->lcd_get_background(); /* GUI */ front_color = rb->lcd_get_foreground(); rb->lcd_getstringsize("X", &xx, &yy); bar_x_minus_50=0; bar_x_minus_20 = (LCD_WIDTH / 2) - (int)(LCD_FACTOR * 20.0f) - xx; bar_x_0 = LCD_WIDTH / 2; bar_x_20 = (LCD_WIDTH / 2) + (int)(LCD_FACTOR * 20.0f) - xx; bar_x_50 = LCD_WIDTH - 2 * xx; /* print_int_xy(0,100,(int) *rb->rec_freq_sampr); print_int_xy(0,110,(int) SAMPLE_RATE); */ } enum plugin_status plugin_start(const void* parameter) NO_PROF_ATTR { (void)parameter; init_everything(); record_and_get_pitch(); return 0; }