diff --git a/tuner.css b/tuner.css
index 4ab0563..aa85bbb 100644
--- a/tuner.css
+++ b/tuner.css
@@ -5,7 +5,8 @@ body {
color: white;
display: flex;
flex-direction: column;
- font-size: 48px;
+ font-family: sans-serif;
+ font-size: 2em;
height: 100vh;
margin: 0;
overflow: hidden;
@@ -28,10 +29,10 @@ h1, h2 {
}
h1 {
- font-size: 96px;
+ font-size: 6em;
}
h2 {
- font-size: 72px;
+ font-size: 3em;
}
diff --git a/tuner.html b/tuner.html
index 9b69b59..6180ca0 100644
--- a/tuner.html
+++ b/tuner.html
@@ -3,11 +3,11 @@
+
Tuner
- Tuner
- Listening...
- ?? kHz
- A+
+ Listening...
+
+ (?? kHz sample rate)
diff --git a/tuner.js b/tuner.js
index ee89877..e5b4eb7 100644
--- a/tuner.js
+++ b/tuner.js
@@ -1,7 +1,5 @@
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
// gives us finer-graned FFT buckets
const TARGET_SAMPLE_RATE = 8000;
@@ -11,6 +9,7 @@ let dom_rate;
let dom_note;
let dom_tune;
+
const setup = () => {
dom_frequency = document.getElementById("frequency");
dom_rate = document.getElementById("rate");
@@ -24,27 +23,39 @@ const setup = () => {
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 audioContext = new AudioContext({
sampleRate: TARGET_SAMPLE_RATE,
});
const analyser = audioContext.createAnalyser();
- analyser.fftSize = 8192;
+ analyser.fftSize = 32768;
analyser.minDecibels = -90;
analyser.maxDecibels = -10;
- analyser.smoothingTimeConstant = 0.1;
+ analyser.smoothingTimeConstant = 0;
const bufferLength = analyser.frequencyBinCount;
const data = new Uint8Array(bufferLength);
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
- setInterval(tune(analyser, data), 200);
+ setInterval(tune(analyser, data), 500);
};
+
const tune = (analyser, data) => () => {
analyser.getByteFrequencyData(data);
@@ -55,8 +66,6 @@ const tune = (analyser, data) => () => {
let max = 0;
let maxBucket = -1;
-
-
data.forEach((value, bucket) => {
if (value > max) {
max = value;
@@ -64,18 +73,26 @@ const tune = (analyser, data) => () => {
}
});
+ if (maxBucket === -1) {
+ return;
+ }
+
const frequency = maxBucket * bucketWidth;
- dom_frequency.innerText = `${frequency} Hz`;
+ dom_frequency.innerText = `${Number.parseFloat(frequency).toFixed(2)} Hz`;
const semitones = frequencyToSemitones(frequency);
+ const margin = frequencyToSemitones(frequency + bucketWidth / 2) - semitones;
+
dom_note.innerText = semitonesToNote(semitones);
- dom_tune.innerText = errorPercentage(semitones);
- document.body.className = semitonesToClassname(semitones);
+ dom_tune.innerText = errorPercentage(semitones, margin);
+ document.body.className = semitonesToClassname(semitones, margin);
};
+
const frequencyToSemitones = frequency =>
12 * Math.log2(frequency / 440) + 69;
+
const semitonesToNote = semitones => {
const rounded = Math.round(semitones - 69);
@@ -86,17 +103,25 @@ const semitonesToNote = semitones => {
return NOTE_NAMES[index];
}
-const errorPercentage = semitones => {
+
+const errorPercentage = (semitones, margin) => {
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 error = Math.abs(semitones-rounded);
- if (error <= 0.05) {
+ const ok = margin > 0.05 ? margin : 0.05
+
+ if (error <= ok) {
return "";
}