You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-07-15 00:44:11 +00:00
The tuner code seems to be in reasonably good shape, so let's attack the slow screen updates by making the sample acquisition side do a simple circular buffer, and instead of doing the Hanning function at sample acquisition time, do it at analysis time. Then we can do the analysis by sliding over the pristine sample buffer in smaller increments - doing the Hanning at that point as we copy it to the FFT buffer and turn it into the complex domain. That way we can get more frequent FFT's for those sliding windows and a more responsive UI - and a natural smoothing of the results. The total latency is not really any better, but it *feels* much better, and instead of getting a jerky update with no smoothing, you get a much more realistic feel for how close the tuning is when the display updates with the sliding results. And the occasional bad tuning results when you were unlucky and hit some timeframe when you had bad interactions with picking or happened to get some other effect now feel like small blips rather than horrible big events that you have to wait a second to fix themselves, because the screen update has gone from 1.5 frames per second to being about 23 frames per second. And the code isn't hardly any more complicated, so it's an unambiguous win. .. except for the added memory use. Now we need that circular buffer of samples to be separate from the FFT buffer, and it needs to have more samples in it too. So this makes that tuner - that was already using a fair amount of precious RAM in the rp2354 - use even more RAM. Of course, I think the FFT size is probably unnecessarily large, and so maybe the answer is to make FFT_SHIFT smaller. We could use a 8x downsampling too. So there are solutions to the memory use, and right now it all fits so I won't worry about it. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
575 lines
15 KiB
C
575 lines
15 KiB
C
/*
|
|
* Tuner UI with both a chromatic base version that tries to show
|
|
* any dominant tone frequency, and a polyphonic display above it
|
|
* that shows the individual string tunings.
|
|
*
|
|
* Note that the polyphonic mode shows a fixed string tuning target,
|
|
* and right now that target tuning is fixed at the standard guitar
|
|
* tuning (440Hz 'A'), but the code is set up to easily add other
|
|
* tunings (but would then also need some setting UI to pick them).
|
|
*
|
|
* If you want microtonal tuning with non-standard scales for the
|
|
* chromatic tuner, you're on your own, but it shouldn't be anything
|
|
* horribly hard.
|
|
*/
|
|
#define MAX_STRINGS 8
|
|
|
|
struct tune_target {
|
|
const char *name;
|
|
float base_freq;
|
|
};
|
|
|
|
struct tuning {
|
|
const char *name;
|
|
int num_strings;
|
|
const struct tune_target strings[MAX_STRINGS];
|
|
};
|
|
|
|
// The magnitude array re-uses the fft array to not be
|
|
// quite so piggy in memory use. But we might still have
|
|
// to shrink the FFT size at some point.
|
|
struct {
|
|
union {
|
|
complex_t fft[FFT_SIZE];
|
|
float magnitudes[FFT_SIZE / 2];
|
|
};
|
|
float max_mag, avg_mag;
|
|
|
|
// Chromatic tuning data
|
|
float dominant_freq;
|
|
float dominant_mag;
|
|
|
|
// Polyphonic data
|
|
float string_freq[MAX_STRINGS];
|
|
float string_mag[MAX_STRINGS];
|
|
} tuner_state;
|
|
|
|
struct tune_result {
|
|
int note_idx; // 0 means no result
|
|
int cents;
|
|
float mag;
|
|
};
|
|
|
|
struct tuner_results {
|
|
int num_results;
|
|
struct tune_result results[1 + MAX_STRINGS];
|
|
};
|
|
|
|
struct peak_info {
|
|
float bin;
|
|
float freq;
|
|
float mag;
|
|
};
|
|
|
|
static inline bool get_peak(int i, struct peak_info *peak, float min_peak)
|
|
{
|
|
float *center = tuner_state.magnitudes + i;
|
|
float mag = *center;
|
|
|
|
//
|
|
// Note that the min_peak argument is only a quick
|
|
// quick-and-dirty bin peak magnitude filter.
|
|
//
|
|
// In particular, it does not take into account that
|
|
// the final magnitude of the peak is the combination
|
|
// of the nearby bins and may be bigger than this first
|
|
// simple filtering.
|
|
//
|
|
if (mag < min_peak)
|
|
return false;
|
|
|
|
//
|
|
// Avoid noise: any peak we detect has to be
|
|
// clearly above the noise floor, here fairly
|
|
// arbitrarily defined to be "five times the
|
|
// average magnitude and at least 5.0"
|
|
//
|
|
if (mag < 5.0f || mag < tuner_state.avg_mag * 5.0f)
|
|
return false;
|
|
|
|
//
|
|
// We know the maximum magnitude we've seen.
|
|
// Don't bother looking at anything smaller
|
|
// than 5% of the max. It may be a local peak,
|
|
// but it's still not interesting.
|
|
//
|
|
if (mag < tuner_state.max_mag * 0.05f)
|
|
return false;
|
|
|
|
//
|
|
// It needs to be sharp enough to not only be at least
|
|
// equal to its direct neighbors, but slightly bigger than
|
|
// something two bins away
|
|
//
|
|
if (mag <= center[-1] || mag <= center[+1] ||
|
|
mag <= 1.2f * center[-2] || mag <= 1.2f * center[+2])
|
|
return false;
|
|
|
|
//
|
|
// Ok, we have something that looks like a peak bin.
|
|
//
|
|
// Let's try to figure out what the actual exact
|
|
// frequency would be that causes this bin pattern.
|
|
//
|
|
float y1 = center[-1];
|
|
float y2 = mag;
|
|
float y3 = center[+1];
|
|
float p;
|
|
|
|
// The audio analyzer uses a Hann window (see hanning() in analyze.h).
|
|
// For a Hann window, the Grandke interpolation algorithm provides a
|
|
// mathematically exact fractional bin offset. It calculates the offset
|
|
// depending on which side of the peak is bigger, avoiding log() calls.
|
|
if (y3 > y1) {
|
|
p = (2.0f * y3 - y2) / (y3 + y2);
|
|
} else {
|
|
p = (y2 - 2.0f * y1) / (y1 + y2);
|
|
}
|
|
|
|
peak->bin = i + p;
|
|
peak->freq = peak->bin * (12000.0f / FFT_SIZE);
|
|
|
|
// A simple parabolic estimate is still fine for the magnitude, as we
|
|
// mainly use it for relative peak comparisons and thresholding.
|
|
peak->mag = y2 - 0.25f * (y1 - y3) * p;
|
|
return true;
|
|
}
|
|
|
|
static inline void suppress_harmonics(void)
|
|
{
|
|
int max_bin = FFT_SIZE / 2;
|
|
for (int i = 2; i < max_bin / 2; i++) {
|
|
struct peak_info peak;
|
|
if (!get_peak(i, &peak, 0.0f))
|
|
continue;
|
|
|
|
for (int h = 2; h * peak.bin < max_bin; h++) {
|
|
float target_exact = h * peak.bin;
|
|
|
|
// Restrict suppression to the exact +/- 2.0 bin Hann window footprint
|
|
int low = (int)floorf(target_exact - 2.0f);
|
|
int high = (int)ceilf(target_exact + 2.0f);
|
|
|
|
for (int j = low; j <= high; j++) {
|
|
if (j >= max_bin) break;
|
|
if (j < 0) continue;
|
|
|
|
float d = fabsf((float)j - target_exact);
|
|
float w = 0.0f;
|
|
if (d <= 1.0f) {
|
|
w = 1.0f - 0.5f * d * d;
|
|
} else if (d <= 2.0f) {
|
|
float x = 2.0f - d;
|
|
w = 0.5f * x * x;
|
|
}
|
|
|
|
float suppression = peak.mag * w;
|
|
if (tuner_state.magnitudes[j] > suppression)
|
|
tuner_state.magnitudes[j] -= suppression;
|
|
else
|
|
tuner_state.magnitudes[j] = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static inline void tuner_magnitudes(void)
|
|
{
|
|
// Find the dominant fundamental for chromatic tuning
|
|
float dominant_freq = 0.0f;
|
|
float dominant_mag = 0.0f;
|
|
|
|
// Search frequencies between ~15Hz and ~1.5kHz (E6 is 1318.5Hz)
|
|
int min_bin = (int)(15.0f * FFT_SIZE / 12000.0f);
|
|
int max_bin = (int)(1500.0f * FFT_SIZE / 12000.0f);
|
|
if (min_bin < 2) min_bin = 2; // Need margin for i-2 check
|
|
if (max_bin > FFT_SIZE / 2 - 3) max_bin = FFT_SIZE / 2 - 3; // Need margin for i+2 check
|
|
|
|
for (int i = min_bin; i < max_bin; i++) {
|
|
struct peak_info peak;
|
|
|
|
if (!get_peak(i, &peak, dominant_mag))
|
|
continue;
|
|
|
|
//
|
|
// We heavily prefer fundamentals over their harmonics,
|
|
// so if we already have seen a dominant peak, discount
|
|
// new peaks that are far away..
|
|
//
|
|
if (dominant_freq) {
|
|
float distance = peak.freq / dominant_freq;
|
|
if (peak.mag / distance < dominant_mag)
|
|
continue;
|
|
}
|
|
|
|
dominant_freq = peak.freq;
|
|
dominant_mag = peak.mag;
|
|
}
|
|
|
|
tuner_state.dominant_freq = dominant_freq;
|
|
tuner_state.dominant_mag = dominant_mag;
|
|
}
|
|
|
|
static const struct tuning EADGBE = {
|
|
.name = "Standard",
|
|
.num_strings = 6,
|
|
.strings = {
|
|
{"E", 82.41f}, // E2
|
|
{"A", 110.00f}, // A2
|
|
{"D", 146.83f}, // D3
|
|
{"G", 196.00f}, // G3
|
|
{"B", 246.94f}, // B3
|
|
{"E", 329.63f}, // E4
|
|
},
|
|
};
|
|
|
|
static const struct tuning DADGAD = {
|
|
.name = "DADGAD",
|
|
.num_strings = 6,
|
|
.strings = {
|
|
{"D", 73.42f}, // D2
|
|
{"A", 110.00f}, // A2
|
|
{"D", 146.83f}, // D3
|
|
{"G", 196.00f}, // G3
|
|
{"A", 220.00f}, // A3
|
|
{"D", 293.66f}, // D4
|
|
},
|
|
};
|
|
|
|
// 6-string standard bass tuning
|
|
static const struct tuning BEADGC = {
|
|
.name = "BEADGC",
|
|
.num_strings = 6,
|
|
.strings = {
|
|
{"B", 30.87f}, // B0
|
|
{"E", 41.20f}, // E1
|
|
{"A", 55.00f}, // A1
|
|
{"D", 73.42f}, // D2
|
|
{"G", 98.00f}, // G2
|
|
{"C", 130.81f}, // C3
|
|
},
|
|
};
|
|
|
|
// 4-string standard bass tuning
|
|
static const struct tuning EADG = {
|
|
.name = "EADG",
|
|
.num_strings = 4,
|
|
.strings = {
|
|
{"E", 41.20f}, // E1
|
|
{"A", 55.00f}, // A1
|
|
{"D", 73.42f}, // D2
|
|
{"G", 98.00f}, // G2
|
|
},
|
|
};
|
|
|
|
|
|
static const struct tuning *const tunings[4] = {
|
|
&EADGBE,
|
|
&DADGAD,
|
|
&BEADGC,
|
|
&EADG,
|
|
};
|
|
|
|
static inline void find_string_peak(const struct tuning *current_tuning, int s)
|
|
{
|
|
float target_freq = current_tuning->strings[s].base_freq;
|
|
float target_bin = target_freq * ((float)FFT_SIZE / 12000.0f);
|
|
|
|
// We restrict the search window to +/- 2.5% (about 43 cents).
|
|
// This ensures that any peak we find is guaranteed to be closer to this
|
|
// target note than to any adjacent note, allowing us to safely derive
|
|
// the target note index directly from the measured frequency later.
|
|
// It also neatly matches our UI, which visually clips the polyphonic
|
|
// tuning arrows to a +/- 40 cent range anyway.
|
|
int min_b = lrintf(target_bin * 0.975f);
|
|
int max_b = lrintf(target_bin * 1.025f);
|
|
if (min_b < 2) min_b = 2;
|
|
|
|
float max_mag = 0.0f;
|
|
int peak_b = 0;
|
|
for (int i = min_b; i <= max_b; i++) {
|
|
if (tuner_state.magnitudes[i] > max_mag) {
|
|
max_mag = tuner_state.magnitudes[i];
|
|
peak_b = i;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Check that it's an actual peak.
|
|
//
|
|
// If get_peak() fails, it will leave the
|
|
// (zeroed) 'peak' variable unchanged.
|
|
//
|
|
struct peak_info peak = { };
|
|
get_peak(peak_b, &peak, 0.0f);
|
|
|
|
tuner_state.string_freq[s] = peak.freq;
|
|
tuner_state.string_mag[s] = peak.mag;
|
|
}
|
|
|
|
static inline void polyphonic_tuner_magnitudes(const struct tuning *current_tuning)
|
|
{
|
|
for (int s = 0; s < current_tuning->num_strings; s++)
|
|
find_string_peak(current_tuning, s);
|
|
}
|
|
|
|
|
|
// Forward declare to_ascii from ui.h
|
|
static char *to_ascii(unsigned char term, uint32_t val, char *p, int digits, int decimals);
|
|
static char *float_to_ascii(float val, int places);
|
|
|
|
// Helper to calculate the MIDI note number (69 = A4 440Hz) and cent deviation.
|
|
static inline struct tune_result calculate_note_and_cents(float freq)
|
|
{
|
|
struct tune_result res = { 0, 0 };
|
|
if (freq <= 0.0f)
|
|
return res;
|
|
|
|
float note_float = 69.0f + 12.0f * log2f(freq / 440.0f);
|
|
res.note_idx = (int)(rintf(note_float));
|
|
res.cents = (int)lrintf((note_float - res.note_idx) * 100.0f);
|
|
return res;
|
|
}
|
|
|
|
static void compute_tuner_results(const struct tuning *current_tuning, struct tuner_results *out)
|
|
{
|
|
out->num_results = 1 + current_tuning->num_strings;
|
|
|
|
// Chromatic
|
|
out->results[0] = calculate_note_and_cents(tuner_state.dominant_freq);
|
|
out->results[0].mag = tuner_state.dominant_mag;
|
|
|
|
// Polyphonic
|
|
for (int s = 0; s < current_tuning->num_strings; s++) {
|
|
if (tuner_state.string_mag[s] > 0.0f) {
|
|
out->results[1 + s] = calculate_note_and_cents(tuner_state.string_freq[s]);
|
|
out->results[1 + s].mag = tuner_state.string_mag[s];
|
|
} else {
|
|
out->results[1 + s].note_idx = 0;
|
|
out->results[1 + s].cents = 0;
|
|
out->results[1 + s].mag = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
static const char *const note_names[12] = {
|
|
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
|
|
};
|
|
|
|
static void draw_chromatic(const struct tune_result *result)
|
|
{
|
|
if (!result->note_idx)
|
|
return;
|
|
|
|
int note_idx = result->note_idx;
|
|
float cents = result->cents;
|
|
|
|
int name_idx = note_idx % 12;
|
|
if (name_idx < 0) name_idx += 12;
|
|
|
|
const char *name = note_names[name_idx];
|
|
int octave = (note_idx / 12) - 1;
|
|
|
|
// Build a string like "C#4"
|
|
char full_name[8];
|
|
int name_len = 0;
|
|
while (name[name_len]) {
|
|
full_name[name_len] = name[name_len];
|
|
name_len++;
|
|
}
|
|
full_name[name_len++] = '0' + octave;
|
|
full_name[name_len] = '\0';
|
|
|
|
char cents_str[8];
|
|
char *end = cents_str + 8;
|
|
end = to_ascii('\0', abs((int)cents), end, 1, 0);
|
|
if (cents <= -0.5f) *--end = '-';
|
|
else if (cents >= 0.5f) *--end = '+';
|
|
|
|
sh1106_puts_8x16(64 - name_len * 4, 46, full_name);
|
|
|
|
int cents_len = strlen(end);
|
|
sh1106_puts_6x8(64 - cents_len * 3, 66, end);
|
|
|
|
// Display exact frequency to the right of the chromatic section
|
|
sh1106_puts_6x8(126 - 5 * 6, 50, float_to_ascii(tuner_state.dominant_freq, 4));
|
|
|
|
// Huge needle spanning most of screen
|
|
int bar_x = 64 + (int)cents; // 50 cents = 50 pixels -> 14 to 114
|
|
if (bar_x < 14) bar_x = 14;
|
|
if (bar_x > 114) bar_x = 114;
|
|
|
|
sh1106_hline(14, 104, 100); // Axis line
|
|
sh1106_vline(64, 94, 21); // Center tick
|
|
sh1106_rectangle(bar_x - 2, 84, 5, 41, rect_filled); // Needle
|
|
}
|
|
|
|
static void draw_polyphonic(const struct tune_result *result, int s, int base_x, int col_w)
|
|
{
|
|
int s_x = base_x + s * col_w;
|
|
|
|
if (!result->note_idx) {
|
|
// Inactive string: no result, so we draw nothing.
|
|
return;
|
|
}
|
|
|
|
int name_idx = result->note_idx % 12;
|
|
if (name_idx < 0) name_idx += 12;
|
|
const char *name = note_names[name_idx];
|
|
|
|
// Center the 8x16 char in the column
|
|
sh1106_puts_8x16(s_x + (col_w - 8) / 2, 0, name);
|
|
|
|
int cents = result->cents >> 2;
|
|
|
|
// Build an arrow pointing in the right direction
|
|
//
|
|
// 10 is not entirely random: the sprites are
|
|
// max 24 pixels high (32 bit in the sprite map,
|
|
// but shifted up to 8 bits by the Y position)
|
|
//
|
|
cents = cents < -10 ? -10 : cents > 10 ? 10 : cents;
|
|
unsigned int arrow[32];
|
|
int arrow_w = col_w;
|
|
if (arrow_w > 32) arrow_w = 32;
|
|
|
|
arrow_w = (arrow_w-3)/2;
|
|
unsigned int pixels = 0;
|
|
for (int i = 0; i < arrow_w; i++) {
|
|
unsigned int val = 10 + cents*i/arrow_w;
|
|
pixels |= 7 << val;
|
|
arrow[i] = pixels;
|
|
arrow[2*arrow_w - i - 1] = pixels;
|
|
}
|
|
sh1106_sprite(s_x, 17, 2*arrow_w, arrow, arrow);
|
|
}
|
|
|
|
static void render_tuner_results(const struct tuner_results *results, const struct tuning *current_tuning)
|
|
{
|
|
sh1106_clear(0, 0, 128, 128);
|
|
|
|
draw_chromatic(&results->results[0]);
|
|
|
|
// Clear top background for polyphonic tuning display
|
|
sh1106_clear(0, 0, 128, 36);
|
|
|
|
int col_w = 128 / current_tuning->num_strings;
|
|
int base_x = (128 - (current_tuning->num_strings * col_w)) / 2;
|
|
|
|
for (int s = 0; s < current_tuning->num_strings; s++) {
|
|
draw_polyphonic(&results->results[1 + s], s, base_x, col_w);
|
|
}
|
|
|
|
sh1106_draw();
|
|
}
|
|
|
|
static int prev_note_idx[1 + MAX_STRINGS] = {0};
|
|
|
|
static void send_tuner_midi(const struct tuner_results *results)
|
|
{
|
|
for (int i = 0; i < results->num_results; i++) {
|
|
int current_note = results->results[i].note_idx;
|
|
int prev_note = prev_note_idx[i];
|
|
int cents = results->results[i].cents;
|
|
|
|
uint8_t ch = i; // Chromatic on ch 0, Strings on ch 1-8
|
|
|
|
if (current_note != prev_note) {
|
|
if (prev_note != 0) {
|
|
send_midi_note_off(ch, prev_note, 0);
|
|
}
|
|
if (current_note != 0) {
|
|
send_midi_note_on(ch, current_note, 100);
|
|
}
|
|
prev_note_idx[i] = current_note;
|
|
}
|
|
|
|
if (current_note != 0) {
|
|
send_midi_pitch_bend(ch, cents * 41);
|
|
|
|
float mag = results->results[i].mag;
|
|
int vol = (int)(log2f(mag + 1.0f) * 10.5f);
|
|
if (vol > 127) vol = 127;
|
|
if (vol < 0) vol = 0;
|
|
send_midi_channel_pressure(ch, vol);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void draw_analyzer(void)
|
|
{
|
|
unsigned int tuning_idx = settings.tuning;
|
|
if (tuning_idx >= ARRAY_SIZE(tunings))
|
|
tuning_idx = 0;
|
|
const struct tuning *current_tuning = tunings[tuning_idx];
|
|
|
|
unsigned int write_idx = analyzer.write_index;
|
|
|
|
// Catch up if CPU0 falls too far behind CPU1
|
|
if (write_idx - analyzer.read_index > ANALYZE_RING_SIZE - FFT_SIZE) {
|
|
analyzer.read_index = write_idx - FFT_SIZE;
|
|
}
|
|
|
|
if (write_idx - analyzer.read_index < FFT_SIZE) {
|
|
if (!remote_tuner_data)
|
|
return;
|
|
|
|
struct tuner_results remote_results;
|
|
remote_results.num_results = 1 + current_tuning->num_strings;
|
|
|
|
for (int i = 0; i < remote_results.num_results; i++) {
|
|
remote_results.results[i].note_idx = remote_note_idx[i];
|
|
remote_results.results[i].cents = remote_cents[i];
|
|
remote_results.results[i].mag = 0.0f; // We don't render remote magnitude on OLED yet
|
|
}
|
|
render_tuner_results(&remote_results, current_tuning);
|
|
return;
|
|
}
|
|
|
|
// Copy data from ring buffer and apply Hann window
|
|
for (int i = 0; i < FFT_SIZE; i++) {
|
|
float sample = analyzer.ring_buf[(analyzer.read_index + i) & ANALYZE_RING_MASK];
|
|
tuner_state.fft[i] = sample * hanning(i);
|
|
}
|
|
|
|
// Run FFT
|
|
fft(tuner_state.fft, FFT_SHIFT);
|
|
|
|
// Compute the magnitude of the bins
|
|
//
|
|
// NOTE! The fft[] and magnitudes[] arrays are a
|
|
// union in the tuner_state structure. This only works
|
|
// because we just walk the (bigger) fft 'complex_t'
|
|
// forward and then store the resulting magnitude
|
|
// result on top of old fft values.
|
|
//
|
|
float sum_mag = 0.0f;
|
|
float max_mag = 0.0f;
|
|
for (int i = 0; i < FFT_SIZE / 2; i++) {
|
|
float mag = __builtin_cabsf(tuner_state.fft[i]);
|
|
tuner_state.magnitudes[i] = mag;
|
|
sum_mag += mag;
|
|
if (mag > max_mag)
|
|
max_mag = mag;
|
|
}
|
|
|
|
tuner_state.max_mag = max_mag;
|
|
tuner_state.avg_mag = sum_mag / (FFT_SIZE / 2);
|
|
|
|
suppress_harmonics();
|
|
|
|
tuner_magnitudes();
|
|
|
|
polyphonic_tuner_magnitudes(current_tuning);
|
|
|
|
struct tuner_results results;
|
|
compute_tuner_results(current_tuning, &results);
|
|
|
|
send_tuner_midi(&results);
|
|
render_tuner_results(&results, current_tuning);
|
|
|
|
// Overlap by advancing read_idx by a fraction of FFT_SIZE
|
|
// FFT_SIZE / 16 = 512 samples. At 12kHz, this means 23 updates per second.
|
|
analyzer.read_index += FFT_SIZE / 16;
|
|
}
|