Build a Music Keyboard Piano in Javascript Using Audio.js and jQuery Full Project For Beginners

Build a Music Keyboard Piano in Javascript Using Audio.js and jQuery Full Project For Beginners

 

Welcome folks today in this blog post we will be building a music keyboard piano using audio.js in javascript. All the full source code of the application is shown below.

 

 

Get Started

 

 

In order to get started you need to make index.html file and copy paste the following code

 

index.html

 

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <title>HTML5 JavaScript Piano - with Data URIs</title>
        <meta name="description" content="A synth piano synth that generates all of its audio files dynamically using JavaScript and data URIs. Choose from a variety of sounds styles and play with the looper, visual waveforms, and more." />
        <link rel="stylesheet" type="text/css" href="piano.css" />
        <meta property="og:image" content="http://mrcoles.com/media/img/piano-visual-mode.png">
        <link rel="image_src" href="http://mrcoles.com/media/img/piano-visual-mode.png" />
    </head>
    <body>
        <div id="content">
            <div id="content-inner">
                <div id="piano">
                    <h1>Javascript Piano</h1>
                    <div class="help show" tabindex="1">
                        <div class="help-inner">
                            <div id="synth-settings"></div>
                            <div class="opts">
                                <p><strong>Controls:</strong></p>
                                <p>play using home row & above   /</p>
                                <p>change playable keys: “,” & “.”   /</p>
                                <p>shift keyboard: ← & → <span id="shift"></span></p>
                            </div>
                            <div class="opts">
                                <p><strong>Extras:</strong></p>
                                <p class="toggle-color toggle hold">Color - c   /</p>
                                <p class="toggle-demo toggle">Demo - m   /</p>
                                <p class="toggle-animate toggle">Visual mode - 8   /</p>
                                <p class="toggle-looper">Looper - 9   /</p>
                                <p>Help - 0</p>
                            </div>
                        </div>
                    </div>
                    <div class="loop" tabindex="2">loop</div>
                </div>
            </div>
        </div>
        <div id="below">
            <p id="info">info</p>
            <p id="top">↑</p>
            <div id="below-inner">
                <h2>HTML5 Javascript Piano</h2>
                <p>This synth piano is written <em>solely</em> using HTML, JS, and CSS. It contains a small handful of synths with configurable decays, a looper, demo mode, visual mode, and a variety of colors. View controls by hitting the top-right button on the piano.</p>
                <p>
                    It doesn't use a single static audio file, instead it generates them on the fly at the byte level and then converts them to files using the data URI schema. On a similar note, it doesn't use a single image either, just CSS.
                    You can make pretty much any kind of file with a data URI, such as a
                    <a href="http://mrcoles.com/low-res-paint/">bitmap file</a> or a
                    <a href="http://mrcoles.com/favicon-generator/">favicon</a>.
                </p>
                <p><span title="May 16, 2012">Currently</span> iOS doesn’t support HTML5 audio well enough in the browser to make this work. So, no go on iPhone and iPad, for now.</p>
                <p>Various people have been experimenting with audio files and data URIs for a while now. I first came across <a href="http://www.sk89q.com/playground/jswav/">this implementation</a> by sk89q, which was helpful in getting my audio files to work. For future projects, I will probably use an audio api, but currently it’s just <a href="http://caniuse.com/audio-api">firefox & chrome</a>. For further reading, here is a good overview of <a href="http://matt.west.co.tt/music/jasmid-midi-synthesis-with-javascript-and-html5-audio/">the state of HTML5 audio</a> from December 2010.</p>
                <p>
                    By <a href="http://mrcoles.com">Peter Coles</a>
                    / <a href="http://twitter.com/lethys">@lethys</a>
                    – May 16, 2012
                </p>
            </div>
            <a href="https://twitter.com/share" class="twitter-share-button" data-url="http://mrcoles.com/piano/" data-text="HTML5 Piano - with a data URI synth" data-via="lethys">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
        </div>
        <script src="jquery-1.7.1.min.js"></script>
        <script src="audio.js"></script>
        <script src="piano.js"></script>
    </body>
</html>

 

 

Now just make an audio.js file and copy paste the following code

 

audio.js

 

// paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/
window.log = function f(){ log.history = log.history || []; log.history.push(arguments); if(this.console) { var args = arguments, newarr; args.callee = args.callee.caller; newarr = [].slice.call(args); if (typeof console.log === 'object') log.apply.call(console.log, console, newarr); else console.log.apply(console, newarr);}};
// make it safe to use console.log always
(function(a){function b(){}for(var c="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),d;!!(d=c.pop());){a[d]=a[d]||b;}})
(function(){try{console.log();return window.console;}catch(a){return (window.console={});}}());


/*! Copyright (c) 2013 - Peter Coles (mrcoles.com)
 *  Licensed under the MIT license: http://mrcoles.com/media/mit-license.txt
 */
(function() {

    // test if we can use blobs
    var canBlob = false;
    if (window.webkitURL && window.Blob) {
        try {
            new Blob();
            canBlob = true;
        } catch(e) {}
    }

    function asBytes(value, bytes) {
        // Convert value into little endian hex bytes
        // value - the number as a decimal integer (representing bytes)
        // bytes - the number of bytes that this value takes up in a string

        // Example:
        // asBytes(2835, 4)
        // > '\x13\x0b\x00\x00'
        var result = [];
        for (; bytes>0; bytes--) {
            result.push(String.fromCharCode(value & 255));
            value >>= 8;
        }
        return result.join('');
    }

    function attack(i) {
        return i < 200 ? (i/200) : 1;
    }

    var DataGenerator = $.extend(function(styleFn, volumeFn, cfg) {
        cfg = $.extend({
            freq: 440,
            volume: 32767,
            sampleRate: 11025, // Hz
            seconds: .5,
            channels: 1
        }, cfg);

        var data = [];
        var maxI = cfg.sampleRate * cfg.seconds;
        for (var i=0; i < maxI; i++) {
            for (var j=0; j < cfg.channels; j++) {
                data.push(
                    asBytes(
                        volumeFn(
                            styleFn(cfg.freq, cfg.volume, i, cfg.sampleRate, cfg.seconds, maxI),
                            cfg.freq, cfg.volume, i, cfg.sampleRate, cfg.seconds, maxI
                        ) * attack(i), 2
                    )
                );
            }
        }
        return data;
    }, {
        style: {
            wave: function(freq, volume, i, sampleRate, seconds) {
                // wave
                // i = 0 -> 0
                // i = (sampleRate/freq)/4 -> 1
                // i = (sampleRate/freq)/2 -> 0
                // i = (sampleRate/freq)*3/4 -> -1
                // i = (sampleRate/freq) -> 0
                return Math.sin((2 * Math.PI) * (i / sampleRate) * freq);
            },
            squareWave: function(freq, volume, i, sampleRate, seconds, maxI) {
                // square
                // i = 0 -> 1
                // i = (sampleRate/freq)/4 -> 1
                // i = (sampleRate/freq)/2 -> -1
                // i = (sampleRate/freq)*3/4 -> -1
                // i = (sampleRate/freq) -> 1
                var coef = sampleRate / freq;
                return (i % coef) / coef < .5 ? 1 : -1;
            },
            triangleWave: function(freq, volume, i, sampleRate, seconds, maxI) {
                return Math.asin(Math.sin((2 * Math.PI) * (i / sampleRate) * freq));
            },
            sawtoothWave: function(freq, volume, i, sampleRate, seconds, maxI) {
                // sawtooth
                // i = 0 -> -1
                // i = (sampleRate/freq)/4 -> -.5
                // i = (sampleRate/freq)/2 -> 0
                // i = (sampleRate/freq)*3/4 -> .5
                // i = (sampleRate/freq) - delta -> 1
                var coef = sampleRate / freq;
                return -1 + 2 * ((i % coef) / coef);
            }
        },
        volume: {
            flat: function(data, freq, volume) {
                return volume * data;
            },
            linearFade: function(data, freq, volume, i, sampleRate, seconds, maxI) {
                return volume * ((maxI - i) / maxI) * data;
            },
            quadraticFade: function(data, freq, volume, i, sampleRate, seconds, maxI) {
                // y = -a(x - m)(x + m); and given point (m, 0)
                // y = -(1/m^2)*x^2 + 1;
                return volume * ((-1/Math.pow(maxI, 2))*Math.pow(i, 2) + 1) * data;
            }
        }
    });
    DataGenerator.style.default = DataGenerator.style.wave;
    DataGenerator.volume.default = DataGenerator.volume.linearFade;


    function toDataURI(cfg) {

        cfg = $.extend({
            channels: 1,
            sampleRate: 11025, // Hz
            bitDepth: 16, // bits/sample
            seconds: .5,
            volume: 20000,//32767,
            freq: 440
        }, cfg);

        //
        // Format Sub-Chunk
        //

        var fmtChunk = [
            'fmt ', // sub-chunk identifier
            asBytes(16, 4), // chunk-length
            asBytes(1, 2), // audio format (1 is linear quantization)
            asBytes(cfg.channels, 2),
            asBytes(cfg.sampleRate, 4),
            asBytes(cfg.sampleRate * cfg.channels * cfg.bitDepth / 8, 4), // byte rate
            asBytes(cfg.channels * cfg.bitDepth / 8, 2),
            asBytes(cfg.bitDepth, 2)
        ].join('');

        //
        // Data Sub-Chunk
        //

        var sampleData = DataGenerator(
            cfg.styleFn || DataGenerator.style.default,
            cfg.volumeFn || DataGenerator.volume.default,
            cfg);
        var samples = sampleData.length;

        var dataChunk = [
            'data', // sub-chunk identifier
            asBytes(samples * cfg.channels * cfg.bitDepth / 8, 4), // chunk length
            sampleData.join('')
        ].join('');

        //
        // Header + Sub-Chunks
        //

        var data = [
            'RIFF',
            asBytes(4 + (8 + fmtChunk.length) + (8 + dataChunk.length), 4),
            'WAVE',
            fmtChunk,
            dataChunk
        ].join('');

        if (canBlob) {
            // so chrome was blowing up, because it just blows up sometimes
            // if you make dataURIs that are too large, but it lets you make
            // really large blobs...
            var view = new Uint8Array(data.length);
            for (var i = 0; i < view.length; i++) {
                view[i] = data.charCodeAt(i);
            }
            var blob = new Blob([view], {type: 'audio/wav'});
            return  window.webkitURL.createObjectURL(blob);
        } else {
            return 'data:audio/wav;base64,' + btoa(data);
        }
    }

    function noteToFreq(stepsFromMiddleC) {
        return 440 * Math.pow(2, (stepsFromMiddleC+3) / 12);
    }

    var Notes = {
        sounds: {},
        getDataURI: function(n, cfg) {
            cfg = cfg || {};
            cfg.freq = noteToFreq(n);
            return toDataURI(cfg);
        },
        getCachedSound: function(n, data) {
            var key = n, cfg;
            if (data && typeof data == "object") {
                cfg = data;
                var l = [];
                for (var attr in data) {
                    l.push(attr);
                    l.push(data[attr]);
                }
                l.sort();
                key += '-' + l.join('-');
            } else if (typeof data != 'undefined') {
                key = n + '.' + key;
            }

            var sound = this.sounds[key];
            if (!sound) {
                sound = this.sounds[key] = new Audio(this.getDataURI(n, cfg));
            }
            return sound;
        },
        noteToFreq: noteToFreq
    };

    window.DataGenerator = DataGenerator;
    window.Notes = Notes;
})();

 

See also  React Hooks Simple Calculator From Scratch

 

Now just create an piano.css file and copy paste the following code

 

piano.css

 

/*! Copyright (c) 2013 - Peter Coles (mrcoles.com)
 *  Licensed under the MIT license: http://mrcoles.com/media/mit-license.txt
 */

html,body,div,span,h1,p{margin:0;padding:0;}

html {
    background: #fff;
    font: normal 14px/24px futura, Helvetica, Arial, sans serif;
    color: #999;
}

strong { font-weight: normal; }
a { text-decoration: none; }

html, body { height: 100%; }

#content {
    min-height: 100%;
    text-align: left;
    overflow: hidden;
}

#content-inner {
    padding: 60px 0;
}

#piano {
    position: relative;
    margin: 0 auto;
    padding: 80px 40px 16px;
    width: 782px;
    background: #f33;
    border-radius: 20px;
    box-shadow: 0 1px 0 #999, 0 2px 0 #999, 0 3px 0 #999, 1px 4px 0 #999, 2px 5px 0 #999, 3px 6px 0 #999, 4px 7px 0 #999, 5px 8px 0 #999, 0 0 8px #000;
    z-index: 2;
}


/** keys */

#piano .keys {
    /*width: 1144px;*/
    width: 780px;
    padding: 2px 0 0 2px;
    overflow: hidden;
    background: #000;
    border-radius: 0 0 4px 4px;
}

.key {
    float: left;
    position: relative;
    width: 50px;
    height: 180px;
    margin: 0 2px 2px 0;
    background: #fff;
    border-radius: 0 0 4px 4px;
}
.key.pressed,
.key:active {
    background: #f4f3f3;
    box-shadow: inset 3px 2px 3px #999, inset -3px 2px 3px #999;
}
.key.black {
    width: 0;
    margin: 0;
    z-index: 2;
}
.key.black:after {
    content: "";
    position: absolute;
    top: -2px;
    left: -16px;
    display: block;
    width: 32px;
    height: 117px;
    background: #000;
    border-radius: 0 0 4px 4px;
    box-shadow: 1px 1px 0 #555, 2px 2px 0 #555;
}
.key.black1:after {
    left: -20px;
}
.key.black3:after {
    left: -16px;
}
.key.black.pressed:after,
.key.black:active:after {
    background-color: #222;
}

/* **/


/** header */

h1 {
    position: absolute;
    top: 25px;
    left: 40px;
    font-size: 16px;
    line-height: 20px;
    color: #fff;
    text-shadow: -1px -1px 0 #000
}

/* **/


/** help */

.loop,
.help {
    position: absolute;
    top: 25px;
    right: 42px;
    width: 20px;
    height: 20px;
    color: transparent;
    background: rgba(255, 255, 255, .25);
    box-shadow: 1px 1px 0 rgba(0, 0, 0, .35), 1px 0 0 rgba(0, 0, 0, .35), 0 1px 0 rgba(0, 0, 0, .35), inset 1px 1px 0 rgba(255,255,255,.4);
    border-radius: 2px;
    z-index: 10;
    font-size: 12px;
    cursor: pointer;
}

.loop.active,
.loop:active,
.help.show,
.help:active {
    box-shadow: inset 1px 1px 0 rgba(0, 0, 0, .35);
}

.loop {
    right: 82px;
}

.help .help-inner {
    position: absolute;
    top: 0;
    right: 0;
    width: 0;
    height: 0;
    line-height: 0;
    overflow: hidden;

    transition: all 1s;
    -moz-transition: all 1s; /* Firefox 4 */
    -webkit-transition: all 1s; /* Safari and Chrome */
    -o-transition: all 1s; /* Opera */
}

.help.show .help-inner {
    top: 55px;
    padding: 20px;
    width: 740px;
    height: 142px;
    line-height: 24px;
    color: #eee;
    background: rgba(0, 0, 0, .85);
    border-radius: 4px;
    cursor: default;
}

.help.show:focus { outline: none; }

.opts { margin: 0; overflow: hidden; zoom: 1; width: 740px; height: 36px; line-height: 36px; }
.opts p { float: left; margin: 0 10px 0 0; }
.opts p:first-child { width: 6em; }
.opts p .selected { color: #999; font-style: italic; }

.toggle { cursor: pointer; }

.help a {
    display: inline-block;
    padding: 0 2px;
    color: #ccc;
    font-weight: bold;
    text-decoration: none;
}

.help a:before {
    content: "_";
}
.help a:after {
    content: "_";
}

/* **/

/** below */

#below {
    position: relative;
    min-height: 100%;
    border-top: 2px solid #222;
    background: #333;
    line-height: 20px;
    color: #eee;
    text-shadow: 1px 1px 0 #000;
}

#info {
    position: absolute;
    top: -42px;
    left: 0;
    padding: 10px 20px;
    line-height: 20px;
    color: #333;
    text-shadow: none;
    cursor: pointer;
}

#top {
    position: absolute;
    top: 0;
    right: 0;
    padding: 10px 20px;
    font-size: 20px;
    line-height: 20px;
    font-weight: bold;
    cursor: pointer;
}

.twitter-share-button {
    position: absolute;
    bottom: 40px;
    right: 0;
}

#top:hover {
    color: red;
}

#info:hover {
    color: #eee;
    text-shadow: 1px 1px 0 #000;
    background: #333;
    border-radius: 0 10px 0 0;
}

#below-inner {
    margin: 0 auto;
    padding: 18px 0 20px;
    width: 780px;
    text-align: left;
}

#below-inner h2 {
    font-size: 16px;
    font-weight: bold;
}

#below-inner p {
    margin-bottom: 20px;
}

#below-inner a {
    color: #fff;
    font-weight: bold;
}

/* **/

.note {
    position: fixed;
    top: 20%;
    left: 30%;
    width: 40%;
    background: #111;
    background: rgba(0,0,0,.85);
    border-radius: 10px;
    padding: 60px 0;
    color: #fff;
    font-size: 16px;
    line-height: 20px;
    text-align: center;
    z-index: 999;
}

 

 

Now just create an piano.js file and copy paste the following code

 

piano.js

 

/*! Copyright (c) 2013 - Peter Coles (mrcoles.com)
 *  Licensed under the MIT license: http://mrcoles.com/media/mit-license.txt
 */

(function() {

    //
    // Setup keys!
    //

    var notesOffset = 0;

    var blackKeys = {
        1: 1,
        3: 3,
        6: 1,
        8: 2,
        10: 3
    };
    $.each(blackKeys, function(k, v) {
        blackKeys[k] = ' black black'+v;;
    });

    function blackKeyClass(i) {
        return blackKeys[(i % 12) + (i < 0 ? 12 : 0)] || '';
    }

    var $keys = $('<div>', {'class': 'keys'}).appendTo('#piano');

    var buildingPiano = false;

    var isIos = navigator.userAgent.match(/(iPhone|iPad)/i);

    function buildPiano() {
        if (buildingPiano) return;
        buildingPiano = true;

        $keys.trigger('build-start.piano');
        $keys.empty().off('.play');

        function addKey(i) {
            var dataURI = isIos ? '' : Notes.getDataURI(i);

            // trick to deal with note getting hit multiple times before finishing...
            var sounds = [
                new Audio(dataURI),
                new Audio(dataURI),
                new Audio(dataURI)
            ];
            var curSound = 0;
            var pressedTimeout;
            dataURI = null;
            function play(evt) {
                // sound
                sounds[curSound].pause();
                try {
                    sounds[curSound].currentTime = 0.001; //HACK - was for mobile safari, but sort of doesn't matter...
                } catch (x) {
                    console.log(x);
                }
                sounds[curSound].play();
                curSound = ++curSound % sounds.length;

                var $k = $keys.find('[data-key='+i+']').addClass('pressed');

                //TODO - it'd be nice to have a single event for triggering and reading
                $keys.trigger('played-note.piano', [i, $k]);

                // visual feedback
                window.clearTimeout(pressedTimeout);
                pressedTimeout = window.setTimeout(function() {
                    $k.removeClass('pressed');
                }, 200);
            }
            $keys.on('note-'+i+'.play', play);
            var $key = $('<div>', {
                'class': 'key' + blackKeyClass(i),
                'data-key': i,
                mousedown: function(evt) { $keys.trigger('note-'+i+'.play'); }
            }).appendTo($keys);
        }

        // delayed for-loop to stop browser from crashing :'(
        // go slower on Chrome...
        var i = -12, max = 14, addDelay = /Chrome/i.test(navigator.userAgent) ? 80 : 0;
        (function go() {
            addKey(i + notesOffset);
            if (++i < max) {
                window.setTimeout(go, addDelay);
            } else {
                buildingPiano = false;
                $keys.trigger('build-done.piano');
            }
        })();
    }

    buildPiano();


    //
    // Setup synth controls
    //

    function camelToText(x) {
        x = x.replace(/([A-Z])/g, ' $1');
        return x.charAt(0).toUpperCase() + x.substring(1);
    }

    $.each(['volume', 'style'], function(i, setting) {
        var $opts = $('<div>', {
            'class': 'opts',
            html: '<p><strong>' + camelToText(setting) + ':</strong></p>'
        }).appendTo('#synth-settings');

        $.each(DataGenerator[setting], function(name, fn) {
            if (name != 'default') {
                $('<p>')
                    .append($('<a>', {
                        text: camelToText(name),
                        href: '#',
                        'class': fn === DataGenerator[setting].default ? 'selected' : '',
                        click: function(evt) {
                            evt.preventDefault();
                            DataGenerator[setting].default = fn;
                            buildPiano();
                            var $this = $(this);
                            $this.closest('.opts').find('.selected').removeClass('selected');
                            $this.addClass('selected');
                        }
                    }))
                    .appendTo($opts);
            }
        });
    });


    //
    // Setup keyboard interaction
    //

    var keyNotes = {
        /*a*/ 65: 0, // c
        /*w*/ 87: 1, // c#
        /*s*/ 83: 2, // d
        /*e*/ 69: 3, // d#
        /*d*/ 68: 4, // e
        /*f*/ 70: 5, // f
        /*t*/ 84: 6, // f#
        /*g*/ 71: 7, // g
        /*y*/ 89: 8, // g#
        /*h*/ 72: 9, // a
        /*u*/ 85: 10, // a#
        /*j*/ 74: 11, // b
        /*k*/ 75: 12, // c
        /*o*/ 79: 13, // c#
        /*l*/ 76: 14, // d
        /*p*/ 80: 15, // d#
        /*;*/ 186: 16, // e
        /*;*/ 59: 16, // e ... gotta figure out why it's sometimes 186 and sometimes 59
        /*,*/ 222: 17, // f
        /*]*/ 221: 18, // f#
        /*enter*/ 13: 19 // g
    };
    var notesShift = -12;
    var downKeys = {};

    function isModifierKey(evt) {
        return evt.metaKey || evt.shiftKey || evt.altKey;
    }

    $(window).keydown(function(evt) {
        var keyCode = evt.keyCode;
        // prevent repeating keys
        if (!downKeys[keyCode] && !isModifierKey(evt)) {
            downKeys[keyCode] = 1;
            var key = keyNotes[keyCode];
            if (typeof key != 'undefined') {
                $keys.trigger('note-'+(key+notesShift+notesOffset)+'.play');
                evt.preventDefault();
            } else if (evt.keyCode == 188) {
                notesShift = -12;
            } else if (evt.keyCode == 190) {
                notesShift = 0;
            } else if (keyCode == 37 || keyCode == 39) {
                notesOffset += (keyCode == 37 ? -1 : 1) * 12;
                buildPiano();
            }
        }
    }).keyup(function(evt) {
        delete downKeys[evt.keyCode];
    });


    //
    // Piano colors
    //

    var colors = 'f33 33f 3f3 ff3 f3f 3ff'.split(' '),
        curColor = 0;

    function colorHandler(evt) {
        if (evt.type === 'click' || (evt.keyCode == 67 && !isModifierKey(evt))) {
            if (++curColor >= colors.length) curColor = 0;
            document.getElementById('piano').style.backgroundColor = '#' + colors[curColor];
        }
    }

    $(window).keyup(colorHandler);
    $('.toggle-color').click(colorHandler);

    //
    // Help controls
    //

    var $help = $('.help');

    $(window).click(function(evt) {
        var $closestHelp = $(evt.target).closest('.help');
        if (!((evt.target.nodeName == 'A' || ~evt.target.className.search('hold')) && $closestHelp.length) &&
            ($closestHelp.length || $help.hasClass('show'))) {
            $help.toggleClass('show');
        }
    });

    var qTimeout, qCanToggle = true;;
    $(window).keypress(function(evt) {
        // trigger help when ? is pressed, but make sure it doesn't repeat crazy
        if (evt.which == 63 || evt.which == 48) {
            window.clearTimeout(qTimeout);
            qTimeout = window.setTimeout(function() {
                qCanToggle = true;
            }, 1000);
            if (qCanToggle) {
                qCanToggle = false;
                $help.toggleClass('show');
            }
        }
    });

    window.setTimeout(function() {
        $help.removeClass('show');
    }, 700);

    // prevent quick find...
    $(window).keydown(function(evt) {
        if (evt.target.nodeName != 'INPUT' && evt.target.nodeName != 'TEXTAREA') {
            if (evt.keyCode == 222) {
                evt.preventDefault();
                return false;
            }
        }
        return true;
    });

    //
    // Scroll nav
    //
    $.each([['#info', '#below'], ['#top', '#content']], function(i, x) {
        $(x[0]).click(function() {
            $('html,body').animate({
                scrollTop: $(x[1]).offset().top
            }, 1000);
        });
    });


    //
    // Demo
    //
    (function(undefined) {
        var chopsticks = (function() {
            var data = [
                {
                    'style': 'wave',
                    'volume': 'linearFade',
                    'notesOffset': 0
                }
            ];

            var main = [
                [6, -7, -5],
                [6, -7, -5],
                [6, -7, -5],
                [6, -7, -5],
                [6, -7, -5],
                [6, -7, -5],

                [6, -8, -5],
                [6, -8, -5],
                [6, -8, -5],
                [6, -8, -5],
                [6, -8, -5],
                [6, -8, -5],

                [6, -10, -1],
                [6, -10, -1],
                [6, -10, -1],
                [6, -10, -1],
                [6, -10, -1],
                [6, -10, -1],

                [6, -12, 0],
                [6, -12, 0],
                [6, -12, 0]
            ];

            data.push.apply(data, main);
            data.push(
                [6, -12, 0],
                [6, -10, -1],
                [6, -8, -3]
            );
            data.push.apply(data, main);
            data.push(
                [6, -12, 0],
                [6, -5],
                [6, -8],

                [6, -12],
                [12]
            );

            var main2 = [
                [6, 0, 4],
                [6, -1, 2],
                [6],

                [6, -3, 0],
                [6, -5, -1],
                [6],

                [6, -7, -3],
                [6, -8, -5],
                [6],

                [6, 0, 4],
                [6, 0, 4],
                [6],

                [6, -8, -5],
                [6, -10, -7],
                [6],

                [6, -1, 2],
                [6, -1, 2],
                [6]
            ];
            data.push.apply(data, main2);
            data.push(
                [6, -10, -7],
                [6, -12, -8],
                [6],

                    [6, -8, 0],
                [6, -8, 0],
                [6]
            );
            data.push.apply(data, main2);
            data.push(
                [6, -5, -1],
                [6, -8, 0],
                [6, -5],

                [6, -8],
                [6, -12],
                [6]
            );
            return data;
        })();


        var demoing = false, demoingTimeout;
        function demo(data) {
            var cfg = data[0];
            if (!buildingPiano && !demoing) {
                demoing = true;
                cfg.style && (DataGenerator.style.default = DataGenerator.style[cfg.style]);
                cfg.volume && (DataGenerator.volume.default = DataGenerator.volume[cfg.volume]);
                cfg.notesOffset !== undefined && (notesOffset = cfg.notesOffset);
                $keys.one('build-done.piano', function() {
                    //NOTE - jQuery.map flattens arrays
                    var i = 0, song = $.map(data, function(x, i) { return i == 0 ? null : [x]; });
                    (function play() {
                        if (!demoing) return;
                        if (i >= song.length) { i = 0; }
                        var part = song[i++];
                        if (part) {
                            var delay = part[0];
                            demoingTimeout = window.setTimeout(function() {
                                demoing && play();
                                for (var j=1, len=part.length; j<len; j++) {
                                    $keys.trigger('note-'+(part[j]+notesOffset)+'.play');
                                }
                            }, delay*50);
                        }
                    })();
                });
                buildPiano();
            }
        }

        function demoHandler(evt) {
            if (evt.type === 'click' || (evt.keyCode == 77 && !isModifierKey(evt))) {
                if (demoing) {
                    demoing = false;
                    window.clearTimeout(demoingTimeout);
                    $keys.unbind('build-done.piano');
                } else {
                    demo(chopsticks);
                }
            }
        }

        $(window).keyup(demoHandler);
        $('.toggle-demo').click(demoHandler);
    })();


    //
    // Looper
    //
    (function() {
        var $looper = $('.loop'),
            recording = false,
            startTime,
            totalTime,
            data,
            stopTimeout,
            loopInterval, loopTimeouts = [];

        $keys.on('played-note.piano', function(evt, key) {
            if (recording) {
                data.push({'key': key, 'time': new Date().getTime()});
            }
        });

        function recordStart() {
            if (!recording) {
                data = [];
                startTime = new Date().getTime();
                recording = true;
                window.clearTimeout(stopTimeout);
                stopTimeout = window.setTimeout(recordStop, 60*1000); // 1 minute max?
                $looper.addClass('active');

                // stop old loop
                window.clearInterval(loopInterval);
                $.each(loopTimeouts, function(i, x) { window.clearTimeout(x); });
            }
        }
        function recordStop() {
            if (recording) {
                recording = false;
                totalTime = new Date().getTime() - startTime;
                window.clearTimeout(stopTimeout);
                for (var i=0, len=data.length; i<len; i++) {
                    data[i].time = data[i].time - startTime;
                }
                if (data.length) {
                    playLoop(data, totalTime);
                }
                $looper.removeClass('active');
            }
        }

        function playLoop(data, totalTime) {
            loopInterval = window.setInterval(function() {
                loopTimeouts = [];
                $.each(data, function(i, x) {
                    loopTimeouts.push(window.setTimeout(function() {
                        $keys.trigger('note-'+x.key+'.play');
                    }, x.time));
                });
            }, totalTime);
        }

        $looper.mousedown(recordStart).mouseup(recordStop);

        $(window).on('keydown keyup', function(evt) {
            if (evt.which == 57 && !isModifierKey(evt)) {
                evt.type == 'keydown' ? recordStart() : recordStop();
            }
        });
    })();


    //
    // Silly colors
    //
    (function() {
        var shouldAnimate = true,
            $piano = $('#piano'),
            W = $piano.width(),
            H = 500,
            $canvas = $('<canvas>', {
                css: {
                    position: 'absolute',
                    top: ($piano.offset().top + $piano.outerHeight() - 1) + 'px',
                    left: '50%',
                    marginLeft: Math.floor(-W/2) + 'px', // need to figure this out...
                    width: W,
                    height: H
                }
            })
            .attr('width', W)
            .attr('height', H)
            .prependTo('body'),
            canvas = $canvas.get(0),
            ctx = canvas.getContext('2d');

        function choice(x) {
            return x[Math.floor(Math.random()*x.length)];
        }

        function getData(note) {
            var data = [], freq = Notes.noteToFreq(note), vol = 1, sampleRate = 2024, secs = .1;
            var volumeFn = DataGenerator.volume.default;
            var styleFn = DataGenerator.style.default;
            var maxI = sampleRate * secs;
            for (var i=0; i<maxI; i++) {
                var sf = styleFn(freq, vol, i, sampleRate, secs, maxI);
                data.push(volumeFn(
                    styleFn(freq, vol, i, sampleRate, secs, maxI),
                    freq, vol, i, sampleRate, secs, maxI));
            }
            return data;
        }

        var keyToData = {},
            keyAnimCounts = {};

        $keys.on('build-done.piano', function() {
            $keys.find('.key').each(function() {
                var key = $(this).data('key');
                keyToData[key] = getData(key);
            });
        });

        $keys.on('played-note.piano', function(evt, key, $elt) {
            if (!shouldAnimate) return;

            var eOffset = $elt.offset(),
                eWidth = $elt.width(),
                cOffset = $canvas.offset(),
                startX = (eOffset.left + eWidth/2) - cOffset.left,
                startY = 0,
                endY = 200,
                amplitude = 8,
                data = keyToData[key],
                animCount = keyAnimCounts[key] = (keyAnimCounts[key] || 0) + 1;

            if (!data) return;

            var len = data.length,
                maxTime = 500,
                stepRate = 80,
                cleanupStepDelay = 8,
                steps = Math.floor(maxTime / stepRate),
                iPerStep = len / steps,
                yPerStep = (endY - startY) / steps,
                yIncrement = yPerStep / iPerStep,
                step = 0,
                i = 0,
                color = '#' + choice('f33 33f 3f3 ff3 f3f 3ff'.split(' '));

            // startY -> endY in steps
            // each step is yPerStep = (endY - startY) / steps long
            // each step covers iPerStep = len / steps data points
            //     at an increment of yIncrement = yPerStep / iPerStep

            (function draw() {

                if (step < steps) {
                    ctx.strokeStyle = color;
                    ctx.lineWidth = 1;
                    ctx.beginPath();
                    ctx.moveTo(startX, startY);
                    var newMax = i + iPerStep, first = true;
                    for (; i<=newMax; i++) {
                        startY += yIncrement;
                        ctx[first ? 'moveTo' : 'lineTo'](startX + data[i]*amplitude, startY);
                        first = false;
                        if (startY > H) return;
                    }
                    i--; // keep an overlap between draws
                    startY -= yIncrement;
                    ctx.stroke();
                }

                if (keyAnimCounts[key] == animCount && step >= cleanupStepDelay) {
                    var cleanupStep = step - cleanupStepDelay;
                    ctx.clearRect(startX - amplitude - 5, yPerStep * cleanupStep,
                                  (amplitude + 5) * 2, yPerStep * (cleanupStep + 1));
                }

                if (++step < steps + cleanupStepDelay) {
                    window.setTimeout(draw, stepRate);
                }
            })();
        });

        // button
        var bW = 20,
            bH = 20,
            $loop = $('.loop'),
            $button = $('<canvas>', {
                css: {
                    position: 'absolute',
                    top: (parseInt($loop.css('top')) + 1) + 'px',
                    right: (parseInt($loop.css('right')) + 34) + 'px',
                    width: bW,
                    height: bH,
                    cursor: 'pointer'
                }
            })
            .attr('width', bW)
            .attr('height', bH)
            .appendTo('#piano'),
            button = $button.get(0),
            bctx = button.getContext('2d'),
            coords = [
                [15, 1],
                [5, 9],
                [9, 11],
                [5, 19],
                [15, 11],
                [11, 9]
            ],
            coordsLen = coords.length;

        bctx.strokeStyle = 'rgba(0,0,0,.5)';
        bctx.lineWidth = .5;

        function draw() {
            bctx.fillStyle = shouldAnimate ? 'rgba(255,255,0,.75)' : 'rgba(0,0,0,.25)';
            bctx.clearRect(0, 0, bW, bH);
            bctx.beginPath();
            for (var i=0; i<coordsLen; i++) {
                bctx[i == 0 ? 'moveTo' : 'lineTo'](coords[i][0], coords[i][1]);
            }
            bctx.closePath();
            if (shouldAnimate) bctx.stroke();
            bctx.fill();
        }
        draw();

        // handlers
        function toggleAnimate(evt) {
            if (evt.type === 'click' || (evt.keyCode == 56 && !isModifierKey(evt))) {
                shouldAnimate = !shouldAnimate;
                draw();
            }
        }
        $(window).keyup(toggleAnimate);
        $('.toggle-animate').click(toggleAnimate);
        $button.click(toggleAnimate);
    })();

    if (isIos) {
        $(function() {
            var $note = $('<div>', {
                'class': 'note',
                'text': 'Note: sound does not work on iOS, but you can still enjoy pretty wave forms!'
            }).appendTo('body');

            window.setTimeout(function() {
                $note.fadeOut();
            }, 6000);
        });
    }



    // the below code was a failed experiment to support iOS...

    // //
    // // Generate files for dl...
    // //

    // function generateFilesForDL() {
    //     // backup solution for iOS... since they won't play my files :'(
    //     // add audio elts to page and then download them all!
    //     // https://addons.mozilla.org/en-US/firefox/addon/downthemall/?src=search

    //     for (var i=0; i<5; i++) {
    //         var dataURI = Notes.getDataURI(i);
    //         $('body').prepend("<br><br>");
    //         $('<audio>', {controls: 'controls'})
    //             .append('Note ' + i)
    //             .append($('<source>', {
    //                 src: dataURI,
    //                 type: 'audio/wav'
    //             }))
    //             .prependTo('body');
    //         $('body').prepend(i + ": ");
    //     }

    //     $('body').prepend("<br><br>");
    //     $('<audio>', {controls: 'controls', src: 'note.caf', type: 'audio/wav'}).prependTo('body');
    //     $('body').prepend("note: ");

    // }
    // generateFilesForDL();

})();

 

See also  How to add Custom Fonts in JSPDF | JSPDF Tutorial | Html to Pdf Tutorial

 

Now if you open the index.html file inside the browser you will see the below output

 

Leave a Reply