├── 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 | ![Guitar Bro Snapshot](https://user-images.githubusercontent.com/768070/27518743-17a23ec4-59e7-11e7-8873-5b8ee3be5251.png) 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 = $("