A Pen by Oliver Keller on CodePen.
Created
August 4, 2019 20:11
-
-
Save ozel/1a3e34d06fdfac9e8cec4c4b818ba7a6 to your computer and use it in GitHub Desktop.
audio scope w trigger
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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     e⁻: 0     α: 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 + | |
"     e⁻: " + | |
this.electrons + | |
"    α: " + | |
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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