Draw a melody, change the base frequency, tempo and waveform, then play and export it as a tiny JS snippet useable in your mini demos!
for(_="[i]~~=f~[KarJsquJeIi,jG`:F?`=1thonor(dive=intextJeatrianglesawtoo><{wid:18style=[]${value siz2 =<putj=0;j<90;j++)mouse clickC~[j] id==o([G]},is,background;fi;i<26;i++)f(``+t./1e3).replace(`0.`,`.`)} typradio namw =w='d.write`<y/pbr>z440> Hz / t200> ms /' checked>seI'>I'>'><px cols=90 rows=5/pbuttp>PLAY & EXPORT<>body{mJg:1em}#y00px}p{cleJ:bo}.npx;height:18px;float:left;border:1px solid}[l=\"13\"]{:#aaa`;C;w=h=``C~,m=h+=`< class=n l=i} 0) over1) =:#(!j&&[1,3,6,8,11,13,15,18,20,23].cludes(i-1))000F(!j||i=3)F`ddd`}/>`;y.nerHTML=h; down= up=>m^;o=(G,t,f)=>j&&(f&&m?:f||(^),t..=#000Fi=3#aaaF`#ddd`);p.=>{fif()f.push([G]);k;v;fi f)kK0],vK1];eval(x.=`wi(new AudioCtext)[k}].map((v,i)=>{wi(createOscillat))v&&stJt([v}]~*,cnect(destati),frequency.setValueAtTime(z.}*1.06**(12-v),0)w,typ'w}'F``}),stop(e+)})`)}";G=/[^ -EHL-}]/.exec(_);)with(_.split(G))_=join(shift());eval(_)
Zm9yKF89IltpXX5+PWZ+W0thckpzcXVKZUlpLGpHYDpGP2AfPTEedGgdb24cb3IoG2RpdhplPRlpbhh0ZXh0SmVhF3RyaWFuZ2xlFnNhd3Rvbx0VPjwUe3dpZB06MTgTc3R5bGUSPVtdESR7EHZhbHVlDyBzaXoZMiAPPQ48GHB1dAxqPTA7ajw5MDtqKyspCxxtb3VzZQkcY2xpY2sIQ35bal0HIGlkPQY9bygQW0ddfSwdaXMsBWJhY2tncm91bmQEO2YbaR47aTwyNjtpKyspZhsDEChgYCt0Lg8vMWUzKS5yZXBsYWNlKGAwLmAsYC5gKX0CIAwgdHlwGXJhZGlvIG5hbRl3IAg9dz0nAWQud3JpdGVgPBoGeRQvGhRwFGJyPgwGeg40NDA+IEh6IC8gDAZ0DjIwMD4gbXMgLwEnIGNoZWNrZWQ+cxhlAUknPkkBFSc+FQEWJz4WPHAUFwZ4IGNvbHM9OTAgcm93cz01FC8XFHAUYnV0dBwGcD5QTEFZICYgRVhQT1JUPBI+Ym9keXttSmcYOjFlbX0jeRMwMHB4fXB7Y2xlSjpibx19Lm4TcHg7aGVpZ2h0OjE4cHg7ZmxvYXQ6bGVmdDtib3JkZXI6MXB4IHNvbGlkfVtsPVwiMTNcIl17BDojYWFhYDtDETt3PWg9YGADQ34RLG09C2grPWA8GiBjbGFzcz1uIGw9EGl9IAgFMCkgCW92ZXIFMSkgEj0EOiMQKCFqJiZbMSwzLDYsOCwxMSwxMywxNSwxOCwyMCwyM10uGGNsdWRlcyhpLTEpKR8wMDBGKCFqfHxpPR4zKR9GYGRkZGB9FC8aPmA7eS4YbmVySFRNTD1oOwlkb3duPQl1cD0ZPm1eHjtvPShHLHQsZik9PmomJihmJiZtPwceOmZ8fCgHXh4pLHQuEi4EPQcfIzAwMEZpPR4zHyNhYWFGYCNkZGRgKTtwLgg9GT57ZhEDC2lmKAcpZi5wdXNoKFtHXSk7axE7dhE7ZhtpIBggZilrSzBdLHZLMV07ZXZhbCh4Lg89YHdpHShuZXcgQXVkaW9DHHRleHQpWxBrfV0ubWFwKCh2LGkpPT57d2kdKGNyZWF0ZU9zY2lsbGF0GykpdiYmc3RKdCgZWxB2fV1+KgIsYxxuZWN0KGRlc3QYYXRpHCksZnJlcXVlbmN5LnNldFZhbHVlQXRUaW1lKBB6Lg99KjEuMDYqKigxMi12KSwwKRB3Hyx0eXAZJxB3fSdGYGB9KSxzdG9wKGUrAil9KWApfSI7Rz0vW14gLUVITC19XS8uZXhlYyhfKTspd2l0aChfLnNwbGl0KEcpKV89am9pbihzaGlmdCgpKTtldmFsKF8p
// Draw the GUI (HTML + CSS)
// The radio buttons set the type of waveform (w) used by the AudioContext's Oscillator, directly on click.
// The other elements have ids or classes used by the CSS and the JS code below.
d.write`
<div id=c></div>
<p><br><input id=z size=2 value=440> Hz /
<input id=t size=2 value=200> ms /
<input type=radio name=w onclick=w='' checked>sine <input type=radio name=w onclick=w='square'>square <input type=radio name=w onclick=w='sawtooth'>sawtooth <input type=radio name=w onclick=w='triangle'>triangle
<p><textarea id=x cols=90 rows=5></textarea>
<p><button id=p>PLAY & EXPORT
<style>
#b {
margin: 1em
}
#c {
width: 1800px
}
p {
clear: both
}
.n{
width: 18px;
height: 18px;
float: left;
border: 1px solid
}
[l="13"]{
background: #aaa
`;
// Reset the data array (d), the grid's innerHTML (h) and the default waveform (w).
d = []; w = h = ``;
// Loop on the 25 lines of the grid (loop var: i).
for(i = 1; i < 26; i++)
// Initialize an array in d[i].
// Reset the mousedown flag (m).
// Loop on the 90 columns of the grid (loop var: j).
for(d[i] = [], m = j = 0; j < 90; j++)
// Add the cell's HTML code in the string h.
// The background color is set to light gray for normal cells, darker grey for the middle line (440Hz), and piano key colors (black and white) for the legend (the first column).
h+=`<div class=n l=${i} onclick=o(${[i,j]},this,0) onmouseover=o(${[i,j]},this,1) style=background:#${
(!j && [1, 3, 6, 8, 11, 13, 15, 18, 20, 23].includes(i - 1)) ? `000` : (j == 0 || i == 13) ? `` : `ddd`
}></div>`;
// Draw the grid in the div with the id "c".
c.innerHTML = h;
// Toggle the mousedown flag on mouse down and on mouse up.
onmousedown = onmouseup = e => m ^= 1;
// The function o() updated the data array (d) and the cells background after each mouse event on a cell:
// The value of the cell is toggled (0/1) on click (d[i][j] ^= 1).
// The value of the cell is set to 1 on hover, if the mousedown flag is set.
// The first column is ignored (because it's the legend).
// If the cell's value is set, its background becomes black. If it's unset, the background is reset to light or dark grey.
o = (i, j, t, f) => j && (f && m ? d[i][j] = 1 : f || (d[i][j] ^= 1), t.style.background = d[i][j] ? `#000` : 13 == i ? `#aaa` : `#ddd`);
// Click on the "play & export" button:
p.onclick = e => {
// Initialize an array of frequencies (f).
f = [];
// Loop on all the lines of the grid.
for(i = 1; i < 26; i++)
// Loop on all the columns of the grid.
for(j = 0; j < 90; j++)
// If that cell is set in d, add its coordinates to f.
if(d[i][j])
f.push([i, j]);
// Initialize a keys array (k).
k = [];
// Initialize a values array (v).
v = [];
// Loop in f.
for(i in f)
// Add every value of f in k and v.
k[i] = f[i][0],
v[i] = f[i][1];
// The following code generates the JS snippet that plays the music, and evaluates it:
eval(
// This snippet is put in the textarea called x.
x.value = `
// Use a new AudioContext implicitly (with "with").
with(new AudioContext)
// The array k is written here, and we loop in its values.
// v represents the current value of k, and i the index of this value.
[${k}].map((v, i) => {
// For each key in k, create an Oscillator and use it implicitly.
with(createOscillator())
// If v is not null:
v &&
// The array v is written here, and v[i] is stored in e.
// Start the oscillator after e * (t.value / 1000) seconds.
// t.value is the value if the text input representing the length of each note in ms.
// If "t.value / 1000" is lower than 1, we use a string replace to remove the trailing 0 (ex: "0.2" => ".2").
start(e = [${v}][i] * ${(`` + t.value / 1e3).replace(`0.`, `.`)},
// Connect the oscillator to the AudioContext's destination (i.e. the speakers).
connect(destination),
// Set the frequency of the oscillator.
// This used to be done with a simple "frequency.value = f", but Chrome has deprecated this notation in 2018, in profit to "frequency.setValueAtTime(f, t)".
// The time (t) is usually AudioContext.currentTime, but it's set to O here to save bytes.
// The frequency (f) is computed with the formula F * sqrt(1/12) ^ N (where F is the middle line frequency -440Hz by default- and N is the offset between the current note's line and the middle line (+13 to -13).
// sqrt(1/12) is roughly 1.06.
// If the waveform is different than "sine" (i.e. w is different than ""), we add ",type=${w}" to the generated code to force the waveform stored in w as the oscillator's type.
frequency.setValueAtTime(${z.value} * 1.06 ** (12 - v), 0) ${w ? `,type = '${w}'` : ``}),
// Stop the note after "e + (t.value / 1000)" seconds.
// Here again, the leading zero in "t.value / 1000" is stripped if needed.
stop(e + ${(`` + t.value / 1e3).replace(`0.`, `.`)})
})
`)
}