You've already forked ivan-torvalds-GuitarPedal
forked from AllSpiceMirrors/torvalds-GuitarPedal
The attention brightness setting stopped doing anything visible when the second LED went away. It had been shown by settings_effect.intense = settings_effect.active_pot == 4; which lit LED2 while you were sitting on that pot - and LED2 does not exist any more. So the one setting whose whole purpose is to change how the LED looks became the one setting you could not see yourself change. While fixing that, deal with the other two users of the same brightness, because there are exactly three and they had never been written down together: - the output hitting full scale - the audio core missing its DMA deadline - now, previewing the attention setting itself Keep all three sharing the LED, on purpose. With one bit of light there is no way to spell out which it is, but the two failures sound completely different and the ear does the work: clipping follows how hard you play and may well be something you want, while sample loss means too many effects are stacked up and everything has gone to mush and stays there. "Something is wrong, listen" is the part worth signalling. What was actually wrong was the *internal* conflation, and that goes: missing a deadline used to set the clipping flag, so the pedal told the world it was clipping when the signal had been nowhere near full scale, and you went looking for a level problem. Now 'samples_dropped' is only a count and 'output_clipped' is only clipping, which also means MIDI_CC_AUDIO_CLIPPING finally means what it says. A smart LED will have colours to spend and can start telling them apart for real. The timing gets written down while here, because it reads like an accident and is not. These flags are set on the audio core at 48kHz and cleared by update_ui() at about 25Hz, and that asymmetry is the whole mechanism. A clipped sample lasts twenty microseconds, which no eye will catch; holding the flag until the next UI tick stretches it to 40ms, which is about the shortest thing worth showing a human. And it does the averaging for free - clip one sample in a hundred and the LED stays solidly lit, because the audio core sets the flag far faster than the UI clears it, so "once in a while" and "all the time" look different without counting or filtering anything. Both properties disappear if this is ever moved to a faster loop. The preview holds for half a second rather than the single 40ms tick a change would otherwise get, so a nudge from the web app is visible and not just theoretically visible. The three live in status.h, which is what they are and which only this translation unit includes - types.h is also pulled in by the USB code, where they would be nothing but unused-variable warnings. They are named for what they hold and share one type, rather than being 'clipping' and 'dropped' and disagreeing about signedness for no reason. Settings pots get names while here. pot[4] was the attention level and pot[5] the tuning, and neither said so. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
256 lines
7.1 KiB
C
256 lines
7.1 KiB
C
#include "lfo.h"
|
|
|
|
sample_t get_usb_audio_input(void);
|
|
|
|
typedef float (*pot_convert_fn)(unsigned char);
|
|
|
|
//
|
|
// How an effect's wet and dry get mixed together.
|
|
//
|
|
// Linear is right when the wet signal is a filtered version of the dry
|
|
// and the two stay correlated - amplitudes add, so half of each gives
|
|
// you back what you started with. Almost everything here is like that.
|
|
//
|
|
// Equal power is right when they aren't correlated: a modulated delay,
|
|
// an echo tail, a reverb. There the *powers* add rather than the
|
|
// amplitudes, and a linear mix leaves a 3dB hole in the middle of the
|
|
// sweep. sin^2 + cos^2 == 1 fills it.
|
|
//
|
|
// Getting this backwards costs 3dB either way, so it is per effect
|
|
// rather than one law for all of them.
|
|
//
|
|
enum mix_law {
|
|
MIX_LINEAR,
|
|
MIX_POWER,
|
|
};
|
|
|
|
struct pot_descr {
|
|
const char *label;
|
|
const char *unit;
|
|
pot_convert_fn convert;
|
|
unsigned char def_val;
|
|
const char *const *enum_names;
|
|
};
|
|
|
|
//
|
|
// Primary interface for audio DSP algorithms.
|
|
//
|
|
// Each effect plugin defines its runtime processing callbacks and UI
|
|
// layout.
|
|
//
|
|
// Note: effects[] may leave `pots` array with NULL labels, which tells
|
|
// the OLED and I2C pollers to omit querying/drawing parameters that the
|
|
// hook doesn't use.
|
|
//
|
|
// We have two set of pot values, because cpu 0 - the UI core - will
|
|
// prepare the pot state in the inactive set, and then atomically switch
|
|
// that state so that code 1 - the audio core - will never use pot
|
|
// values that are in some halfway state. The active state is the LSB of
|
|
// the 'seq' value, which is used to tell whether the state has changed.
|
|
//
|
|
// 'mix' is the current mixing state, and 'target' is the target mixing
|
|
// state for fading in and fading out the effect (in fractions of
|
|
// EFF_ENABLE_STEPS)
|
|
//
|
|
struct effect {
|
|
const char *name, *short_name;
|
|
unsigned int mix, target;
|
|
float mix_pot;
|
|
float def_mix;
|
|
enum mix_law mix_law;
|
|
|
|
// What the mix law works out to, and where we are on the way
|
|
// there. Slewed rather than applied straight so that dragging
|
|
// the mix around doesn't click.
|
|
float dry, wet;
|
|
float dry_target, wet_target;
|
|
unsigned int seq, last;
|
|
unsigned char intense, active_pot;
|
|
unsigned char pot_values[2][10];
|
|
void (*init)(unsigned char[10]);
|
|
void (*load)(struct effect *, unsigned char[10]);
|
|
void (*save)(struct effect *, unsigned char[10]);
|
|
float (*step)(float);
|
|
const struct pot_descr pots[10];
|
|
};
|
|
|
|
#define EFFECT_POT(...) { __VA_ARGS__ }
|
|
|
|
//
|
|
// How many effects one scene can route.
|
|
//
|
|
// This is a storage limit, not a limit on how many effects exist - the
|
|
// point of routing is that there are more effects to choose from than
|
|
// you can have in a chain at once. eeprom.h checks that a scene has the
|
|
// slots for it.
|
|
//
|
|
#define MAX_ROUTED_EFFECTS 14
|
|
|
|
// Effects and MIDI mapping auto-generated from scripts/gen_effects.py
|
|
extern uint8_t effect_chain[MAX_ROUTED_EFFECTS];
|
|
extern uint8_t routed_effect_count;
|
|
#include "effect_map.h"
|
|
|
|
|
|
//
|
|
// Work out the two multipliers for a mix setting.
|
|
//
|
|
// Only called when the setting changes, which is why the equal-power
|
|
// case can afford a fastsincos(): a quarter cycle, so sin climbs from 0
|
|
// to 1 as cos falls from 1 to 0, and sin^2 + cos^2 stays 1 the whole way
|
|
// across. fastsincos() takes its phase in cycles rather than radians.
|
|
//
|
|
static void set_mix_pot(struct effect *eff, float m)
|
|
{
|
|
eff->mix_pot = m;
|
|
|
|
if (eff->mix_law == MIX_POWER) {
|
|
struct sincos w = fastsincos(0.25f * m);
|
|
eff->dry_target = w.cos;
|
|
eff->wet_target = w.sin;
|
|
} else {
|
|
eff->dry_target = 1.0f - m;
|
|
eff->wet_target = m;
|
|
}
|
|
}
|
|
|
|
// How fast the multipliers chase their target: ~10ms at 48kHz
|
|
#define MIX_SLEW (1.0f / 512)
|
|
|
|
// Effects are purely mono... For now
|
|
static inline sample_t do_effect_step(struct effect *effect, sample_t val)
|
|
{
|
|
if (effect->mix != effect->target) {
|
|
int dir = effect->mix < effect->target ? +1 : -1;
|
|
effect->mix += dir;
|
|
}
|
|
|
|
if (effect->mix == 0) return val;
|
|
|
|
// Chase the mix setting, so moving it doesn't step the gain
|
|
effect->dry += (effect->dry_target - effect->dry) * MIX_SLEW;
|
|
effect->wet += (effect->wet_target - effect->wet) * MIX_SLEW;
|
|
|
|
// ...and fade the whole thing in on top of that, which is what
|
|
// 'mix' counting up to 'target' is for now that it no longer
|
|
// carries the setting itself.
|
|
float r = effect->mix * (1.0f / EFF_ENABLE_STEPS);
|
|
float dry = 1.0f + r * (effect->dry - 1.0f);
|
|
float wet = r * effect->wet;
|
|
|
|
float effect_val = effect->step(val.left);
|
|
val.left = dry * val.left + wet * effect_val;
|
|
val.right = dry * val.right + wet * effect_val;
|
|
|
|
return val;
|
|
}
|
|
|
|
#include "process.h"
|
|
|
|
static int disable_all;
|
|
|
|
#define BLOCKSIZE 200
|
|
|
|
static raw_sample_t __attribute__((aligned(128))) i2s_dma_buf[16];
|
|
static int dma_tx;
|
|
static int dma_rx;
|
|
static unsigned int cpu_idx = 0;
|
|
|
|
static inline raw_sample_t *i2s_dma_tx_ptr(void)
|
|
{
|
|
return (raw_sample_t *) (dma_hw->ch[dma_tx].read_addr & ~7);
|
|
}
|
|
|
|
static inline raw_sample_t *i2s_dma_rx_ptr(void)
|
|
{
|
|
return (raw_sample_t *) (dma_hw->ch[dma_rx].write_addr & ~7);
|
|
}
|
|
|
|
static inline void __audio_func(single_sample)(float mix)
|
|
{
|
|
raw_sample_t *cpu_ptr = i2s_dma_buf + cpu_idx;
|
|
cpu_idx = (cpu_idx + 1) & 15;
|
|
|
|
// Wait for RX DMA to produce the sample
|
|
while (cpu_ptr == i2s_dma_rx_ptr())
|
|
tight_loop_contents();
|
|
|
|
// Check we're safely ahead of TX DMA
|
|
// Missing the deadline is not clipping, even though the LED
|
|
// shows both - see status.h.
|
|
unsigned int tx_idx = i2s_dma_tx_ptr() - i2s_dma_buf;
|
|
if (((cpu_idx - tx_idx) & 15) < 2)
|
|
samples_dropped++;
|
|
|
|
// In-place processing
|
|
raw_sample_t sample = *cpu_ptr;
|
|
sample_t in = process_input(sample);
|
|
sample_t usb_in = get_usb_audio_input();
|
|
|
|
// We need to do the USB input as stereo too
|
|
if (settings.usb_input == USB_IN_PRE_FX) {
|
|
in.left += usb_in.left;
|
|
in.right += usb_in.right;
|
|
}
|
|
|
|
sample_t out = in;
|
|
out = do_effect_step(effects[0], out); // Gate is always index 0 and runs first
|
|
for (int i = 0; i < routed_effect_count; i++) {
|
|
out = do_effect_step(effects[effect_chain[i]], out);
|
|
}
|
|
|
|
out.left = linear(mix, in.left, out.left);
|
|
out.right = linear(mix, in.right, out.right);
|
|
|
|
if (settings.usb_input == USB_IN_MIX) {
|
|
out.left += usb_in.left;
|
|
out.right += usb_in.right;
|
|
}
|
|
|
|
*cpu_ptr = process_output(out, sample);
|
|
}
|
|
|
|
static void bypass(void)
|
|
{
|
|
for (int i = 0; i < BLOCKSIZE; i++) {
|
|
single_sample(0.0);
|
|
}
|
|
}
|
|
|
|
static __attribute__((noinline)) void __audio_func(make_one_noise)(void)
|
|
{
|
|
for (int i = 0; i < ARRAY_SIZE(effects); i++) {
|
|
struct effect *effect = effects[i];
|
|
|
|
unsigned seq = smp_load_acquire(&effect->seq);
|
|
if (seq == effect->last)
|
|
continue;
|
|
|
|
//
|
|
// An effect that isn't running doesn't need its
|
|
// coefficients recomputed - but don't mark the update as
|
|
// consumed either, or it just gets lost. 'target' is
|
|
// already set by the time an effect is routed back in, so
|
|
// this picks the change up before it can be heard.
|
|
//
|
|
if (!effect->mix && !effect->target)
|
|
continue;
|
|
|
|
effect->last = seq;
|
|
effect->init(effect->pot_values[seq & 1]);
|
|
}
|
|
|
|
static int disable = 0;
|
|
while (disable != disable_all) {
|
|
float mix = disable / (float) EFF_ENABLE_STEPS;
|
|
disable += (disable < disable_all) ? 1 : -1;
|
|
single_sample(mix);
|
|
}
|
|
|
|
if (disable)
|
|
return bypass();
|
|
|
|
for (int i = 0; i < BLOCKSIZE; i++)
|
|
single_sample(1.0);
|
|
}
|