├── .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 |
6 |
--------------------------------------------------------------------------------
/img/play.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------