website/random/cassettevid/cv-decoder.js
2024-04-22 03:13:17 -05:00

303 lines
No EOL
6.2 KiB
JavaScript

class CVDecoder extends AudioWorkletProcessor
{
constructor( options )
{
super( options );
let config = options.processorOptions;
this.sig =
{
LMin: 0.0,
LMax: 1.0,
CMin: -1.0,
CMax: 1.0,
};
this.hPhase = 0;
this.vPhase = 0;
this.pulse =
{
length: 0,
timeout: 0,
luma: 0,
lumaPrev: 0,
chroma: 0,
chromaPrev: 0,
edge: false,
count: false
};
this.timing =
{
time: 0,
lastV: 0,
lastH: 0,
};
this.field = 0;
this.chromaField = 0;
this.chromaDelay = [];
this.chromaDelayIndex = 0;
this.currLine =
{
x1: 0,
y: 0,
maxPhase: 0,
colors: []
};
this.overScan = config.overScan;
this.hOffset = config.hOffset;
this.pulseLength = config.pulseLength;
this.brightness = config.brightness;
this.saturation = config.saturation;
this.hPerTarget = 1.0 / config.hFreq * sampleRate;
this.vPerTarget = 1.0 / config.vFreq * sampleRate;
this.hPeriod = this.hPerTarget;
this.vPeriod = this.vPerTarget;
}
hPhaseToX( hPhase, vPhase, field )
{
return ( ( hPhase - this.hOffset ) / this.overScan );
}
vPhaseToY( hPhase, vPhase, field )
{
return ( vPhase + ( field / this.vPeriod ) * this.hPeriod * 0.5 );
}
YCbCrToRGB( y, cb, cr )
{
let r = y + 45 * cr / 32;
let g = y - ( 11 * cb + 23 * cr ) / 32;
let b = y + 113 * cb / 64;
return [ r, g, b ]
}
process( inputs, outputs, parameters )
{
if( inputs[ 0 ].length != 2 )
return;
let lSamples = inputs[ 0 ][ 0 ];
let cSamples = inputs[ 0 ][ 1 ];
let s = this.sig;
let p = this.pulse;
let t = this.timing;
let blank = false;
for( let i = 0; i < lSamples.length; i++ )
{
t.time += 1;
let lSample = lSamples[ i ] + Math.random() * 0.01 - 0.005;
let cSample = cSamples[ i ] + Math.random() * 0.01 - 0.005;
if( lSample < s.LMin ) s.LMin = lSample;
if( lSample > s.LMax ) s.LMax = lSample;
s.LMin *= 1.0 - ( 1.0 / sampleRate );
s.LMax *= 1.0 - ( 1.0 / sampleRate );
if( s.LMin > -0.025 ) s.LMin = -0.025;
if( s.LMax < 0.025 ) s.LMax = 0.025;
if( cSample < s.CMin ) s.CMin = cSample;
if( cSample > s.CMax ) s.CMax = cSample;
s.CMin *= 1.0 - ( 1.0 / sampleRate );
s.CMax *= 1.0 - ( 1.0 / sampleRate );
if( s.CMin > -0.05 ) s.CMin = -0.05;
if( s.CMax < 0.05 ) s.CMax = 0.05;
let luma = ( lSample * 2.0 - s.LMin ) / ( s.LMax - s.LMin ) * this.brightness * 255;
let chroma = ( ( cSample * 2.0 - s.CMin ) / ( s.CMax - s.CMin ) * 255 - 128 ) * this.saturation;
let chromaLast = this.chromaDelay[ this.chromaDelayIndex ] || 0;
if( this.chromaDelayIndex < sampleRate / 10.0 )
{
this.chromaDelay[ this.chromaDelayIndex ] = chroma;
this.chromaDelayIndex++;
}
let [ r, g, b ] = ( this.chromaField == 0 ) ?
this.YCbCrToRGB( luma, chromaLast, chroma ) :
this.YCbCrToRGB( luma, chroma, chromaLast );
if( this.currLine.colors.length < this.hPerTarget * 2.0 )
this.currLine.colors.push(
{
phase: this.hPhase,
r: Math.max( Math.min( Math.round( r ), 255 ), 0 ),
g: Math.max( Math.min( Math.round( g ), 255 ), 0 ),
b: Math.max( Math.min( Math.round( b ), 255 ), 0 )
}
);
this.currLine.maxPhase = this.hPhase;
this.hPhase += 1.0 / this.hPeriod;
this.vPhase += 1.0 / this.vPeriod;
this.currLine.x2 = this.hPhaseToX( this.hPhase, this.vPhase, this.field );
if( ( ( s.LMax - s.LMin ) > 0.1 ) && ( ( s.CMax - s.CMin ) > 0.1 ) )
{
if( lSample < s.LMin * 0.5 )
p.luma = -1;
else if( lSample > s.LMax * 0.5 )
p.luma = 1;
else
p.luma = 0;
if( cSample < s.CMin * 0.5 )
p.chroma = -1;
else if( cSample > s.CMax * 0.5 )
p.chroma = 1;
else
p.chroma = 0;
if( ( p.luma != p.lumaPrev ) || ( p.chroma != p.chromaPrev ) )
{
p.length = 0;
p.lumaPrev = p.luma;
p.chromaPrev = p.chroma;
p.edge = true;
}
if( ( p.luma != 0 ) && ( p.chroma != 0 ) )
{
p.length += 1.0 / sampleRate;
if( ( p.length > this.pulseLength * 0.5 ) && ( p.edge == true ) )
{
p.edge = false;
p.count += 1;
p.timeout = this.pulseLength * 1.25;
if( p.count == 2 )
{
p.count = 0;
blank = true;
if( ( t.time - t.lastH < this.hPerTarget * 1.5 ) &&
( t.time - t.lastH > this.hPerTarget * 0.5 ) )
this.hPeriod = this.hPeriod * 0.9 + ( t.time - t.lastH ) * 0.1;
t.lastH = t.time;
this.hPhase = 0;
this.chromaDelayIndex = 0;
if( p.luma > 0 )
this.chromaField = 0;
else
this.chromaField = 1;
if( p.luma != p.chroma )
{
if( ( t.time - t.lastV < this.vPerTarget * 1.5 ) &&
( t.time - t.lastV > this.vPerTarget * 0.5 ) )
this.vPeriod = this.vPeriod * 0.75 + ( t.time - t.lastV ) * 0.25;
t.lastV = t.time;
this.vPhase = 0;
this.chromaField = 1;
if( p.luma > 0 )
this.field = 0;
else
this.field = 1;
}
}
}
}
if( p.count > 0 )
{
p.timeout -= 1.0 / sampleRate;
if( p.timeout <= 0 )
p.count = 0;
}
}
else
{
p.luma = p.lumaPrev = 0;
p.chroma = p.chromaPrev = 0;
p.edge = false;
p.count = 0;
}
this.hPeriod = this.hPeriod * ( 1.0 - 1.0 / sampleRate ) + this.hPerTarget * ( 1.0 / sampleRate );
this.vPeriod = this.vPeriod * ( 1.0 - 1.0 / sampleRate ) + this.vPerTarget * ( 1.0 / sampleRate );
if( this.hPhase >= 1.0 )
{
blank = true;
this.hPhase -= 1.0;
this.chromaDelayIndex = 0;
if( this.chromaField == 1 )
this.chromaField = 0;
else
this.chromaField = 1;
}
if( this.vPhase >= 1.0 )
{
blank = true;
this.vPhase -= 1.0;
if( this.field == 0 )
this.field = 1;
else
this.field = 0;
}
if( blank )
{
if(
( this.currLine.colors.length > 5 ) &&
( this.currLine.maxPhase > 0 )
)
{
this.port.postMessage( this.currLine );
}
this.currLine =
{
x1: this.hPhaseToX( this.hPhase, this.vPhase, this.field ),
y: this.vPhaseToY( this.hPhase, this.vPhase, this.field ),
maxPhase: 0,
colors: []
};
blank = false;
}
}
return true
}
}
registerProcessor( 'cv-decoder', CVDecoder );