_ _ | |_ ___ __| |____ ___ _ _ | __/ _ \/ _` |_ / / _ \ | | | | || __/ (_| |/ / | __/ |_| | \__\___|\__,_/___(_)___|\__,_|
NOT DONE YET!
First I created a simple index.html that provides a hyperlink to the decoder, so that I can navigate into and back out of the web app in the browser.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>index page</title>
</head>
<body>
<p><a href="decoder.html">decoder.html</a></p>
</body>
</html>
This is the work-in-progress decoder web app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="description" content="Contact TM Timbre Modulation demo for Dublin Maker 2025">
<meta name="keywords" content="timbre modulation, music synthesis, steganography, esoterica, arcana">
<meta name="author" content="Ted Burke">
<title>Timbre modulation (TM) decoder and visualizer</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Timbre modulation decoder and visualizer</h1>
<canvas id="drawingCanvas"></canvas>
<div id="controls">
<button id="startButton" onclick="startScanning()">START</button> <br>
<button id="pauseButton" onclick="pauseScanning()" disabled="disabled">PAUSE</button>
</div>
<div id="status">Paused</div>
<canvas id="spectrumCanvas" width="320" height="240"></canvas>
<div id="footer"> Copyright 2025 Ted Burke. See <a href="http://tedz.eu/">tedz.eu</a> for more details. </div>
<script src="decoder.js"></script>
</body></html>
body {width:800px; text-align:center; margin:auto; background-color:#000000; color:#FFFFFF; font-family:arial;}
#drawingCanvas {display:inline-block; vertical-align:top; margin:0; padding:0;}
#controls {display:inline-block; vertical-align:top; width:120px; height:240px; margin:0; padding:0;}
button {font-size:160%; margin:20px;}
#status {textalign:center; margin:10px; font-family: arial; font-size:200%;}
#spectrumCanvas {margin:auto;}
#footer {font-size:60%; text-align:center;}
"use strict";
// Global identifiers for elements, etc.
var drawingCanvas = document.getElementById("drawingCanvas");
var spectrumCanvas = document.getElementById("spectrumCanvas");
var startButton = document.getElementById("startButton");
var pauseButton = document.getElementById("pauseButton");
var drawingContext, spectrumContext;
/*
var drawW = drawingCanvas.width;
var drawH = drawingCanvasHeight;
var specW = spectrumCanvas.width;
var specH = spectrumCanvas.height;
*/
var N=1024, n;
var buffer = [], binary = [], s = [];
var requestpauseflag = 0; // set to 1 when pause button is pressed
var scanningstate; // 1 is scanning, 2 is paused
var validationtime = 0;
resetScanning();
window.onload = function()
{
// Get contexts for both canvases
drawingContext = drawingCanvas.getContext("2d");
spectrumContext = spectrumCanvas.getContext("2d");
// Error callback function for getUserMedia method
var errorCallback = function(error) {console.log("Video capture error: ", error.code);};
// For cross-browser compatibility, select getUserMedia method for current browser
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia || navigator.msGetUserMedia;
// Open video capture stream
navigator.getUserMedia({video:{mandatory:{minWidth:vidw,maxWidth:vidw,minHeight:vidh,maxHeight:vidh}}}, function(stream)
{
//vid.src = window.URL.createObjectURL(stream);
vid.srcObject=stream;
vid.play();
}, errorCallback);
window.setInterval(draw, 100);
}
function resetScanning()
{
n = N;
while (n--) buffer[n] = binary[n] = s[n] = 0;
validationtime = 0;
document.getElementById("pausebutton").disabled = false;
document.getElementById("startbutton").disabled = true;
document.getElementById("resetbutton").disabled = false;
scanningstate = 1;
}
function startScanning()
{
document.getElementById("pausebutton").disabled = false;
document.getElementById("startbutton").disabled = true;
document.getElementById("resetbutton").disabled = false;
scanningstate = 1;
}
function requestPause()
{
requestpauseflag = 1;
}
function pauseScanning()
{
document.getElementById("pausebutton").disabled = true;
document.getElementById("startbutton").disabled = false;
document.getElementById("resetbutton").disabled = false;
requestpauseflag = 0;
scanningstate = 2;
}
function draw()
{
vidctx.drawImage(vid,0,0);
var idata = vidctx.getImageData(0,0,vidw,vidh); // get pixel data
var data = idata.data; // extract data
var x, y, m, left=0, right=0;
// Calculate current left/right balanace
for (y=-boxr ; y < boxr; y++)
{
for (x=-boxr ; x < boxr; x++)
{
m = 4*(vidw*(y1+y)+(x1+x));
left += data[m] + data[m+1] + data[m+2];
m = 4*(vidw*(y2+y)+(x2+x));
right += data[m] + data[m+1] + data[m+2];
}
}
if (scanningstate == 1)
{
// Log data point
n = ++n % N;
buffer[n] = (right-left)/(4.0*boxr*boxr*3.0*255.0);
binary[n] = buffer[n] > 0;
}
// Draw graph
var ox = 100, oy = 80, dx = 4, dy = 80.0;
graphctx.clearRect(0, 0, graphw, graphh);
// Draw grid lines on graph
graphctx.beginPath();
graphctx.strokeStyle = "#3F3F3F";
graphctx.lineWidth = 1.0;
graphctx.moveTo(ox, oy);
graphctx.lineTo(ox+150*dx, oy);
for (m=5 ; m<150 ; m+=5)
{
graphctx.moveTo(ox+m*dx, 0);
graphctx.lineTo(ox+m*dx, 220);
}
graphctx.stroke();
graphctx.font="20px Arial";
graphctx.fillStyle = "#BFBFBF";
graphctx.textAlign = "center";
for (m=0 ; m<=15 ; ++m)
{
graphctx.fillText(m, ox + m*10*dx, 240);
}
// Plot samples
graphctx.font="20px Arial";
graphctx.fillStyle = "#00FFFF";
graphctx.textAlign = "right";
graphctx.fillText("0", ox-5, oy+7);
graphctx.beginPath();
graphctx.strokeStyle = "#00FFFF";
graphctx.lineWidth = 2.0;
graphctx.moveTo(ox, oy - dy * buffer[(n+1)%N]);
for (m=1 ; m<N ; ++m)
{
graphctx.lineTo(ox + m*dx, oy - dy * buffer[(n+1+m)%N]);
}
graphctx.stroke();
// Plot binary signal
graphctx.font="20px Arial";
graphctx.fillStyle = "#00FF00";
graphctx.textAlign = "right";
graphctx.fillText("1", ox-5, 160+7);
graphctx.fillText("0", ox-5, 200+7);
graphctx.beginPath();
graphctx.strokeStyle = "#00FF00";
graphctx.lineWidth = 2.0;
graphctx.moveTo(ox, 200 - 40 * binary[(n+1)%N]);
for (m=1 ; m<N ; ++m)
{
graphctx.lineTo(ox + m*dx, 200 - 40 * binary[(n+m)%N]);
graphctx.lineTo(ox + m*dx, 200 - 40 * binary[(n+1+m)%N]);
}
graphctx.stroke();
// Draw bounding rectangle around graph
graphctx.strokeStyle = "#BFBFBF";
graphctx.lineWidth = 2.0;
graphctx.strokeRect(ox, 1, (N-1)*dx, 218);
// Check if current buffer matches code
graphctx.font="20px Arial";
graphctx.fillStyle = "#000000";
graphctx.textAlign = "center";
// Create a chronological buffer
var k;
for (k=0 ; k<N ; ++k) s[k] = binary[(n+1+k)%N];
// Find leading edge of byte 1 start bit
var state = 0, bit, byte1 = 0, byte2 = 0;
k = 0;
while (state == 0)
{
k++;
if (k >= N) state = -1;
else if (s[k-1] == 0 && s[k] == 1)
{
firstedge = k;
state = 1;
}
}
// Check start bit of byte 1
while (state == 1)
{
k = k + 2;
if (k >= N) state = -1;
else if (s[k] == 1) state = 2;
else state = -1;
}
// Parse 8 data bits of byte 1
byte1 = 0;
while (state >= 2 && state <= 9)
{
k = k + 5;
if (k >= N) state = -1;
else
{
bit = state - 2;
byte1 += s[k]*Math.pow(2,bit);
state++;
}
}
// Check stop bit of byte 1
while (state == 10)
{
k = k + 5;
if (k >= N) state = -1;
else if (s[k] == 0) state = 11;
else state = -1;
}
// Find leading edge of byte 2 start bit
while (state == 11)
{
k++;
if (k >= N) state = -1;
else if (s[k-1] == 0 && s[k] == 1) state = 12;
}
// Check start bit of byte 2
while (state == 12)
{
k = k + 2;
if (k >= N) state = -1;
else if (s[k] == 1) state = 13;
else state = -1;
}
// Parse 8 data bits of byte 2
byte2 = 0;
while (state >= 13 && state <= 20)
{
k = k + 5;
if (k >= N) state = -1;
else
{
bit = state - 13;
byte2 += s[k]*Math.pow(2,bit);
state++;
}
}
// Check stop bit of byte 2
while (state == 21)
{
k = k + 5;
if (k >= N) state = -1;
else if (s[k] == 0) state = 22;
else state = -1;
}
while (state == 22)
{
// If sum of bytes is 255 and byte1 is lower value, then pattern is matched.
k = k + 5; // Allow full stop bit to appear on graph
if (k >= N) state = -1;
else if (byte1 + byte2 != 255) state = -1;
else if (byte1 > byte2) state = -1;
else if (firstedge%5 > 0) state = -1;
else patternMatch();
}
// Check if a pause is pending. If so stop when leading edge
// coincides with a grid line.
if (requestpauseflag == 1 && firstedge%5 == 0)
{
pauseScanning();
}
// Display status
if (scanningstate == 2)
{
document.getElementById("status").innerHTML = "Paused";
}
else
{
document.getElementById("status").innerHTML = "Scanning";
}
// Display 1 or 0 on video sampling boxes
vidctx.fillStyle = "#FF0000";
vidctx.font="40px Arial";
if (buffer[n] < 0) vidctx.fillText("0", x1-boxr+8, y1-boxr-5);
else vidctx.fillText("1", x2-boxr+8, y2-boxr-5);
// Display video sampling boxes
vidctx.strokeStyle = "#FF0000";
vidctx.lineWidth = 2.0;
vidctx.strokeRect(x1-boxr,y1-boxr,boxr+boxr,boxr+boxr);
vidctx.strokeRect(x2-boxr,y2-boxr,boxr+boxr,boxr+boxr);
}
Useful article on HTML5 audio/video capture:
This was more or less what I started with...
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=windows-1252"><title>Timbre modulation decoder and visualizer</title>
<style>
body {width:800px; text-align:center; margin:auto; background-color:#000000; color:#FFFFFF; font-family:arial;}
#vid {display:none;}
#viddiv {margin:auto;}
button {font-size:160%; margin:20px;}
#controls {display:inline-block; vertical-align:top; width:120px; height:240px; margin:0; padding:0;}
#vidcanv {display:inline-block; vertical-align:top; margin:0; padding:0;}
#status {textalign:center; margin:10px; font-family: arial; font-size:200%;}
#graphcanv {margin:auto;}
#footer {font-size:60%; text-align:center;}
</style>
</head>
<body>
<h1>Timbre modulation decoder and visualizer</h1>
<div id="viddiv">
<video id="vid" autoplay="autoplay" src=""></video>
<canvas id="vidcanv" width="320" height="240"></canvas>
<div id="controls">
<button id="resetbutton" onclick="resetScanning()">RESET</button> <br>
<button id="startbutton" onclick="startScanning()">START</button> <br>
<button id="pausebutton" onclick="pauseScanning()" disabled="disabled">PAUSE</button>
</div>
</div>
<div id="status">Paused</div>
<canvas id="graphcanv" width="320" height="240"></canvas>
<div id="footer">
See <a href="http://tedz.eu/">tedz.eu</a> for more details. <br>
Copyright 2025 Ted Burke.
</div>
<script>
"use strict";
var vid = document.getElementById("vid");
var vidcanv = document.getElementById("vidcanv");
var graphcanv = document.getElementById("graphcanv");
var graphctx, vidctx;
var vidw=320,vidh=240;
var x1=vidw/4, y1=vidh/2, x2=3*vidw/4, y2=vidh/2, boxr=20;
var graphw=graphcanv.width, graphh=graphcanv.height;
var N=151, n;
var buffer = [], binary = [], s = [];
var requestpauseflag = 0; // set to 1 when pause button is pressed
var scanningstate; // 1 is scanning, 2 is paused
var validationtime = 0;
resetScanning();
function resetScanning()
{
n = N;
while (n--) buffer[n] = binary[n] = s[n] = 0;
validationtime = 0;
document.getElementById("pausebutton").disabled = false;
document.getElementById("startbutton").disabled = true;
document.getElementById("resetbutton").disabled = false;
scanningstate = 1;
}
function startScanning()
{
document.getElementById("pausebutton").disabled = false;
document.getElementById("startbutton").disabled = true;
document.getElementById("resetbutton").disabled = false;
scanningstate = 1;
}
function requestPause()
{
requestpauseflag = 1;
}
function pauseScanning()
{
document.getElementById("pausebutton").disabled = true;
document.getElementById("startbutton").disabled = false;
document.getElementById("resetbutton").disabled = false;
requestpauseflag = 0;
scanningstate = 2;
}
window.onload = function()
{
// Get contexts for both canvases
graphctx = graphcanv.getContext("2d");
vidctx = vidcanv.getContext("2d");
// Error callback function for getUserMedia method
var errorCallback = function(error) {console.log("Video capture error: ", error.code);};
// For cross-browser compatibility, select getUserMedia method for current browser
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia || navigator.msGetUserMedia;
// Open video capture stream
navigator.getUserMedia({video:{mandatory:{minWidth:vidw,maxWidth:vidw,minHeight:vidh,maxHeight:vidh}}}, function(stream)
{
//vid.src = window.URL.createObjectURL(stream);
vid.srcObject=stream;
vid.play();
}, errorCallback);
window.setInterval(draw, 100);
}
function draw()
{
vidctx.drawImage(vid,0,0);
var idata = vidctx.getImageData(0,0,vidw,vidh); // get pixel data
var data = idata.data; // extract data
var x, y, m, left=0, right=0;
// Calculate current left/right balanace
for (y=-boxr ; y < boxr; y++)
{
for (x=-boxr ; x < boxr; x++)
{
m = 4*(vidw*(y1+y)+(x1+x));
left += data[m] + data[m+1] + data[m+2];
m = 4*(vidw*(y2+y)+(x2+x));
right += data[m] + data[m+1] + data[m+2];
}
}
if (scanningstate == 1)
{
// Log data point
n = ++n % N;
buffer[n] = (right-left)/(4.0*boxr*boxr*3.0*255.0);
binary[n] = buffer[n] > 0;
}
// Draw graph
var ox = 100, oy = 80, dx = 4, dy = 80.0;
graphctx.clearRect(0, 0, graphw, graphh);
// Draw grid lines on graph
graphctx.beginPath();
graphctx.strokeStyle = "#3F3F3F";
graphctx.lineWidth = 1.0;
graphctx.moveTo(ox, oy);
graphctx.lineTo(ox+150*dx, oy);
for (m=5 ; m<150 ; m+=5)
{
graphctx.moveTo(ox+m*dx, 0);
graphctx.lineTo(ox+m*dx, 220);
}
graphctx.stroke();
graphctx.font="20px Arial";
graphctx.fillStyle = "#BFBFBF";
graphctx.textAlign = "center";
for (m=0 ; m<=15 ; ++m)
{
graphctx.fillText(m, ox + m*10*dx, 240);
}
// Plot samples
graphctx.font="20px Arial";
graphctx.fillStyle = "#00FFFF";
graphctx.textAlign = "right";
graphctx.fillText("0", ox-5, oy+7);
graphctx.beginPath();
graphctx.strokeStyle = "#00FFFF";
graphctx.lineWidth = 2.0;
graphctx.moveTo(ox, oy - dy * buffer[(n+1)%N]);
for (m=1 ; m<N ; ++m)
{
graphctx.lineTo(ox + m*dx, oy - dy * buffer[(n+1+m)%N]);
}
graphctx.stroke();
// Plot binary signal
graphctx.font="20px Arial";
graphctx.fillStyle = "#00FF00";
graphctx.textAlign = "right";
graphctx.fillText("1", ox-5, 160+7);
graphctx.fillText("0", ox-5, 200+7);
graphctx.beginPath();
graphctx.strokeStyle = "#00FF00";
graphctx.lineWidth = 2.0;
graphctx.moveTo(ox, 200 - 40 * binary[(n+1)%N]);
for (m=1 ; m<N ; ++m)
{
graphctx.lineTo(ox + m*dx, 200 - 40 * binary[(n+m)%N]);
graphctx.lineTo(ox + m*dx, 200 - 40 * binary[(n+1+m)%N]);
}
graphctx.stroke();
// Draw bounding rectangle around graph
graphctx.strokeStyle = "#BFBFBF";
graphctx.lineWidth = 2.0;
graphctx.strokeRect(ox, 1, (N-1)*dx, 218);
// Check if current buffer matches code
graphctx.font="20px Arial";
graphctx.fillStyle = "#000000";
graphctx.textAlign = "center";
// Create a chronological buffer
var k;
for (k=0 ; k<N ; ++k) s[k] = binary[(n+1+k)%N];
// Find leading edge of byte 1 start bit
var state = 0, bit, byte1 = 0, byte2 = 0;
k = 0;
while (state == 0)
{
k++;
if (k >= N) state = -1;
else if (s[k-1] == 0 && s[k] == 1)
{
firstedge = k;
state = 1;
}
}
// Check start bit of byte 1
while (state == 1)
{
k = k + 2;
if (k >= N) state = -1;
else if (s[k] == 1) state = 2;
else state = -1;
}
// Parse 8 data bits of byte 1
byte1 = 0;
while (state >= 2 && state <= 9)
{
k = k + 5;
if (k >= N) state = -1;
else
{
bit = state - 2;
byte1 += s[k]*Math.pow(2,bit);
state++;
}
}
// Check stop bit of byte 1
while (state == 10)
{
k = k + 5;
if (k >= N) state = -1;
else if (s[k] == 0) state = 11;
else state = -1;
}
// Find leading edge of byte 2 start bit
while (state == 11)
{
k++;
if (k >= N) state = -1;
else if (s[k-1] == 0 && s[k] == 1) state = 12;
}
// Check start bit of byte 2
while (state == 12)
{
k = k + 2;
if (k >= N) state = -1;
else if (s[k] == 1) state = 13;
else state = -1;
}
// Parse 8 data bits of byte 2
byte2 = 0;
while (state >= 13 && state <= 20)
{
k = k + 5;
if (k >= N) state = -1;
else
{
bit = state - 13;
byte2 += s[k]*Math.pow(2,bit);
state++;
}
}
// Check stop bit of byte 2
while (state == 21)
{
k = k + 5;
if (k >= N) state = -1;
else if (s[k] == 0) state = 22;
else state = -1;
}
while (state == 22)
{
// If sum of bytes is 255 and byte1 is lower value, then pattern is matched.
k = k + 5; // Allow full stop bit to appear on graph
if (k >= N) state = -1;
else if (byte1 + byte2 != 255) state = -1;
else if (byte1 > byte2) state = -1;
else if (firstedge%5 > 0) state = -1;
else patternMatch();
}
// Check if a pause is pending. If so stop when leading edge
// coincides with a grid line.
if (requestpauseflag == 1 && firstedge%5 == 0)
{
pauseScanning();
}
// Display status
if (scanningstate == 2)
{
document.getElementById("status").innerHTML = "Paused";
}
else
{
document.getElementById("status").innerHTML = "Scanning";
}
// Display 1 or 0 on video sampling boxes
vidctx.fillStyle = "#FF0000";
vidctx.font="40px Arial";
if (buffer[n] < 0) vidctx.fillText("0", x1-boxr+8, y1-boxr-5);
else vidctx.fillText("1", x2-boxr+8, y2-boxr-5);
// Display video sampling boxes
vidctx.strokeStyle = "#FF0000";
vidctx.lineWidth = 2.0;
vidctx.strokeRect(x1-boxr,y1-boxr,boxr+boxr,boxr+boxr);
vidctx.strokeRect(x2-boxr,y2-boxr,boxr+boxr,boxr+boxr);
}
</script>
</body></html>
I used the Creative Commons license "chooser" to generate the following:
Timbre modulation encoder and decoder © 2025 by Ted Burke is licensed under CC BY 4.0