- show FFT bucket-width in cents
  - visual polish
  - up FFT size to max
This commit is contained in:
Aaron Gutierrez
2021-12-27 18:01:57 -08:00
parent c13225fb9e
commit 7c39ec3c2b
3 changed files with 47 additions and 21 deletions

View File

@@ -5,7 +5,8 @@ body {
color: white; color: white;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 48px; font-family: sans-serif;
font-size: 2em;
height: 100vh; height: 100vh;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
@@ -28,10 +29,10 @@ h1, h2 {
} }
h1 { h1 {
font-size: 96px; font-size: 6em;
} }
h2 { h2 {
font-size: 72px; font-size: 3em;
} }

View File

@@ -3,11 +3,11 @@
<meta name="viewport" conetnt="width=device-width, initial-scale=1"> <meta name="viewport" conetnt="width=device-width, initial-scale=1">
<link rel="stylesheet" href="tuner.css"> <link rel="stylesheet" href="tuner.css">
<script src="tuner.js"></script> <script src="tuner.js"></script>
<title>Tuner</title>
<body onload="setup()"> <body onload="setup()">
<h2 id="title">Tuner</h2> <h1 id="note">Listening...</h2>
<div id="frequency">Listening...</div>
<div><span id="rate">??</span> kHz</div>
<h1 id="note">A+</h2>
<h2 id="tune"></h3> <h2 id="tune"></h3>
<div id="frequency"></div>
<div>(<span id="rate">??</span> kHz sample rate)</div>
</bdoy> </bdoy>
</html> </html>

View File

@@ -1,7 +1,5 @@
const NOTE_NAMES = ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"] const NOTE_NAMES = ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"]
const TIMESLICE_MS = 500;
// We don't care about fundamenatls above 4kHz, so setting a lower sample rate // We don't care about fundamenatls above 4kHz, so setting a lower sample rate
// gives us finer-graned FFT buckets // gives us finer-graned FFT buckets
const TARGET_SAMPLE_RATE = 8000; const TARGET_SAMPLE_RATE = 8000;
@@ -11,6 +9,7 @@ let dom_rate;
let dom_note; let dom_note;
let dom_tune; let dom_tune;
const setup = () => { const setup = () => {
dom_frequency = document.getElementById("frequency"); dom_frequency = document.getElementById("frequency");
dom_rate = document.getElementById("rate"); dom_rate = document.getElementById("rate");
@@ -24,27 +23,39 @@ const setup = () => {
console.error("Error calling getUserMedia", err); console.error("Error calling getUserMedia", err);
}); });
}; };
if (navigator.wakeLock && navigator.wakeLock.request) {
try {
navigator.wakeLock
.request('screen')
.then(wakeLock =>
setTimeout(() => wakeLock.release(), 60000)
);
} catch (err) {}
}
}; };
const handleStream = stream => { const handleStream = stream => {
const audioContext = new AudioContext({ const audioContext = new AudioContext({
sampleRate: TARGET_SAMPLE_RATE, sampleRate: TARGET_SAMPLE_RATE,
}); });
const analyser = audioContext.createAnalyser(); const analyser = audioContext.createAnalyser();
analyser.fftSize = 8192; analyser.fftSize = 32768;
analyser.minDecibels = -90; analyser.minDecibels = -90;
analyser.maxDecibels = -10; analyser.maxDecibels = -10;
analyser.smoothingTimeConstant = 0.1; analyser.smoothingTimeConstant = 0;
const bufferLength = analyser.frequencyBinCount; const bufferLength = analyser.frequencyBinCount;
const data = new Uint8Array(bufferLength); const data = new Uint8Array(bufferLength);
const source = audioContext.createMediaStreamSource(stream); const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser); source.connect(analyser);
setInterval(tune(analyser, data), 200); setInterval(tune(analyser, data), 500);
}; };
const tune = (analyser, data) => () => { const tune = (analyser, data) => () => {
analyser.getByteFrequencyData(data); analyser.getByteFrequencyData(data);
@@ -55,8 +66,6 @@ const tune = (analyser, data) => () => {
let max = 0; let max = 0;
let maxBucket = -1; let maxBucket = -1;
data.forEach((value, bucket) => { data.forEach((value, bucket) => {
if (value > max) { if (value > max) {
max = value; max = value;
@@ -64,18 +73,26 @@ const tune = (analyser, data) => () => {
} }
}); });
if (maxBucket === -1) {
return;
}
const frequency = maxBucket * bucketWidth; const frequency = maxBucket * bucketWidth;
dom_frequency.innerText = `${frequency} Hz`; dom_frequency.innerText = `${Number.parseFloat(frequency).toFixed(2)} Hz`;
const semitones = frequencyToSemitones(frequency); const semitones = frequencyToSemitones(frequency);
const margin = frequencyToSemitones(frequency + bucketWidth / 2) - semitones;
dom_note.innerText = semitonesToNote(semitones); dom_note.innerText = semitonesToNote(semitones);
dom_tune.innerText = errorPercentage(semitones); dom_tune.innerText = errorPercentage(semitones, margin);
document.body.className = semitonesToClassname(semitones); document.body.className = semitonesToClassname(semitones, margin);
}; };
const frequencyToSemitones = frequency => const frequencyToSemitones = frequency =>
12 * Math.log2(frequency / 440) + 69; 12 * Math.log2(frequency / 440) + 69;
const semitonesToNote = semitones => { const semitonesToNote = semitones => {
const rounded = Math.round(semitones - 69); const rounded = Math.round(semitones - 69);
@@ -86,17 +103,25 @@ const semitonesToNote = semitones => {
return NOTE_NAMES[index]; return NOTE_NAMES[index];
} }
const errorPercentage = semitones => {
const errorPercentage = (semitones, margin) => {
const rounded = Math.round(semitones); const rounded = Math.round(semitones);
return Math.round((semitones - rounded ) * 100) + "%"; const cents = Math.round((semitones - rounded) * 100);
const accuracy = Number.parseFloat(margin * 100).toFixed(1);
const sign = cents > 0 ? "+" : ""
return `${sign}${cents} cents ± ${accuracy}`;
} }
const semitonesToClassname = semitones => {
const semitonesToClassname = (semitones, margin) => {
const rounded = Math.round(semitones); const rounded = Math.round(semitones);
const error = Math.abs(semitones-rounded); const error = Math.abs(semitones-rounded);
if (error <= 0.05) { const ok = margin > 0.05 ? margin : 0.05
if (error <= ok) {
return ""; return "";
} }