SampleGrid3 BETA 1

User avatar
IXix
Posts: 1158
Joined: Wed Nov 23, 2011 3:24 pm

Re: SampleGrid3 BETA 1

Post by IXix »

thepedal wrote: Sat Aug 17, 2024 6:31 pm It's amazing you've done all you've done so far with the BTDsys machines. :dance:
Lind of you to say so but I've not done anywhere near as much as I would have liked to. There's still a lot to do.
User avatar
magmavander
Posts: 722
Joined: Tue Nov 22, 2011 5:22 pm
Location: France
Contact:

Re: SampleGrid3 BETA 1

Post by magmavander »

Lind of you to say so but I've not done anywhere near as much as I would have liked to. There's still a lot to do.
This is a killer machine and you expanded it to be a Masterpiece.
Grids
Posts: 114
Joined: Tue Dec 27, 2011 10:52 am

Re: SampleGrid3 BETA 1

Post by Grids »

Is there a 64 bit version of Samplegrid by any chance?

I can't remember if it is only 32 bit.
User avatar
IXix
Posts: 1158
Joined: Wed Nov 23, 2011 3:24 pm

Re: SampleGrid3 BETA 1

Post by IXix »

Grids wrote: Sat Aug 24, 2024 3:58 am Is there a 64 bit version of Samplegrid by any chance?

I can't remember if it is only 32 bit.
Only 32bit so far.
User avatar
thepedal
Posts: 79
Joined: Wed Nov 23, 2011 5:44 pm
Contact:

Re: SampleGrid3 BETA 1

Post by thepedal »

Cheeky question to tack onto this thread, probably for IXix really....

I use Peerctl a *lot* and when I'm performing I have a Behringer controller so with everything is mapped to knobs that's the focus but when I write on the laptop I tend to need the 'Mixer GUI' function of Peerctl a lot. It pains me that opening it always requires a right-click on the mouse, fiddle down the options and select. Do you think it's possible (easy) to create a machine which would only open that window on double-click? Without delving fully into how Peerctl is written I wonder if this would be a simple way to achieve what I need...My other bane is that the 'Mixer GUI' window doesn't resize at all properly on modern size desktops but that's for another time or perhaps a proper rewrite :)
User avatar
IXix
Posts: 1158
Joined: Wed Nov 23, 2011 3:24 pm

Re: SampleGrid3 BETA 1

Post by IXix »

I haven't really gone into the PeerCtrl code, it's mostly a mystery to me.

Not likely to look anytime soon either I'm afraid. Probably quicker to learn how to code it yourself than wait for me to get around to it. :lol:
User avatar
thepedal
Posts: 79
Joined: Wed Nov 23, 2011 5:44 pm
Contact:

Re: SampleGrid3 BETA 1

Post by thepedal »

No worries, that's entirely fair enough. I may just do that! :)
User avatar
thepedal
Posts: 79
Joined: Wed Nov 23, 2011 5:44 pm
Contact:

Re: SampleGrid3 BETA 1

Post by thepedal »

Are the changes George applied to Matilde tracker relevant to SampleGrid and possible there too? Perhaps it was written with better sample handling from the start?

viewtopic.php?p=17783
User avatar
thepedal
Posts: 79
Joined: Wed Nov 23, 2011 5:44 pm
Contact:

Re: SampleGrid3 BETA 1

Post by thepedal »

Did SampleGrid sources never make it up to the Sourceforge page? Is there a reason for that?
User avatar
IXix
Posts: 1158
Joined: Wed Nov 23, 2011 3:24 pm

Re: SampleGrid3 BETA 1

Post by IXix »

thepedal wrote: Tue Oct 21, 2025 8:02 pm Are the changes George applied to Matilde tracker relevant to SampleGrid and possible there too? Perhaps it was written with better sample handling from the start?

viewtopic.php?p=17783
I don't know tbh. The sample handling code in SGrid is a mystery to me. All I ever did was minor tinkering.
Did SampleGrid sources never make it up to the Sourceforge page? Is there a reason for that?
SGrid 2 is on there. SampleGrid 3 isn't but there's no real reason for that. I could make it available.

Really at some point I should move the BTD stuff over to GitHub, just haven't got around to it yet.
User avatar
thepedal
Posts: 79
Joined: Wed Nov 23, 2011 5:44 pm
Contact:

Re: SampleGrid3 BETA 1

Post by thepedal »

Would be cool if you could one day get the BTD stuff over to GitHub. I tried for a while to use Claude AI to either create a modern replica of PeerCtrl or at least tinker with the bits of it that annoy me but it was a nightmare. With a mind to get it working in ReBuzz. I'm no programmer, especially where Microsoft libs are concerned and it just spat out tons of uncompilable guff. By the time I'd worked through all the really dumb errors all I ended up with in both cases were some binaries that did nothing. Proper waste of time and after a few weeks on it I realised I'm much better off just making tunes and leaving this stuff to those who really know what they are doing :) This ai coding stuff is improving all the time though and I'll probably try it again sometime when I'm out of musical inspiration again. PeerCtrl is just such an incredible tool when you have a large midi controller and the tweaks I'm after for now are not that major really.
oskari
Site Admin
Posts: 322
Joined: Mon Nov 21, 2011 2:04 pm

Re: SampleGrid3 BETA 1

Post by oskari »

claude code etc can actually do very nice buzz machines if you tell them so. give source code of existing buzz machines for reference.

for example, give it this: https://jeskola.net/buzz/beta/files/dev ... /Limiter.h and MachineInterface.h and ask it to make another machine based on it. works quite well in my experience.
User avatar
IXix
Posts: 1158
Joined: Wed Nov 23, 2011 3:24 pm

Re: SampleGrid3 BETA 1

Post by IXix »

oskari wrote: Thu Oct 30, 2025 3:12 pm claude code etc can actually do very nice buzz machines if you tell them so. give source code of existing buzz machines for reference.

for example, give it this: https://jeskola.net/buzz/beta/files/dev ... /Limiter.h and MachineInterface.h and ask it to make another machine based on it. works quite well in my experience.
:o :shock: :o
oskari
Site Admin
Posts: 322
Joined: Mon Nov 21, 2011 2:04 pm

Re: SampleGrid3 BETA 1

Post by oskari »

did you try it yourself?

here's what i got when said the above

Code: Select all

// FDN Reverb - Modern Feedback Delay Network Reverb for Buzz
// Implements Hadamard mixing, modulated delays, and multi-stage diffusion

#include <windef.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>

#define _USE_MATH_DEFINES
#include <math.h>

#include "MachineInterface.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

// Modern FDN Reverb with:
// - Hadamard feedback matrix for diffuse reflections
// - Modulated delay lines to reduce metallic artifacts
// - Multi-stage diffusers for early reflections
// - Tone control via damping filters

#define FDN_DELAYS 8
#define MAX_DELAY_SAMPLES 96000  // 2 seconds at 48kHz
#define DIFFUSER_STAGES 4
#define MAX_PREDELAY_SAMPLES 9600  // 100ms at 96kHz

// Parameters
#define PARAM_SIZE			0
#define PARAM_DAMPING		1
#define PARAM_MIX			2
#define PARAM_PREDELAY		3

// Simple one-pole filter for tone shaping
class OnePole
{
public:
	OnePole() : z1(0.0f), coeff(0.0f) {}

	void SetCutoff(float frequency, float sampleRate)
	{
		coeff = expf(-2.0f * M_PI * frequency / sampleRate);
	}

	float Process(float input)
	{
		z1 = input + coeff * (z1 - input);
		return z1;
	}

	void Reset() { z1 = 0.0f; }

private:
	float z1;
	float coeff;
};

// Allpass diffuser for building dense early reflections
class Allpass
{
public:
	Allpass() : writePos(0), delayLength(0), gain(0.7f)
	{
		for (int i = 0; i < 8192; i++)
			buffer[i] = 0.0f;
	}

	void SetDelay(int samples, float g = 0.7f)
	{
		delayLength = std::min(samples, 8191);
		gain = g;
	}

	float Process(float input)
	{
		int readPos = (writePos - delayLength) & 8191;
		float delayed = buffer[readPos];

		float output = -gain * input + delayed;
		buffer[writePos] = input + gain * output;

		writePos = (writePos + 1) & 8191;

		return output;
	}

	void Reset()
	{
		for (int i = 0; i < 8192; i++)
			buffer[i] = 0.0f;
		writePos = 0;
	}

private:
	float buffer[8192];
	int writePos;
	int delayLength;
	float gain;
};

// Modulated delay line with interpolation
class ModulatedDelay
{
public:
	ModulatedDelay() : writePos(0), delayLength(0), modPhase(0.0f), modDepth(0), modRate(0.0f)
	{
		buffer = new float[MAX_DELAY_SAMPLES];
		for (int i = 0; i < MAX_DELAY_SAMPLES; i++)
			buffer[i] = 0.0f;
	}

	~ModulatedDelay()
	{
		delete[] buffer;
	}

	void SetDelay(int samples)
	{
		delayLength = std::min(samples, MAX_DELAY_SAMPLES - 1);
	}

	void SetModulation(float rate, int depth)
	{
		modRate = rate;
		modDepth = std::min(depth, delayLength / 4);
	}

	void Write(float input)
	{
		buffer[writePos] = input;
		writePos++;
		if (writePos >= MAX_DELAY_SAMPLES)
			writePos = 0;
	}

	float Read()
	{
		// Add LFO modulation
		float modOffset = modDepth * sinf(modPhase);
		modPhase += modRate;
		if (modPhase > 2.0f * M_PI)
			modPhase -= 2.0f * M_PI;

		float readPosFloat = writePos - delayLength - modOffset;
		if (readPosFloat < 0)
			readPosFloat += MAX_DELAY_SAMPLES;

		// Linear interpolation
		int readPos0 = (int)readPosFloat;
		int readPos1 = (readPos0 + 1) % MAX_DELAY_SAMPLES;
		float frac = readPosFloat - readPos0;

		return buffer[readPos0] * (1.0f - frac) + buffer[readPos1] * frac;
	}

	void Reset()
	{
		for (int i = 0; i < MAX_DELAY_SAMPLES; i++)
			buffer[i] = 0.0f;
		writePos = 0;
		modPhase = 0.0f;
	}

private:
	float *buffer;
	int writePos;
	int delayLength;
	float modPhase;
	int modDepth;
	float modRate;
};

// Parameter structure
#pragma pack(1)
struct Params
{
	byte size;			// 0-255 (decay time)
	byte damping;		// 0-255 (high frequency absorption)
	byte mix;			// 0-255 (dry/wet mix)
	byte predelay;		// 0-255 (predelay in ms)
};
#pragma pack()

// Parameter definitions
CMachineParameter const paraSize =
{
	pt_byte,
	"Size",
	"Decay Time (0=short, FF=long)",
	0,
	255,
	0xFF,
	MPF_STATE,
	127
};

CMachineParameter const paraDamping =
{
	pt_byte,
	"Damping",
	"High Frequency Absorption (0=bright, FF=dark)",
	0,
	255,
	0xFF,
	MPF_STATE,
	127
};

CMachineParameter const paraMix =
{
	pt_byte,
	"Mix",
	"Dry/Wet Mix (0=dry, FF=wet)",
	0,
	255,
	0xFF,
	MPF_STATE,
	64
};

CMachineParameter const paraPredelay =
{
	pt_byte,
	"Predelay",
	"Initial Delay (0-FF ms)",
	0,
	255,
	0xFF,
	MPF_STATE,
	0
};

CMachineParameter const *pParameters[] =
{
	&paraSize,
	&paraDamping,
	&paraMix,
	&paraPredelay
};

#pragma pack(1)
CMachineInfo const MacInfo =
{
	MT_EFFECT,
	MI_VERSION,
	MIF_STEREO_EFFECT,
	0,
	0,
	4,
	0,
	pParameters,
	0,
	NULL,
	"FDN Reverb",
	"FDNReverb",
	"Modern FDN Reverb",
	NULL,
	NULL
};
#pragma pack()

// Machine implementation
class mi : public CMachineInterface
{
public:
	mi();
	virtual ~mi();
	virtual void Init(CMachineDataInput * const pi);
	virtual void Tick();
	virtual bool Work(float *psamples, int numsamples, int const mode);
	virtual void Stop();
	virtual void SetNumTracks(int const n) {}
	virtual void Command(int const i);
	virtual char const *DescribeValue(int const param, int const value);

private:
	void HadamardMix(float *input, float *output);
	void UpdateParameters();

	Params params;

	ModulatedDelay delays[FDN_DELAYS];
	OnePole dampingFilters[FDN_DELAYS];
	Allpass inputDiffuserL[DIFFUSER_STAGES];
	Allpass inputDiffuserR[DIFFUSER_STAGES];

	float feedback[FDN_DELAYS];
	float mix;
	float size;
	float damping;

	float *predelayBufferL;
	float *predelayBufferR;
	int predelayPos;
	int predelayLength;

	float sampleRate;
	bool paramChanged;
};

// DLL exports
DLL_EXPORTS

mi::mi()
{
	GlobalVals = &params;
	TrackVals = NULL;
	AttrVals = NULL;

	predelayBufferL = new float[MAX_PREDELAY_SAMPLES];
	predelayBufferR = new float[MAX_PREDELAY_SAMPLES];

	for (int i = 0; i < MAX_PREDELAY_SAMPLES; i++)
	{
		predelayBufferL[i] = 0.0f;
		predelayBufferR[i] = 0.0f;
	}

	predelayPos = 0;
	predelayLength = 0;
	paramChanged = false;
}

mi::~mi()
{
	delete[] predelayBufferL;
	delete[] predelayBufferR;
}

void mi::Init(CMachineDataInput * const pi)
{
	sampleRate = (float)pMasterInfo->SamplesPerSec;

	// Prime number delays for FDN (in samples at 48kHz)
	int baseDelays[FDN_DELAYS] = { 3001, 3203, 3407, 3617, 3823, 4027, 4229, 4441 };

	for (int i = 0; i < FDN_DELAYS; i++)
	{
		delays[i].SetDelay((int)(baseDelays[i] * sampleRate / 48000.0f));
		delays[i].SetModulation(0.0001f + i * 0.00005f, 8);
		dampingFilters[i].SetCutoff(4000.0f, sampleRate);
	}

	// Early reflection diffusers
	int diffuserDelays[DIFFUSER_STAGES] = { 142, 107, 379, 277 };
	for (int i = 0; i < DIFFUSER_STAGES; i++)
	{
		inputDiffuserL[i].SetDelay((int)(diffuserDelays[i] * sampleRate / 48000.0f), 0.65f);
		inputDiffuserR[i].SetDelay((int)((diffuserDelays[i] + 11) * sampleRate / 48000.0f), 0.65f);
	}

	// Set default parameters
	params.size = 127;
	params.damping = 127;
	params.mix = 64;
	params.predelay = 0;

	UpdateParameters();
}

void mi::Tick()
{
	// Check for parameter changes
	if (params.size != 0xFF)
		paramChanged = true;
	if (params.damping != 0xFF)
		paramChanged = true;
	if (params.mix != 0xFF)
		paramChanged = true;
	if (params.predelay != 0xFF)
		paramChanged = true;
}

void mi::UpdateParameters()
{
	// Size (decay time)
	if (params.size != 0xFF)
	{
		size = params.size / 255.0f;

		// Map size to feedback gain
		for (int i = 0; i < FDN_DELAYS; i++)
		{
			float variance = 0.98f + (i * 0.005f);
			feedback[i] = size * 0.95f * variance;
		}
	}

	// Damping (high frequency absorption)
	if (params.damping != 0xFF)
	{
		damping = params.damping / 255.0f;
		float cutoff = 1000.0f + (1.0f - damping) * 14000.0f;

		for (int i = 0; i < FDN_DELAYS; i++)
			dampingFilters[i].SetCutoff(cutoff, sampleRate);
	}

	// Mix (dry/wet)
	if (params.mix != 0xFF)
	{
		mix = params.mix / 255.0f;
	}

	// Predelay
	if (params.predelay != 0xFF)
	{
		float milliseconds = params.predelay * 100.0f / 255.0f;  // 0-100ms
		predelayLength = std::min((int)(milliseconds * sampleRate / 1000.0f), MAX_PREDELAY_SAMPLES - 1);
	}

	paramChanged = false;
}

bool mi::Work(float *psamples, int numsamples, int const mode)
{
	if (paramChanged)
		UpdateParameters();

	if (mode == WM_NOIO)
		return false;

	if (mode == WM_READ)
		return false;

	float *pin = psamples;
	float *pout = psamples;

	for (int i = 0; i < numsamples; i++)
	{
		float dryL = pin[i * 2];
		float dryR = pin[i * 2 + 1];

		// Predelay
		int predelayReadPos = predelayPos - predelayLength;
		if (predelayReadPos < 0)
			predelayReadPos += MAX_PREDELAY_SAMPLES;

		float inputL = predelayBufferL[predelayReadPos];
		float inputR = predelayBufferR[predelayReadPos];

		predelayBufferL[predelayPos] = dryL;
		predelayBufferR[predelayPos] = dryR;
		predelayPos = (predelayPos + 1) % MAX_PREDELAY_SAMPLES;

		// Input diffusion
		float diffusedL = inputL;
		float diffusedR = inputR;

		for (int d = 0; d < DIFFUSER_STAGES; d++)
		{
			diffusedL = inputDiffuserL[d].Process(diffusedL);
			diffusedR = inputDiffuserR[d].Process(diffusedR);
		}

		// Read from delay lines
		float delayOutputs[FDN_DELAYS];
		for (int d = 0; d < FDN_DELAYS; d++)
			delayOutputs[d] = delays[d].Read();

		// Hadamard feedback matrix
		float mixed[FDN_DELAYS];
		HadamardMix(delayOutputs, mixed);

		// Apply feedback, damping, and write back
		for (int d = 0; d < FDN_DELAYS; d++)
		{
			float feedbackSignal = mixed[d] * feedback[d];
			feedbackSignal = dampingFilters[d].Process(feedbackSignal);

			// Inject input into first 4 lines (L) and last 4 (R)
			if (d < 4)
				feedbackSignal += diffusedL * 0.25f;
			else
				feedbackSignal += diffusedR * 0.25f;

			delays[d].Write(feedbackSignal);
		}

		// Output: sum delay lines to stereo
		float outL = 0.0f, outR = 0.0f;
		for (int d = 0; d < FDN_DELAYS / 2; d++)
		{
			outL += delayOutputs[d] + delayOutputs[d + 4];
			outR += delayOutputs[d] - delayOutputs[d + 4];
		}

		outL *= 0.25f;
		outR *= 0.25f;

		// Mix dry/wet
		pout[i * 2] = dryL * (1.0f - mix) + outL * mix;
		pout[i * 2 + 1] = dryR * (1.0f - mix) + outR * mix;
	}

	return true;
}

void mi::HadamardMix(float *input, float *output)
{
	float temp[8];

	// First stage (4x 2-point)
	temp[0] = input[0] + input[1];
	temp[1] = input[0] - input[1];
	temp[2] = input[2] + input[3];
	temp[3] = input[2] - input[3];
	temp[4] = input[4] + input[5];
	temp[5] = input[4] - input[5];
	temp[6] = input[6] + input[7];
	temp[7] = input[6] - input[7];

	// Second stage (2x 4-point)
	float temp2[8];
	temp2[0] = temp[0] + temp[2];
	temp2[1] = temp[1] + temp[3];
	temp2[2] = temp[0] - temp[2];
	temp2[3] = temp[1] - temp[3];
	temp2[4] = temp[4] + temp[6];
	temp2[5] = temp[5] + temp[7];
	temp2[6] = temp[4] - temp[6];
	temp2[7] = temp[5] - temp[7];

	// Third stage (1x 8-point)
	output[0] = temp2[0] + temp2[4];
	output[1] = temp2[1] + temp2[5];
	output[2] = temp2[2] + temp2[6];
	output[3] = temp2[3] + temp2[7];
	output[4] = temp2[0] - temp2[4];
	output[5] = temp2[1] - temp2[5];
	output[6] = temp2[2] - temp2[6];
	output[7] = temp2[3] - temp2[7];

	// Normalize (1/sqrt(8))
	for (int i = 0; i < 8; i++)
		output[i] *= 0.353553f;
}

void mi::Stop()
{
	// Reset all delay lines and filters
	for (int i = 0; i < FDN_DELAYS; i++)
	{
		delays[i].Reset();
		dampingFilters[i].Reset();
	}

	for (int i = 0; i < DIFFUSER_STAGES; i++)
	{
		inputDiffuserL[i].Reset();
		inputDiffuserR[i].Reset();
	}

	for (int i = 0; i < MAX_PREDELAY_SAMPLES; i++)
	{
		predelayBufferL[i] = 0.0f;
		predelayBufferR[i] = 0.0f;
	}

	predelayPos = 0;
}

void mi::Command(int const i)
{
	// No commands implemented
}

char const *mi::DescribeValue(int const param, int const value)
{
	static char txt[16];

	switch(param)
	{
	case PARAM_SIZE:
		sprintf(txt, "%.1f%%", value * 100.0f / 255.0f);
		break;
	case PARAM_DAMPING:
		sprintf(txt, "%.1f%%", value * 100.0f / 255.0f);
		break;
	case PARAM_MIX:
		sprintf(txt, "%.1f%%", value * 100.0f / 255.0f);
		break;
	case PARAM_PREDELAY:
		sprintf(txt, "%.1fms", value * 100.0f / 255.0f);
		break;
	default:
		return NULL;
	}

	return txt;
}

User avatar
IXix
Posts: 1158
Joined: Wed Nov 23, 2011 3:24 pm

Re: SampleGrid3 BETA 1

Post by IXix »

oskari wrote: Fri Oct 31, 2025 12:17 pm did you try it yourself?

here's what i got when said the above...
No, I didn't try. That's pretty mind blowing, assuming it actually works. Did you compile it? Certainly looks legit from a quick read through.
User avatar
mcbpete
Posts: 405
Joined: Tue Nov 22, 2011 9:45 pm

Re: SampleGrid3 BETA 1

Post by mcbpete »

Curious how this bit works (if it does ?!). To me this reads as: 'when any of the sliders aren't at maximum value, then they've changed'

Code: Select all

// Check for parameter changes
	if (params.size != 0xFF)
		paramChanged = true;
	if (params.damping != 0xFF)
		paramChanged = true;
	if (params.mix != 0xFF)
		paramChanged = true;
	if (params.predelay != 0xFF)
		paramChanged = true;
User avatar
IXix
Posts: 1158
Joined: Wed Nov 23, 2011 3:24 pm

Re: SampleGrid3 BETA 1

Post by IXix »

mcbpete wrote: Tue Nov 04, 2025 1:12 pm Curious how this bit works (if it does ?!). To me this reads as: 'when any of the sliders aren't at maximum value, then they've changed'
Yeah, that's normal. Parameters have a "no value", usually 0xFF but it can be something else if you want. The params storage is reset to the null values every tick, so if it's not the null then something has changed. I do it like this though, as I find it easier to read...

Code: Select all

if(gVals.myParam != MyParam.NoValue)
{
   // Do stuff
}
User avatar
mcbpete
Posts: 405
Joined: Tue Nov 22, 2011 9:45 pm

Re: SampleGrid3 BETA 1

Post by mcbpete »

Gotcha - thanks ! So is the normal parameter range: 0x00 -> 0xFE, and 0xFF is an unreachable/un-initialised value ?
Post Reply