Files
Linus Torvalds 87f86b8b43 Give the one LED all three of the things that want it
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>
2026-07-28 16:25:30 -07:00

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);
}