Audio/FaceSynth.cs
using System;

namespace SquishyTerry.Audio;

/// <summary>
/// C# port of the original game's Heavy (hvcc / Pure Data) patch "facesynth", the procedural voice of the face.
/// <para>
/// The patch has four voices, all mixed through an area dependent body resonator:
/// <b>slap</b> (release smack: filtered noise burst plus a ringing vcf~),
/// <b>slosh</b> (wet droplets whose density follows how fast the face volume changes),
/// <b>gloop</b> (two bubble voices with rising pitch, triggered by fast drag changes) and
/// <b>stretch</b> (two "creak" voices built from a recorded click grain, driven by the rate of change of area).
/// </para>
/// <para>
/// The port is split in three files:
/// this file holds the public API, the Heavy message runtime and the per sample signal graph (a line by line
/// port of the patch's DSP loop, keeping the original operation order so the output matches the original to
/// the last bit in most samples); <c>FaceSynth.PowF.cs</c> is musl's powf (so [pow] / mtof match the original);
/// <c>FaceSynthGraph.cs</c> is the patch's control message graph, generated from
/// the original binary (every Pd object keeps its original memory offset as its name suffix);
/// <c>FaceSynthTables.cs</c> holds the table data.
/// </para>
/// <para>The synth always runs at 44100 Hz (the patch hard codes 44100 in the gloop timer and the creak grain rate).</para>
/// </summary>
public sealed partial class FaceSynth
{
	/// <summary>Internal sample rate. The original patch hard codes this value in several places.</summary>
	public const int SampleRate = 44100;

	/// <summary>Hash of the patch's internal loadbang receiver (<c>__hv_init</c>).</summary>
	const uint LoadbangHash = 0xCE5CC65B;

	/// <summary>Number of samples processed so far (the Heavy timestamp clock).</summary>
	uint _now;

	/// <summary>Pending scheduled messages, sorted by timestamp (stable).</summary>
	QEntry _queue;

	/// <summary>The queue entry currently being dispatched (used by delay objects to clear their slot).</summary>
	QEntry _current;

	public FaceSynth()
	{
		InitTables();
		InitObjects();
		Schedule( 0, ReceiverFunction( LoadbangHash ), 0, Msg.Of( 0, Atom.Bang ) );
	}

	/// <summary>
	/// Sends a float to one of the patch's receivers (same names as the original game: volume, area, dragging,
	/// drag_delta_x/y/z, drag_distance, slap, init, vol_master, vol_slap, vol_slosh, vol_gloop, vol_stretch ...).
	/// The message is applied at the first sample of the next <see cref="Process"/> call. Unknown names are ignored.
	/// </summary>
	public void SetParameter( string name, float value )
	{
		int fn = ReceiverFunction( StringToHash( name ) );
		if ( fn == 0 ) return;
		Schedule( _now, fn, 0, Msg.Of( _now, Atom.Float( value ) ) );
	}

	/// <summary>Renders <c>left.Length</c> stereo frames (both spans must have the same length).</summary>
	public void Process( Span<float> left, Span<float> right )
	{
		int n = Math.Min( left.Length, right.Length );
		for ( int i = 0; i < n; i++ )
		{
			RunMessages();
			ProcessSample( out left[i], out right[i] );
			_now++;
		}
	}

	/// <summary>Renders <c>frames</c> interleaved stereo frames into <c>interleaved</c> (L, R, L, R ...).</summary>
	public void ProcessInterleaved( Span<float> interleaved, int frames )
	{
		for ( int i = 0; i < frames; i++ )
		{
			RunMessages();
			ProcessSample( out interleaved[i * 2], out interleaved[i * 2 + 1] );
			_now++;
		}
	}

	// ------------------------------------------------------------------------------------------------
	// Message runtime (Heavy semantics)
	// ------------------------------------------------------------------------------------------------

	/// <summary>A message atom: bang (0), float (1), symbol (2, stored as its hash) or hash (3).</summary>
	struct Atom
	{
		public int Type;
		public float F;
		public uint H;

		public static Atom Bang => new Atom { Type = 0 };
		public static Atom Float( float f ) => new Atom { Type = 1, F = f };
		public static Atom Symbol( uint hash ) => new Atom { Type = 2, H = hash };
		public static Atom Hash( int hash ) => new Atom { Type = 3, H = (uint)hash };

		/// <summary>Heavy's msg_getHash: bang -1, float its bit pattern, symbol / hash the hash.</summary>
		public int GetHash()
		{
			switch ( Type )
			{
				case 0: return -1;
				case 1: return BitConverter.SingleToInt32Bits( F );
				case 2:
				case 3: return (int)H;
				default: return 0;
			}
		}
	}

	/// <summary>A Heavy message: a timestamp (in samples) and up to four atoms.</summary>
	struct Msg
	{
		public uint Ts;
		public int Count;
		public Atom A0, A1, A2, A3;

		public static Msg Of( uint ts, Atom a ) => new Msg { Ts = ts, Count = 1, A0 = a };
		public static Msg Of( uint ts, Atom a, Atom b ) => new Msg { Ts = ts, Count = 2, A0 = a, A1 = b };
		public static Msg Of( uint ts, Atom a, Atom b, Atom c ) => new Msg { Ts = ts, Count = 3, A0 = a, A1 = b, A2 = c };

		public readonly Atom At( int i )
		{
			switch ( i )
			{
				case 0: return A0;
				case 1: return A1;
				case 2: return A2;
				default: return A3;
			}
		}

		public void Set( int i, Atom a )
		{
			switch ( i )
			{
				case 0: A0 = a; break;
				case 1: A1 = a; break;
				case 2: A2 = a; break;
				default: A3 = a; break;
			}
		}

		public readonly int TypeAt( int i ) => i < Count ? At( i ).Type : -1;
		public readonly float FloatAt( int i ) => At( i ).F;
		public readonly int HashAt( int i ) => At( i ).GetHash();
		public readonly bool IsFloat( int i ) => i < Count && At( i ).Type == 1;
		public readonly bool IsSymbol( int i, uint hash ) => i < Count && At( i ).Type == 2 && At( i ).H == hash;
	}

	/// <summary>An entry of the message queue.</summary>
	sealed class QEntry
	{
		public uint Ts;
		public int Fn;
		public int LetIn;
		public Msg M;
		public QEntry Next;
		public bool Queued;
	}

	/// <summary>A Heavy table (buffer plus the head used by tabwrite~ / delay lines).</summary>
	sealed class HvTable
	{
		public float[] Buffer;
		public int Length;
		public int Size;
		public int Head;
	}

	/// <summary>Control [delay]: delay in samples plus up to 8 pending messages.</summary>
	sealed class DelayObj
	{
		public uint Delay;
		public readonly QEntry[] Slots = new QEntry[8];
	}

	/// <summary>Control [list split] / [route] helper: start index and length.</summary>
	sealed class SliceObj
	{
		public int I;
		public int N;
	}

	/// <summary>Control [tabread].</summary>
	sealed class TabreadObj
	{
		public HvTable Table;
	}

	/// <summary>Heavy's [random] (Park-Miller style generator).</summary>
	sealed class RandomObj
	{
		public uint State;
	}

	/// <summary>Control [spigot].</summary>
	sealed class SpigotObj
	{
		public bool Open;
	}

	/// <summary>Control [pack].</summary>
	sealed class PackObj
	{
		public Msg M;
	}

	/// <summary>Write head of a tabwrite~ (delay line writer).</summary>
	sealed class TabheadObj
	{
		public HvTable Table;
		public int Head;
	}

	/// <summary>Heavy signal line~ state (output is <c>N &lt; 0 ? T : X</c>).</summary>
	struct SLine
	{
		public int N;
		public float X;
		public float M;
		public float T;
	}

	/// <summary>Biquad state (x[n-1], x[n-2], y[n-1], y[n-2]).</summary>
	struct Biquad
	{
		public float X1, X2, Y1, Y2;
	}

	static readonly uint HashClear = StringToHash( "clear" );
	static readonly uint HashFlush = StringToHash( "flush" );
	static readonly uint HashStop = StringToHash( "stop" );
	static readonly uint HashBang = StringToHash( "bang" );
	static readonly uint HashFloat = StringToHash( "float" );
	static readonly uint HashResize = StringToHash( "resize" );
	static readonly uint HashMirror = StringToHash( "mirror" );
	static readonly uint HashSamplerate = StringToHash( "samplerate" );
	static readonly uint HashNumIn = StringToHash( "numInputChannels" );
	static readonly uint HashNumOut = StringToHash( "numOutputChannels" );
	static readonly uint HashCurrentTime = StringToHash( "currentTime" );
	static readonly uint HashTable = StringToHash( "table" );
	static readonly uint HashLength = StringToHash( "length" );
	static readonly uint HashSize = StringToHash( "size" );
	static readonly uint HashHead = StringToHash( "head" );

	/// <summary>Heavy's string hash (MurmurHash2 variant seeded with the length).</summary>
	public static uint StringToHash( string s )
	{
		if ( s == null ) return 0;
		const uint n = 0x5bd1e995;
		int len = s.Length;
		uint x = (uint)len;
		int i = 0;
		while ( len >= 4 )
		{
			uint k = (uint)(s[i] & 0xff) | ((uint)(s[i + 1] & 0xff) << 8) | ((uint)(s[i + 2] & 0xff) << 16) | ((uint)(s[i + 3] & 0xff) << 24);
			k *= n;
			k ^= k >> 24;
			k *= n;
			x *= n;
			x ^= k;
			i += 4;
			len -= 4;
		}
		if ( len == 3 ) x ^= (uint)(s[i + 2] & 0xff) << 16;
		if ( len >= 2 ) x ^= (uint)(s[i + 1] & 0xff) << 8;
		if ( len >= 1 )
		{
			x ^= (uint)(s[i] & 0xff);
			x *= n;
		}
		x ^= x >> 13;
		x *= n;
		x ^= x >> 15;
		return x;
	}

	/// <summary>Heavy's ms to samples conversion (truncating).</summary>
	static uint MsToSamples( float ms )
	{
		float c = ms > 0f ? ms : 0f;
		double d = c * 0.001 * SampleRate;
		if ( double.IsNaN( d ) || d >= 4294967296.0 || d <= -1.0 ) return 0;
		return (uint)d;
	}

	/// <summary>wasm-style float to int truncation (NaN and out of range give int.MinValue).</summary>
	static int Trunc( float x )
	{
		if ( float.IsNaN( x ) || x >= 2147483648f || x <= -2147483648f ) return int.MinValue;
		return (int)x;
	}

	/// <summary>wasm-style float to uint truncation (NaN and out of range give 0).</summary>
	static uint TruncU( float x )
	{
		if ( float.IsNaN( x ) || x >= 4294967296f || x <= -1f ) return 0;
		return (uint)x;
	}

	/// <summary>fminf semantics (a NaN operand yields the other one).</summary>
	static float FMin( float x, float b )
	{
		if ( float.IsNaN( x ) ) return b;
		if ( float.IsNaN( b ) ) return x;
		return x < b ? x : b;
	}

	/// <summary>fmaxf semantics (a NaN operand yields the other one).</summary>
	static float FMax( float x, float b )
	{
		if ( float.IsNaN( x ) ) return b;
		if ( float.IsNaN( b ) ) return x;
		return x > b ? x : b;
	}

	/// <summary>Inserts a message into the queue after every entry with a timestamp &lt;= <paramref name="ts"/>.</summary>
	QEntry Schedule( uint ts, int fn, int letIn, Msg m )
	{
		m.Ts = ts;
		var e = new QEntry { Ts = ts, Fn = fn, LetIn = letIn, M = m, Queued = true };
		if ( _queue == null || ts < _queue.Ts )
		{
			e.Next = _queue;
			_queue = e;
			return e;
		}
		var p = _queue;
		while ( p.Next != null && p.Next.Ts <= ts ) p = p.Next;
		e.Next = p.Next;
		p.Next = e;
		return e;
	}

	void Cancel( QEntry e )
	{
		if ( e == null || !e.Queued ) return;
		e.Queued = false;
		if ( _queue == e )
		{
			_queue = e.Next;
			return;
		}
		var p = _queue;
		while ( p != null && p.Next != e ) p = p.Next;
		if ( p != null ) p.Next = e.Next;
	}

	/// <summary>Dispatches every queued message due at (or before) the current sample.</summary>
	void RunMessages()
	{
		while ( _queue != null && _queue.Ts <= _now )
		{
			var e = _queue;
			_current = e;
			Dispatch( e.Fn, e.LetIn, e.M );
			_current = null;
			// Heavy pops the queue head after the callback returns.
			var h = _queue;
			if ( h != null )
			{
				_queue = h.Next;
				h.Queued = false;
			}
		}
	}

	void Send( int fn, int letIn, in Msg m )
	{
		if ( fn != 0 ) Dispatch( fn, letIn, m );
	}

	void SendFloat( int fn, uint ts, float f )
	{
		if ( fn == 0 ) return;
		var m = Msg.Of( ts, Atom.Float( f ) );
		Dispatch( fn, 0, m );
	}

	/// <summary>Heavy binary operators (the op codes of the original ControlBinop).</summary>
	static float BinopApply( int op, float a, float b )
	{
		switch ( op )
		{
			case 0: return b + a;
			case 1: return a - b;
			case 2: return b * a;
			case 3: return b == 0f ? 0f : a / b;
			case 4:
				{
					int ib = Trunc( b );
					if ( ib == 0 ) return 0f;
					int ia = Trunc( a );
					if ( ia == int.MinValue && ib == -1 ) return 0f;
					return ia / ib;
				}
			case 5:
				{
					int ib = Trunc( b );
					if ( ib == 0 ) return 0f;
					int ia = Trunc( a );
					if ( ib == -1 ) return 0f;
					return ia % ib;
				}
			case 6:
				{
					float e;
					if ( b == 0f ) e = 0f;
					else
					{
						int ib = Trunc( b );
						int ia = Trunc( a );
						e = ib == 0 || ib == -1 ? 0f : ia % ib;
					}
					return e < 0f ? e + MathF.Abs( b ) : e;
				}
			case 7: return Trunc( a ) << Trunc( b );
			case 8: return Trunc( a ) >> Trunc( b );
			case 9: return Trunc( b ) & Trunc( a );
			case 10: return Trunc( b ) ^ Trunc( a );
			case 11: return Trunc( b ) | Trunc( a );
			case 12: return a == b ? 1f : 0f;
			case 13: return a != b ? 1f : 0f;
			case 14: return a == 0f || b == 0f ? 0f : 1f;
			case 15: return a == 0f && b == 0f ? 0f : 1f;
			case 16: return a < b ? 1f : 0f;
			case 17: return a <= b ? 1f : 0f;
			case 18: return a > b ? 1f : 0f;
			case 19: return a >= b ? 1f : 0f;
			case 20: return FMax( a, b );
			case 21: return FMin( a, b );
			case 22: return a > 0f ? PowF( a, b ) : 0f;
			case 23: return a == 0f && b == 0f ? 0f : (float)Math.Atan2( a, b );
			default: return 0f;
		}
	}

	/// <summary>[op ] with a right operand held in <paramref name="k"/> (inlet 1 sets it).</summary>
	void Binop( ref float k, int op, int inlet, in Msg m, int cb )
	{
		if ( inlet == 0 )
		{
			if ( !m.IsFloat( 0 ) ) return;
			if ( m.Count > 1 && m.A1.Type == 1 ) k = m.A1.F;
			SendFloat( cb, m.Ts, BinopApply( op, m.A0.F, k ) );
		}
		else if ( inlet == 1 )
		{
			if ( m.IsFloat( 0 ) ) k = m.A0.F;
		}
	}

	/// <summary>[op k] with a constant right operand.</summary>
	void BinopK( int op, float k, in Msg m, int cb )
	{
		if ( !m.IsFloat( 0 ) ) return;
		if ( m.Count > 1 && m.A1.Type == 1 ) k = m.A1.F;
		SendFloat( cb, m.Ts, BinopApply( op, m.A0.F, k ) );
	}

	/// <summary>[f ] / [symbol ] storage object.</summary>
	void Var( ref Atom v, int inlet, in Msg m, int cb )
	{
		int t = m.TypeAt( 0 );
		if ( inlet == 0 )
		{
			switch ( t )
			{
				case 0:
					if ( v.Type == 1 ) SendFloat( cb, m.Ts, v.F );
					else if ( v.Type == 3 )
					{
						var o = Msg.Of( m.Ts, Atom.Hash( (int)v.H ) );
						Send( cb, 0, o );
					}
					break;
				case 1:
					v = Atom.Float( m.A0.F );
					Send( cb, 0, m );
					break;
				case 2:
				case 3:
					v = Atom.Hash( m.HashAt( 0 ) );
					Send( cb, 0, m );
					break;
			}
		}
		else if ( inlet == 1 )
		{
			if ( t == 1 ) v = Atom.Float( m.A0.F );
			else if ( t == 2 || t == 3 ) v = Atom.Hash( m.HashAt( 0 ) );
		}
	}

	/// <summary>Type casts: 0 = bang, 1 = float, 2 = symbol.</summary>
	void Cast( int type, in Msg m, int cb )
	{
		switch ( type )
		{
			case 0:
				{
					var o = Msg.Of( m.Ts, Atom.Bang );
					Send( cb, 0, o );
					break;
				}
			case 1:
				if ( m.IsFloat( 0 ) ) SendFloat( cb, m.Ts, m.A0.F );
				break;
			case 2:
				{
					int t = m.TypeAt( 0 );
					if ( t == 0 ) { var o = Msg.Of( m.Ts, Atom.Symbol( HashBang ) ); Send( cb, 0, o ); }
					else if ( t == 1 ) { var o = Msg.Of( m.Ts, Atom.Symbol( HashFloat ) ); Send( cb, 0, o ); }
					else if ( t == 2 ) Send( cb, 0, m );
					break;
				}
		}
	}

	/// <summary>Unary math operators.</summary>
	void Unop( int op, in Msg m, int cb )
	{
		if ( !m.IsFloat( 0 ) ) return;
		float f = m.A0.F;
		float g;
		switch ( op )
		{
			case 0: g = (float)Math.Asin( f ); break;
			case 1: g = (float)Math.Asinh( f ); break;
			case 2: g = (float)Math.Acos( f ); break;
			case 3: g = (float)Math.Acosh( f ); break;
			case 4: g = (float)Math.Atan( f ); break;
			case 5: g = (float)Math.Atanh( f ); break;
			case 6: g = (float)Math.Sin( f ); break;
			case 7: g = (float)Math.Sinh( f ); break;
			case 8: g = (float)Math.Cos( f ); break;
			case 9: g = (float)Math.Cosh( f ); break;
			case 10: g = (float)Math.Tan( f ); break;
			case 11: g = (float)Math.Tanh( f ); break;
			case 12: g = (float)Math.Exp( f ); break;
			case 13: g = MathF.Abs( f ); break;
			case 14: g = f > 0f ? MathF.Sqrt( f ) : 0f; break;
			case 15: g = f > 0f ? (float)Math.Log( f ) : 0f; break;
			case 16: g = f > 0f ? (float)Math.Log( f ) * 1.44269502f : 0f; break;
			case 17: g = f > 0f ? (float)Math.Log( f ) * 0.434294492f : 0f; break;
			case 18: g = MathF.Ceiling( f ); break;
			case 19: g = MathF.Floor( f ); break;
			case 20:
				{
					double h = f;
					g = (float)(h >= 0.0 ? Math.Floor( h + 0.5 ) : Math.Ceiling( h - 0.5 ));
					break;
				}
			default: return;
		}
		SendFloat( cb, m.Ts, g );
	}

	/// <summary>Messages to a signal line~: "target", "target time" or "stop".</summary>
	static void LineMsg( ref SLine l, in Msg m )
	{
		if ( m.IsFloat( 0 ) )
		{
			if ( m.Count > 1 && m.A1.Type == 1 )
			{
				int n = (int)MsToSamples( m.A1.F );
				float x = l.N > 0 ? l.M + l.X : l.T;
				l.X = x;
				l.N = n;
				l.M = (m.A0.F - x) / n;
				l.T = m.A0.F;
				return;
			}
			l.N = 0;
			l.X = m.A0.F;
			l.M = 0f;
			l.T = m.A0.F;
			return;
		}
		if ( m.IsSymbol( 0, HashStop ) )
		{
			l.N = 0;
			l.X = l.X + l.M;
			l.M = 0f;
			l.T = l.X;
		}
	}

	/// <summary>Reads a line~ (one sample) and advances it.</summary>
	static float Line( ref SLine l )
	{
		float v = l.N < 0 ? l.T : l.X;
		l.N--;
		l.X += l.M;
		return v;
	}

	/// <summary>Heavy's biquad, with the original operation order.</summary>
	static float Bq( ref Biquad s, float x, float b0, float b1, float b2, float a1, float a2 )
	{
		float y = ((s.X1 * b1) + (b0 * x) + (s.X2 * b2)) - ((s.Y2 * a2) + (s.Y1 * a1));
		s.X2 = s.X1;
		s.X1 = x;
		s.Y2 = s.Y1;
		s.Y1 = y;
		return y;
	}

	/// <summary>List slice (outlet 0: the slice, outlet 1: bang when the list is too short).</summary>
	void Slice( SliceObj o, int inlet, in Msg m, int cb )
	{
		switch ( inlet )
		{
			case 0:
				if ( o.I < m.Count )
				{
					int g = m.Count - o.I;
					int c = o.N;
					if ( g < c ) c = g;
					int len = o.N > 0 ? c : g;
					var r = new Msg { Ts = m.Ts, Count = len };
					for ( int i = 0; i < len; i++ ) r.Set( i, m.At( o.I + i ) );
					Send( cb, 0, r );
				}
				else
				{
					var b = Msg.Of( m.Ts, Atom.Bang );
					Send( cb, 1, b );
				}
				break;
			case 1:
				if ( m.IsFloat( 0 ) )
				{
					o.I = Trunc( m.A0.F );
					if ( m.Count > 1 && m.A1.Type == 1 ) o.N = Trunc( m.A1.F );
				}
				break;
			case 2:
				if ( m.IsFloat( 0 ) ) o.N = Trunc( m.A0.F );
				break;
		}
	}

	void Tabread( TabreadObj o, int inlet, in Msg m, int cb )
	{
		if ( inlet == 0 )
		{
			if ( !m.IsFloat( 0 ) || !(m.A0.F >= 0f) || o.Table == null ) return;
			uint idx = TruncU( m.A0.F );
			if ( (uint)o.Table.Size > idx ) SendFloat( cb, m.Ts, o.Table.Buffer[idx] );
		}
		else if ( inlet == 1 )
		{
			int t = m.TypeAt( 0 );
			if ( t == 2 || t == 3 ) o.Table = TableForHash( m.HashAt( 0 ) );
		}
	}

	void Delay( DelayObj o, int inlet, in Msg m, int cb )
	{
		switch ( inlet )
		{
			case 0:
				if ( m.IsSymbol( 0, HashFlush ) )
				{
					for ( int i = 0; i < 8; i++ )
					{
						var e = o.Slots[i];
						if ( e == null ) continue;
						e.M.Ts = m.Ts;
						Send( cb, 0, e.M );
						Cancel( e );
					}
					Array.Clear( o.Slots, 0, 8 );
				}
				else if ( !m.IsSymbol( 0, HashClear ) )
				{
					for ( int i = 0; i < 8; i++ )
					{
						if ( o.Slots[i] != null ) continue;
						o.Slots[i] = Schedule( m.Ts + o.Delay, cb, 0, m );
						break;
					}
				}
				else
				{
					for ( int i = 0; i < 8; i++ ) Cancel( o.Slots[i] );
					Array.Clear( o.Slots, 0, 8 );
				}
				break;
			case 1:
				if ( m.IsFloat( 0 ) ) o.Delay = MsToSamples( m.A0.F );
				break;
			case 2:
				if ( m.IsFloat( 0 ) ) o.Delay = TruncU( m.A0.F > 0f ? m.A0.F : 0f );
				break;
		}
	}

	/// <summary>Called by a delay's output callback: forgets the message that is firing now.</summary>
	void DelayClear( DelayObj o, in Msg m )
	{
		for ( int i = 0; i < 8; i++ )
		{
			if ( o.Slots[i] != null && o.Slots[i] == _current )
			{
				o.Slots[i] = null;
				return;
			}
		}
	}

	void Random( RandomObj o, int inlet, in Msg m, int cb )
	{
		if ( inlet == 0 )
		{
			o.State = (uint)((ulong)o.State * 279470273UL % 4294967291UL);
			SendFloat( cb, m.Ts, (o.State >> 9) * 1.1920929e-07f );
		}
		else if ( inlet == 1 )
		{
			if ( m.IsFloat( 0 ) ) o.State = TruncU( m.A0.F );
		}
	}

	void Spigot( SpigotObj o, int inlet, in Msg m, int cb )
	{
		if ( inlet == 0 ) Send( cb, o.Open ? 1 : 0, m );
		else if ( inlet == 1 && m.IsFloat( 0 ) ) o.Open = m.A0.F != 0f;
	}

	void Pack( PackObj o, int inlet, in Msg m, int cb )
	{
		if ( inlet > 0 )
		{
			int t = m.TypeAt( 0 );
			if ( t == 1 ) o.M.Set( inlet, Atom.Float( m.A0.F ) );
			else if ( t == 2 || t == 3 ) o.M.Set( inlet, Atom.Hash( m.HashAt( 0 ) ) );
			return;
		}
		int c = Math.Min( o.M.Count, m.Count );
		for ( int i = c - 1; i >= 0; i-- )
		{
			int t = m.TypeAt( i );
			if ( t == 1 ) o.M.Set( i, Atom.Float( m.At( i ).F ) );
			else if ( t == 2 || t == 3 ) o.M.Set( i, Atom.Hash( m.HashAt( i ) ) );
		}
		o.M.Ts = m.Ts;
		var copy = o.M;
		Send( cb, 0, copy );
	}

	/// <summary>Heavy "system" queries (samplerate, currentTime, table length ...).</summary>
	void SystemQuery( in Msg m, int cb )
	{
		float f;
		if ( m.IsSymbol( 0, HashSamplerate ) ) f = SampleRate;
		else if ( m.IsSymbol( 0, HashNumIn ) ) f = 0f;
		else if ( m.IsSymbol( 0, HashNumOut ) ) f = 2f;
		else if ( m.IsSymbol( 0, HashCurrentTime ) ) f = m.Ts;
		else if ( m.IsSymbol( 0, HashTable ) )
		{
			var t = TableForHash( m.HashAt( 1 ) );
			if ( t == null ) return;
			if ( m.IsSymbol( 2, HashLength ) ) f = t.Length;
			else if ( m.IsSymbol( 2, HashSize ) ) f = t.Size;
			else if ( m.IsSymbol( 2, HashHead ) ) f = t.Head;
			else return;
		}
		else return;
		SendFloat( cb, m.Ts, f );
	}

	/// <summary>Sets the phase of a signal phasor (inlet 1).</summary>
	static void PhasorPhase( ref uint phase, int inlet, in Msg m )
	{
		if ( inlet != 1 || !m.IsFloat( 0 ) ) return;
		float e = m.A0.F;
		while ( e < 0f ) e += 1f;
		while ( e > 1f ) e -= 1f;
		double d = e * 4294967296.0;
		phase = double.IsNaN( d ) || d >= 4294967296.0 || d <= -1.0 ? 0 : (uint)d;
	}

	/// <summary>Sets an integer signal variable (noise seeds).</summary>
	static void IntSet( ref int v, in Msg m )
	{
		if ( m.IsFloat( 0 ) ) v = Trunc( m.A0.F );
	}

	/// <summary>Table messages ("resize n", "mirror").</summary>
	void Tablemsg( HvTable t, int inlet, in Msg m, int cb )
	{
		if ( m.IsSymbol( 0, HashResize ) && m.Count > 1 && m.A1.Type == 1 && m.A1.F >= 0f )
		{
			int g = Trunc( MathF.Ceiling( m.A1.F ) );
			if ( t.Size != g )
			{
				var nb = new float[g + 1];
				Array.Copy( t.Buffer, nb, Math.Min( t.Buffer.Length, g + 1 ) );
				t.Buffer = nb;
				t.Length = g;
				t.Size = g;
			}
			SendFloat( cb, m.Ts, (uint)g );
			return;
		}
		if ( m.IsSymbol( 0, HashMirror ) ) t.Buffer[t.Size] = t.Buffer[0];
	}

	/// <summary>Messages to a tabwrite~ head (inlet 1: restart / stop, inlet 2: set table).</summary>
	void Tabhead( TabheadObj o, int inlet, in Msg m, int cb )
	{
		if ( inlet == 1 )
		{
			switch ( m.TypeAt( 0 ) )
			{
				case 0: o.Head = 0; break;
				case 1: o.Head = m.A0.F >= 0f ? (int)TruncU( m.A0.F ) : -1; break;
				case 2: if ( m.IsSymbol( 0, HashStop ) ) o.Head = -1; break;
			}
		}
		else if ( inlet == 2 )
		{
			int t = m.TypeAt( 0 );
			if ( t == 2 || t == 3 ) o.Table = TableForHash( m.HashAt( 0 ) );
		}
	}

	// ------------------------------------------------------------------------------------------------
	// Signal graph (one sample). Field names end with the object's byte offset in the original patch.
	// ------------------------------------------------------------------------------------------------

	const float NoiseScale = 4.6566101e-10f;

	/// <summary>(float)(phase &gt;&gt; 9) / 2^23, i.e. the phasor's position in [0, 1).</summary>
	static float Phase01( uint p ) => (p >> 9) * 1.1920929e-07f;

	static void TabWrite( TabheadObj w, float x )
	{
		var t = w.Table;
		if ( w.Head >= 0 && w.Head < t.Buffer.Length ) t.Buffer[w.Head] = x;
		w.Head++;
		t.Head = w.Head;
	}

	/// <summary>Reads a delay line: linear interpolation at <c>head - 1 - delay</c>, wrapping by <paramref name="wrap"/>.</summary>
	static float DelayRead( HvTable t, float delay, float wrap )
	{
		float pos = ((uint)t.Head + -1.0f) - delay;
		float fl = MathF.Floor( pos );
		float idx = (fl < 0f ? wrap : 0f) + fl;
		int i = Trunc( idx );
		float b = Tap( t.Buffer, i + 1 );
		float a = Tap( t.Buffer, i );
		return ((b - a) * (pos - fl)) + a;
	}

	static float Tap( float[] b, int i ) => i >= 0 && i < b.Length ? b[i] : 0f;

	/// <summary>Softclip used by the output stage: x(27 + x^2) / (27 + 9x^2), with the original operation order.</summary>
	static float SoftclipFactor( float x )
	{
		float x2 = x * x;
		float d = (x2 * 9.0f) + 27.0f;
		float r = (x2 + 27.0f) / d;
		return d != 0f ? r : 0f;
	}

	/// <summary>Polynomial sine used by the slosh and gloop oscillators (input is the phasor state).</summary>
	static float OscAngle( uint phase ) => (MathF.Abs( Phase01( phase ) + -0.5f ) + -0.25f) * 6.28318548f;

	void ProcessSample( out float outL, out float outR )
	{
		// ============================== SLAP ==============================
		vari10868 = vari10868 * 16807;
		float noise = vari10868 * NoiseScale;
		sig11096 = noise;

		float b0 = Line( ref line136 );
		float b2 = Line( ref line152 );
		float a1 = Line( ref line168 );
		float a2 = Line( ref line184 );
		float bp1 = Bq( ref bq200, noise, b0, 0f, b2, a1, a2 );              // BP ~8 kHz (drag_delta_x)
		b0 = Line( ref line216 );
		b2 = Line( ref line232 );
		a1 = Line( ref line248 );
		a2 = Line( ref line264 );
		float bp2 = Bq( ref bq280, noise, b0, 0f, b2, a1, a2 );              // BP ~3.5 kHz (drag_delta_y)
		float mix = (0.4f * bp1) + bp2;
		b0 = Line( ref line296 );
		b2 = Line( ref line312 );
		a1 = Line( ref line328 );
		a2 = Line( ref line344 );
		float bp3 = Bq( ref bq360, noise, b0, 0f, b2, a1, a2 );              // BP ~400 Hz
		float click = (mix - (bp3 * 0.3f)) * Line( ref line376 );
		float burst = Line( ref line392 );
		burst *= burst;
		b0 = Line( ref line408 );
		float b1 = Line( ref line424 );
		b2 = Line( ref line440 );
		a1 = Line( ref line456 );
		a2 = Line( ref line472 );
		float lowNoise = Bq( ref bq488, sig11096, b0, b1, b2, a1, a2 );      // LP ~800 Hz

		// vcf~ (Pd), Q = 3: coefficient maths with Heavy's polynomial sine / cosine
		float vin = sig11096;
		float cf = FMax( Line( ref line504 ), 0f ) * sig11248;
		float kr = (sig11268 * (cf * 6.28318024f)) + -1.0f;
		if ( !(sig11236 != 0f) || kr == 0f ) kr = 0f;
		float negR = float.IsNaN( kr ) ? 0f : (kr < 0f ? kr : 0f);
		float gain = ((negR + 1.0f) * vin) * sig11240;
		float s = cf + -0.25f;
		s = (MathF.Abs( (s - MathF.Floor( s )) + -0.5f ) + -0.25f) * 6.28318977f;
		float s2 = s * s;
		float s3 = s2 * s;
		float s5 = s3 * s2;
		float im = sig524;
		float sinR = negR * (((s3 * -0.166666999f) + s) + (s5 * 0.00784313958f));
		float re = sig520;
		float c = (MathF.Abs( (cf + -0.5f) - MathF.Floor( cf ) ) + -0.25f) * 6.28318977f;
		float c2 = c * c;
		float cosR = negR * ((((c2 * 0.00784313958f) + -0.166666999f) * (c2 * c)) + c);
		float vcf = ((im * sinR) - (re * cosR)) + gain;
		sig520 = vcf;
		sig524 = -((re * sinR) + (im * cosR));
		float ring = Line( ref line528 );
		float slap = (sig10848 * ((click * 0.4f) + ((lowNoise * burst) + ((ring * ring) * (vcf * 50.0f))))) * sig11280;

		// ============================== SLOSH ==============================
		vari8828 = vari8828 * 16807;
		float nraw = vari8828;
		float dens = (sig8872 * sig8936) - (sig544 * sig8952);
		sig544 = dens;
		float u = (nraw * 2.32830505e-10f) + 0.5f;
		float trig = u <= dens ? 1f : 0f;
		float env = (trig * sig8860) - (sig548 * sig8836);
		sig548 = env;
		float loud = sig8784;
		float prevTrig = sig552;
		sig552 = trig;
		if ( trig < prevTrig ) sig556 = u;
		else u = sig556;
		uint ph = phasor560;
		phasor560 = ph + (uint)Trunc( sig568 * ((u * sig8632) + sig8756 ) );
		float ang = OscAngle( ph );
		float ang2 = ang * ang;
		float amp = ((loud * env) * sig8892) * sig8900;
		float ang3 = ang2 * ang;
		float slosh = amp * (((ang3 * -0.166666672f) + ang) + ((ang3 * ang2) * 0.00783333369f));
		sig8320 = slosh + slap;

		// ============================== BODY RESONATOR ==============================
		float prevGR = sig8328;
		float fade = Line( ref line572 );
		fade *= fade;
		float resIn = (sig11544 + sig8512) + sig6344;

		b0 = Line( ref line588 );
		b2 = Line( ref line604 );
		a1 = Line( ref line620 );
		a2 = Line( ref line636 );
		TabWrite( tabhead668, Bq( ref bq652, resIn, b0, 0f, b2, a1, a2 ) );
		sig676 = (sig7536 * sig7820) - (sig676 * sig7548);
		float tap1 = DelayRead( table7800, FMax( FMin( sig7568 * sig676, sig7564 ), 0f ), sig7580 );

		b0 = Line( ref line708 );
		b2 = Line( ref line724 );
		a1 = Line( ref line740 );
		a2 = Line( ref line756 );
		TabWrite( tabhead788, Bq( ref bq772, resIn, b0, 0f, b2, a1, a2 ) );
		sig796 = (sig7496 * sig7444) - (sig796 * sig7500);
		float tap2 = DelayRead( table7368, FMax( FMin( sig796 * sig7464, sig7448 ), 0f ), sig7472 ) + tap1;

		sig828 = (sig8036 * sig7848) - (sig828 * sig8016);
		float tap3 = DelayRead( table8116, FMax( FMin( sig7988 * sig828, sig7976 ), 0f ), sig7980 );
		b0 = Line( ref line860 );
		b2 = Line( ref line876 );
		a1 = Line( ref line892 );
		a2 = Line( ref line908 );
		TabWrite( tabhead940, Bq( ref bq924, resIn, b0, 0f, b2, a1, a2 ) );

		float res = (tap3 + tap2) + sig8320;
		float resFaded = res * fade;
		float right = FMax( FMin( ((prevGR + res) * fade) * sig8508, 3f ), -3f );

		// ============================== GLOOP ==============================
		float voiceA = GloopVoice( ref line948, ref sig988, ref phasor992, sig1000, sig9812, sig10016,
			sig9648, sig9636, sig9576, sig9616, table10308, table8988 );
		voiceA = GloopWindowA( Line( ref line1028 ), sig10032, sig10056, table10116 ) * voiceA;
		float voiceB = GloopVoice( ref line1068, ref sig1108, ref phasor1112, sig1120, sig9128, sig9132,
			sig9344, sig9380, sig9468, sig9496, table10308, table8988 );
		float gloop = (((GloopWindowB( Line( ref line1148 ), sig9212, sig9208, table10116 ) * voiceB) + voiceA) * 1.5f) * sig10076;
		sig11544 = gloop;

		// ============================== STRETCH ==============================
		float creakIn = sig4024 + sig5812;
		b0 = Line( ref line1188 );
		b1 = Line( ref line1204 );
		b2 = Line( ref line1220 );
		a1 = Line( ref line1236 );
		a2 = Line( ref line1252 );
		float stretch = ((Bq( ref bq1268, creakIn, b0, b1, b2, a1, a2 ) * sig6168) * sig2216) * sig1800;
		CreakVoice1();
		CreakVoice2();

		// ============================== OUTPUT / PAN ==============================
		float g = gloop + stretch;
		sig1548 = (sig8484 * sig8348) - (sig1548 * sig8332);
		float pan256 = FMax( FMin( sig1548, 1f ), -1f ) * 256.0f;
		float iL = FMax( FMin( 256.0f - pan256, sig8424 ), 0f );
		float cL = table8368.Buffer[Trunc( iL )];
		float pL = 257.0f - pan256;
		float fracL = pL - MathF.Floor( pL );
		float wL = 1.0f - fracL;
		float jL = FMax( FMin( pL, sig8388 ), 0f );
		float xL = FMax( FMin( (((wL * cL) + 1.0f) + (fracL * table8368.Buffer[Trunc( jL )])) * 0.855000019f, 3f ), -3f );
		float kL = SoftclipFactor( xL );

		float iR = FMax( FMin( pan256 + 256.0f, sig8444 ), 0f );
		float cR = table8368.Buffer[Trunc( iR )];
		float pR = pan256 + 257.0f;
		float fracR = pR - MathF.Floor( pR );
		float jR = FMax( FMin( pR, sig8460 ), 0f );
		float xR = FMax( FMin( (((table8368.Buffer[Trunc( jR )] * fracR) + 1.0f) + ((1.0f - fracR) * cR)) * 0.855000019f, 3f ), -3f );
		sig8328 = (xR * g) * SoftclipFactor( xR );
		float gl = (xL * g) * kL;
		sig8324 = gl;
		sig6344 = stretch;
		sig8512 = slosh * 0.200000003f;

		float left = FMax( FMin( sig8516 * ((gl * fade) + resFaded), 3f ), -3f );
		outL = SoftclipFactor( left ) * left;
		outR = SoftclipFactor( right ) * right;
	}

	/// <summary>
	/// One gloop bubble: a timer counting seconds since the last trigger (reset through the 2-point "1986-imp" table),
	/// a sine whose pitch glides up with the timer, and the 512-point "1986-env" envelope read at timer / duration.
	/// </summary>
	static float GloopVoice( ref SLine timerLine, ref float timer, ref uint phase, float step, float freq, float dur,
		float t1Hi, float t0Hi, float e0Hi, float e1Hi, HvTable imp, HvTable envTab )
	{
		float tl = Line( ref timerLine );
		float t1 = tl + 1.0f;
		float imp1 = imp.Buffer[Trunc( FMax( FMin( t1, t1Hi ), 0f ) )];
		float fr = t1 - MathF.Floor( t1 );
		float imp0 = imp.Buffer[Trunc( FMax( FMin( tl, t0Hi ), 0f ) )];
		float t = ((((1.0f - fr) * imp0) + (fr * imp1)) * timer) + 2.26757365e-05f;
		timer = t;
		uint ph = phase;
		phase = ph + (uint)Trunc( step * ((t * freq) + freq) );
		float ang = OscAngle( ph );
		float ang2 = ang * ang;
		float ang3 = ang2 * ang;
		float osc = ((ang3 * ang2) * 0.00783333369f) + (ang - (ang3 * 0.166666672f));
		float q = t / dur;
		float e = FMax( FMin( dur != 0f ? q : 0f, 1f ), 0f ) * 512.0f;
		float env0 = envTab.Buffer[Trunc( FMax( FMin( e, e0Hi ), 0f ) )];
		float e1 = e + 1.0f;
		float ef = e1 - MathF.Floor( e1 );
		float env1 = envTab.Buffer[Trunc( FMax( FMin( e1, e1Hi ), 0f ) )];
		return (((1.0f - ef) * env0) + (ef * env1)) * osc;
	}

	/// <summary>Raised-cosine window of gloop voice A (1986-sin table read at the window line~ position).</summary>
	static float GloopWindowA( float w, float w1Hi, float w0Hi, HvTable sin )
	{
		float w1 = w + 1.0f;
		float s1 = sin.Buffer[Trunc( FMax( FMin( w1, w1Hi ), 0f ) )];
		float fr = w1 - MathF.Floor( w1 );
		float s0 = sin.Buffer[Trunc( FMax( FMin( w, w0Hi ), 0f ) )];
		return (((fr * s1) + ((1.0f - fr) * s0)) * 0.5f) + 0.5f;
	}

	/// <summary>Raised-cosine window of gloop voice B.</summary>
	static float GloopWindowB( float w, float w0Hi, float w1Hi, HvTable sin )
	{
		float s0 = sin.Buffer[Trunc( FMax( FMin( w, w0Hi ), 0f ) )];
		float w1 = w + 1.0f;
		float fr = w1 - MathF.Floor( w1 );
		float s1 = sin.Buffer[Trunc( FMax( FMin( w1, w1Hi ), 0f ) )];
		return ((((1.0f - fr) * s0) + (fr * s1)) * 0.5f) + 0.5f;
	}

	/// <summary>Index into a creak grain: phase * 44100 / f, clamped to [0, 1024] and then to [0, hi].</summary>
	static float GrainIndex( float phase, float f, float hi )
	{
		float rate = 44100.0f / f;
		return FMax( FMin( FMax( FMin( phase * (f != 0f ? rate : 0f), 1024f ), 0f ), hi ), 0f );
	}

	/// <summary>Squared polyBLEP style corrections used by the creak pulse and buzz oscillators.</summary>
	static void Blep( float t, float dt, out float lead, out float trail )
	{
		float inv = 1.0f / dt;
		if ( !(dt != 0f) ) inv = 0f;
		float tt = inv * t;
		float a = tt + -1.0f;
		a *= a;
		lead = t <= dt ? a : 0f;
		float b = (1.0f - inv) + tt;
		b *= b;
		trail = t >= 1.0f - dt ? b : 0f;
	}

	/// <summary>
	/// Creak voice 1 ("vocal fry"): a pulse gate (grain + saw at rate fA, compared to a width threshold) that
	/// chops a buzz (grain + saw at pitch fB), followed by a one-pole low-pass, two one-pole high-passes and
	/// a smoothed amplitude. The output (@4024) feeds the stretch high-pass on the next sample.
	/// </summary>
	void CreakVoice1()
	{
		// part A: pulse rate fA with noise jitter
		vari2616 = vari2616 * 16807;
		float fA = ((sig3676 * sig4008) * sig3848) - (sig1284 * sig3844);
		sig1284 = fA;
		float jitA = (sig4068 * (((vari2616 * NoiseScale) * fA) * sig3664)) - (sig1288 * sig4064);
		sig1288 = jitA;
		uint p1 = phasor1292;
		float t1 = Phase01( p1 );
		float f = jitA + fA;
		phasor1292 = p1 + (uint)Trunc( sig1300 * f );
		float idx = GrainIndex( t1, f, sig3952 );
		float fl = MathF.Floor( idx );
		int i = Trunc( fl );
		float g1 = Tap( table3912.Buffer, i + 1 );
		float g0 = Tap( table3912.Buffer, i );
		uint p2 = phasor1328;
		float t2 = Phase01( p2 );
		phasor1328 = p2 + (uint)Trunc( sig1336 * f );
		float saw = (t2 * 2.0f) + -1.0f;
		Blep( t2, f * sig4016, out float lead, out float trail );
		float delta = (g1 - g0) * (idx - fl);
		float gate = (1.0f - ((((((g0 + 1.0f) + delta) + saw) + lead) - trail) * 0.5f)) >= sig3876 ? 1f : 0f;

		// part B: buzz pitch fB with noise jitter
		vari3972 = vari3972 * 16807;
		float fB = ((sig3668 * sig4164) * sig2508) - (sig1340 * sig2532);
		sig1340 = fB;
		float jitB = (sig3680 * (((vari3972 * NoiseScale) * fB) * sig3888)) - (sig1344 * sig3700);
		sig1344 = jitB;
		uint p3 = phasor1348;
		float t3 = Phase01( p3 );
		float fb = jitB + fB;
		phasor1348 = p3 + (uint)Trunc( sig1356 * fb );
		idx = GrainIndex( t3, fb, sig3652 );
		fl = MathF.Floor( idx );
		i = Trunc( fl );
		float gb0 = Tap( table3612.Buffer, i );
		float grain = ((idx - fl) * (Tap( table3612.Buffer, i + 1 ) - gb0)) + gb0;
		uint p4 = phasor1384;
		float t4 = Phase01( p4 );
		phasor1384 = p4 + (uint)Trunc( sig1392 * fb );
		Blep( t4, sig2600 * fb, out lead, out trail );

		sig1396 = ((((((grain + -1.0f) + (t4 * 2.0f)) + lead) - trail) * sig2604) * gate) - (sig1396 * sig4096);
		float prev = sig1400;
		sig1400 = sig1396;
		sig1404 = ((sig1396 - prev) * sig4092) - (sig1404 * sig2564);
		prev = sig1408;
		sig1408 = sig1404;
		float hp = (sig1404 - prev) * sig2588;
		sig1412 = (sig4136 * sig2644) - (sig1412 * sig4148);
		sig4024 = sig1412 * hp;
	}

	/// <summary>Creak voice 2: same structure as voice 1 with its own noise, tables and parameters (output @5812).</summary>
	void CreakVoice2()
	{
		vari5832 = vari5832 * 16807;
		float fA = ((sig4428 * sig5808) * sig5564) - (sig1416 * sig5552);
		sig1416 = fA;
		float jitA = (sig4268 * (((vari5832 * NoiseScale) * fA) * sig5548)) - (sig1420 * sig4252);
		sig1420 = jitA;
		uint p5 = phasor1424;
		float t5 = Phase01( p5 );
		float f = jitA + fA;
		phasor1424 = p5 + (uint)Trunc( sig1432 * f );
		float idx = GrainIndex( t5, f, sig4304 );
		float fl = MathF.Floor( idx );
		int i = Trunc( fl );
		float g0 = Tap( table4280.Buffer, i );
		float grainA = ((idx - fl) * (Tap( table4280.Buffer, i + 1 ) - g0)) + g0;
		uint p6 = phasor1460;
		float t6 = Phase01( p6 );
		phasor1460 = p6 + (uint)Trunc( sig1468 * f );
		Blep( t6, sig5836 * f, out float leadA, out float trailA );
		float width = sig4440;

		vari5792 = vari5792 * 16807;
		float fB = ((sig5740 * sig5796) * sig5600) - (sig1472 * sig5624);
		sig1472 = fB;
		float jitB = (((fB * (vari5792 * NoiseScale)) * sig4436) * sig5704) - (sig1476 * sig5708);
		sig1476 = jitB;
		uint p7 = phasor1480;
		float t7 = Phase01( p7 );
		float fb = jitB + fB;
		phasor1480 = p7 + (uint)Trunc( sig1488 * fb );
		idx = GrainIndex( t7, fb, sig5872 );
		fl = MathF.Floor( idx );
		i = Trunc( fl );
		float gb0 = Tap( table5876.Buffer, i );
		float grainB = ((Tap( table5876.Buffer, i + 1 ) - gb0) * (idx - fl)) + gb0;
		uint p8 = phasor1516;
		float t8 = Phase01( p8 );
		phasor1516 = p8 + (uint)Trunc( sig1524 * fb );
		Blep( t8, sig4564 * fb, out float leadB, out float trailB );

		float gate = (1.0f - ((((grainA + (t6 * 2.0f)) + leadA) - trailA) * 0.5f)) >= width ? 1f : 0f;
		sig1528 = ((gate * sig4572) * ((((grainB + -1.0f) + (t8 * 2.0f)) - trailB) + leadB)) - (sig1528 * sig5680);
		float prev = sig1532;
		sig1532 = sig1528;
		sig1536 = (sig5652 * (sig1528 - prev)) - (sig1536 * sig4352);
		prev = sig1540;
		sig1540 = sig1536;
		float hp = (sig1536 - prev) * sig4348;
		sig1544 = (sig4232 * sig4432) - (sig1544 * sig4216);
		sig5812 = sig1544 * hp;
	}

}