0d81702f52
git-svn-id: svn://svn.rockbox.org/rockbox/trunk@22907 a1c6a512-1295-4272-9138-f99709370657
1141 lines
36 KiB
C
1141 lines
36 KiB
C
/**************************************************************************
|
|
* __________ __ ___.
|
|
* 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 <mlang@delysid.org> ], 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
|
|
* - 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
|
|
* 08.31.2009 Lots of changes:
|
|
* Added a menu to tweak settings
|
|
* Converted everything to fixed point (greatly improving
|
|
* latency)
|
|
* Improved the display
|
|
* Improved efficiency with judicious use of cpu_boost, the
|
|
* backlight, and volume detection to limit unneeded
|
|
* calculation
|
|
* Fixed a problem that caused an octave-off error
|
|
* -David
|
|
*
|
|
*
|
|
* CURRENT LIMITATIONS:
|
|
* - No gapless recording. Strictly speaking true gappless isn't possible,
|
|
* since the algorithm takes longer to calculate than the length of the
|
|
* sample, but latency could be improved a bit with proper use of the DMA
|
|
* recording functions.
|
|
* - Due to how the Yin algorithm works, latency is higher for lower
|
|
* frequencies.
|
|
*/
|
|
|
|
#include "plugin.h"
|
|
#include "lib/pluginlib_actions.h"
|
|
#include "lib/picture.h"
|
|
#include "pluginbitmaps/pitch_notes.h"
|
|
|
|
PLUGIN_HEADER
|
|
|
|
/* First figure out what sample rate we're going to use */
|
|
#if (REC_SAMPR_CAPS & SAMPR_CAP_44)
|
|
#define SAMPLE_RATE SAMPR_44
|
|
#elif (REC_SAMPR_CAPS & SAMPR_CAP_22)
|
|
#define SAMPLE_RATE SAMPR_22
|
|
#elif (REC_SAMPR_CAPS & SAMPR_CAP_11)
|
|
#define SAMPLE_RATE SAMPR_11
|
|
#endif
|
|
|
|
/* Some fixed point calculation stuff */
|
|
typedef int32_t fixed_data;
|
|
struct _fixed
|
|
{
|
|
fixed_data a;
|
|
};
|
|
typedef struct _fixed fixed;
|
|
#define FIXED_PRECISION 18
|
|
#define FP_MAX ((fixed) {0x7fffffff})
|
|
#define FP_MIN ((fixed) {-0x80000000})
|
|
#define int2fixed(x) ((fixed){(x) << FIXED_PRECISION})
|
|
#define int2mantissa(x) ((fixed){x})
|
|
#define fixed2int(x) ((int)((x).a >> FIXED_PRECISION))
|
|
#define fixed2float(x) (((float)(x).a) / ((float)(1 << FIXED_PRECISION)))
|
|
#define float2fixed(x) \
|
|
((fixed){(fixed_data)(x * (float)(1 << FIXED_PRECISION))})
|
|
/* I adapted these ones from the Rockbox fixed point library */
|
|
#define fp_mul(x, y) \
|
|
((fixed){(((int64_t)((x).a)) * ((int64_t)((y).a))) >> (FIXED_PRECISION)})
|
|
#define fp_div(x, y) \
|
|
((fixed){(((int64_t)((x).a)) << (FIXED_PRECISION)) / ((int64_t)((y).a))})
|
|
/* Operators for fixed point */
|
|
#define fp_add(x, y) ((fixed){(x).a + (y).a})
|
|
#define fp_sub(x, y) ((fixed){(x).a - (y).a})
|
|
#define fp_shl(x, y) ((fixed){(x).a << y})
|
|
#define fp_shr(x, y) ((fixed){(x).a >> y})
|
|
#define fp_neg(x) ((fixed){-(x).a})
|
|
#define fp_gt(x, y) ((x).a > (y).a)
|
|
#define fp_gte(x, y) ((x).a >= (y).a)
|
|
#define fp_lt(x, y) ((x).a < (y).a)
|
|
#define fp_lte(x, y) ((x).a <= (y).a)
|
|
#define fp_sqr(x) fp_mul((x), (x))
|
|
#define fp_equal(x, y) ((x).a == (y).a)
|
|
#define fp_round(x) (fixed2int(fp_add((x), float2fixed(0.5))))
|
|
#define fp_data(x) ((x).a)
|
|
#define fp_frac(x) (fp_sub((x), int2fixed(fixed2int(x))))
|
|
#define FP_ZERO ((fixed){0})
|
|
#define FP_LOW ((fixed){1})
|
|
|
|
/* Some defines for converting between period and frequency */
|
|
|
|
/* I introduce some divisors in this because the fixed point */
|
|
/* variables aren't big enough to hold higher than a certain */
|
|
/* value. This loses a bit of precision but it means we */
|
|
/* don't have to use 32.32 variables (yikes). */
|
|
/* With an 18-bit decimal precision, the max value in the */
|
|
/* integer part is 8192. Divide 44100 by 7 and it'll fit in */
|
|
/* that variable. */
|
|
#define fp_period2freq(x) fp_div(int2fixed(SAMPLE_RATE / 7), \
|
|
fp_div((x),int2fixed(7)))
|
|
#define fp_freq2period(x) fp_period2freq(x)
|
|
#define period2freq(x) (SAMPLE_RATE / (x))
|
|
#define freq2period(x) period2freq(x)
|
|
|
|
#define sqr(x) ((x)*(x))
|
|
|
|
/* Some constants for tuning */
|
|
#define A_FREQ float2fixed(440.0f)
|
|
#define D_NOTE float2fixed(1.059463094359f)
|
|
#define LOG_D_NOTE float2fixed(1.0f/12.0f)
|
|
#define D_NOTE_SQRT float2fixed(1.029302236643f)
|
|
#define LOG_2 float2fixed(1.0f)
|
|
|
|
/* 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 */
|
|
/* If we ever switch to Yin FFT algorithm then this needs to be
|
|
a power of 2 */
|
|
#define BUFFER_SIZE 4096
|
|
#define SAMPLE_SIZE 4096
|
|
#define SAMPLE_SIZE_MIN 1024
|
|
#define YIN_BUFFER_SIZE (BUFFER_SIZE / 4)
|
|
|
|
#define LCD_FACTOR (fp_div(int2fixed(LCD_WIDTH), int2fixed(100)))
|
|
/* The threshold for the YIN algorithm */
|
|
#define DEFAULT_YIN_THRESHOLD 5 /* 0.10 */
|
|
const fixed yin_threshold_table[] =
|
|
{
|
|
float2fixed(0.01),
|
|
float2fixed(0.02),
|
|
float2fixed(0.03),
|
|
float2fixed(0.04),
|
|
float2fixed(0.05),
|
|
float2fixed(0.10),
|
|
float2fixed(0.15),
|
|
float2fixed(0.20),
|
|
float2fixed(0.25),
|
|
float2fixed(0.30),
|
|
float2fixed(0.35),
|
|
float2fixed(0.40),
|
|
float2fixed(0.45),
|
|
float2fixed(0.50),
|
|
};
|
|
|
|
/* Structure for the reference frequency (frequency of A)
|
|
* It's used for scaling the frequency before finding out
|
|
* the note. The frequency is scaled in a way that the main
|
|
* algorithm can assume the frequency of A to be 440 Hz.
|
|
*/
|
|
struct freq_A_entry
|
|
{
|
|
const int frequency; /* Frequency in Hz */
|
|
const fixed ratio; /* 440/frequency */
|
|
const fixed logratio; /* log2(factor) */
|
|
};
|
|
|
|
const struct freq_A_entry freq_A[] =
|
|
{
|
|
{435, float2fixed(1.011363636), float2fixed( 0.016301812)},
|
|
{436, float2fixed(1.009090909), float2fixed( 0.013056153)},
|
|
{437, float2fixed(1.006818182), float2fixed( 0.009803175)},
|
|
{438, float2fixed(1.004545455), float2fixed( 0.006542846)},
|
|
{439, float2fixed(1.002272727), float2fixed( 0.003275132)},
|
|
{440, float2fixed(1.000000000), float2fixed( 0.000000000)},
|
|
{441, float2fixed(0.997727273), float2fixed(-0.003282584)},
|
|
{442, float2fixed(0.995454545), float2fixed(-0.006572654)},
|
|
{443, float2fixed(0.993181818), float2fixed(-0.009870244)},
|
|
{444, float2fixed(0.990909091), float2fixed(-0.013175389)},
|
|
{445, float2fixed(0.988636364), float2fixed(-0.016488123)},
|
|
};
|
|
|
|
/* Index of the entry for 440 Hz in the table (default frequency for A) */
|
|
#define DEFAULT_FREQ_A 5
|
|
#define NUM_FREQ_A (sizeof(freq_A)/sizeof(freq_A[0]))
|
|
|
|
/* How loud the audio has to be to start displaying pitch */
|
|
/* Must be between 0 and 100 */
|
|
#define VOLUME_THRESHOLD (50)
|
|
|
|
/* Change to AUDIO_SRC_LINEIN if you want to record from line-in */
|
|
#ifdef HAVE_MIC_IN
|
|
#define INPUT_TYPE AUDIO_SRC_MIC
|
|
#else
|
|
#define INPUT_TYPE AUDIO_SRC_LINEIN
|
|
#endif
|
|
|
|
/* How many decimal places to display for the Hz value */
|
|
#define DISPLAY_HZ_PRECISION 100
|
|
|
|
/* Where to put the various GUI elements */
|
|
int note_y;
|
|
int bar_grad_y;
|
|
#define LCD_RES_MIN (LCD_HEIGHT < LCD_WIDTH ? LCD_HEIGHT : LCD_WIDTH)
|
|
#define BAR_PADDING (LCD_RES_MIN / 32)
|
|
#define BAR_Y (LCD_HEIGHT * 3 / 4)
|
|
#define BAR_HEIGHT (LCD_RES_MIN / 4 - BAR_PADDING)
|
|
#define BAR_HLINE_Y (BAR_Y - BAR_PADDING)
|
|
#define BAR_HLINE_Y2 (BAR_Y + BAR_HEIGHT + BAR_PADDING - 1)
|
|
#define HZ_Y 0
|
|
#define GRADUATION 10 /* Subdivisions of the whole 100-cent scale */
|
|
|
|
/* Bitmaps for drawing the note names. These need to have height
|
|
<= (bar_grad_y - note_y), or 15/32 * LCD_HEIGHT
|
|
*/
|
|
#define NUM_NOTE_IMAGES 9
|
|
#define NOTE_INDEX_A 0
|
|
#define NOTE_INDEX_B 1
|
|
#define NOTE_INDEX_C 2
|
|
#define NOTE_INDEX_D 3
|
|
#define NOTE_INDEX_E 4
|
|
#define NOTE_INDEX_F 5
|
|
#define NOTE_INDEX_G 6
|
|
#define NOTE_INDEX_SHARP 7
|
|
#define NOTE_INDEX_FLAT 8
|
|
const struct picture note_bitmaps =
|
|
{
|
|
pitch_notes,
|
|
BMPWIDTH_pitch_notes,
|
|
BMPHEIGHT_pitch_notes,
|
|
BMPHEIGHT_pitch_notes/NUM_NOTE_IMAGES
|
|
};
|
|
|
|
|
|
typedef signed short audio_sample_type;
|
|
/* It's stereo, so make the buffer twice as big */
|
|
audio_sample_type audio_data[BUFFER_SIZE];
|
|
fixed yin_buffer[YIN_BUFFER_SIZE];
|
|
static int recording=0;
|
|
|
|
/* Description of a note of scale */
|
|
struct note_entry
|
|
{
|
|
const char *name; /* Name of the note, e.g. "A#" */
|
|
const fixed freq; /* Note frequency, Hz */
|
|
const fixed logfreq; /* log2(frequency) */
|
|
};
|
|
|
|
/* Notes within one (reference) scale */
|
|
static const struct note_entry notes[] =
|
|
{
|
|
{"A" , float2fixed(440.0000000f), float2fixed(8.781359714f)},
|
|
{"A#", float2fixed(466.1637615f), float2fixed(8.864693047f)},
|
|
{"B" , float2fixed(493.8833013f), float2fixed(8.948026380f)},
|
|
{"C" , float2fixed(523.2511306f), float2fixed(9.031359714f)},
|
|
{"C#", float2fixed(554.3652620f), float2fixed(9.114693047f)},
|
|
{"D" , float2fixed(587.3295358f), float2fixed(9.198026380f)},
|
|
{"D#", float2fixed(622.2539674f), float2fixed(9.281359714f)},
|
|
{"E" , float2fixed(659.2551138f), float2fixed(9.364693047f)},
|
|
{"F" , float2fixed(698.4564629f), float2fixed(9.448026380f)},
|
|
{"F#", float2fixed(739.9888454f), float2fixed(9.531359714f)},
|
|
{"G" , float2fixed(783.9908720f), float2fixed(9.614693047f)},
|
|
{"G#", float2fixed(830.6093952f), float2fixed(9.698026380f)},
|
|
};
|
|
|
|
/* GUI */
|
|
static unsigned back_color, front_color;
|
|
static int font_w,font_h;
|
|
static int bar_x_0;
|
|
static int lbl_x_minus_50, lbl_x_minus_20, lbl_x_0, lbl_x_20, lbl_x_50;
|
|
|
|
/* Settings for the plugin */
|
|
struct tuner_settings
|
|
{
|
|
unsigned volume_threshold;
|
|
unsigned record_gain;
|
|
unsigned sample_size;
|
|
unsigned lowest_freq;
|
|
unsigned yin_threshold;
|
|
int freq_A; /* Index of the frequency of A */
|
|
bool use_sharps;
|
|
bool display_hz;
|
|
} tuner_settings;
|
|
|
|
/*=================================================================*/
|
|
/* Settings loading and saving(adapted from the clock plugin) */
|
|
/*=================================================================*/
|
|
|
|
#define SETTINGS_FILENAME PLUGIN_APPS_DIR "/.pitch_settings"
|
|
|
|
enum message
|
|
{
|
|
MESSAGE_LOADING,
|
|
MESSAGE_LOADED,
|
|
MESSAGE_ERRLOAD,
|
|
MESSAGE_SAVING,
|
|
MESSAGE_SAVED,
|
|
MESSAGE_ERRSAVE
|
|
};
|
|
|
|
enum settings_file_status
|
|
{
|
|
LOADED, ERRLOAD,
|
|
SAVED, ERRSAVE
|
|
};
|
|
|
|
/* The settings as they exist on the hard disk, so that
|
|
* we can know at saving time if changes have been made */
|
|
struct tuner_settings hdd_tuner_settings;
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
|
|
bool settings_needs_saving(struct tuner_settings* settings)
|
|
{
|
|
return(rb->memcmp(settings, &hdd_tuner_settings, sizeof(*settings)));
|
|
}
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
|
|
void tuner_settings_reset(struct tuner_settings* settings)
|
|
{
|
|
settings->volume_threshold = VOLUME_THRESHOLD;
|
|
settings->record_gain = rb->global_settings->rec_mic_gain;
|
|
settings->sample_size = BUFFER_SIZE;
|
|
settings->lowest_freq = period2freq(BUFFER_SIZE / 4);
|
|
settings->yin_threshold = DEFAULT_YIN_THRESHOLD;
|
|
settings->freq_A = DEFAULT_FREQ_A;
|
|
settings->use_sharps = true;
|
|
settings->display_hz = false;
|
|
}
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
|
|
enum settings_file_status tuner_settings_load(struct tuner_settings* settings,
|
|
char* filename)
|
|
{
|
|
int fd = rb->open(filename, O_RDONLY);
|
|
if(fd >= 0){ /* does file exist? */
|
|
/* basic consistency check */
|
|
if(rb->filesize(fd) == sizeof(*settings)){
|
|
rb->read(fd, settings, sizeof(*settings));
|
|
rb->close(fd);
|
|
rb->memcpy(&hdd_tuner_settings, settings, sizeof(*settings));
|
|
return(LOADED);
|
|
}
|
|
}
|
|
/* Initializes the settings with default values at least */
|
|
tuner_settings_reset(settings);
|
|
return(ERRLOAD);
|
|
}
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
|
|
enum settings_file_status tuner_settings_save(struct tuner_settings* settings,
|
|
char* filename)
|
|
{
|
|
int fd = rb->creat(filename);
|
|
if(fd >= 0){ /* does file exist? */
|
|
rb->write (fd, settings, sizeof(*settings));
|
|
rb->close(fd);
|
|
return(SAVED);
|
|
}
|
|
return(ERRSAVE);
|
|
}
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
|
|
void load_settings(void)
|
|
{
|
|
tuner_settings_load(&tuner_settings, SETTINGS_FILENAME);
|
|
|
|
rb->storage_sleep();
|
|
}
|
|
|
|
/*---------------------------------------------------------------------*/
|
|
|
|
void save_settings(void)
|
|
{
|
|
if(!settings_needs_saving(&tuner_settings))
|
|
return;
|
|
|
|
tuner_settings_save(&tuner_settings, SETTINGS_FILENAME);
|
|
}
|
|
|
|
/*=================================================================*/
|
|
/* MENU */
|
|
/*=================================================================*/
|
|
|
|
/* Keymaps */
|
|
const struct button_mapping* plugin_contexts[]={
|
|
generic_actions,
|
|
generic_increase_decrease,
|
|
generic_directions,
|
|
#if NB_SCREENS == 2
|
|
remote_directions
|
|
#endif
|
|
};
|
|
#define PLA_ARRAY_COUNT sizeof(plugin_contexts)/sizeof(plugin_contexts[0])
|
|
|
|
/* Option strings */
|
|
|
|
/* This has to match yin_threshold_table */
|
|
static const struct opt_items yin_threshold_text[] =
|
|
{
|
|
{ "0.01", -1 },
|
|
{ "0.02", -1 },
|
|
{ "0.03", -1 },
|
|
{ "0.04", -1 },
|
|
{ "0.05", -1 },
|
|
{ "0.10", -1 },
|
|
{ "0.15", -1 },
|
|
{ "0.20", -1 },
|
|
{ "0.25", -1 },
|
|
{ "0.30", -1 },
|
|
{ "0.35", -1 },
|
|
{ "0.40", -1 },
|
|
{ "0.45", -1 },
|
|
{ "0.50", -1 },
|
|
};
|
|
|
|
static const struct opt_items accidental_text[] =
|
|
{
|
|
{ "Flat", -1 },
|
|
{ "Sharp", -1 },
|
|
};
|
|
|
|
void set_min_freq(int new_freq)
|
|
{
|
|
tuner_settings.sample_size = freq2period(new_freq) * 4;
|
|
|
|
/* clamp the sample size between min and max */
|
|
if(tuner_settings.sample_size <= SAMPLE_SIZE_MIN)
|
|
tuner_settings.sample_size = SAMPLE_SIZE_MIN;
|
|
else if(tuner_settings.sample_size >= BUFFER_SIZE)
|
|
tuner_settings.sample_size = BUFFER_SIZE;
|
|
/* sample size must be divisible by 4 */
|
|
else if(tuner_settings.sample_size % 4 != 0)
|
|
tuner_settings.sample_size += 4 - (tuner_settings.sample_size % 4);
|
|
}
|
|
|
|
bool main_menu(void)
|
|
{
|
|
int selection = 0;
|
|
bool done = false;
|
|
bool exit_tuner = false;
|
|
int choice;
|
|
int freq_val;
|
|
bool reset;
|
|
|
|
MENUITEM_STRINGLIST(menu,"Tuner Settings",NULL,
|
|
"Return to Tuner",
|
|
"Volume Threshold",
|
|
"Listening Volume",
|
|
"Lowest Frequency",
|
|
"Algorithm Pickiness",
|
|
"Accidentals",
|
|
"Display Frequency (Hz)",
|
|
"Frequency of A (Hz)",
|
|
"Reset Settings",
|
|
"Quit");
|
|
|
|
while(!done)
|
|
{
|
|
choice = rb->do_menu(&menu, &selection, NULL, false);
|
|
switch(choice)
|
|
{
|
|
case 1:
|
|
rb->set_int("Volume Threshold", "%", UNIT_INT,
|
|
&tuner_settings.volume_threshold,
|
|
NULL, 5, 5, 95, NULL);
|
|
break;
|
|
case 2:
|
|
rb->set_int("Listening Volume", "%", UNIT_INT,
|
|
&tuner_settings.record_gain,
|
|
NULL, 1, rb->sound_min(SOUND_MIC_GAIN),
|
|
rb->sound_max(SOUND_MIC_GAIN), NULL);
|
|
break;
|
|
case 3:
|
|
rb->set_int("Lowest Frequency", "Hz", UNIT_INT,
|
|
&tuner_settings.lowest_freq, set_min_freq, 1,
|
|
/* Range depends on the size of the buffer */
|
|
SAMPLE_RATE / (BUFFER_SIZE / 4),
|
|
SAMPLE_RATE / (SAMPLE_SIZE_MIN / 4), NULL);
|
|
break;
|
|
case 4:
|
|
rb->set_option(
|
|
"Algorithm Pickiness (Lower -> more discriminating)",
|
|
&tuner_settings.yin_threshold,
|
|
INT, yin_threshold_text,
|
|
sizeof(yin_threshold_text) / sizeof(yin_threshold_text[0]),
|
|
NULL);
|
|
break;
|
|
case 5:
|
|
rb->set_option("Display Accidentals As",
|
|
&tuner_settings.use_sharps,
|
|
BOOL, accidental_text, 2, NULL);
|
|
break;
|
|
case 6:
|
|
rb->set_bool("Display Frequency (Hz)",
|
|
&tuner_settings.display_hz);
|
|
break;
|
|
case 7:
|
|
freq_val = freq_A[tuner_settings.freq_A].frequency;
|
|
rb->set_int("Frequency of A (Hz)",
|
|
"Hz", UNIT_INT, &freq_val, NULL,
|
|
1, freq_A[0].frequency, freq_A[NUM_FREQ_A-1].frequency,
|
|
NULL);
|
|
tuner_settings.freq_A = freq_val - freq_A[0].frequency;
|
|
break;
|
|
case 8:
|
|
reset = false;
|
|
rb->set_bool("Reset Tuner Settings?", &reset);
|
|
if (reset)
|
|
tuner_settings_reset(&tuner_settings);
|
|
break;
|
|
case 9:
|
|
exit_tuner = true;
|
|
done = true;
|
|
break;
|
|
case 0:
|
|
default:
|
|
/* Return to the tuner */
|
|
done = true;
|
|
break;
|
|
}
|
|
}
|
|
return exit_tuner;
|
|
}
|
|
|
|
/*=================================================================*/
|
|
/* Binary Log */
|
|
/*=================================================================*/
|
|
|
|
/* Fixed-point log base 2*/
|
|
/* Adapted from python code at
|
|
http://en.wikipedia.org/wiki/Binary_logarithm#Algorithm
|
|
*/
|
|
fixed log(fixed inp)
|
|
{
|
|
fixed x = inp;
|
|
fixed fp = int2fixed(1);
|
|
fixed res = int2fixed(0);
|
|
|
|
if(fp_lte(x, FP_ZERO))
|
|
{
|
|
return FP_MIN;
|
|
}
|
|
|
|
/* Integer part*/
|
|
/* while x<1 */
|
|
while(fp_lt(x, int2fixed(1)))
|
|
{
|
|
res = fp_sub(res, int2fixed(1));
|
|
x = fp_shl(x, 1);
|
|
}
|
|
/* while x>=2 */
|
|
while(fp_gte(x, int2fixed(2)))
|
|
{
|
|
res = fp_add(res, int2fixed(1));
|
|
x = fp_shr(x, 1);
|
|
}
|
|
|
|
/* Fractional part */
|
|
/* while fp > 0 */
|
|
while(fp_gt(fp, FP_ZERO))
|
|
{
|
|
fp = fp_shr(fp, 1);
|
|
x = fp_mul(x, x);
|
|
/* if x >= 2 */
|
|
if(fp_gte(x, int2fixed(2)))
|
|
{
|
|
x = fp_shr(x, 1);
|
|
res = fp_add(res, fp);
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/*=================================================================*/
|
|
/* GUI Stuff */
|
|
/*=================================================================*/
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Print out the frequency etc */
|
|
void print_str(char* s)
|
|
{
|
|
rb->lcd_set_foreground(front_color);
|
|
rb->lcd_putsxy(0, HZ_Y, s);
|
|
}
|
|
|
|
/* 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 note bitmap */
|
|
void draw_note(const char *note)
|
|
{
|
|
int i;
|
|
int note_x = (LCD_WIDTH - BMPWIDTH_pitch_notes) / 2;
|
|
int accidental_index = NOTE_INDEX_SHARP;
|
|
|
|
i = note[0]-'A';
|
|
|
|
if(note[1] == '#')
|
|
{
|
|
if(!(tuner_settings.use_sharps))
|
|
{
|
|
i = (i + 1) % 7;
|
|
accidental_index = NOTE_INDEX_FLAT;
|
|
}
|
|
|
|
vertical_picture_draw_sprite(rb->screens[0],
|
|
¬e_bitmaps,
|
|
accidental_index,
|
|
LCD_WIDTH / 2,
|
|
note_y);
|
|
note_x = LCD_WIDTH / 2 - BMPWIDTH_pitch_notes;
|
|
}
|
|
|
|
vertical_picture_draw_sprite(rb->screens[0], ¬e_bitmaps, i,
|
|
note_x,
|
|
note_y);
|
|
}
|
|
/* Draw the red bar and the white lines */
|
|
void draw_bar(fixed wrong_by_cents)
|
|
{
|
|
unsigned n;
|
|
int x;
|
|
|
|
#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, BAR_HLINE_Y);
|
|
rb->lcd_hline(0,LCD_WIDTH-1, BAR_HLINE_Y2);
|
|
|
|
/* Draw graduation lines on the off-by readout */
|
|
for(n = 0; n <= GRADUATION; n++)
|
|
{
|
|
x = (LCD_WIDTH * n + GRADUATION / 2) / GRADUATION;
|
|
if (x >= LCD_WIDTH)
|
|
x = LCD_WIDTH - 1;
|
|
rb->lcd_vline(x, BAR_HLINE_Y, BAR_HLINE_Y2);
|
|
}
|
|
|
|
print_int_xy(lbl_x_minus_50 ,bar_grad_y, -50);
|
|
print_int_xy(lbl_x_minus_20 ,bar_grad_y, -20);
|
|
print_int_xy(lbl_x_0 ,bar_grad_y, 0);
|
|
print_int_xy(lbl_x_20 ,bar_grad_y, 20);
|
|
print_int_xy(lbl_x_50 ,bar_grad_y, 50);
|
|
|
|
#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 (fp_gt(wrong_by_cents, FP_ZERO))
|
|
{
|
|
rb->lcd_fillrect(bar_x_0, BAR_Y,
|
|
fixed2int(fp_mul(wrong_by_cents, LCD_FACTOR)), BAR_HEIGHT);
|
|
}
|
|
else
|
|
{
|
|
rb->lcd_fillrect(bar_x_0 + fixed2int(fp_mul(wrong_by_cents,LCD_FACTOR)),
|
|
BAR_Y,
|
|
fixed2int(fp_mul(wrong_by_cents, LCD_FACTOR)) * -1,
|
|
BAR_HEIGHT);
|
|
}
|
|
}
|
|
|
|
/* Calculate how wrong the note is and draw the GUI */
|
|
void display_frequency (fixed freq)
|
|
{
|
|
fixed ldf, mldf;
|
|
fixed lfreq, nfreq;
|
|
fixed orig_freq;
|
|
int i, note = 0;
|
|
char str_buf[30];
|
|
|
|
if (fp_lt(freq, FP_LOW))
|
|
freq = FP_LOW;
|
|
|
|
/* We calculate the frequency and its log as if */
|
|
/* the reference frequency of A were 440 Hz. */
|
|
orig_freq = freq;
|
|
lfreq = fp_add(log(freq), freq_A[tuner_settings.freq_A].logratio);
|
|
freq = fp_mul(freq, freq_A[tuner_settings.freq_A].ratio);
|
|
|
|
/* This calculates a log freq offset for note A */
|
|
/* Get the frequency to within the range of our reference table, */
|
|
/* i.e. into the right octave. */
|
|
while (fp_lt(lfreq, fp_sub(notes[0].logfreq, fp_shr(LOG_D_NOTE, 1))))
|
|
lfreq = fp_add(lfreq, LOG_2);
|
|
while (fp_gte(lfreq, fp_sub(fp_add(notes[0].logfreq, LOG_2),
|
|
fp_shr(LOG_D_NOTE, 1))))
|
|
lfreq = fp_sub(lfreq, LOG_2);
|
|
mldf = LOG_D_NOTE;
|
|
for (i=0; i<12; i++)
|
|
{
|
|
ldf = fp_gt(fp_sub(lfreq,notes[i].logfreq), FP_ZERO) ?
|
|
fp_sub(lfreq,notes[i].logfreq) : fp_neg(fp_sub(lfreq,notes[i].logfreq));
|
|
if (fp_lt(ldf, mldf))
|
|
{
|
|
mldf = ldf;
|
|
note = i;
|
|
}
|
|
}
|
|
nfreq = notes[note].freq;
|
|
while (fp_gt(fp_div(nfreq, freq), D_NOTE_SQRT))
|
|
nfreq = fp_shr(nfreq, 1);
|
|
while (fp_gt(fp_div(freq, nfreq), D_NOTE_SQRT))
|
|
{
|
|
nfreq = fp_shl(nfreq, 1);
|
|
}
|
|
|
|
ldf = fp_mul(int2fixed(1200), log(fp_div(freq,nfreq)));
|
|
|
|
rb->lcd_clear_display();
|
|
draw_bar(ldf); /* The red bar */
|
|
if(fp_round(freq) != 0)
|
|
{
|
|
draw_note(notes[note].name);
|
|
if(tuner_settings.display_hz)
|
|
{
|
|
rb->snprintf(str_buf,30, "%s : %d cents (%d.%02dHz)",
|
|
notes[note].name, fp_round(ldf) ,fixed2int(orig_freq),
|
|
fp_round(fp_mul(fp_frac(orig_freq),
|
|
int2fixed(DISPLAY_HZ_PRECISION))));
|
|
print_str(str_buf);
|
|
}
|
|
}
|
|
rb->lcd_update();
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------
|
|
* 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(fixed *s, unsigned buflen)
|
|
{
|
|
unsigned j, pos=0.0f;
|
|
fixed tmp = s[0];
|
|
for (j=0; j < buflen; j++)
|
|
{
|
|
if(fp_gt(tmp, s[j]))
|
|
{
|
|
pos = j;
|
|
tmp = s[j];
|
|
}
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
|
|
fixed aubio_quadfrac(fixed s0, fixed s1, fixed s2, fixed pf)
|
|
{
|
|
/* Original floating point version: */
|
|
/* tmp = s0 + (pf/2.0f) * (pf * ( s0 - 2.0f*s1 + s2 ) -
|
|
3.0f*s0 + 4.0f*s1 - s2);*/
|
|
/* Converted to explicit operator precedence: */
|
|
/* tmp = s0 + ((pf/2.0f) * ((((pf * ((s0 - (2*s1)) + s2)) -
|
|
(3*s0)) + (4*s1)) - s2)); */
|
|
|
|
/* I made it look like this so I could easily track the precedence and */
|
|
/* make sure it matched the original expression */
|
|
/* Oy, this is when I really wish I could do C++ operator overloading */
|
|
fixed tmp = fp_add
|
|
(
|
|
s0,
|
|
fp_mul
|
|
(
|
|
fp_shr(pf, 1),
|
|
fp_sub
|
|
(
|
|
fp_add
|
|
(
|
|
fp_sub
|
|
(
|
|
fp_mul
|
|
(
|
|
pf,
|
|
fp_add
|
|
(
|
|
fp_sub
|
|
(
|
|
s0,
|
|
fp_shl(s1, 1)
|
|
),
|
|
s2
|
|
)
|
|
),
|
|
fp_mul
|
|
(
|
|
float2fixed(3.0f),
|
|
s0
|
|
)
|
|
),
|
|
fp_shl(s1, 2)
|
|
),
|
|
s2
|
|
)
|
|
)
|
|
);
|
|
return tmp;
|
|
}
|
|
|
|
#define QUADINT_STEP float2fixed(1.0f/200.0f)
|
|
|
|
fixed vec_quadint_min(fixed *x, unsigned bufsize, unsigned pos, unsigned span)
|
|
{
|
|
fixed res, frac, s0, s1, s2;
|
|
fixed exactpos = int2fixed(pos);
|
|
/* init resold to something big (in case x[pos+-span]<0)) */
|
|
fixed resold = FP_MAX;
|
|
|
|
if ((pos > span) && (pos < bufsize-span))
|
|
{
|
|
s0 = x[pos-span];
|
|
s1 = x[pos] ;
|
|
s2 = x[pos+span];
|
|
/* increase frac */
|
|
for (frac = float2fixed(0.0f);
|
|
fp_lt(frac, float2fixed(2.0f));
|
|
frac = fp_add(frac, QUADINT_STEP))
|
|
{
|
|
res = aubio_quadfrac(s0, s1, s2, frac);
|
|
if (fp_lt(res, resold))
|
|
{
|
|
resold = res;
|
|
}
|
|
else
|
|
{
|
|
/* exactpos += (frac-QUADINT_STEP)*span - span/2.0f; */
|
|
exactpos = fp_add(exactpos,
|
|
fp_sub(
|
|
fp_mul(
|
|
fp_sub(frac, QUADINT_STEP),
|
|
int2fixed(span)
|
|
),
|
|
int2fixed(span)
|
|
)
|
|
);
|
|
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. */
|
|
|
|
fixed pitchyin(audio_sample_type *input, fixed *yin)
|
|
{
|
|
fixed retval;
|
|
unsigned j,tau = 0;
|
|
int period;
|
|
unsigned yin_size = tuner_settings.sample_size / 4;
|
|
|
|
fixed tmp = FP_ZERO, tmp2 = FP_ZERO;
|
|
yin[0] = int2fixed(1);
|
|
for (tau = 1; tau < yin_size; tau++)
|
|
{
|
|
yin[tau] = FP_ZERO;
|
|
for (j = 0; j < yin_size; j++)
|
|
{
|
|
tmp = fp_sub(int2mantissa(input[2 * j]),
|
|
int2mantissa(input[2 * (j + tau)]));
|
|
yin[tau] = fp_add(yin[tau], fp_mul(tmp, tmp));
|
|
}
|
|
tmp2 = fp_add(tmp2, yin[tau]);
|
|
if(!fp_equal(tmp2, FP_ZERO))
|
|
{
|
|
yin[tau] = fp_mul(yin[tau], fp_div(int2fixed(tau), tmp2));
|
|
}
|
|
period = tau - 3;
|
|
if(tau > 4 && fp_lt(yin[period],
|
|
yin_threshold_table[tuner_settings.yin_threshold])
|
|
&& fp_lt(yin[period], yin[period+1]))
|
|
{
|
|
retval = vec_quadint_min(yin, yin_size, period, 1);
|
|
return retval;
|
|
}
|
|
}
|
|
retval = vec_quadint_min(yin, yin_size,
|
|
vec_min_elem(yin, yin_size), 1);
|
|
return retval;
|
|
/*return FP_ZERO;*/
|
|
}
|
|
|
|
/*-----------------------------------------------------------------*/
|
|
|
|
uint32_t buffer_magnitude(audio_sample_type *input)
|
|
{
|
|
unsigned n;
|
|
uint64_t tally = 0;
|
|
|
|
/* Operate on only one channel of the stereo signal */
|
|
for(n = 0; n < tuner_settings.sample_size; n+=2)
|
|
{
|
|
tally += (uint64_t)input[n] * (uint64_t)input[n];
|
|
}
|
|
|
|
tally /= tuner_settings.sample_size / 2;
|
|
|
|
/* now tally holds the average of the squares of all the samples */
|
|
/* It must be between 0 and 0x7fff^2, so it fits in 32 bits */
|
|
return (uint32_t)tally;
|
|
}
|
|
|
|
/* 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;
|
|
bool redraw = true;
|
|
/* For tracking the latency */
|
|
/*
|
|
long timer;
|
|
char debug_string[20];
|
|
*/
|
|
#ifndef SIMULATOR
|
|
fixed period;
|
|
bool waiting = false;
|
|
#endif
|
|
|
|
while(!quit)
|
|
{
|
|
#ifndef SIMULATOR
|
|
/* Start recording */
|
|
rb->pcm_record_data(recording_callback, (void *) audio_data,
|
|
(size_t) tuner_settings.sample_size *
|
|
sizeof(audio_sample_type));
|
|
#endif
|
|
recording=1;
|
|
|
|
while (recording && !quit) /* wait for the buffer to be filled */
|
|
{
|
|
rb->yield();
|
|
#ifdef SIMULATOR
|
|
/* Only do this loop once if this is the simulator */
|
|
recording = 0;
|
|
#endif
|
|
button=pluginlib_getaction(0, plugin_contexts, PLA_ARRAY_COUNT);
|
|
switch(button)
|
|
{
|
|
case PLA_QUIT:
|
|
quit=true;
|
|
rb->yield();
|
|
break;
|
|
|
|
case PLA_MENU:
|
|
if(main_menu())
|
|
quit=true;
|
|
else redraw = true;
|
|
rb->yield();
|
|
break;
|
|
|
|
default:
|
|
rb->yield();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!quit)
|
|
{
|
|
#ifndef SIMULATOR
|
|
/* Only do the heavy lifting if the volume is high enough */
|
|
if(buffer_magnitude(audio_data) >
|
|
sqr(tuner_settings.volume_threshold *
|
|
rb->sound_max(SOUND_MIC_GAIN)))
|
|
{
|
|
if(waiting)
|
|
{
|
|
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
|
|
rb->cpu_boost(true);
|
|
#endif
|
|
waiting = false;
|
|
}
|
|
|
|
rb->backlight_on();
|
|
redraw = false;
|
|
|
|
/* This returns the period of the detected pitch in samples */
|
|
period = pitchyin(audio_data, yin_buffer);
|
|
/* Hz = sample rate / period */
|
|
if(fp_gt(period, FP_ZERO))
|
|
{
|
|
display_frequency(fp_period2freq(period));
|
|
}
|
|
else
|
|
{
|
|
display_frequency(FP_ZERO);
|
|
}
|
|
}
|
|
else if(redraw || !waiting)
|
|
{
|
|
waiting = true;
|
|
redraw = false;
|
|
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
|
|
rb->cpu_boost(false);
|
|
#endif
|
|
/*rb->backlight_off();*/
|
|
display_frequency(FP_ZERO);
|
|
}
|
|
#else /* SIMULATOR */
|
|
/* Display a preselected frequency */
|
|
display_frequency(int2fixed(445));
|
|
#endif
|
|
}
|
|
}
|
|
rb->pcm_close_recording();
|
|
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
|
|
rb->cpu_boost(false);
|
|
#endif
|
|
}
|
|
|
|
/* Init recording, tuning, and GUI */
|
|
void init_everything(void)
|
|
{
|
|
load_settings();
|
|
|
|
/* --------- Init the audio recording ----------------- */
|
|
rb->audio_set_output_source(AUDIO_SRC_PLAYBACK);
|
|
rb->audio_set_input_source(INPUT_TYPE, SRCF_RECORDING);
|
|
|
|
/* set to maximum gain */
|
|
rb->audio_set_recording_gain(tuner_settings.record_gain,
|
|
tuner_settings.record_gain,
|
|
AUDIO_GAIN_MIC);
|
|
|
|
rb->pcm_set_frequency(SAMPLE_RATE);
|
|
rb->pcm_apply_settings();
|
|
|
|
rb->pcm_init_recording();
|
|
|
|
/* GUI */
|
|
back_color = rb->lcd_get_background();
|
|
front_color = rb->lcd_get_foreground();
|
|
rb->lcd_getstringsize("X", &font_w, &font_h);
|
|
|
|
bar_x_0 = LCD_WIDTH / 2;
|
|
lbl_x_minus_50 = 0;
|
|
lbl_x_minus_20 = (LCD_WIDTH / 2) -
|
|
fixed2int(fp_mul(LCD_FACTOR, int2fixed(20))) - font_w;
|
|
lbl_x_0 = (LCD_WIDTH - font_w) / 2;
|
|
lbl_x_20 = (LCD_WIDTH / 2) +
|
|
fixed2int(fp_mul(LCD_FACTOR, int2fixed(20))) - font_w;
|
|
lbl_x_50 = LCD_WIDTH - 2 * font_w;
|
|
|
|
bar_grad_y = BAR_Y - BAR_PADDING - font_h;
|
|
/* Put the note right between the top and bottom text elements */
|
|
note_y = ((font_h + bar_grad_y - note_bitmaps.slide_height) / 2);
|
|
}
|
|
|
|
|
|
enum plugin_status plugin_start(const void* parameter) NO_PROF_ATTR
|
|
{
|
|
(void)parameter;
|
|
|
|
init_everything();
|
|
record_and_get_pitch();
|
|
save_settings();
|
|
|
|
return 0;
|
|
}
|