You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-08-18 13:13:35 +00:00
'Software' was the directory everything that was not KiCad ended up in, which stopped describing anything a while ago - Validation and the web app are software too. Worse, it put the shared parts inside the firmware, where they read as the firmware's own. They are not. Effects/ has three consumers built from it: the firmware, Validation's bench, and the web app's controls, all generated from the same POT: comments by gen_effects.py. Audio/ has two - the bench compiles the same biquads, the same envelope followers and the same single_sample(), which is the whole reason a measurement on a workstation says anything about the pedal. Neither belongs under Firmware/, so neither is under it any more: Effects/ one file per effect Audio/ the DSP they are built from, and the audio loop Firmware/ the rest of what runs on the pedal, and the submodules WebMIDI/ the web app scripts/ what the build runs Validation/ unchanged Hardware/, Documentation/, Images/ CMakeLists.txt and the wrapper Makefile move to the top with them, because the build now consumes four of those directories and generates into a fifth. board.local and build/ come along; MIDI_CC_MAP.md is generated into Documentation/ rather than into the old Software/ root. scripts/ goes with the build rather than staying under the firmware, because six of the ten had nothing to do with the firmware: gen_effects.py reads Effects/ and writes to three different places, pow2/log2/quarter_sine generate Audio/'s tables, check-readme.py compares Effects/ against the README, and server.py serves the web app. Four of them are invoked from Validation, which was reaching into Firmware/ for tooling - the same burying this commit is undoing. The four that really are about the firmware are ELF checks the top-level build drives anyway, and a second scripts directory would only be a second place to look. C includes say "Audio/foo.h" and the generated map says "Effects/bar.h", with the repository root on the include path for both the firmware and the bench. Spelling the directory out rather than relying on a bare name is what keeps Audio/cycles.h shimmable: a quoted include searches the including file's own directory first. The submodules are renamed as well as moved. git mv updates their paths but leaves the section names, and 'Software/pico-sdk' surviving in .gitmodules would be the word this commit removes, still load-bearing. That meant the nested modules under pico-sdk too - six .git files pointing into .git/modules/Software - which is why 'git submodule update --init --recursive' is worth running once after pulling this. Verified rather than assumed: a clean configure and build, make check (failing only on the missing-eeprom case it already failed on), check-effects, all four analysis pages reproducing every series and drawing every chart, and a flash to the board that still measures a routed reverb where it did before. One latent bug fell out of it. bench/coeff declared only quarter_sine.h of the three generated math tables, and Audio/util.h includes pow2.h and log2.h as well - so building that target with an empty gen/ could never have worked. 'make bench' builds bench/bench first, which generates all three, so it stayed hidden until this rebuilt everything from nothing. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
490 lines
13 KiB
C
490 lines
13 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, mag;
|
|
|
|
//
|
|
// We look at two bins to either side, so anything that close to
|
|
// the ends of the array simply isn't a peak we can evaluate.
|
|
//
|
|
// That's just one more "not a peak" case as far as the callers are
|
|
// concerned, and a lot better than expecting each of them to
|
|
// remember the margins.
|
|
//
|
|
if (i < 2 || i > FFT_SIZE/2 - 3)
|
|
return false;
|
|
|
|
center = tuner_state.magnitudes + i;
|
|
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);
|
|
|
|
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 __not_in_flash("audio") 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 __not_in_flash("audio") 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 __not_in_flash("audio") 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 __not_in_flash("audio") 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 __not_in_flash("audio") 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);
|
|
|
|
// get_peak() looks after itself, but the scan below indexes the
|
|
// array directly, so that much we do have to bound.
|
|
if (min_b < 0) min_b = 0;
|
|
if (max_b > FFT_SIZE/2 - 1) max_b = FFT_SIZE/2 - 1;
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// 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 int prev_note_idx[1 + MAX_STRINGS] = {0};
|
|
|
|
//
|
|
// Stop everything the tuner has sounding.
|
|
//
|
|
// send_tuner_midi() emits a note-off only when the detected note
|
|
// *changes*, which is right while the tuner is running and wrong the
|
|
// moment it stops: leaving tuner mode simply stops calling it, so
|
|
// whatever was last heard is still held down as far as the host knows.
|
|
// With the web app's synth enabled that is an oscillator left droning.
|
|
//
|
|
// Deliberately the blocking write, unlike almost everything else that
|
|
// yields now. A dropped status report costs a stale reading until the
|
|
// next one; a dropped note-off is a note that never stops, so this is
|
|
// the one place where waiting is better than missing. It happens once,
|
|
// on the way out of a mode nobody records audio in.
|
|
//
|
|
static void tuner_silence(void)
|
|
{
|
|
for (int i = 0; i < ARRAY_SIZE(prev_note_idx); i++) {
|
|
if (!prev_note_idx[i])
|
|
continue;
|
|
send_midi_note_off(i, prev_note_idx[i], 0);
|
|
prev_note_idx[i] = 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 tuner_mode_ui(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 = smp_load_acquire(&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)
|
|
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);
|
|
|
|
// 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;
|
|
}
|