├── img ├── og.png ├── snapshot.png └── allow_mic_arrow.png ├── js ├── sharing.js ├── helper_functions.js ├── health_drawer.js ├── audio_wave.js ├── explosion_effect.js ├── fretboard.js ├── config.js ├── song_loader.js ├── audio_processor.js └── app.js ├── README.md ├── LICENSE ├── css └── app.css └── index.html /img/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makaroni4/guitar_bro/HEAD/img/og.png -------------------------------------------------------------------------------- /img/snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makaroni4/guitar_bro/HEAD/img/snapshot.png -------------------------------------------------------------------------------- /img/allow_mic_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makaroni4/guitar_bro/HEAD/img/allow_mic_arrow.png -------------------------------------------------------------------------------- /js/sharing.js: -------------------------------------------------------------------------------- 1 | $(document).on("click", ".share-button", function(e) { 2 | var $this = $(this); 3 | 4 | ga("send", "event", "Game", "Share", $this.data("eventLabel")); 5 | 6 | $this.customerPopup(e); 7 | }); 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guitar Bro – browser game that helps you learn notes on guitar 2 | 3 |  4 | 5 | ## Description 6 | 7 | Guitar Bro works completely in browser and is based on [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). Currently Guitar Bro works only in Chrome, since only Chrome allows to change the [resolution of FFT](https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/fftSize) to distinguish different notes on guitar. 8 | 9 | [Try it out!](https://makaroni4.github.io/guitar_bro/) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anatoli Makarevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /js/helper_functions.js: -------------------------------------------------------------------------------- 1 | function randInt(min, max, positive) { 2 | let num; 3 | if (positive === false) { 4 | num = Math.floor(Math.random() * max) - min; 5 | num *= Math.floor(Math.random() * 2) === 1 ? 1 : -1; 6 | } else { 7 | num = Math.floor(Math.random() * max) + min; 8 | } 9 | 10 | return num; 11 | } 12 | 13 | function pickRandom(array) { 14 | return array[Math.floor(Math.random() * array.length)]; 15 | } 16 | 17 | function getChromeVersion() { 18 | var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); 19 | 20 | return raw ? parseInt(raw[2], 10) : false; 21 | } 22 | 23 | function randomArray(length, max) { 24 | return Array.apply(null, Array(length)).map(function() { 25 | return Math.round(Math.random() * max); 26 | }); 27 | } 28 | 29 | $.fn.customerPopup = function (e, intWidth, intHeight, blnResize) { 30 | // Prevent default anchor event 31 | e.preventDefault(); 32 | 33 | // Set values for window 34 | intWidth = intWidth || '500'; 35 | intHeight = intHeight || '400'; 36 | strResize = (blnResize ? 'yes' : 'no'); 37 | 38 | // Set title and open popup with focus on it 39 | var strTitle = ((typeof this.attr('title') !== 'undefined') ? this.attr('title') : 'Social Share'), 40 | strParam = 'width=' + intWidth + ',height=' + intHeight + ',resizable=' + strResize, 41 | objWindow = window.open(this.attr('href'), strTitle, strParam).focus(); 42 | } 43 | -------------------------------------------------------------------------------- /js/health_drawer.js: -------------------------------------------------------------------------------- 1 | function HealthDrawer(ctx) { 2 | var heartWidth = 40; 3 | var heartHeight = 25; 4 | var c1 = 2; 5 | var c2 = 2; 6 | 7 | function drawBezierCurve(x0, y0, x1, y1, x2, y2, x3, y3) { 8 | ctx.moveTo(x0, y0); 9 | ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3); 10 | } 11 | 12 | return { 13 | draw: function(health, isSandboxMode) { 14 | if(isSandboxMode) { 15 | return; 16 | } 17 | 18 | var prevFillStyle = ctx.fillStyle; 19 | var prevStrokeStyle = ctx.strojeStyle; 20 | 21 | ctx.fillStyle = gameConfig.colors.red; 22 | ctx.strokeStyle = gameConfig.colors.red; 23 | 24 | for(var i = 0; i < health; i++) { 25 | var x = ctx.canvas.width - heartWidth - i * (heartWidth + 10); 26 | var y = 20; 27 | 28 | ctx.beginPath(); 29 | drawBezierCurve(x, y, x, y - heartHeight / 2, x - heartWidth / 2, y - heartHeight / 2, x - heartWidth / 2, y); 30 | drawBezierCurve(x - heartWidth / 2, y, x - heartWidth / 2, y + heartHeight / 2, x, y + heartHeight / 2 * c1, x, y + heartHeight / 2 * c2); 31 | drawBezierCurve(x, y + heartHeight / 2 * c2, x, y + heartHeight / 2 * c1, x + heartWidth / 2, y + heartHeight / 2, x + heartWidth / 2, y); 32 | drawBezierCurve(x + heartWidth / 2, y, x + heartWidth / 2, y - heartHeight / 2, x, y - heartHeight / 2, x, y); 33 | ctx.closePath(); 34 | ctx.fill(); 35 | 36 | ctx.beginPath(); 37 | ctx.moveTo(x - heartWidth / 2, y); 38 | ctx.lineTo(x + heartWidth / 2, y); 39 | ctx.lineTo(x, y + heartHeight / 2 * c2); 40 | ctx.closePath(); 41 | ctx.stroke(); 42 | ctx.fill() 43 | } 44 | 45 | ctx.fillStyle = prevFillStyle; 46 | ctx.strokeStyle = prevStrokeStyle; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /js/audio_wave.js: -------------------------------------------------------------------------------- 1 | function AudioWaveChart() { 2 | var $audioWave = $(".audio-wave"); 3 | 4 | var w = $audioWave.width(); 5 | var h = $audioWave.height(); 6 | 7 | var x_scale = d3.scaleLinear().range([0, w]).domain([0, 1]); 8 | var y_scale = d3.scaleLinear().range([h, 0]).domain([0, 1]); 9 | 10 | var line = d3.line() 11 | .x(function(d) { 12 | return x_scale(d.x);}) 13 | .y(function(d) { 14 | return y_scale(d.y); 15 | }) 16 | 17 | var graph = d3.select(".audio-wave").append("svg:svg") 18 | .attr("width", w) 19 | .attr("height", h) 20 | .append("svg:g"); 21 | 22 | graph.append("svg:path").attr("class", "line"); 23 | 24 | var setDomain = function(data_xy){ 25 | x_scale.domain(d3.extent(data_xy, function(d){ return d.x})); 26 | 27 | var y_range = d3.extent(data_xy, function(d){ return d.y}); 28 | 29 | y_scale.domain([ Math.min(-0.1, y_range[0]), Math.max(0.1, y_range[1]) ]); 30 | }; 31 | 32 | var plotD3Wave = function(data_xy) { 33 | setDomain(data_xy); 34 | var svg = d3.select("body").transition(); 35 | svg.select(".line") 36 | .duration(0) 37 | .attr("d", line(data_xy)); 38 | } 39 | 40 | return { 41 | plotWave: function(wave) { 42 | let found_good_ind = 0; 43 | for (let i = 0; i < wave.length - 1; i++){ 44 | if (wave[i] < 0 && wave[i + 1] >= 0){ 45 | found_good_ind = i; 46 | break; 47 | } 48 | } 49 | found_good_ind = Math.min(found_good_ind, wave.length - 500); 50 | 51 | wave_short = wave.slice(found_good_ind, found_good_ind + 500); 52 | data_xy = []; 53 | for (let i = 0; i < wave_short.length; i++){ 54 | data_xy.push({x: i, y: wave_short[i]}); 55 | } 56 | 57 | plotD3Wave(data_xy); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /js/explosion_effect.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/43498923/html5-canvas-particle-explosion 2 | function ExplosionEffect(ctx) { 3 | const particlesPerExplosion = 25; 4 | const particlesMinSpeed = 5; 5 | const particlesMaxSpeed = 10; 6 | const particlesMinSize = 2; 7 | const particlesMaxSize = 4; 8 | var explosions = []; 9 | 10 | function particle(x, y, correctAnswer) { 11 | this.x = x; 12 | this.y = y; 13 | this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false); 14 | this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false); 15 | this.size = randInt(particlesMinSize, particlesMaxSize, true); 16 | this.color = correctAnswer ? gameConfig.colors.green : gameConfig.colors.red; 17 | } 18 | 19 | function explosion(x, y, correctAnswer) { 20 | this.particles = []; 21 | 22 | for (let i = 0; i < particlesPerExplosion; i++) { 23 | this.particles.push( 24 | new particle(x, y, correctAnswer) 25 | ); 26 | } 27 | } 28 | 29 | return { 30 | draw: function() { 31 | if (explosions.length === 0) { 32 | return; 33 | } 34 | 35 | for (let i = 0; i < explosions.length; i++) { 36 | 37 | const explosion = explosions[i]; 38 | const particles = explosion.particles; 39 | 40 | if (particles.length === 0) { 41 | explosions.splice(i, 1); 42 | return; 43 | } 44 | 45 | const particlesAfterRemoval = particles.slice(); 46 | for (let ii = 0; ii < particles.length; ii++) { 47 | 48 | const particle = particles[ii]; 49 | 50 | // Check particle size 51 | // If 0, remove 52 | if (particle.size <= 0) { 53 | particlesAfterRemoval.splice(ii, 1); 54 | continue; 55 | } 56 | 57 | ctx.beginPath(); 58 | ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false); 59 | ctx.closePath(); 60 | ctx.fillStyle = particle.color; 61 | ctx.fill(); 62 | 63 | // Update 64 | particle.x += particle.xv; 65 | particle.y += particle.yv; 66 | particle.size -= .1; 67 | } 68 | 69 | explosion.particles = particlesAfterRemoval; 70 | } 71 | }, 72 | add: function(x, y, correctAnswer) { 73 | explosions.push( 74 | new explosion(x, y, correctAnswer) 75 | ); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /js/fretboard.js: -------------------------------------------------------------------------------- 1 | function Fretboard(canvas, songLoader, string, rockWidth, pegWidth) { 2 | var ctx = canvas.getContext("2d"), 3 | blockHeight = rockWidth, 4 | block = { 5 | x: 0, 6 | y: canvas.height - blockHeight, 7 | width: canvas.width, 8 | height: blockHeight 9 | }; 10 | 11 | var highlightedFret, 12 | highlightedColor = gameConfig.colors.yellow; 13 | 14 | function drawCircle(x, y) { 15 | var circleSize = (blockHeight / 6 - 1) / 2; 16 | 17 | ctx.fillStyle = gameConfig.colors.white; 18 | ctx.beginPath(); 19 | ctx.arc(x, y, circleSize, 0, 2 * Math.PI); 20 | ctx.fill(); 21 | } 22 | 23 | function drawLine(x, y, x1, y1) { 24 | ctx.beginPath(); 25 | ctx.moveTo(x, y); 26 | ctx.lineTo(x1, y1); 27 | ctx.stroke(); 28 | } 29 | 30 | return { 31 | draw: function() { 32 | ctx.strokeStyle = gameConfig.colors.white; 33 | ctx.lineWidth = 1; 34 | for(var i = 1; i < gameConfig.strings[string].notes.length; i++) { 35 | var x = i * rockWidth + pegWidth; 36 | drawLine(x, block.y, x, canvas.height); 37 | } 38 | drawLine(0, block.y, canvas.width, block.y); 39 | 40 | // draw single circles 41 | var circleFrets = [2, 4, 6, 8]; 42 | var cirlceColor = gameConfig.colors.white; 43 | var verticalMiddle = canvas.height - blockHeight / 2; 44 | var circleSize = (blockHeight / 6 - 1) / 2; 45 | 46 | circleFrets.forEach(function(fret) { 47 | drawCircle((rockWidth * fret - 1) + rockWidth / 2 + pegWidth, verticalMiddle); 48 | }); 49 | 50 | // draw double circles 51 | var doubleCirclesFret = 12; 52 | drawCircle((rockWidth * 11) + rockWidth / 2 + pegWidth, canvas.height - circleSize * 2.5); 53 | drawCircle((rockWidth * 11) + rockWidth / 2 + pegWidth, canvas.height - block.height + 2.5 * circleSize); 54 | 55 | if(typeof(highlightedFret) === "number") { 56 | ctx.fillStyle = highlightedColor; 57 | ctx.fillRect(highlightedFret * rockWidth + pegWidth, block.y, rockWidth, rockWidth); 58 | } 59 | }, 60 | highlightFret: function(note, color) { 61 | var fretIndex = songLoader.findNoteIndex(note, string); 62 | 63 | highlightedFret = fretIndex; 64 | highlightedColor = color ? color : gameConfig.colors.yellow; 65 | 66 | setTimeout(function() { 67 | highlightedFret = undefined; 68 | }, 100); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /js/config.js: -------------------------------------------------------------------------------- 1 | var gameConfig = { 2 | fps: 50, 3 | colors: { 4 | green: "#9BC53D", 5 | yellow: "#FDE74C", 6 | red: "#E55934", 7 | white: "#F1FAEE", 8 | dark_blue: "#1D3557" 9 | }, 10 | strings: { 11 | 1: { 12 | name: "1. E-string (thinnest)", 13 | range: [325, 665], 14 | freqs: [ 15 | [329.6, "E"], 16 | [349.2, "F"], 17 | [370.0, "F#"], 18 | [392.0, "G"], 19 | [415.3, "G#"], 20 | [440.0, "A"], 21 | [466.1, "A#"], 22 | [493.8, "B"], 23 | [523.2, "C"], 24 | [554.3, "C#"], 25 | [587.3, "D"], 26 | [622.2, "D#"], 27 | [659.2, "E"], 28 | ] 29 | }, 30 | 2: { 31 | name: "2. B-string", 32 | range: [242, 499], 33 | freqs: [ 34 | [246.9, "B"], 35 | [261.6, "C"], 36 | [277.2, "C#"], 37 | [293.7, "D"], 38 | [311.1, "D#"], 39 | [329.6, "E"], 40 | [349.2, "F"], 41 | [370.0, "F#"], 42 | [392.0, "G"], 43 | [415.3, "G#"], 44 | [440.0, "A"], 45 | [466.2, "A#"], 46 | [493.9, "B"] 47 | ] 48 | }, 49 | 3: { 50 | name: "3. G-string", 51 | range: [191, 499], 52 | freqs: [ 53 | [196.0, "G"], 54 | [207.7, "G#"], 55 | [220.0, "A"], 56 | [233.1, "A#"], 57 | [246.9, "B"], 58 | [261.6, "C"], 59 | [277.2, "C#"], 60 | [293.7, "D"], 61 | [311.1, "D#"], 62 | [329.6, "E"], 63 | [349.2, "F"], 64 | [370.0, "F#"], 65 | [392.0, "G"] 66 | ] 67 | }, 68 | 4: { 69 | name: "4. D-string", 70 | range: [142, 289], 71 | freqs: [ 72 | [146.8, "D"], 73 | [155.6, "D#"], 74 | [164.8, "E"], 75 | [174.6, "F"], 76 | [185.0, "F#"], 77 | [196.0, "G"], 78 | [207.7, "G#"], 79 | [220.0, "A"], 80 | [233.1, "A#"], 81 | [246.9, "B"], 82 | [261.6, "C"], 83 | [277.2, "C#"], 84 | [293.7, "D"] 85 | ] 86 | }, 87 | 5: { 88 | name: "5. A-string", 89 | range: [105, 215], 90 | freqs: [ 91 | [110.0, "A"], 92 | [116.5, "A#"], 93 | [123.5, "B"], 94 | [130.8, "C"], 95 | [138.6, "C#"], 96 | [146.8, "D"], 97 | [155.6, "D#"], 98 | [164.8, "E"], 99 | [174.6, "F"], 100 | [185.0, "F#"], 101 | [196.0, "G"], 102 | [207.7, "G#"], 103 | [220.0, "A"] 104 | ] 105 | }, 106 | 6: { 107 | name: "6. E-string (thickest)", 108 | range: [75, 170], 109 | freqs: [ 110 | [82.4, "E"], 111 | [87.3, "F"], 112 | [92.5, "F#"], 113 | [98.0, "G"], 114 | [103.8, "G#"], 115 | [110.0, "A"], 116 | [116.5, "A#"], 117 | [123.5, "B"], 118 | [130.8, "C"], 119 | [138.6, "C#"], 120 | [146.8, "D"], 121 | [155.6, "D#"], 122 | [164.8, "E"], 123 | ] 124 | }, 125 | 7: { 126 | name: "7. For those who do the metal...", 127 | range: [55, 130], 128 | freqs: [ 129 | [61.7, "B"], 130 | [65.4, "C"], 131 | [69.3, "C#"], 132 | [73.4, "D"], 133 | [77.8, "D#"], 134 | [82.4, "E"], 135 | [87.3, "F"], 136 | [92.5, "F#"], 137 | [98.0, "G"], 138 | [103.8, "G#"], 139 | [110.0, "A"], 140 | [116.5, "A#"], 141 | [123.5, "B"], 142 | ] 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /js/song_loader.js: -------------------------------------------------------------------------------- 1 | function SongLoader() { 2 | const randomSongLength = 10; 3 | 4 | Object.keys(gameConfig.strings).forEach(function(string) { 5 | var rows = gameConfig.strings[string].freqs.slice(1, 13); 6 | var notes = rows.map(function(row) { 7 | var note = row[1]; 8 | 9 | return note; 10 | }) 11 | 12 | gameConfig.strings[string].notes = notes; 13 | }); 14 | 15 | const songs = { 16 | "Random notes": randomArray(20, 11).join("--------"), 17 | "Happy Birthday": "0-0-2--0--5-4----0-0-2--0----7-5----0-0-9--7-5-4--2-2----10-10-9--5--7-5", 18 | "Guess what": "0--3--5---0--3--6--5---0--3--5---3--0", 19 | 20 | "Abba: Money Money Money v2 (Tempo=200)": [['F', 4], ['G', 8], ['G#', 4], ['F', 8], ['G', 4], ['G#', 4], ['-', 4], ['G#', 4], ['F', 8], ['G', 4], ['G#', 4], ['-', 4], ['G', 4], ['F', 8], ['G#', 4], ['G#', 4], ['-', 16], ['F', 1]], 21 | 22 | "Coca Cola(Tempo=125)": [['A', 8], ['A', 8], ['A', 8], ['A', 8], ['A#', 4], ['A', 8], ['G', 4], ['G', 8], ['C', 8], ['A', 4], ['F', 4], ['-', 2]], 23 | 24 | "Eiffel 65: Blue v1 (Tempo=140)": [['A', 4], ['A#', 4], ['G', 8], ['A#', 8], ['C', 8], ['F', 8], ['A', 8], ['A#', 4], ['G', 8], ['A#', 8], ['D', 8], ['D#', 4], ['D', 8], ['C', 8], ['A#', 4], ['G', 8], ['A#', 8], ['C', 8], ['F', 8], ['A', 8], ['A#', 4], ['G', 8], ['A#', 8], ['D', 8], ['D#', 4], ['D', 8], ['C', 8], ['A#', 4], ['G', 8], ['A#', 8], ['C', 8], ['F', 8], ['A', 8], ['A#', 4], ['G', 8], ['A#', 8], ['D', 8], ['D#', 4], ['D', 8], ['C', 8], ['A#', 4], ['G', 8], ['A#', 8], ['A', 8], ['F', 8], ['F', 8], ['G', 2]], 25 | 26 | "Europe: The Final Countdown (Tempo=125)": [['-', 4], ['-', 8], ['C', 16], ['A#', 16], ['C', 4], ['F', 4], ['-', 4], ['-', 8], ['C#', 16], ['C', 16], ['C#', 8], ['C', 8], ['A#', 4], ['-', 4], ['-', 8], ['C#', 16], ['C', 16], ['C#', 4], ['F', 4], ['-', 4], ['-', 8], ['A#', 16], ['G#', 16], ['A#', 8], ['G#', 8], ['G', 8], ['A#', 8], ['G#', 4], ['-', 8], ['G', 16], ['G#', 16], ['A#', 4], ['-', 8], ['G#', 16], ['A#', 16], ['C', 8], ['A#', 8], ['G#', 8], ['G', 8], ['F', 4], ['C#', 4], ['C', 2], ['-', 4], ['C', 16], ['C#', 16], ['C', 16], ['A#', 16], ['C', 1]], 27 | 28 | "Haddaway: What is Love (Tempo=225)": [['A#', 4], ['A', 4], ['A#', 4], ['G', 4], ['A#', 4], ['A', 4], ['A#', 4], ['G', 4], ['A#', 4], ['A', 4], ['A#', 4], ['F', 4], ['A#', 4], ['A', 4], ['A#', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4]], 29 | 30 | 31 | "James Bond: Tomorrow Never Dies (Tempo=125)": [['F', 8], ['G', 16], ['G', 16], ['G', 8], ['G', 4], ['F', 8], ['F', 8], ['F', 8], ['F', 8], ['G#', 16], ['G#', 16], ['G#', 8], ['G#', 4], ['G', 8], ['G', 8], ['G', 8], ['F', 8], ['G', 16], ['G', 16], ['G', 8], ['G', 4], ['F', 8], ['F', 8], ['F', 8], ['F', 8], ['G#', 16], ['G#', 16], ['G#', 8], ['G#', 4], ['G', 8], ['G', 8], ['G', 8]], 32 | 33 | 34 | "Nirvana: Come as You Are (Tempo=225)": [['F', 8], ['F', 8], ['F#', 8], ['G', 8], ['-', 4], ['-', 4], ['A#', 8], ['G', 8], ['A#', 8], ['G', 8], ['G', 8], ['F#', 8], ['F', 8], ['C', 8], ['F', 8], ['F', 8], ['-', 4], ['-', 4], ['C', 8], ['F', 8], ['F#', 8], ['G', 8], ['-', 4], ['-', 4], ['A#', 8], ['G', 8], ['A#', 8], ['G', 8], ['G', 8], ['F#', 8], ['F', 8], ['C', 8], ['F', 8], ['F', 8], ['-', 4], ['-', 4], ['C', 8]], 35 | 36 | 37 | "Ricky Martin: Livin La Vida Loca (Tempo=160)": [['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 16], ['-', 32], ['F#', 8], ['G#', 8], ['B', 16], ['-', 8], ['-', 16], ['B', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 4], ['-', 8], ['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 16], ['-', 32], ['F#', 8], ['F', 8], ['G#', 8], ['-', 8], ['G#', 8], ['-', 8], ['F#', 4], ['-', 4], ['-', 8], ['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 8], ['F#', 8], ['G#', 8], ['B', 16], ['-', 8], ['-', 16], ['B', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 4], ['-', 8], ['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 8], ['F#', 8], ['F', 8], ['G#', 16], ['-', 8], ['-', 16], ['G#', 8], ['-', 8], ['F#', 4], ['-', 8]], 38 | 39 | "Smoke on the Water (Tempo=112)": [['F', 4], ['G#', 4], ['A#', 4], ['F', 4], ['G#', 4], ['B', 8], ['A#', 4], ['-', 4], ['F', 4], ['G#', 4], ['A#', 4], ['G#', 4], ['F', 4], ['-', 2], ['-', 8], ['F', 4], ['G#', 4], ['A#', 4], ['F', 4], ['G#', 4], ['B', 8], ['A#', 4], ['-', 4], ['F', 4], ['G#', 4], ['A#', 4], ['G#', 4], ['F', 4], ['-', 4]] 40 | } 41 | 42 | function parseSong(encodedSong, string) { 43 | let song = []; 44 | let duration = 0; 45 | let last_note; 46 | for (let i = 0; i < encodedSong.length; i++){ 47 | if (encodedSong[i] != "-"){ 48 | if (duration > 0){ 49 | song.push([last_note, 8 / duration]); 50 | } 51 | 52 | let fret = parseInt(encodedSong[i]); 53 | last_note = fret === 0 ? "E" : gameConfig.strings[string].notes[fret - 1]; 54 | duration = 0; 55 | } else { 56 | duration += 1; 57 | } 58 | } 59 | song.push([last_note, 8/8.0]); 60 | return song; 61 | } 62 | 63 | 64 | return { 65 | loadSong: function(songIndex, string) { 66 | var encodedSong = songs[songIndex]; 67 | if (encodedSong.constructor === Array){ 68 | return encodedSong; 69 | } 70 | return parseSong(encodedSong, string); 71 | }, 72 | findNoteIndex: function(note, string) { 73 | return gameConfig.strings[string].notes.findIndex(function(n) { 74 | return note === n; 75 | }); 76 | }, 77 | populateSelectMenu: function($songSelect) { 78 | for(song in songs) { 79 | var $option = $(""); 80 | $option.val(song); 81 | $option.text(song); 82 | 83 | if(song === "Random") { 84 | $option.attr("selected", "selected"); 85 | } 86 | 87 | $songSelect.append($option); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /js/audio_processor.js: -------------------------------------------------------------------------------- 1 | // https://github.com/GoogleChrome/guitar-tuner 2 | function AudioProcessor() { 3 | this.FFTSIZE = 2048 * 4; 4 | this.stream = null; 5 | this.audioContext = new AudioContext(); 6 | this.analyser = this.audioContext.createAnalyser(); 7 | this.gainNode = this.audioContext.createGain(); 8 | this.microphone = null; 9 | 10 | this.gainNode.gain.value = 0; 11 | this.analyser.fftSize = this.FFTSIZE; 12 | this.analyser.smoothingTimeConstant = 0.1; 13 | 14 | this.frequencyBufferLength = this.FFTSIZE; 15 | this.frequencyBuffer = new Float32Array(this.frequencyBufferLength / 2); 16 | this.timeBuffer = new Float32Array(this.frequencyBufferLength); 17 | 18 | this.sendingAudioData = false; 19 | 20 | this.lastNoteEnergy = 0; 21 | this.wave_power_threshold = 0.006; 22 | this.last_note_time = -1; 23 | 24 | var audioWaveChart = new AudioWaveChart(); 25 | 26 | var string; 27 | var that = this; 28 | 29 | that.requestUserMedia = function () { 30 | navigator.getUserMedia({audio: true}, (stream) => { 31 | that.sendingAudioData = true; 32 | that.stream = stream; 33 | that.microphone = that.audioContext.createMediaStreamSource(stream); 34 | that.microphone.connect(that.analyser); 35 | that.analyser.connect(that.gainNode); 36 | that.gainNode.connect(that.audioContext.destination); 37 | 38 | requestAnimationFrame(that.dispatchAudioData); 39 | 40 | ga("send", "event", "Game", "MicEnabled"); 41 | 42 | $(".allow-mic").removeClass("allow-mic--active"); 43 | }, (err) => { 44 | ga("send", "event", "Game", "MicDisabled"); 45 | 46 | $(document).trigger("no_mic"); 47 | 48 | $(".allow-mic").removeClass("allow-mic--active"); 49 | 50 | console.log('Unable to access the microphone'); 51 | console.log(err); 52 | }); 53 | } 54 | 55 | this.attached = function() { 56 | // Set up the stream kill / setup code for visibility changes. 57 | document.addEventListener('visibilitychange', this.onVisibilityChange); 58 | 59 | // Then call it. 60 | this.onVisibilityChange(); 61 | } 62 | 63 | this.detached = function() { 64 | this.sendingAudioData = false; 65 | } 66 | 67 | this.onVisibilityChange = function() { 68 | if (document.hidden) { 69 | that.sendingAudioData = false; 70 | 71 | if (that.stream) { 72 | // Chrome 47+ 73 | that.stream.getAudioTracks().forEach((track) => { 74 | if ('stop' in track) { 75 | track.stop(); 76 | } 77 | }); 78 | 79 | // Chrome 46- 80 | if ('stop' in that.stream) { 81 | that.stream.stop(); 82 | } 83 | } 84 | 85 | that.stream = null; 86 | } else { 87 | that.requestUserMedia(); 88 | } 89 | 90 | } 91 | 92 | this.setString = function(string_num){ 93 | string = gameConfig.strings[string_num]; 94 | } 95 | 96 | this.findNoteFreq = function(time) { 97 | let freq_step = that.audioContext.sampleRate / this.FFTSIZE; 98 | let min_freq_ind = Math.round(string.range[0] / freq_step); 99 | let max_freq_ind = Math.round(string.range[1] / freq_step); 100 | 101 | // Fill up the data. 102 | 103 | that.analyser.getFloatTimeDomainData(that.timeBuffer); 104 | that.analyser.getFloatFrequencyData(that.frequencyBuffer); 105 | freq = that.frequencyBuffer; 106 | wave = that.timeBuffer; 107 | 108 | audioWaveChart.plotWave(wave); 109 | 110 | for (let d = Math.round(Math.max(min_freq_ind - 20 / freq_step - 5, 0)); d < Math.min(max_freq_ind + 20 / freq_step + 5, freq.length); d++) { 111 | freq[d] = Math.pow(10, 5 + freq[d] / 10); 112 | } 113 | 114 | 115 | let max_A = -1000000; 116 | let arg_max = -1; 117 | for (let i = min_freq_ind; i < max_freq_ind; i++) { 118 | if (freq[i] > max_A){ 119 | max_A = freq[i]; 120 | arg_max = i; 121 | } 122 | } 123 | 124 | let total_energy = 0; 125 | for (let i = min_freq_ind; i < max_freq_ind; i++) { 126 | total_energy += freq[i]; 127 | } 128 | 129 | let maximum_energy = 0; 130 | for (let i = Math.round(arg_max - 20 / freq_step - 1); i <= Math.round(arg_max + 20 / freq_step + 1); i++){ 131 | maximum_energy += freq[i]; 132 | } 133 | 134 | if (maximum_energy < 0.1 || maximum_energy / total_energy < 0.96){ 135 | return -1; 136 | } 137 | 138 | if (time > this.last_note_time + 100){ 139 | this.lastNoteEnergy = 0; 140 | } 141 | 142 | // if (maximum_energy < this.lastNoteEnergy){ 143 | // return -1; 144 | // } 145 | // console.log(arg_max * freq_step, maximum_energy); 146 | 147 | this.last_note_time = time; 148 | this.lastNoteEnergy = maximum_energy; 149 | 150 | return arg_max * freq_step; 151 | } 152 | 153 | 154 | this.dispatchAudioData = function(time) { 155 | 156 | if (that.sendingAudioData) { 157 | requestAnimationFrame(that.dispatchAudioData); 158 | } 159 | 160 | let frequency = that.findNoteFreq(time); 161 | if (frequency < 0){ 162 | return; 163 | } 164 | 165 | let freqs = string.freqs; 166 | 167 | let min_freq_error = 10000; 168 | let best_chord_ind = 0; 169 | for (let i = 0; i < freqs.length; i++){ 170 | let chord_freq = freqs[i][0]; 171 | let chord = freqs[i][1]; 172 | 173 | //let n_div = frequency / chord_freq; //for future 174 | let n_div = 1; 175 | let error = Math.abs( Math.round(n_div) * chord_freq - frequency); 176 | if (error < min_freq_error){ 177 | best_chord_ind = i; 178 | min_freq_error = error; 179 | } 180 | } 181 | 182 | if (min_freq_error < 20){ 183 | $(document).trigger("note_detected", freqs[best_chord_ind][1]); 184 | } 185 | 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } 8 | 9 | html { 10 | box-sizing: border-box; 11 | } 12 | 13 | *, *:before, *:after { 14 | box-sizing: inherit; 15 | } 16 | 17 | .allow-mic { 18 | display: none; 19 | 20 | background-color: transparent; 21 | } 22 | 23 | .allow-mic--active { 24 | display: block; 25 | } 26 | 27 | .allow-mic__arrow { 28 | position: absolute; 29 | left: 408px; 30 | top: 60px; 31 | width: 60px; 32 | height: 72px; 33 | 34 | background-image: url("../img/allow_mic_arrow.png"); 35 | background-size: cover; 36 | } 37 | 38 | .allow-mic__message { 39 | position: absolute; 40 | width: 250px; 41 | left: 330px; 42 | top: 140px; 43 | 44 | font-family: 'Kalam', cursive; 45 | font-size: 24px; 46 | color: #F1FAEE; 47 | } 48 | 49 | .header { 50 | display: flex; 51 | align-items: center; 52 | height: 60px; 53 | } 54 | 55 | .header__audio-wave { 56 | position: absolute; 57 | top: 2px; 58 | left: 10px; 59 | } 60 | 61 | .header__score { 62 | display: none; 63 | position: absolute; 64 | top: 20px; 65 | right: 40px; 66 | 67 | font-size: 32px; 68 | line-height: 48px; 69 | font-weight: bold; 70 | color: #A8DADC; 71 | } 72 | 73 | .real-guitar-hero { 74 | background-color: #1D3557; 75 | } 76 | 77 | .real-guitar-hero__header { 78 | position: absolute; 79 | } 80 | 81 | .real-guitar-hero__canvas { 82 | } 83 | 84 | .real-guitar-hero__settings { 85 | display: block; 86 | position: absolute; 87 | top: 10px; 88 | right: 10px; 89 | 90 | font-family: 'Source Sans Pro', sans-serif; 91 | font-weight: bold; 92 | color: #F1FAEE; 93 | text-decoration: none; 94 | } 95 | 96 | .game-settings { 97 | width: 100%; 98 | } 99 | 100 | 101 | .game-settings__bpm-input-label { 102 | display: block; 103 | 104 | font-family: 'Source Sans Pro', sans-serif; 105 | font-weight: bold; 106 | color: #1D3557; 107 | text-decoration: none; 108 | } 109 | 110 | .game-settings__bpm-input { 111 | display: block; 112 | padding: 8px; 113 | width: 100%; 114 | 115 | font-size: 24px; 116 | line-height: 32px; 117 | } 118 | 119 | .game-settings__song-select-label, 120 | .game-settings__string-select-label, 121 | .game-settings__mode-select-label { 122 | display: block; 123 | margin-top: 16px; 124 | 125 | font-family: 'Source Sans Pro', sans-serif; 126 | font-weight: bold; 127 | color: #1D3557; 128 | text-decoration: none; 129 | } 130 | 131 | .game-settings__song-select, 132 | .game-settings__string-select { 133 | display: block; 134 | width: 100%; 135 | height: 52px; 136 | 137 | background-color: #FFF; 138 | 139 | font-family: 'Source Sans Pro', sans-serif; 140 | color: #1D3557; 141 | text-decoration: none; 142 | font-size: 18px; 143 | line-height: 24px; 144 | } 145 | 146 | .game-settings__mode-select { 147 | font-family: 'Source Sans Pro', sans-serif; 148 | color: #1D3557; 149 | text-decoration: none; 150 | font-size: 18px; 151 | line-height: 24px; 152 | } 153 | 154 | .game-settings__mode-select-hint { 155 | font-size: 14px; 156 | line-height: 14px; 157 | } 158 | 159 | .game-settings__mode-select-item + .game-settings__mode-select-item { 160 | margin-top: 10px; 161 | } 162 | 163 | .real-guitar-hero__score { 164 | font-size: 24px; 165 | color: #0654C8; 166 | } 167 | 168 | .sidebar-menu { 169 | display: none; 170 | flex-direction: column; 171 | justify-content: space-between; 172 | padding: 16px 20px; 173 | position: absolute; 174 | width: 400px; 175 | height: 100%; 176 | top: 0; 177 | right: 0; 178 | z-index: 100; 179 | 180 | background-color: #E4EEFB; 181 | } 182 | 183 | .sidebar-menu--active { 184 | display: flex; 185 | } 186 | 187 | .sidebar-menu__footer { 188 | text-align: right; 189 | } 190 | 191 | .footer-link { 192 | font-family: 'Source Sans Pro', sans-serif; 193 | font-size: 16px; 194 | line-height: 20px; 195 | color: #1D3557; 196 | text-decoration: none; 197 | } 198 | 199 | .sidebar-menu__footer a + a { 200 | margin-left: 15px; 201 | } 202 | 203 | .sidebar-menu__header { 204 | margin: 0; 205 | padding: 0; 206 | 207 | font-family: 'Source Sans Pro', sans-serif; 208 | font-size: 24px; 209 | line-height: 32px; 210 | color: #1D3557; 211 | } 212 | 213 | .sidebar-menu__subheader { 214 | margin-top: 20px; 215 | 216 | font-family: 'Source Sans Pro', sans-serif; 217 | font-size: 16px; 218 | line-height: 20px; 219 | color: #1D3557; 220 | } 221 | 222 | .sidebar-menu__settings { 223 | margin-top: 20px; 224 | } 225 | 226 | .sidebar-menu__start-button { 227 | margin: 50px auto 0; 228 | display: block; 229 | width: 350px; 230 | padding: 20px 25px; 231 | 232 | border: none; 233 | border-radius: 2px; 234 | 235 | background-color: #7C69F4; 236 | 237 | font-family: 'Source Sans Pro', sans-serif; 238 | font-weight: bold; 239 | font-size: 24px; 240 | line-height: 32px; 241 | color: #FFF; 242 | letter-spacing: 4px; 243 | 244 | outline: none; 245 | cursor: pointer; 246 | } 247 | 248 | .sidebar-menu__start-button:hover { 249 | opacity: 0.9; 250 | } 251 | 252 | .sidebar-menu__install-chrome, 253 | .sidebar-menu__update-chrome { 254 | display: none; 255 | margin-top: 10px; 256 | 257 | font-family: 'Source Sans Pro', sans-serif; 258 | font-size: 16px; 259 | line-height: 20px; 260 | color: #1D3557; 261 | } 262 | 263 | .sidebar-menu__install-chrome--active, 264 | .sidebar-menu__update-chrome--active { 265 | display: block; 266 | } 267 | 268 | .sidebar-menu__install-chrome i, 269 | .sidebar-menu__update-chrome i { 270 | color: red; 271 | } 272 | 273 | .sidebar-menu__install-chrome a, 274 | .sidebar-menu__update-chrome a { 275 | font-weight: bold; 276 | text-decoration: none; 277 | } 278 | 279 | .sidebar-menu__install-chrome a:hover, 280 | .sidebar-menu__update-chrome a:hover { 281 | text-decoration: underline; 282 | } 283 | 284 | .sidebar-menu__sharing-buttons { 285 | display: flex; 286 | margin-top: 20px; 287 | align-items: center; 288 | justify-content: center; 289 | } 290 | 291 | .sharing-buttons { 292 | display: flex; 293 | margin-bottom: 20px; 294 | } 295 | 296 | .share-button { 297 | display: inline-block; 298 | padding: 10px 15px; 299 | 300 | border-radius: 2px; 301 | 302 | font-family: 'Source Sans Pro', sans-serif; 303 | font-size: 16px; 304 | line-height: 20px; 305 | color: #F1FAEE; 306 | text-decoration: none; 307 | } 308 | 309 | .share-button:hover { 310 | opacity: 0.9; 311 | } 312 | 313 | .share-button i { 314 | margin-right: 4px; 315 | } 316 | 317 | .share-button + .share-button { 318 | margin-left: 8px; 319 | } 320 | 321 | .share-button--fb { 322 | background-color: #3b5998; 323 | } 324 | 325 | .share-button--tw { 326 | background-color: #4099FF; 327 | } 328 | 329 | .audio-wave { 330 | display: none; 331 | 332 | width: 60px; 333 | height: 32px; 334 | } 335 | 336 | .audio-wave--active { 337 | display: block; 338 | } 339 | 340 | .audio-wave path { 341 | stroke: #F1FAEE; 342 | stroke-width: 1; 343 | fill: none; 344 | opacity: 0.5; 345 | } 346 | 347 | .no-sound { 348 | display: none; 349 | padding-top: 8px; 350 | 351 | color: white; 352 | font-size: 30px; 353 | } 354 | 355 | .no-sound--active { 356 | display: block; 357 | } 358 | 359 | .github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}} 360 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |