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