├── .gitignore ├── favicon.ico ├── img ├── stop.svg └── play.svg ├── js ├── notes.js └── audio.js ├── index.html └── css ├── base.css └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/origamid/natal-audio-api/HEAD/favicon.ico -------------------------------------------------------------------------------- /img/stop.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /img/play.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /js/notes.js: -------------------------------------------------------------------------------- 1 | function mathFrequency(number, total) { 2 | for (let i = 0; i < total; i++) { 3 | number = number + number; 4 | } 5 | return number; 6 | } 7 | 8 | const frequencies = { 9 | C: 16.35, 10 | 'C#': 17.32, 11 | D: 18.35, 12 | Eb: 19.45, 13 | E: 20.6, 14 | F: 21.83, 15 | 'F#': 23.12, 16 | G: 24.5, 17 | 'G#': 25.96, 18 | A: 27.5, 19 | Bb: 29.14, 20 | B: 30.87, 21 | }; 22 | 23 | let notes = {}; 24 | 25 | const keys = Object.keys(frequencies); 26 | for (let i = 0; i <= 6; i++) { 27 | const newNotes = keys.reduce((prev, key) => { 28 | prev[key + i] = mathFrequency(frequencies[key], i); 29 | return prev; 30 | }, {}); 31 | notes = { ...notes, ...newNotes }; 32 | } 33 | 34 | export default notes; 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Natal Audio Api 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | body, 8 | input, 9 | textarea, 10 | button { 11 | font-size: 1rem; 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 13 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 14 | } 15 | 16 | h1, 17 | h2, 18 | h3, 19 | p, 20 | body, 21 | ul { 22 | margin: 0px; 23 | } 24 | 25 | h1, 26 | h2, 27 | h3 { 28 | font-family: Georgia, 'Times New Roman', Times, serif; 29 | margin-bottom: 0.5rem; 30 | } 31 | 32 | label { 33 | display: block; 34 | margin-bottom: 5px; 35 | } 36 | 37 | input, 38 | textarea { 39 | display: block; 40 | padding: 0.5rem; 41 | border: 2px solid #ccc; 42 | background: rgba(0, 0, 0, 0.03); 43 | width: 100%; 44 | margin-bottom: 0.75rem; 45 | border-radius: 2px; 46 | } 47 | 48 | input:focus, 49 | textarea:focus { 50 | outline: none; 51 | border-color: #333; 52 | box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.2); 53 | } 54 | 55 | ul { 56 | list-style: none; 57 | padding: 0px; 58 | } 59 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | body { 4 | background: black; 5 | display: grid; 6 | min-height: 80vh; 7 | align-items: center; 8 | } 9 | 10 | #tree { 11 | margin: 20px auto; 12 | display: grid; 13 | grid-template-columns: repeat(8, 30px); 14 | grid-template-rows: repeat(5, 60px); 15 | place-content: center center; 16 | gap: 5px; 17 | } 18 | 19 | #tree li { 20 | font-size: 0.875rem; 21 | border-radius: 1px; 22 | cursor: pointer; 23 | background: goldenrod; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | transition: 0.3s; 28 | opacity: 0.5; 29 | } 30 | 31 | @keyframes blink { 32 | 0%, 33 | 100% { 34 | box-shadow: none; 35 | opacity: 0.5; 36 | } 37 | 50% { 38 | background: gold; 39 | opacity: 1; 40 | box-shadow: inset 0 0 2px 4px gold; 41 | } 42 | } 43 | 44 | #tree li.blink { 45 | animation: blink 0.4s forwards; 46 | } 47 | 48 | #tree li:nth-child(1) { 49 | grid-column: 4/6; 50 | grid-row: 1; 51 | } 52 | 53 | #tree li:nth-child(2) { 54 | grid-column: 3/5; 55 | grid-row: 2; 56 | } 57 | 58 | #tree li:nth-child(3) { 59 | grid-column: 5/7; 60 | grid-row: 2; 61 | } 62 | 63 | #tree li:nth-child(4) { 64 | grid-column: 2/4; 65 | grid-row: 3; 66 | } 67 | 68 | #tree li:nth-child(5) { 69 | grid-column: 4/6; 70 | grid-row: 3; 71 | } 72 | 73 | #tree li:nth-child(6) { 74 | grid-column: 6/8; 75 | grid-row: 3; 76 | } 77 | 78 | #tree li:nth-child(7) { 79 | grid-column: 1/3; 80 | grid-row: 4; 81 | } 82 | 83 | #tree li:nth-child(8) { 84 | grid-column: 3/5; 85 | grid-row: 4; 86 | } 87 | 88 | #tree li:nth-child(9) { 89 | grid-column: 5/7; 90 | grid-row: 4; 91 | } 92 | 93 | #tree li:nth-child(10) { 94 | grid-column: 7/9; 95 | grid-row: 4; 96 | } 97 | 98 | #tree li:nth-child(11) { 99 | grid-column: 4/6; 100 | grid-row: 5; 101 | } 102 | 103 | button { 104 | display: block; 105 | font-size: 1rem; 106 | background: #555; 107 | /* color: white; */ 108 | border: 0px; 109 | border-radius: 2px; 110 | cursor: pointer; 111 | text-decoration: none; 112 | } 113 | 114 | button:focus, 115 | button:active { 116 | background: #888; 117 | outline: none; 118 | } 119 | 120 | #play { 121 | text-indent: -200px; 122 | background: url('../img/play.svg') no-repeat center center; 123 | } 124 | 125 | #stop { 126 | text-indent: 200px; 127 | background: url('../img/stop.svg') no-repeat center center; 128 | } 129 | 130 | #play, 131 | #stop { 132 | overflow: hidden; 133 | width: 40px; 134 | height: 40px; 135 | background-color: #555; 136 | } 137 | 138 | #play:active, 139 | #stop:active { 140 | background-color: #333; 141 | outline: none; 142 | } 143 | 144 | .buttons { 145 | margin: 2rem auto; 146 | display: grid; 147 | grid-template-columns: 40px 40px; 148 | justify-content: center; 149 | gap: 5px; 150 | } 151 | 152 | #footer { 153 | position: fixed; 154 | bottom: 20px; 155 | color: #888; 156 | text-align: center; 157 | width: 100%; 158 | } 159 | 160 | #footer a { 161 | color: #ccc; 162 | text-decoration: none; 163 | } 164 | -------------------------------------------------------------------------------- /js/audio.js: -------------------------------------------------------------------------------- 1 | import notes from './notes.js'; 2 | 3 | const audioContext = (/** @type {AudioContext} */ context) => (frequency) => { 4 | const o = context.createOscillator(); 5 | const g = context.createGain(); 6 | o.connect(g); 7 | o.type = 'sine'; 8 | o.frequency.value = frequency; 9 | g.gain.value = 0.25; 10 | g.connect(context.destination); 11 | g.gain.setValueAtTime(g.gain.value, context.currentTime); 12 | g.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 2); 13 | o.start(context.currentTime); 14 | setTimeout(() => { 15 | g.gain.setValueAtTime(g.gain.value, context.currentTime); 16 | g.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.03); 17 | o.stop(); 18 | }, 1200); 19 | }; 20 | 21 | window.tID = []; 22 | 23 | function playSong(song) { 24 | const context = new (window.AudioContext || window.webkitAudioContext)(); 25 | const playNote = audioContext(context); 26 | for (let i = 0; i < song.length; i++) { 27 | const tID = setTimeout(() => { 28 | playNote(notes[song[i][0]]); 29 | const li = document.querySelector(`[data-note="${song[i][0]}"]`); 30 | blink(li); 31 | }, song[i][1]); 32 | window.tID.push(tID); 33 | } 34 | } 35 | 36 | document.getElementById('stop').addEventListener('click', () => { 37 | const tID = window.tID || []; 38 | tID.forEach((id) => clearTimeout(id)); 39 | window.tID = []; 40 | }); 41 | 42 | function notesTime(song) { 43 | const songTimed = []; 44 | let acc = 0; 45 | for (let i = 0; i < song.length; i++) { 46 | const time = (song[i - 1] ? song[i - 1][1] : 0) * 300; 47 | acc += time; 48 | songTimed.push([song[i][0], acc]); 49 | } 50 | return songTimed; 51 | } 52 | 53 | const song = [ 54 | ['G3', 2], 55 | ['A3', 1], 56 | ['G3', 2], 57 | ['E3', 4], 58 | ['G3', 2], 59 | ['A3', 1], 60 | ['G3', 2], 61 | ['E3', 4], 62 | ['D4', 3], 63 | ['D4', 2], 64 | ['B3', 3], 65 | ['C4', 3], 66 | ['C4', 2], 67 | ['G3', 3], 68 | ['A3', 3], 69 | ['A3', 2], 70 | ['C4', 2], 71 | ['B3', 1], 72 | ['A3', 3], 73 | ['G3', 2], 74 | ['A3', 1], 75 | ['G3', 2], 76 | ['E3', 3], 77 | ['A3', 2], 78 | ['A3', 1], 79 | ['C4', 3], 80 | ['B3', 1], 81 | ['A3', 2], 82 | ['G3', 3], 83 | ['A3', 1], 84 | ['G3', 2], 85 | ['E3', 3], 86 | ['D4', 2], 87 | ['D4', 1], 88 | ['F4', 2], 89 | ['D4', 2], 90 | ['B3', 2], 91 | ['C4', 3], 92 | ['E4', 3], 93 | ['C4', 2], 94 | ['G3', 1], 95 | ['E3', 2], 96 | ['G3', 2], 97 | ['F3', 2], 98 | ['D3', 2], 99 | ['C3', 4], 100 | ]; 101 | 102 | function blink(el) { 103 | setTimeout(() => el.classList.add('blink')); 104 | setTimeout(() => el.classList.remove('blink'), 400); 105 | } 106 | 107 | window.playGlobalNote = (frequency) => { 108 | const context = new (window.AudioContext || window.webkitAudioContext)(); 109 | const playNote = audioContext(context); 110 | playNote(frequency); 111 | }; 112 | 113 | function makeTree(song) { 114 | const tree = document.getElementById('tree'); 115 | const uniqueNotes = [...new Set(song.map((n) => n[0]))]; 116 | uniqueNotes.map((note) => { 117 | const f = notes[note]; 118 | tree.innerHTML += `
  • ${note}
  • `; 119 | }); 120 | const lis = document.querySelectorAll('#tree li'); 121 | lis.forEach((li) => { 122 | li.addEventListener('click', () => blink(li)); 123 | }); 124 | } 125 | 126 | makeTree(song); 127 | document 128 | .getElementById('play') 129 | .addEventListener('click', () => playSong(notesTime(song))); 130 | --------------------------------------------------------------------------------