Skip to content

Instantly share code, notes, and snippets.

@ozel
Created August 4, 2019 20:11
Show Gist options
  • Save ozel/1a3e34d06fdfac9e8cec4c4b818ba7a6 to your computer and use it in GitHub Desktop.
Save ozel/1a3e34d06fdfac9e8cec4c4b818ba7a6 to your computer and use it in GitHub Desktop.
audio scope w trigger
<!DOCTYPE html>
<html>
<head>
<title>Oscilloscope</title>
<style>
body {
margin: 0;
background-color: #1a1a1a;
color: #dddddd;
}
canvas {
display: block;
}
</style>
</head>
<body>
<!-- <script src="//unpkg.com/oscilloscope@1.1.0/dist/oscilloscope.min.js"></script> -->
<!-- Examples -->
<!-- <script src="audio-element.js"></script> -->
<!-- <script src="custom.js"></script> -->
<script src="https://cdn.rawgit.com/ygoe/msgpack.js/master/msgpack.min.js"></script>
<script src="https://cdn.rawgit.com/eligrey/FileSaver.js/master/src/FileSaver.js"></script>
<script src="https://cdn.rawgit.com/mohayonao/get-float-time-domain-data/master/build/get-float-time-domain-data.min.js">
</script>
</body>
<div class="header">
DIY Particle Detector <br> <i>Pulse Recorder</i>
<div class="right" id="rate">rate: </div>
</div>
<div class="main">
<div class="left">Triggered pulses:</div><br>
<div id="statistic" class="left">sum: 0 &emsp;&emsp;&emsp; e&#8315;: 0 &emsp;&emsp;&emsp; &alpha;: 0</div><br>
<div class="right">Move trigger:<br>
swipe up/down or use <br>
keys '+' and '-'</div>
<div class="left">Recording since:</div><br>
<div id='time' class="left"> 00:00:00</div><br>
<div class="right">
<input id="reset" class="button" type="button" value="reset" />
<input id="saveData" class="button" type="button" value="save data" />
</div>
</div>
</html>
function webAudioTouchUnlock(context) {
return new Promise(function(resolve, reject) {
if (context.state === "suspended" && "ontouchstart" in window) {
var unlock = function() {
context.resume().then(
function() {
document.body.removeEventListener("touchstart", unlock);
document.body.removeEventListener("touchend", unlock);
resolve(true);
},
function(reason) {
reject(reason);
}
);
};
document.body.addEventListener("touchstart", unlock, false);
document.body.addEventListener("touchend", unlock, false);
} else {
resolve(false);
}
});
}
// credit: http://www.javascriptkit.com/javatutor/touchevents2.shtml
function swipedetect(el, callback) {
var touchsurface = el,
swipedir,
startX,
startY,
distX,
distY,
threshold = 150, //required min distance traveled to be considered swipe
restraint = 100, // maximum distance allowed at the same time in perpendicular direction
allowedTime = 300, // maximum time allowed to travel that distance
elapsedTime,
startTime,
handleswipe = callback || function(swipedir) {};
touchsurface.addEventListener(
"touchstart",
function(e) {
var touchobj = e.changedTouches[0];
swipedir = "none";
dist = 0;
startX = touchobj.pageX;
startY = touchobj.pageY;
startTime = new Date().getTime(); // record time when finger first makes contact with surface
e.preventDefault();
},
false
);
touchsurface.addEventListener(
"touchmove",
function(e) {
e.preventDefault(); // prevent scrolling when inside DIV
},
false
);
touchsurface.addEventListener(
"touchend",
function(e) {
var touchobj = e.changedTouches[0];
distX = touchobj.pageX - startX; // get horizontal dist traveled by finger while in contact with surface
distY = touchobj.pageY - startY; // get vertical dist traveled by finger while in contact with surface
elapsedTime = new Date().getTime() - startTime; // get time elapsed
if (elapsedTime <= allowedTime) {
// first condition for awipe met
if (Math.abs(distX) >= threshold && Math.abs(distY) <= restraint) {
// 2nd condition for horizontal swipe met
swipedir = distX < 0 ? "left" : "right"; // if dist traveled is negative, it indicates left swipe
} else if (
Math.abs(distY) >= threshold &&
Math.abs(distX) <= restraint
) {
// 2nd condition for vertical swipe met
swipedir = distY < 0 ? "up" : "down"; // if dist traveled is negative, it indicates up swipe
}
}
handleswipe(swipedir);
e.preventDefault();
},
false
);
}
class Oscilloscope {
constructor(source, options = {}) {
if (!(source instanceof window.AudioNode)) {
throw new Error("Oscilloscope source must be an AudioNode");
}
if (source instanceof window.AnalyserNode) {
this.analyser = source;
} else {
this.analyser = source.context.createAnalyser();
source.connect(this.analyser);
}
//if (options.fftSize) { this.analyser.fftSize = options.fftSize }
this.analyser.fftSize = 4096 / 2;
this.timeDomain = new Uint8Array(this.analyser.frequencyBinCount);
this.samples = new Float32Array(this.analyser.fftSize);
this.drawRequest = 0;
this.threshold = -1000;
this.alpha_threshold = -1243;
this.data = [];
this.downloadBlob = null;
this.lastTime = null;
this.startTime = new Date();
this.waveforms = 0;
this.alphas = 0;
this.electrons = 0;
}
// begin default signal animation
setThreshold(threshold) {
this.threshold = this.threshold + threshold;
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
console.log(this.threshold);
}
animate(ctx, x0, y0, width, height) {
if (this.drawRequest) {
throw new Error("Oscilloscope animation is already running");
}
this.ctx = ctx;
const drawLoop = () => {
//ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
this.draw(ctx, x0, y0, width, height);
this.update_time();
this.update_stats();
this.drawRequest = window.requestAnimationFrame(drawLoop);
};
drawLoop();
}
// stop default signal animation
stop() {
if (this.drawRequest) {
window.cancelAnimationFrame(this.drawRequest);
this.drawRequest = 0;
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
}
update_time() {
var now = new Date();
var elapsed = now.valueOf() - this.startTime.valueOf(); // time in milliseconds
var dateDiff = new Date(elapsed);
var h = dateDiff.getHours() - 1;
var h = h.toString().padStart(2, "0");
var m = dateDiff
.getMinutes()
.toString()
.padStart(2, "0");
var s = dateDiff
.getSeconds()
.toString()
.padStart(2, "0");
document.getElementById("time").innerHTML = h + ":" + m + ":" + s;
//console.log(dateDiff.getHours())
}
update_stats() {
document.getElementById("statistic").innerHTML =
"sum: " +
this.waveforms +
" &emsp;&emsp;&emsp; e&#8315;: " +
this.electrons +
"&emsp;&emsp;&emsp; &alpha;: " +
this.alphas;
}
// draw signal
draw(
ctx,
x0 = 0,
y0 = 0,
width = ctx.canvas.width - x0,
height = ctx.canvas.height - y0
) {
this.analyser.getByteTimeDomainData(this.timeDomain);
if (typeof this.analyser.getFloatTimeDomainData == "undefined") {
// replacement from https://github.com/mohayonao/get-float-time-domain-data
getFloatTimeDomainData(this.samples);
} else {
this.analyser.getFloatTimeDomainData(this.samples);
}
const x_scale = 1;
const step = width / this.samples.length * x_scale;
const y_scale = 1 / 32;
var min = this.samples.reduce((a, b) => Math.min(a, b));
// samples range is -1 to +1, FIXME: web-audio API should provide internal/native sample resolution
min = min * Math.pow(2, 15); //16 bit signed pcm / 2 = 2^15
if (min < this.threshold) {
var now = new Date();
var PCM16b = new Int16Array(
this.samples.map(function(element) {
return element * Math.pow(2, 15);
}).buffer
);
var waveform = {};
waveform["ts"] = now.valueOf();
waveform["pulse"] = Array.from(PCM16b);
this.data[this.waveforms] = waveform;
this.waveforms += 1;
this.lastTime = now;
ctx.beginPath();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//ctx.moveTo(x0, y0);
//console.log(this.samples[0],this.samples[1])
ctx.strokeStyle = "#ffffff";
// drawing loop
var oldy = y0 + height / 2;
ctx.moveTo(x0, oldy);
for (let i = 0; i < this.samples.length; i += step) {
const percent = this.samples[i]; //y_scale
const x = x0 + i * step;
const y = y0 + height / 2 + this.samples[i] * -1 * height / 2;
ctx.lineTo(x, oldy);
ctx.lineTo(x, y);
oldy = y;
}
ctx.stroke();
if (min < this.alpha_threshold) {
this.alphas += 1;
console.log(min, "alpha");
} else {
this.electrons += 1;
console.log(min, "electron");
}
}
// threshold line
ctx.beginPath();
ctx.strokeStyle = "red";
//ctx.moveTo(x0,y0)
ctx.moveTo(
x0,
y0 + height / 2 + -1 * this.threshold * height / 2 / Math.pow(2, 15)
);
ctx.lineTo(
x0 + width,
y0 + height / 2 + -1 * this.threshold * height / 2 / Math.pow(2, 15)
);
ctx.stroke();
}
}
// shim
//navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
// navigator.mozGetUserMedia || navigator.msGetUserMedia ||
// navigator.mediaDevices.getUserMedia
var AudioContext = window.AudioContext || window.webkitAudioContext;
var audioCtx = new AudioContext();
//{ sampleRate: 48000});
webAudioTouchUnlock(audioCtx).then(
function(unlocked) {
if (unlocked) {
console.log("audioCtx unlocked");
// AudioContext was unlocked from an explicit user action,
// sound should start playing now
} else {
console.log("audioCtx was already unlocked");
// There was no need for unlocking, devices other than iOS
}
},
function(reason) {
console.error(reason);
}
);
document.getElementById("rate").innerHTML =
"rate: " + audioCtx.sampleRate + " kHz";
// setup canvas
var canvas = document.createElement("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
swipedetect(canvas, function(swipedir) {
// swipedir contains either "none", "left", "right", "top", or "down"
console.log(swipedir);
if (swipedir == "up") {
console.log("up");
}
});
// customize drawing options
var ctx = canvas.getContext("2d");
ctx.lineWidth = 2;
ctx.strokeStyle = "#ffffff";
// get user microphone
var constraints = { video: false, audio: true };
function errorCallback(error) {
console.log("navigator.getUserMedia error: ", error);
}
function streamCallback(stream) {
var source = audioCtx.createMediaStreamSource(stream);
// attach oscilloscope
var scope = new Oscilloscope(source);
// start default animation loop
scope.animate(ctx, 10, 0, 1024, 150);
document.getElementById("saveData").addEventListener("click", function(e) {
//var text = document.getElementById('source').innerHTML;
//var file = new File([text], "hello world.txt", {type: "text/plain;charset=utf-8"});
// save messagepack object as binary blob it
if (scope.waveforms > 0) {
var bytes = msgpack.serialize(scope.data);
var blob = new Blob([bytes], { type: "octet/stream" });
//saveAs(blob,"data.dat");
console.log(JSON.stringify(scope.data));
var date =
scope.lastTime.getFullYear() +
"-" +
(scope.lastTime.getMonth() + 1).toString().padStart(2, "0") +
"-" +
scope.lastTime
.getDate()
.toString()
.padStart(2, "0");
var time =
scope.lastTime
.getHours()
.toString()
.padStart(2, "0") +
"-" +
scope.lastTime
.getMinutes()
.toString()
.padStart(2, "0");
//saveAs(blob, "DIY_Particle_Detector_"
// + scope.waveforms + "-pulses_" + date + "_" + time + ".msgp");
saveAs(blob, "test.msgp");
}
});
document.getElementById("reset").addEventListener("click", function(e) {
scope.data = [];
scope.downloadBlob = null;
scope.lastTime = null;
scope.startTime = new Date();
scope.waveforms = 0;
scope.alphas = 0;
scope.electrons = 0;
scope.update_time();
scope.update_stats();
});
function checkKey(e) {
e = e || window.event;
if (e.key == "ArrowUp") {
scope.setThreshold(100);
} else if (e.key == "ArrowDown") {
// down arrow
scope.setThreshold(-100);
}
}
//this.ctx.addEventListener('keydown',checkKey,false);
document.onkeydown = checkKey;
}
navigator.mediaDevices
.getUserMedia(constraints)
.then(streamCallback)
.catch(errorCallback);
/* This text is in Lucida Console */
* {
font-family: monospace,Lucida Console,Lucida Sans Typewriter,monaco,Bitstream Vera Sans Mono;
}
.header {
width: 95%;
height: 20px;
margin: 10px;
padding: 15px;
font-size: large;
}
.left {
font-size: small;
float: left;
}
.right {
font-size: small;
margin-right: 20px;
float: right;
}
.button {
font-size: large;
margin-right: 10px;
float: center;
}
.main {
width: 100%;
float: left;
padding: 5px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment