diff --git a/drum-machine.js b/drum-machine.js new file mode 100644 index 0000000..388acf6 --- /dev/null +++ b/drum-machine.js @@ -0,0 +1,261 @@ +// Create the object that contains functions that use web audio to +// make sound. +var audio = new AudioContext(); + +// Create the data for the drum machine. +var data = { + + // `step` represents the current step (or beat) of the loop. + step: 0, + + // `tracks` holds the six tracks of the drum machine. Each track + // has a sound and sixteen steps (or beats). + tracks: [createTrack("gold", note(audio, 880)), + createTrack("gold", note(audio, 659)), + createTrack("gold", note(audio, 587)), + createTrack("gold", note(audio, 523)), + createTrack("gold", note(audio, 440)), + createTrack("dodgerblue", kick(audio))] +}; + + +// Update +// ------ + +// Runs every hundred milliseconds. +setInterval(function() { + + // Increase `data.step` by one. If `data.step` is `15` (the last + // step) loop back around to `0` (the first step). + data.step = (data.step + 1) % data.tracks[0].steps.length; + + // Find all the tracks where the current step is on. Play the + // sounds for those tracks. + data.tracks + .filter(function(track) { return track.steps[data.step]; }) + .forEach(function(track) { track.playSound(); }); +}, 100); + +// Draw +// ---- + +// Get the `screen` object. This is a bundle of functions that draw +// in the canvas element. +var screen = document.getElementById("screen").getContext("2d"); + +// **draw()** draws the drum machine. Called once at the beginning of +// the program. It's then called 60 times a second forever (see the +// call to `requestAnimationFrame()` below). +(function draw() { + + // Clear away the previous drawing. + screen.clearRect(0, 0, screen.canvas.width, screen.canvas.height); + + // Draw all the tracks. + drawTracks(screen, data); + + // Draw the pink square that indicates the current step (beat). + drawButton(screen, data.step, data.tracks.length, "deeppink"); + + // Ask the browser to call `draw()` again in the near future. + requestAnimationFrame(draw); +})(); + +// Handle events +// ------------- + +// **setupButtonClicking()** sets up the event handler that will make +// mouse clicks turn track buttons on and off. +(function setupButtonClicking() { + + // Every time the user clicks on the canvas... + document.getElementById("screen").addEventListener("click", function(e) { + + // ...Get the coordinates of the mouse pointer relative to the + // canvas... + var p = { x: e.offsetX, y: e.offsetY }; + + // ...Go through every track... + data.tracks.forEach(function(track, row) { + + // ...Go through every button in this track... + track.steps.forEach(function(on, column) { + + // ...If the mouse pointer was inside this button... + if (isPointInButton(p, column, row)) { + + // ...Switch it off if it was on or on if it was off. + track.steps[column] = !on; + } + }); + }); + }); +})(); + +// **note()** plays a note with a pitch of `frequency` for `1` second. +function note(audio, frequency) { + return function() { + var duration = 1; + + // Create the basic note as a sine wave. A sine wave produces a + // pure tone. Set it to play for `duration` seconds. + var sineWave = createSineWave(audio, duration); + + // Set the note's frequency to `frequency`. A greater frequency + // produces a higher note. + sineWave.frequency.value = frequency; + + // Web audio works by connecting nodes together in chains. The + // output of one node becomes the input to the next. In this way, + // sound is created and modified. + chain([ + + // `sineWave` outputs a pure tone. + sineWave, + + // An amplifier reduces the volume of the tone from 20% to 0 + // over the duration of the tone. This produces an echoey + // effect. + createAmplifier(audio, 0.2, duration), + + // The amplified output is sent to the browser to be played + // aloud. + audio.destination]); + }; +}; + +// **kick()** plays a kick drum sound for `1` second. +function kick(audio) { + return function() { + var duration = 2; + + // Create the basic note as a sine wave. A sine wave produces a + // pure tone. Set it to play for `duration` seconds. + var sineWave = createSineWave(audio, duration); + + // Set the initial frequency of the drum at a low `160`. Reduce + // it to 0 over the duration of the sound. This produces that + // BBBBBBBoooooo..... drop effect. + rampDown(audio, sineWave.frequency, 160, duration); + + // Web audio works by connecting nodes together in chains. The + // output of one node becomes the input to the next. In this way, + // sound is created and modified. + chain([ + + // `sineWave` outputs a pure tone. + sineWave, + + // An amplifier reduces the volume of the tone from 40% to 0 + // over the duration of the tone. This produces an echoey + // effect. + createAmplifier(audio, 0.4, duration), + + // The amplified output is sent to the browser to be played + // aloud. + audio.destination]); + }; +}; + +// **createSineWave()** returns a sound node that plays a sine wave +// for `duration` seconds. +function createSineWave(audio, duration) { + + // Create an oscillating sound wave. + var oscillator = audio.createOscillator(); + + // Make the oscillator a sine wave. Different types of wave produce + // different characters of sound. A sine wave produces a pure tone. + oscillator.type = "sine"; + + // Start the sine wave playing right now. + oscillator.start(audio.currentTime); + + // Tell the sine wave to stop playing after `duration` seconds have + // passed. + oscillator.stop(audio.currentTime + duration); + + // Return the sine wave. + return oscillator; +}; + +// **rampDown()** takes `value`, sets it to `startValue` and reduces +// it to almost `0` in `duration` seconds. `value` might be the +// volume or frequency of a sound. +function rampDown(audio, value, startValue, duration) { + value.setValueAtTime(startValue, audio.currentTime); + value.exponentialRampToValueAtTime(0.01, audio.currentTime + duration); +}; + +// **createAmplifier()** returns a sound node that controls the volume +// of the sound entering it. The volume is started at `startValue` +// and ramped down in `duration` seconds to almost `0`. +function createAmplifier(audio, startValue, duration) { + var amplifier = audio.createGain(); + rampDown(audio, amplifier.gain, startValue, duration); + return amplifier; +}; + +// **chain()** connects an array of `soundNodes` into a chain. If +// there are three nodes in `soundNodes`, the output of the first will +// be the input to the second, and the output of the second will be +// the input to the third. +function chain(soundNodes) { + for (var i = 0; i < soundNodes.length - 1; i++) { + soundNodes[i].connect(soundNodes[i + 1]); + } +}; + +// **createTrack()** returns an object that represents a track. This +// track contains an array of 16 steps. Each of these are either on +// (`true`) or off (`false`). It contains `color`, the color to draw +// buttons when they are on. It contains `playSound`, the function +// that plays the sound of the track. +function createTrack(color, playSound) { + var steps = []; + for (var i = 0; i < 16; i++) { + steps.push(false); + } + + return { steps: steps, color: color, playSound: playSound }; +}; + +var BUTTON_SIZE = 26; + +// **buttonPosition()** returns the pixel coordinates of the button at +// `column` and `row`. +function buttonPosition(column, row) { + return { + x: BUTTON_SIZE / 2 + column * BUTTON_SIZE * 1.5, + y: BUTTON_SIZE / 2 + row * BUTTON_SIZE * 1.5 + }; +}; + +// **drawButton()** draws a button in `color` at `column` and `row`. +function drawButton(screen, column, row, color) { + var position = buttonPosition(column, row); + screen.fillStyle = color; + screen.fillRect(position.x, position.y, BUTTON_SIZE, BUTTON_SIZE); +}; + +// **drawTracks()** draws the tracks in the drum machine. +function drawTracks(screen, data) { + data.tracks.forEach(function(track, row) { + track.steps.forEach(function(on, column) { + drawButton(screen, + column, + row, + on ? track.color : "lightgray"); + }); + }); +}; + +// **isPointInButton()** returns true if `p`, the coordinates of a +// mouse click, are inside the button at `column` and `row`. +function isPointInButton(p, column, row) { + var b = buttonPosition(column, row); + return !(p.x < b.x || + p.y < b.y || + p.x > b.x + BUTTON_SIZE || + p.y > b.y + BUTTON_SIZE); +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..7f3e87e --- /dev/null +++ b/index.html @@ -0,0 +1,77 @@ + + + + Drum machine + + + + + + + +

Drum machine

+ +
+ +

+ How to write a simple drum machine that runs in the browser. +

+ +

Demo

+ + + +
+

+ Click the buttons to turn the beats on or off. +

+ + + + + + + + +
+
+ + + + +
+

Annotated source code

+ +

+ The heavily annotated source code is 114 lines long. +

+ +

Get the code

+ +

+ Remix on Glitch +

+ +

Licence

+ +

+ The code is open source, under the MIT licence. +

+ +
+ + + + diff --git a/main.css b/main.css new file mode 100644 index 0000000..0bbd13e --- /dev/null +++ b/main.css @@ -0,0 +1,184 @@ +body { + font-family: "Gill Sans WGL W01 Light"; + font-size: 1.15em; + text-rendering: optimizeLegibility; + font-feature-settings: "kern"; + -webkit-font-feature-settings: "kern"; + -moz-font-feature-settings: "kern"; + -moz-font-feature-settings: "kern=1"; + + width: 637px; + margin-left:auto; + margin-right:auto; + padding: 0 10px 0 10px; + color: #000; + + text-align: justify; +} +.button{ + text-decoration:none; + text-align:center; + padding:11px 32px; + border:solid 1px #004F72; + -webkit-border-radius:4px; + -moz-border-radius:4px; + border-radius: 4px; + font:18px Arial, Helvetica, sans-serif; + font-weight:bold; + color:#E5FFFF; + background-color:#3BA4C7; + background-image: -moz-linear-gradient(top, #3BA4C7 0%, #1982A5 100%); + background-image: -webkit-linear-gradient(top, #3BA4C7 0%, #1982A5 100%); + background-image: -o-linear-gradient(top, #3BA4C7 0%, #1982A5 100%); + background-image: -ms-linear-gradient(top, #3BA4C7 0% ,#1982A5 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#1982A5', endColorstr='#1982A5',GradientType=0 ); + background-image: linear-gradient(top, #3BA4C7 0% ,#1982A5 100%); + -webkit-box-shadow:0px 0px 2px #bababa, inset 0px 0px 1px #ffffff; + -moz-box-shadow: 0px 0px 2px #bababa, inset 0px 0px 1px #ffffff; + box-shadow:0px 0px 2px #bababa, inset 0px 0px 1px #ffffff; + +} + +code { + font-family: courier; + font-size: 0.77em; + background-color: #eee; + padding: 1px 3px; +} + +a { + color:#444; +} + +a:visited { + color:#444; +} + +a:hover { + text-decoration:none; + border:none; +} + +h1 { + font-size:2.8em; + font-weight:normal; + margin-left:auto; + margin-right:auto; + text-align:center; + margin-top:0; + margin-bottom:-1px; +} + +h2 { + font-size: 1.6em; + font-weight:normal; + margin: 0 0 15px -1.5px; + padding: 0; + text-align: left; +} + +h3 { + font-size: 1.3em; + font-weight:normal; + margin-top:16px; + margin-bottom:14px; +} + +h4 { + font-size: 1.2em; + font-weight:normal; + margin-top:16px; + margin-bottom:16px; +} + +p { + margin:0 0 15px 0; + line-height: 1.20em; +} + +hr { + color: #aaa; + background-color:#aaa; + height:1px; + border:0; +} + +pre { + font-size: 0.8em; + border-top:1px solid #bbb; + border-bottom:1px solid #bbb; + padding-left:-50px; + margin-top:20px; + margin-bottom:15px; + padding: 1.1em 0; + overflow: scroll; + font-family: courier; + color: #333; +} + +pre .output { + color: #777; +} + +pre .prompt { + color: #00f; +} + +ul { + padding-left: 25px; +} + +li { + padding: 2px 0; +} + +.links { + width: 100%; + text-align: center; +} + +.footer { + text-align: center; +} + +.video-container { + margin-bottom: 20px; +} + +@media handheld, only screen and (max-width: 767px) { + body { + font-family: "Gill Sans W01 Book"; + width: 350px; + font-size: 1.25em; + } + + #fork-me { + display: none; + } + + #desktop-demo { + display: none; + } + + #mobile-demo { + display: block !important; + } + + /* resize videos to screen size */ + + .video-container { + position: relative; + padding-bottom: 56.25%; + padding-top: 30px; height: 0; overflow: hidden; + } + + .video-container iframe, + .video-container object, + .video-container embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb478ed --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "task3drum", + "version": "0.0.1", + "description": "VSUTHEBEST", + "main": "drum-machine.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/JavaSDragon/Task3.git" + }, + "author": "Usov", + "license": "ISC" +} diff --git a/pattern.js b/pattern.js new file mode 100644 index 0000000..08ce14e --- /dev/null +++ b/pattern.js @@ -0,0 +1,48 @@ +window.onload=function() { + bpm.onclick = function () { + setInterval(function () { + + // Increase `data.step` by one. If `data.step` is `15` (the last + // step) loop back around to `0` (the first step). + data.step = (data.step + 1) % data.tracks[0].steps.length; + + // Find all the tracks where the current step is on. Play the + // sounds for those tracks. + data.tracks + .filter(function (track) { + return track.steps[data.step]; + }) + .forEach(function (track) { + track.playSound(); + }); + }, 60); + }; + var mas = new Array (6); + for (var i = 0; i < 6; i++){ + mas[i] = new Array (16); + } + + + save.onclick = function () { + for (var i = 0; i < 6; i++) { + for (var j = 0; j < 16; j++) { + mas[i][j]=data.tracks[i].steps[j] + } + } + + var saveObj = JSON.stringify(mas); + localStorage.setItem("save", saveObj); + }; + + load.onclick = function () { + mas= JSON.parse(localStorage.getItem("save")); + for (var i = 0; i < 6; i++) { + for (var j = 0; j < 16; j++) { + data.tracks[i].steps[j] = mas[i][j]; + } + } + + } +} + +