
static inline u8 lfohashi(u16 step) {
	return rndtab[step];
}
static inline float lfohashf(u16 step) {
	return (float) (lfohashi(step) * (2.f / 256.f) - 1.f);
}

float EvalTri(float t, u32 step) {
	return 1.f - (t + t);
}
float EvalEnv(float t, u32 step) {
	// unipolar pseudo exponential up/down
	if (step & 1) {
		t *= t;
		t *= t;
		return t;
	}
	else {
		t = 1.f - t;
		t *= t;
		t *= t;
		return 1.f-t;
	}
}
float EvalSin(float t, u32 step) {
	t = t * t * (3.f - t - t);
	return 1.f - (t + t);
}
float EvalSaw(float t, u32 step) {
	return (step &1) ? t-1.f : 1.f-t;
}
float EvalSquare(float t, u32 step) {
	return (step & 1) ? 0.f : 1.f;
}
float EvalBiSquare(float t, u32 step) {
	return (step & 1) ? -1.f : 1.f;
}
float EvalSandcastle(float t, u32 step) {
	return (step & 1) ? ((t<0.5f) ? 0.f : -1.f) : ((t<0.5f) ? 1.f : 0.f);
}
static inline float triggy(float t) {
	t=1.f-(t+t);
	t=t*t;
	return t*t;
}
float EvalTrigs(float t, u32 step) {
	return (step & 1) ? ((t<0.5f) ? 0.f : triggy(1.f-t)) : ((t<0.5f) ? triggy(t) : 0.f);
}
float EvalBiTrigs(float t, u32 step) {
	return (step & 1) ? ((t<0.5f) ? 0.f : -triggy(1.f-t)) : ((t<0.5f) ? triggy(t) : 0.f);
}
float EvalStepNoise(float t, u32 step) {
	return lfohashf(step);
}
float EvalSmoothNoise(float t, u32 step) {
	float n0 = lfohashf(step + (step&1)), n1 = lfohashf(step | 1);
	return n0 + (n1 - n0) * t;
}

float (*lfofuncs[LFO_LAST])(float t, u32 step) = {
		[LFO_TRI]=EvalTri, [LFO_SIN]=EvalSin, [LFO_SMOOTHNOISE]=EvalSmoothNoise, [LFO_STEPNOISE]=EvalStepNoise,
		[LFO_BISQUARE]=EvalBiSquare, [LFO_SQUARE]=EvalSquare, [LFO_SANDCASTLE]=EvalSandcastle, [LFO_BITRIGS]=EvalBiTrigs, [LFO_TRIGS]=EvalTrigs, [LFO_ENV]=EvalEnv,
		[LFO_SAW]=EvalSaw,
};

float lfo_eval(u32 ti, float warp, unsigned int shape) {
	int step = (ti >> 16)<<1;
	float t = (ti & 65535) * (1.f / 65536.f);
	if (t < warp)
		t /= warp;
	else {
		step++;
		t = (1.f - t) / (1.f-warp);
	}
	if (shape>=LFO_LAST) shape=0;
	return (*lfofuncs[shape])(t, step);
}




const char * const paramnames[P_LAST]={
#ifdef NEW_LAYOUT
	[P_A2] = I_ADSR_A "Attack2",
	[P_D2] = I_ADSR_D "Decay2",
	[P_S2] = I_ADSR_S "Sustain2",
	[P_R2] = I_ADSR_R "Release2",
	[P_SWING] = I_TEMPO "Swing",
#else
	[P_ENV_RATE] = I_PERIOD "Env Rate",
	[P_ENV_WARP] = I_WARP "Env Warp",
	[P_ENV_REPEAT] = I_FEEDBACK "Env Repeat",
#endif

	[P_SENS]=I_TOUCH "Sensitivity",
	[P_DRIVE]=I_DISTORT "Distort",
	[P_A]=I_ADSR_A "Attack",
	[P_D]=I_ADSR_D "Decay",
	[P_S]=I_ADSR_S "Sustain",
	[P_R]=I_ADSR_R "Release",


	[P_MIXSYNTH] = I_WAVE "Synth Lvl",
	[P_MIXINPUT] = I_JACK "Input Lvl",
	[P_MIXINWETDRY] = I_JACK "In Wet/Dry",
	[P_MIXWETDRY] = I_REVERB "Main Wet/Dry",
	[P_MIXHPF] = I_HPF "High Pass",
	[P_MIXRESO] = I_DISTORT "Resonance",

	[P_OCT]=I_OCTAVE "Octave",
	[P_PITCH]=I_PIANO "Pitch",
	[P_GLIDE]=I_GLIDE "Glide",
	[P_INTERVAL]=I_INTERVAL "Interval",
	[P_GATE_LENGTH] = I_INTERVAL "Gate Len",
	[P_ENV_LEVEL] = I_AMPLITUDE "Env Level",
	

	
	[P_PWM] = "Shape",
	[P_RVUNUSED] = "<unused>",

	[P_SCALE]=I_PIANO "Scale",
	[P_ROTATE]=I_FEEDBACK "Degree",
	[P_MICROTUNE]=I_MICRO "Microtone",
	[P_STRIDE]=I_INTERVAL "Stride",

	[P_ARPONOFF] = I_NOTES "Arp On/Off",
	[P_LATCHONOFF] = "Latch On/Off",

	[P_ARPMODE]=I_ORDER "Arp",
	[P_ARPDIV]=I_DIVIDE "Divide",
	[P_ARPPROB]=I_PERCENT "Prob %",
	[P_ARPLEN]=I_LENGTH "Euclid Len",
	[P_ARPOCT]=I_OCTAVE "Octaves",
	[P_TEMPO]= "BPM",

	[P_SEQMODE]=I_ORDER "Seq",
	[P_SEQDIV]=I_DIVIDE "Divide",
	[P_SEQPROB]=I_PERCENT "Prob %",
	[P_SEQLEN]=I_LENGTH "Euclid Len",
	[P_SEQSTEP]=I_SEQ "Step Ofs",
	[P_SEQPAT]=I_PRESET "Pattern",

	[P_DLSEND]=I_SEND "Send",
	[P_DLTIME]=I_TIME "Time",
	[P_DLFB]=I_FEEDBACK "Feedback",
	//[P_DLCOLOR]=I_COLOR "Colour",
	[P_DLWOB]=I_AMPLITUDE "Wobble",
	[P_DLRATIO]=I_DIVIDE "2nd Tap",

	[P_RVSEND]=I_SEND "Send",
	[P_RVTIME]=I_TIME "Time",
	[P_RVSHIM]=I_FEEDBACK "Shimmer",
	//[P_RVCOLOR]=I_COLOR "Colour",
	[P_RVWOB]=I_AMPLITUDE "Wobble",
	//[P_RVUNUSED]=" ",

	[P_SAMPLE] = I_WAVE "Sample",
	[P_SMP_POS] = "Scrub",
	[P_SMP_RATE] = I_NOTES "Rate",
	[P_SMP_GRAINSIZE] = I_PERIOD "Grain Sz",
	[P_SMP_TIME] = I_TIME "Timestretch",
	[P_CV_QUANT] = I_JACK "CV Quantise",

	[P_ACCEL_SENS] = I_AMPLITUDE "Accel Sens",
	[P_MIX_WIDTH] = I_AMPLITUDE "Stereo Width",
	[P_NOISE] = I_WAVE "Noise",
	[P_JIT_POS] = "Scrub",
	[P_JIT_RATE] = I_NOTES "Rate",
	[P_JIT_GRAINSIZE] = I_PERIOD "Grain Sz",
	[P_JIT_PULSE] = I_ENV "<unused>",
	[P_HEADPHONE] = "'Phones Vol",

	[P_AOFFSET]=I_OFFSET "CV Offset",
	[P_ASCALE]= I_TIMES "CV Scale",
	[P_ADEPTH]=I_AMPLITUDE "LFO Depth",
	[P_AFREQ]= I_PERIOD "LFO Rate",
	[P_ASHAPE]=I_SHAPE "LFO Shape",
	[P_AWARP]= I_WARP "LFO Warp",

	[P_BOFFSET]=I_OFFSET "CV Offset",
	[P_BSCALE]=I_TIMES "CV Scale",
	[P_BDEPTH]=I_AMPLITUDE "LFO Depth",
	[P_BFREQ]=I_PERIOD "LFO Rate",
	[P_BSHAPE]=I_SHAPE "LFO Shape",
	[P_BWARP]=I_WARP "LFO Warp",

	[P_XOFFSET]=I_OFFSET "CV Offset",
	[P_XSCALE]=I_TIMES "CV Scale",
	[P_XDEPTH]=I_AMPLITUDE "LFO Depth",
	[P_XFREQ]=I_PERIOD "LFO Rate",
	[P_XSHAPE]=I_SHAPE "LFO Shape",
	[P_XWARP]=I_WARP "LFO Warp",

	[P_YOFFSET]=I_OFFSET "CV Offset",
	[P_YSCALE]=I_TIMES "CV Scale",
	[P_YDEPTH]=I_AMPLITUDE "LFO Depth",
	[P_YFREQ]=I_PERIOD "LFO Rate",
	[P_YSHAPE]=I_SHAPE "LFO Shape",
	[P_YWARP]=I_WARP "LFO Warp",

};

#define FLAG_SIGNED 128
#define FLAG_MASK 127


const static u8 param_flags[P_LAST] = {
	[P_PWM]=FLAG_SIGNED,
	[P_ARPMODE] = ARP_LAST,
	[P_ARPDIV] = FLAG_SIGNED,
	[P_ARPPROB] =0, // FLAG_SIGNED, - used to have this signed, but its nice to slam it into 0
	[P_ARPLEN] = 17 + FLAG_SIGNED,
	[P_ARPOCT] = 4,
	[P_TEMPO] = FLAG_SIGNED,

#ifdef NEW_LAYOUT
	[P_A2] = 0,
	[P_D2] = 0,
	[P_S2] = 0,
	[P_R2] = 0,
	[P_SWING] = FLAG_SIGNED,

	[P_ACCEL_SENS] = FLAG_SIGNED,
	[P_MIX_WIDTH] = FLAG_SIGNED,
#else
	[P_ENV_WARP] = FLAG_SIGNED,
	[P_ENV_RATE] = FLAG_SIGNED,
	[P_ENV_REPEAT] = FLAG_SIGNED,
#endif
	[P_MIXWETDRY] = FLAG_SIGNED,
	[P_MIXINWETDRY] = FLAG_SIGNED,

	[P_SEQMODE] = SEQ_LAST,
	[P_SEQDIV] = DIVISIONS_MAX+1,
	[P_SEQPROB] =FLAG_SIGNED,
	[P_SEQLEN] = 17 + FLAG_SIGNED,
	[P_SEQPAT]=24,
	[P_SEQSTEP] = FLAG_SIGNED + 64,

	

	[P_DLSEND]=0,
	[P_DLTIME]=FLAG_SIGNED,
	[P_DLFB]=0,
	//[P_DLCOLOR]=0,
	[P_DLWOB]=0,
	[P_DLRATIO]=0,

	[P_RVSEND]=0,
	[P_RVTIME]=0,
	[P_RVSHIM]=0,
	//[P_RVCOLOR]=0,
	[P_RVWOB]=0,
	//[P_RVUNUSED]=0,

	[P_SENS]=0,
	[P_DRIVE]=FLAG_SIGNED,
	[P_A]=0,
	[P_D]=0,
	[P_S]=0,
	[P_R]=0,


	[P_OCT]=4+FLAG_SIGNED,
	[P_PITCH]=FLAG_SIGNED,
	[P_GLIDE]=0,
	[P_INTERVAL]=FLAG_SIGNED,
	
	[P_SCALE]=S_LAST,
	[P_ROTATE]=FLAG_SIGNED+24,
	[P_MICROTUNE]=0,
	[P_STRIDE]=13,
	
	[P_ASCALE ]=FLAG_SIGNED,
	[P_AOFFSET]=FLAG_SIGNED,
	[P_ADEPTH ]=FLAG_SIGNED,
	[P_AFREQ  ]=FLAG_SIGNED,
	[P_ASHAPE ]=LFO_LAST,
	[P_AWARP  ]=FLAG_SIGNED,

	[P_BSCALE ]=FLAG_SIGNED,
	[P_BOFFSET]=FLAG_SIGNED,
	[P_BDEPTH ]=FLAG_SIGNED,
	[P_BFREQ  ]=FLAG_SIGNED,
	[P_BSHAPE ]=LFO_LAST,
	[P_BWARP  ]=FLAG_SIGNED,

	[P_XSCALE ]=FLAG_SIGNED,
	[P_XOFFSET]=FLAG_SIGNED,
	[P_XDEPTH ]=FLAG_SIGNED,
	[P_XFREQ  ]=FLAG_SIGNED,
	[P_XSHAPE ]=LFO_LAST,
	[P_XWARP  ]=FLAG_SIGNED,

	[P_YSCALE ]=FLAG_SIGNED,
	[P_YOFFSET]=FLAG_SIGNED,
	[P_YDEPTH ]=FLAG_SIGNED,
	[P_YFREQ  ]=FLAG_SIGNED,
	[P_YSHAPE ]=LFO_LAST,
	[P_YWARP  ]=FLAG_SIGNED,

	[P_SAMPLE] = 9,
	[P_SMP_RATE] = FLAG_SIGNED,
	[P_SMP_TIME] = FLAG_SIGNED,
	[P_CV_QUANT] = CVQ_LAST,
	[P_JIT_PULSE] = FLAG_SIGNED,
};

#define C  ( 0*512)
#define Cs ( 1*512)
#define D  ( 2*512)
#define Ds ( 3*512)
#define E  ( 4*512)
#define F  ( 5*512)
#define Fs ( 6*512)
#define G  ( 7*512)
#define Gs ( 8*512)
#define A  ( 9*512)
#define As (10*512)
#define B  (11*512)

#define Es F
#define Bs C

#define Ab Gs
#define Bb As
#define Cb B
#define Db Cs
#define Eb Ds
#define Fb E
#define Gb Fs

#define CENTS(c) (((c)*512)/100)

const static u16 scaletab[S_LAST][16] = {
[S_CHROMATIC]		= {12, C,Cs,D,Ds,E,F,Fs,G,Gs,A,As,B},
[S_MAJOR]			= {7, C,D,E,F,G,A,B},
[S_MINOR]			= {7, C,D,Eb,F,G,Ab,Bb},
[S_HARMMINOR]		= {7,C,D,Ds,F,G,Gs,B},
[S_PENTA]			= {5, C,D,E,G,A},
[S_PENTAMINOR]		= {5, C,Ds,F,G,As},
[S_HIRAJOSHI]		= {5, C,D,Ds,G,Gs},
[S_INSEN]			= {5, C,Cs,F,G,As},
[S_IWATO]			= {5, C,Cs,F,Fs,As},
[S_MINYO]			= {5, C,D,F,G,A},

[S_FIFTHS]			= {2, C,G},
[S_TRIADMAJOR]		= {3, C,E,G},
[S_TRIADMINOR]		= {3, C,Eb,G},

// these are dups of major/minor/rotations thereof, but lets throw them in anyway
[S_DORIAN]			= {7, C,D,Ds,F,G,A,As},
[S_PHYRGIAN]		= {7, C,Db,Eb,F,G,Ab,Bb},
[S_LYDIAN]			= {7, C,D,E,Fs,G,A,B},
[S_MIXOLYDIAN]		= {7, C,D,E,F,G,A,Bb},
[S_AEOLIAN]			= {7, C,D,Eb,F,G,Ab,Bb},
[S_LOCRIAN]			= {7, C,Db,Eb,F,Gb,Ab,Bb},

[S_BLUESMAJOR]		= {6,C,D,Ds,E,G,A},
[S_BLUESMINOR] = {6,C,Ds,F,Fs,G,As},

[S_ROMANIAN]		= {7,C,D,Ds,Fs,G,A,As},
[S_WHOLETONE]		= {6,C,D,E,Fs,Gs,As},

// microtonal stuff
[S_HARMONICS] = {4, C, E - CENTS(14), G + CENTS(2), Bb - CENTS(31)},
[S_HEXANY] = { 5, CENTS(0), CENTS(386), CENTS(498), CENTS(702), CENTS(814)}, // kinda C,E,F,G,G# but the E is quite flat

[S_JUST] = {7, CENTS(0),	CENTS(204), CENTS(386), CENTS(498), CENTS(702), CENTS(884), CENTS(1088)},
[S_DIMINISHED]		= {8,C,D,Ds,F,Fs,Gs,A,B},
};

#undef C
#undef D
#undef E
#undef F
#undef G
#undef A
#undef B
#undef Cs
#undef Ds
#undef Es
#undef Fs
#undef Gs
#undef As
#undef Bs
#undef Cb
#undef Db
#undef Eb
#undef Fb
#undef Gb
#undef Ab
#undef Bb




static inline u8 scalelen(int scale) {
	return scaletab[scale][0];
}
static inline int lookupscale(int scale, int step) {
	u8 len=scaletab[scale][0];
	int oct=step/len;
	step-=oct*len;
	if (step<0) { step+=len; oct--; }
	return oct*(12*512) + scaletab[scale][step+1];
}


int params_premod[P_LAST]; // parameters with the lfos/inputs pre-mixed in
#define FULLBITS 10
#define FULL 1024
#define HALF (FULL/2)
#define QUARTER (FULL/4)
#define EIGHTH (FULL/8)
#define QUANT(v,maxi) ( ((v)*FULL+FULL/2)/(maxi) )

#define FIRST_PRESET_IDX 0
#define LAST_PRESET_IDX 32
#define FIRST_PATTERN_IDX LAST_PRESET_IDX
#define LAST_PATTERN_IDX 128 // 24 patterns x 4 quarters = 96 pages starting from page 32
#define FIRST_SAMPLE_IDX LAST_PATTERN_IDX
#define LAST_SAMPLE_IDX 136
#define LAST_IDX LAST_SAMPLE_IDX
#define OPS_IDX 0xfe
typedef struct PageFooter {
	u8 idx; // preset 0-31, pattern (quarters!) 32-127, sample 128-136, blank=0xff
	u8 version;
	u16 crc;
	u32 seq;
} PageFooter;
typedef struct SysParams {
	u8 curpreset;
	u8 paddy;
	u8 systemflags;
	s8 headphonevol;
	u8 pad[16 - 4];
} SysParams;
enum {
	SYS_DEPRACATED_ARPON=1,
	SYS_DEPRACATED_LATCHON=2,
};
enum {
	FLAGS_ARP=1,
	FLAGS_LATCH=2,
};
// preset version 1: ??
// preset version 2: add SAW lfo shape
#define CUR_PRESET_VERSION 2
typedef struct Preset {
	s16 params[96][8];
	u8 flags;
	s8 loopstart_step_no_offset;
	s8 looplen_step;
	u8 paddy[3];
	u8 version;
	u8 category;
	u8 name[8];
} Preset;
static_assert((sizeof(Preset)&15)==0,"?");
static_assert(sizeof(Preset)+sizeof(SysParams)+sizeof(PageFooter)<=2048, "?");
typedef struct SampleInfo {
	u8 waveform4_b[1024]; // 4 bits x 2048 points, every 1024 samples
	int splitpoints[8];
	int samplelen; // must be after splitpoints, so that splitpoints[8] is always the length.
	s8 notes[8];
	u8 pitched; 
	u8 loop; // bottom bit: loop; next bit: slice vs all
	u8 paddy[2]; 
} SampleInfo;
static_assert(sizeof(SampleInfo) + sizeof(SysParams) + sizeof(PageFooter) <= 2048, "?");
static_assert((sizeof(SampleInfo)&15)==0,"?");
typedef struct FingerRecord {
	u8 pos[4];
	u8 pressure[8];
} FingerRecord;
typedef struct PatternQuarter {
	FingerRecord steps[16][8];
	s8 autoknob[16*8][2];
} PatternQuarter;
static_assert(sizeof(PatternQuarter) + sizeof(SysParams) + sizeof(PageFooter) <= 2048, "?");
static_assert((sizeof(PatternQuarter)&15)==0,"?");
SampleInfo ramsample;
Preset rampreset;
PatternQuarter rampattern[4];
SysParams sysparams;
u8 ramsample1_idx=255;
u8 rampreset_idx=255;
u8 rampattern_idx=255;
u8 updating_bank2 = 0;
u8 edit_preset_pending = 255;
u8 edit_pattern_pending = 255;
u8 edit_sample1_pending = 255;

u8 prev_preset_pending = 255;
u8 prev_pattern_pending = 255;
u8 prev_sample1_pending = 255;

u8 cur_sample1; // this is the one we are playing, derived from param, can modulate. 0 means off, 1-8 is sample
u8 cur_pattern; // this is the current pattern, derived from param, can modulate.
s8 cur_step = 0; // current step
s8 step_offset = 0; // derived from param
u8 edit_sample0 = 0; // this is the one we are editing. no modulation. sample 0-7. not 1 based!
u8 copyrequest = 255;
u8 copyfrompreset = 0;
u8 copyfrompattern = 0;
u8 copyfromsample = 0;
u8 recordingknobs = 0;
s8 last_preset_selection_rotstep = 0; // the thing that gets cleared when you hold down X

float knobbase[2];

enum {
	GEN_PRESET,
	GEN_PAT0,
	GEN_PAT1,
	GEN_PAT2,
	GEN_PAT3,
	GEN_SYS,
	GEN_SAMPLE,
	GEN_LAST
};
u32 flashtime[GEN_LAST]; // for each thing we care about, what have we written to?
u32 ramtime[GEN_LAST]; //...and what has the UI set up? 

static u8 const zero[2048]={0};
typedef struct FlashPage {
	union {
		u8 raw[2048 - sizeof(SysParams) - sizeof(PageFooter)];
		Preset preset;
		PatternQuarter patternquarter;
		SampleInfo sampleinfo;
	};
	SysParams sysparams;
	PageFooter footer;
} FlashPage;
static_assert(sizeof(FlashPage) == 2048, "?");
u8 latestpagesidx[LAST_IDX];
u8 backuppagesidx[LAST_PRESET_IDX];
SysParams sysparams;
static inline FlashPage* GetFlashPagePtr(u8 page) { return (FlashPage*)(FLASH_ADDR_256 + page * 2048); }

static Preset const init_params;
int mod_cur[8]; // 16 bit fp


static inline int GetHeadphoneAsParam(void) {
	return (sysparams.headphonevol + 45) * (FULL / 64);
}

static inline int param_eval_premod(u8 paramidx) {
	if (paramidx == P_HEADPHONE)
		return params_premod[paramidx] = GetHeadphoneAsParam() * 65536 ;
	s16* p = rampreset.params[paramidx];
	int tv = p[M_BASE] << 16;
	tv += (mod_cur[M_A] * p[M_A]);
	tv += (mod_cur[M_B] * p[M_B]);
	tv += (mod_cur[M_X] * p[M_X]);
	tv += (mod_cur[M_Y] * p[M_Y]);
	params_premod[paramidx] = tv;
	return tv;
}



static inline Preset* GetSavedPreset(u8 presetidx) {
#ifdef HALF_FLASH
	return (Preset*)&init_params;
#endif
	if (presetidx >= 32)
		return (Preset*)&init_params;
	FlashPage*fp=GetFlashPagePtr(latestpagesidx[presetidx]);
	if (fp->footer.idx!=presetidx || fp->footer.version!=2)
		return (Preset*)&init_params;
	return (Preset * )fp;
}
static inline PatternQuarter* GetSavedPatternQuarter(u8 patternq) {
#ifdef HALF_FLASH
	return (PatternQuarter*)zero;
#endif
	if (patternq >= 24*4)
		return (PatternQuarter*)zero;
	FlashPage* fp = GetFlashPagePtr(latestpagesidx[patternq + FIRST_PATTERN_IDX]);
	if (fp->footer.idx != patternq+ FIRST_PATTERN_IDX || fp->footer.version != 2)
		return (PatternQuarter*)zero;
	return (PatternQuarter*)fp;
}
static inline SampleInfo* GetSavedSampleInfo(u8 sample0) {
#ifdef HALF_FLASH
	return (SampleInfo*)zero;
#endif
	if (sample0 >= 8)
		return (SampleInfo*)zero;
	FlashPage* fp = GetFlashPagePtr(latestpagesidx[sample0 + FIRST_SAMPLE_IDX]);
	if (fp->footer.idx != sample0 + FIRST_SAMPLE_IDX || fp->footer.version != 2)
		return (SampleInfo*)zero;
	return (SampleInfo*)fp;
}

u16 computehash(const void* data, int nbytes) {
	u16 hash = 123;
	const u8* src = (const u8 * )data;
	for (int i=0;i<nbytes;++i)
		hash = hash* 23 + *src++;
	return hash;
}

const static bool IsGenDirty(int gen) {
	return ramtime[gen] != flashtime[gen];
}
void SwapParams(int a, int b) {
	for (int k = 0; k < 8; ++k) {
		int t = rampreset.params[a][k];
		rampreset.params[a][k] = rampreset.params[b][k];
		rampreset.params[b][k] = t;
	}
}
bool CopyPresetToRam(bool force) {
	if (rampreset_idx == sysparams.curpreset && !force)
		return true; // nothing to do
	if (updating_bank2 || IsGenDirty(GEN_PRESET)) return false; // not copied yet
	memcpy(&rampreset, GetSavedPreset(sysparams.curpreset), sizeof(rampreset));
	for (int m = 1; m < M_LAST; ++m)
		rampreset.params[P_HEADPHONE][m] = 0;
	// upgrade rampreset.version to CUR_PRESET_VERSION
	if (rampreset.version == 0) {
		rampreset.version = 1;
		// swappin these around ;)
		SwapParams(P_MIX_WIDTH,P_ACCEL_SENS);
		rampreset.params[P_MIX_WIDTH][0] = HALF; // set default
	}
	if (rampreset.version == 1) {
		rampreset.version = 2;
		// insert a new lfo shape at LFO_SAW
		for (int p = 0; p < 4; ++p) {
			s16* data = rampreset.params[P_ASHAPE + p * 6];
			*data = (*data * (LFO_LAST - 1)) / (LFO_LAST); // rescale to add extra enum entry
			if (*data >= (LFO_SAW * FULL) / LFO_LAST) // and shift high numbers up
				*data += (1 * FULL) / LFO_LAST;
		}
	}
	rampreset_idx = sysparams.curpreset;
	return true;
}
bool CopySampleToRam(bool force) {
	if (ramsample1_idx == cur_sample1 && !force)
		return true; // nothing to do
	if (updating_bank2 || IsGenDirty(GEN_SAMPLE)) return false; // not copied yet
	if (cur_sample1 == 0)
		memset(&ramsample, 0, sizeof(ramsample));
	else
		memcpy(&ramsample, GetSavedSampleInfo(cur_sample1 - 1), sizeof(ramsample));
	ramsample1_idx = cur_sample1;
	return true;
}
bool CopyPatternToRam(bool force) {
	if (rampattern_idx == cur_pattern && !force)
		return true; // nothing to do
	if (updating_bank2 || IsGenDirty(GEN_PAT0) || IsGenDirty(GEN_PAT1) || IsGenDirty(GEN_PAT2) || IsGenDirty(GEN_PAT3)) 
		return false; // not copied yet
	for (int i = 0; i < 4; ++i)
		memcpy(&rampattern[i], GetSavedPatternQuarter((cur_pattern) * 4 + i), sizeof(rampattern[0]));
	rampattern_idx = cur_pattern;
	return true;
}


u8 next_free_page=0;
u32 next_seq = 0;
void InitParamsOnBoot(void) {
	u8 dummypage = 0;
	memset(latestpagesidx, dummypage, sizeof(latestpagesidx));
	memset(backuppagesidx, dummypage, sizeof(backuppagesidx));
	u32 highest_seq = 0;
	next_free_page = 0;
	memset(&sysparams, 0, sizeof(sysparams));
#ifndef HALF_FLASH
	// scan for the latest page for each object
	for (int page = 0 ; page < 255; ++page) {
		FlashPage* p = GetFlashPagePtr(page);
		int i = p->footer.idx;
		if (i >= LAST_IDX)
			continue;// skip blank
		if (p->footer.version < 2)
			continue; // skip old
		u16 check = computehash(p, 2040);
		if (check != p->footer.crc) {
			DebugLog("flash page %d has a bad crc!\r\n", page);
			if (page == dummypage) {
				// shit, the dummy page is dead! move to a different dummy
				for (int i = 0; i < sizeof(latestpagesidx); ++i) if (latestpagesidx[i] == dummypage)
					latestpagesidx[i]++;
				for (int i = 0; i < sizeof(backuppagesidx); ++i) if (backuppagesidx[i] == dummypage)
					backuppagesidx[i]++;
				dummypage++;
			}
			continue;
		}
		if (p->footer.seq > highest_seq) {
			highest_seq = p->footer.seq;
			next_free_page = page + 1;
			sysparams = p->sysparams;
		}
		FlashPage* existing = GetFlashPagePtr(latestpagesidx[i]);
		if (existing->footer.idx!=i  || p->footer.seq > existing->footer.seq || existing->footer.version<2)
			latestpagesidx[i] = page;
	}
#endif
	next_seq = highest_seq + 1;
	memcpy(backuppagesidx, latestpagesidx, sizeof(backuppagesidx));
	
	// clear remaining state
	edit_preset_pending = -1;
	edit_pattern_pending = -1;
	edit_sample1_pending = -1;
	rampattern_idx = -1;
	ramsample1_idx = -1;
	rampreset_idx = -1;
	edit_sample0 = 0;
	// relocate the first preset and pattern into ram
	copyrequest = 255;
	for (int i = 0; i < GEN_LAST; ++i) {
		ramtime[i] = 0;
		flashtime[i] = 0;
	}
	codec_setheadphonevol(sysparams.headphonevol + 45);
	last_preset_selection_rotstep = sysparams.curpreset;
}

int getheadphonevol(void) { // for emu really
	return sysparams.headphonevol + 45;
}

u8 AllocAndEraseFlashPage(void) {
#ifdef HALF_FLASH
	return 255;
#endif
	while (1) {
		FlashPage* p = GetFlashPagePtr(next_free_page);
		bool inuse = next_free_page == 255;
		inuse |= (p->footer.idx < LAST_IDX&& latestpagesidx[p->footer.idx] == next_free_page);
		inuse |= (p->footer.idx < LAST_PRESET_IDX&& backuppagesidx[p->footer.idx] == next_free_page);
		if (inuse) {
			++next_free_page;
			continue;
		}
		DebugLog("erasing flash page %d\r\n", next_free_page);
		flash_erase_page(next_free_page);
		return next_free_page++;
	}
}


void ProgramPage(void* datasrc, u32 datasize, u8 index) {
#ifndef HALF_FLASH

	updating_bank2 = 1;
	HAL_FLASH_Unlock();
	u8 page = AllocAndEraseFlashPage();
	u8* dst = (u8*)(FLASH_ADDR_256 + page * 2048);
	flash_program_block(dst, datasrc, datasize);
	flash_program_block(dst + 2048 - sizeof(SysParams) - sizeof(PageFooter), &sysparams, sizeof(SysParams));
	PageFooter f;
	f.idx = index;
	f.seq = next_seq++;
	f.version = 2;
	f.crc = computehash(dst, 2040);
	flash_program_block(dst + 2040, &f, 8);
	HAL_FLASH_Lock();
	latestpagesidx[index] = page;
	updating_bank2 = 0;
#endif
}

void SetPreset(u8 preset, bool force) {
	if (preset >= 32)
		return;
	if (preset == sysparams.curpreset && !force)
		return;
	sysparams.curpreset = preset;
	CopyPresetToRam(force);
	ramtime[GEN_SYS]=millis();
}
/*
void SetPattern(u8 pattern, bool force) {
	if (pattern >= 24)
		return;
	if (pattern == cur_pattern && !force)
		return;
	cur_pattern = pattern;
	CopyPatternToRam(force);
	ramtime[GEN_SYS]=millis();
}*/


int GetParam(u8 paramidx, u8 mod) {
	if (paramidx == P_HEADPHONE)
		return mod ? 0 : GetHeadphoneAsParam();
	return rampreset.params[paramidx][mod];
}

void EditParamNoQuant(u8 paramidx, u8 mod, s16 data) {
	if (paramidx >= P_LAST || mod >= M_LAST)
		return;
	if (paramidx == P_HEADPHONE) {
		if (mod == M_BASE) {
			data = clampi(-45, (data / (FULL / 64)) - 45, 18);
			if (data == sysparams.headphonevol)
				return;
			sysparams.headphonevol = data;
			ramtime[GEN_SYS] = millis();
		}
		return;
	}
	if (!CopyPresetToRam(false))
		return; // oh dear we haven't backed up the previous one yet!
	int olddata = GetParam(paramidx, mod);
	if (olddata == data)
		return;
	rampreset.params[paramidx][mod] = data;
	param_eval_premod(paramidx);
	//if (paramidx == P_SEQSTEP && mod == M_BASE)
	//	return; // dont set dirty when you're just moving the current playhead pos.
	ramtime[GEN_PRESET]=millis();
}

void EditParamQuant(u8 paramidx, u8 mod, s16 data) {
	int max = param_flags[paramidx] & FLAG_MASK;
	if (max>0) {
		data %= max; 
		if (data < 0 && !(param_flags[paramidx] & FLAG_SIGNED))
			data += max;
		data = ((data * 2 + 1) * FULL) / (max * 2);
	}
	EditParamNoQuant(paramidx, mod, data);
}

bool NeedWrite(int gen, u32 now) {
	if (ramtime[gen] == flashtime[gen])
		return false;
	if (gen == GEN_PRESET && sysparams.curpreset != rampreset_idx) {
		// the current preset is not equal to the ram preset, but the ram preset is dirty! WE GOTTA WRITE IT NOW!
		return true;
	}
	if (gen == GEN_SAMPLE && cur_sample1 != ramsample1_idx) {
		// the current sample is not equal to the ram preset, but the ram sample is dirty! WE GOTTA WRITE IT NOW!
		return true;
	}
	if (gen >= GEN_PAT0 && gen <= GEN_PAT3 && cur_pattern != rampattern_idx) {
		// the current pattern is not equal to the ram pattern, but the ram pattern is dirty! WE GOTTA WRITE IT NOW!
		return true;
	}
	
	u32 age = ramtime[gen] - flashtime[gen];
	if (age > 60000)
		return true;
	u32 time_since_edit = now - ramtime[gen];
	return time_since_edit > 5000;
}

void WritePattern(u32 now) {
	for (int i = 0; i < 4; ++i) if (NeedWrite(GEN_PAT0 + i, now)) {
		flashtime[GEN_SYS] = ramtime[GEN_SYS];
		flashtime[GEN_PAT0 + i] = ramtime[GEN_PAT0 + i];
		ProgramPage(&rampattern[i], sizeof(PatternQuarter), FIRST_PATTERN_IDX + (rampattern_idx) * 4 + i);
	}
}

void WriteSample(u32 now) {
	if (NeedWrite(GEN_SAMPLE, now)) {
		flashtime[GEN_SYS] = ramtime[GEN_SYS];
		flashtime[GEN_SAMPLE] = ramtime[GEN_SAMPLE];
		if (ramsample1_idx > 0)
			ProgramPage(&ramsample, sizeof(SampleInfo), FIRST_SAMPLE_IDX + ramsample1_idx - 1);
	}
}

void WritePreset(u32 now) {
	if (NeedWrite(GEN_PRESET, now) || NeedWrite(GEN_SYS, now)) {
		flashtime[GEN_SYS] = ramtime[GEN_SYS];
		flashtime[GEN_PRESET] = ramtime[GEN_PRESET];
		ProgramPage(&rampreset, sizeof(Preset), rampreset_idx);
	}
}



void PumpFlashWrites(void) {
	if (enable_audio != EA_PLAY)
		return;
	u32 now = millis();

	if (copyrequest != 255) {
		// we want to copy TO copyrequest, FROM copyfrompreset
		if (copyrequest & 128) {
			// wipe!
			copyrequest &= 63; 
			if (copyrequest < 32) {
				memcpy(&rampreset, &init_params, sizeof(rampreset));
				ramtime[GEN_PRESET] = now;
			}
			else if (copyrequest < 64 - 8) {
				memset(&rampattern, 0, sizeof(rampattern));
				ramtime[GEN_PAT0] = now;
				ramtime[GEN_PAT1] = now;
				ramtime[GEN_PAT2] = now;
				ramtime[GEN_PAT3] = now;
			}
			else {
				memset(&ramsample, 0, sizeof(ramsample));
				ramtime[GEN_SAMPLE] = now;
			}
		}
		else if (copyrequest<64) {
			// copy!
			if (copyrequest < 32) {
				if (copyrequest == copyfrompreset) {
					// toggle
					WritePreset(now + 100000); // flush any writes
					int t = backuppagesidx[copyfrompreset];
					backuppagesidx[copyfrompreset] = latestpagesidx[copyfrompreset];
					latestpagesidx[copyfrompreset] = t;
					memcpy(&rampreset, GetSavedPreset(sysparams.curpreset), sizeof(rampreset));

				}
				else {
					// copy preset
					ProgramPage(GetSavedPreset(copyfrompreset), sizeof(Preset), copyrequest);
				}
				SetPreset(copyrequest, true);
			} 
			else if (copyrequest < 64 - 8) {
				int srcpat = copyfrompattern;
				int dstpat = copyrequest - 32;
				/*if (srcpat == dstpat) { toggle not available for patterns
					// toggle
					WritePattern(now + 100000); // flush any writes
					for (int i = 0; i < 4; ++i) {
						int j = srcpat * 4 + i + 32;
						int t = backuppagesidx[j];
						backuppagesidx[j] = latestpagesidx[j];
						latestpagesidx[j] = t;
						memcpy(&rampattern[i], GetSavedPatternQuarter(srcpat*4+i), sizeof(PatternQuarter));
					}
				}
				else */
				{
					// copy pattern
					for (int i = 0; i < 4; ++i)
						ProgramPage(GetSavedPatternQuarter(srcpat * 4 + i), sizeof(PatternQuarter), 32 + dstpat * 4 + i);
				}
				EditParamQuant(P_SEQPAT, M_BASE, dstpat);
			}
		}
		copyrequest = 255;
		editmode = EM_PLAY;
	}
	

	WritePattern(now);
	WriteSample(now);
	WritePreset(now);
	
}

static Preset const init_params = {
	.looplen_step=8,
	.version=CUR_PRESET_VERSION,
	.params=
	{
		[P_SENS] = {HALF},
		[P_DRIVE] = {0},
		[P_A] = {EIGHTH},
		[P_D] = {QUARTER},
		[P_S] = {FULL},
		[P_R] = {EIGHTH},

		[P_OCT] = {0,0,0},
		[P_PITCH] = {0,0,0},
		[P_SCALE] = {QUANT(S_MAJOR,S_LAST)},
		[P_MICROTUNE] = {EIGHTH},
		[P_STRIDE] = {QUANT(7,13)},
		[P_INTERVAL] = {(0 * FULL) / 12},
		[P_ROTATE] = {0,0,0},

		[P_NOISE] = {0,0,0},

		[P_SMP_RATE] = {HALF},
		[P_SMP_GRAINSIZE] = {HALF},
		[P_SMP_TIME] = {HALF},
		

		//		[P_ARPMODE]={QUANT(ARP_UP,ARP_LAST)},
				[P_ARPDIV] = {QUANT(2,DIVISIONS_MAX) },
				[P_ARPPROB] = {FULL},
				[P_ARPLEN] = {QUANT(8,17)},
				[P_ARPOCT] = {QUANT(0,4)},
				[P_GLIDE] = {0},

				[P_SEQMODE] = {QUANT(SEQ_FWD,SEQ_LAST)},
				[P_SEQDIV] = {QUANT(6,DIVISIONS_MAX+1)},
				[P_SEQPROB] = {FULL},
				[P_SEQLEN] = {QUANT(8,17)},
				[P_SEQPAT] = {QUANT(0,24)},
				[P_SEQSTEP] = {0},
				[P_TEMPO] = {0},

				[P_GATE_LENGTH] = {FULL},

				//[P_DLSEND]={HALF},
				[P_DLTIME] = {QUANT(3,8)},
				[P_DLFB] = {HALF},
				//[P_DLCOLOR]={FULL},
				[P_DLWOB] = {QUARTER},
				[P_DLRATIO] = {FULL},

				[P_RVSEND]={QUARTER},
				[P_RVTIME] = {HALF},
				[P_RVSHIM] = {QUARTER},
				//[P_RVCOLOR]={FULL-QUARTER},
				[P_RVWOB] = {QUARTER},
				//[P_RVUNUSED]={0},


				[P_MIXSYNTH] = {HALF},
				[P_MIX_WIDTH] = {(HALF * 7)/8},
				[P_MIXINWETDRY] = {0},
#ifdef EMU
				[P_MIXINPUT] = {0},
#else
				[P_MIXINPUT] = {HALF},
#endif
				[P_MIXWETDRY] = {0},
							
#ifdef NEW_LAYOUT
		[P_A2] = {EIGHTH},
		[P_D2] = {QUARTER},
		[P_S2] = {FULL},
		[P_R2] = {EIGHTH},
		[P_SWING] = {0},
#else
				[P_ENV_RATE] = {QUARTER},
				[P_ENV_REPEAT] = {0},
				[P_ENV_WARP] = {-FULL},
#endif
				[P_ENV_LEVEL] = {HALF},
				[P_CV_QUANT] = {QUANT(CVQ_OFF,CVQ_LAST)},

				[P_AOFFSET] = {0},
				[P_ASCALE] = {HALF},
				[P_ADEPTH] = {0},
				[P_AFREQ] = {0},
				//[P_ASHAPE] = {QUANT(LFO_ENV,LFO_LAST)},
				[P_AWARP] = {0},

				[P_BOFFSET] = {0},
				[P_BSCALE] = {HALF},
				[P_BDEPTH] = {0},
				[P_BFREQ] = {100},
				[P_BSHAPE] = {0},
				[P_BWARP] = {0},

				[P_XOFFSET] = {0},
				[P_XSCALE] = {HALF},
				[P_XDEPTH] = {0},
				[P_XFREQ] = {-123},
				[P_XSHAPE] = {0},
				[P_XWARP] = {0},

				[P_YOFFSET] = {0},
				[P_YSCALE] = {HALF},
				[P_YDEPTH] = {0},
				[P_YFREQ] = {-315},
				[P_YSHAPE] = {0},
				[P_YWARP] = {0},

				[P_ACCEL_SENS] = {HALF},
				}
}; // init params


u8 lfo_history[16][4];
u8 lfo_history_pos;
uint64_t lfo_pos[4];
u16 finger_rnd[8] = { 0, 1 << 12, 2 << 12, 3 << 12, 4 << 12, 5 << 12, 6 << 12, 7 << 12 }; // incremented by a big prime each time the finger is triggered
u16 any_rnd = { 8 << 12 }; // incremented every time any finger goes down
int tilt16 = 0; // average tilt, 
int env16 = 0; // global attack/decay env - TODO
int pressure16 = 0; // max pressure
static inline int index_to_tilt16(int fingeridx) {
	return fingeridx * 16384 - (28672*2);
}
static inline int param_eval_int_noscale(u8 paramidx, int rnd, int env16, int pressure16) { // 16 bit fp
	s16* p = rampreset.params[paramidx];
	int tv = params_premod[paramidx]; // p[M_BASE] * 65538;
	if (p[M_RND]) {
		u16 ri = (u16)(rnd + paramidx);
		if (p[M_RND] > 0)
			// unsigned uniform distribution
			tv += (rndtab[ri] * p[M_RND]) << 8;
		else {
			// signed! triangular distribution
			ri += ri;
			tv += (((int)rndtab[ri] - (int)rndtab[ri - 1]) * p[M_RND]) << 8;
		}
	}
	//tv += (tilt16 * p[M_TILT]);
	tv += (env16 * p[M_ENV]);
	tv += (maxi(0,pressure16) * p[M_PRESSURE]);
	/*
	tv += (mod_cur[M_A] * p[M_A]);
	tv += (mod_cur[M_B] * p[M_B]);
	tv += (mod_cur[M_X] * p[M_X]);
	tv += (mod_cur[M_Y] * p[M_Y]);
	*/
	u8 flags = param_flags[paramidx];
	u8 maxi = flags & FLAG_MASK;
	return clampi(tv >> FULLBITS, (flags & FLAG_SIGNED) ? -65536 : 0, maxi ? 65535 : 65536);
}

static inline int param_eval_int(u8 paramidx, int rnd, int env16, int pressure16) { // 16 bit fp
	u8 flags = param_flags[paramidx];
	u8 maxi = flags & FLAG_MASK;
	int tv = param_eval_int_noscale(paramidx, rnd, env16, pressure16);
	if (maxi) {
		tv = (tv * maxi) >> 16;
	}
	return tv;
}

