<svelte:head>
	<title>JavaScript Guitar Tuner</title>
</svelte:head>

<div id="tuner_sketch" class="content"></div>

<hr>

<h1>JavaScript Guitar Tuner</h1>

<p>
    JavaScript web app that uses a microphone as input and
    displays frequency, note and a helper line to assist in
    tuning accuracy. Everything is drawn on a canvas including
    the background guitar image.
</p>

<p>
    Application utilizes P5.js library as
    well as ML5.js for a pitch detection assistance.
    Additionally notes and frequencies are fetched via a JSON
    library hosted on Github.
</p>

<hr>

<h5>Click on page to enable permissions and start the tuner</h5>

<style>
	p{
		margin: 1em;
		border-radius: 5px;
		padding: 1em;
		background-color: #333333;
	}

    #tuner_sketch{
        height: 50vh;
        width: calc(50vh / 1.5);
        margin: auto;
    }

    hr{
		background-color: red;
		max-width: 20vw;
	}

    h1{
        font-size: x-large;
        margin: 1em;
    }

    h5{
        font-size: smaller;
        max-width: 40vw;
        margin: auto;
        text-align: center;
    }
</style>



<script>
    import { onMount } from "svelte";
    import { onDestroy } from 'svelte'

    let mic, myp5;

    onMount(async () => {
        const container = document.getElementById('tuner_sketch');
        const p5Module = await import("p5");
        window.p5 = p5Module.default;
        await import("p5/lib/addons/p5.sound");
        const ml5 = await import("ml5");
        window.ml5 = ml5.default;

        let pitch;
        let freq = 0;
        let threshold = 1;
        let canvas;
        let audioContext;
        let WIDTH = container.clientWidth;
        let HEIGHT = container.clientHeight;
        let baseFreq = 440;
        let test = 0;
        let notes = [];
        let img;

        //	ml5 pitchdetection-crepe modle can only handle from C#1 - D6 Reliably
        const model_url = "https://cdn.jsdelivr.net/gh/ml5js/ml5-data-and-models/models/pitch-detection/crepe/";
        const note_frequencies = "https://gist.githubusercontent.com/quimbs/fe52bc25dca44cd3fcb8/raw/f9fe9c6bb68f343a4b81a17445efc200702d45dd/notes.json";

        const sketch = (sketch) => {

            sketch.preload = () => {
                // load image
                img = sketch.loadImage("img/notMyguitar.png");
            }

            sketch.setup = () => {
                // Resize for init
                WIDTH = document.getElementById("tuner_sketch").clientWidth;
                HEIGHT = document.getElementById("tuner_sketch").clientHeight;

                // Create Canvas
                sketch.createCanvas(WIDTH, HEIGHT);

                // request access to mic and starting mic
                audioContext = sketch.getAudioContext();
                mic = new p5.AudioIn();
                mic.start(startPitch);
                console.log('Mic Started');

                //	Get notes/freq json
                getNotes();
            };

            //	fetches notes/freq JSON and overwrites empty note array
            async function getNotes() {
                const response = await fetch(note_frequencies);
                const data = await response.json();
                notes = data[baseFreq];
            }

            sketch.draw = () => {
                WIDTH = container.clientWidth;
                HEIGHT = container.clientHeight;
                sketch.clear();
                resetSketch();
            }

            function resetSketch(){
                sketch.image(img, WIDTH / 2 / 2, 0, WIDTH / 2, HEIGHT);
                sketch.textAlign(sketch.CENTER, sketch.CENTER);
                sketch.fill(105, 105, 105);
                sketch.strokeWeight(3);

                let closestNote = 0;
                let difference = Infinity;
                for (let i = 0; i < notes.length; i++) {
                    let diff = freq - notes[i].frequency;
                    if (Math.abs(diff) < Math.abs(difference)) {
                        closestNote = notes[i];
                        difference = diff;
                    }
                }

                //	change color based on prox to threshold
                if (Math.abs(difference) < threshold) {
                    sketch.strokeWeight(6);
                    sketch.stroke(0, 255, 0);
                    sketch.fill(0, 0, 0);
                } else if (Math.abs(difference) < threshold + 4) {
                    sketch.strokeWeight(4);
                    sketch.stroke(255, 165, 0);
                } else if (Math.abs(difference) < threshold + 13) {
                    sketch.strokeWeight(3);
                    sketch.stroke(255, 165, 0);
                }

                //	freq text
                let freq_message = freq.toFixed(2) + " hz";
                let note_message = closestNote.note;

                //	test if pitch detected
                if ((sketch.i = parseInt(freq_message) < 1)) {
                    freq_message = "no audio detected";
                    sketch.stroke(27, 51, 71);
                    sketch.strokeWeight(3);
                    note_message = "🔇";
                }

                //	draw text
                function drawText(freq_message, note_message) {
                    sketch.textSize(WIDTH / 16);
                    sketch.text(freq_message, WIDTH / 2, (HEIGHT / 2) * 1.8);
                    sketch.textSize(WIDTH / 6);
                    sketch.text(note_message, WIDTH / 2, (HEIGHT / 2) * 1.5);
                }

                drawText(freq_message, note_message);

                //	Canvas radius
                let radius = WIDTH / 2;
                // Default for p5 is RADIANS
                sketch.angleMode(sketch.DEGREES);
                //	set angle to 90 relative to origin
                let angle = -90 + difference;
                //	set origin to center
                let x1 = WIDTH / 2;
                let y1 = HEIGHT / 2;

                //	create circles
                let x2 = Math.cos(angle) * radius;
                let y2 = Math.sin(angle) * radius;

                //	Draw line
                sketch.line(x1, y1, x2 + radius, y2 + radius);
                console.log(
                    "x1=" +
                    x1 +
                    "|" +
                    "y1=" +
                    y1 +
                    "|" +
                    "x2=" +
                    x2 +
                    "|" +
                    "y2=" +
                    y2 +
                    "|" +
                    "radius=" +
                    radius +
                    "|" +
                    "angle=" +
                    angle +
                    "|"
                );
                sketch.stroke(0);
            }

            sketch.windowResized = () => {
                sketch.resizeCanvas(width, height);
                resetSketch();
            }

            //  Create pitch from ml5 lib
            function startPitch() {
                pitch = ml5.pitchDetection(
                    model_url,
                    audioContext,
                    mic.stream,
                    modelLoaded
                );
            }

            // 	Loads pitch model
            function modelLoaded() {
                console.log("Model Loaded");
                getPitch();
            }

            //	recursively keeps getting pitch
            function getPitch() {
                pitch.getPitch(function (err, frequency) {
                    if (frequency) {
                        freq = frequency;
                    } else {
                        freq = -100;
                    }
                    getPitch();
                });
            }
        };

        myp5 = new p5(sketch, container);
    });

    onDestroy(() => {
        mic.stop();
        myp5.remove();
        console.log('Mic Stopped');
    });
</script>