nycki.net/static/qrplay/index.html
nycki 67405ad164
All checks were successful
/ build (push) Successful in 30s
hotfix, percussion bug
2025-07-10 15:33:43 -07:00

230 lines
No EOL
9.5 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<title>qrplay</title>
<style>
body {
font-family: sans-serif;
max-width: 40rem;
margin: 0 auto;
padding: 1rem;
}
</style>
</head>
<body>
<nav><a href="/">home</a></nav>
<h2>qrplay by nycki and SArpnt</h2>
<p>a QR-code sized implementation of ZZT Play, which you may know from the <a href="https://chriskallen.com/zzt/hallofmusic.html">ZZT Hall of Music</a>.</p>
<p>I designed this because I wanted a program I could print on a business card, and <a href="https://git.disroot.org/SArpnt">SArpnt</a> found some clever ways to shrink it even further. The <a href="https://git.hatspace.net/nycki/nycki.net/src/branch/main/static/qrplay/index.html">source</a> on this page is commented, the source in the QR code is minified.</p>
<p>paste ZZT music code in this box, then click the button to #play!</p>
<!-- snip here -->
<meta name=viewport content=initial-scale=1>
<center>
<p><a href=http://nycki.net/qrplay>qrplay v2c</a></p>
<textarea id=f style=width:min(40em,99%);height:9lh>; #title nupa's theme
; #authors nycki bsp
z00
@qcceg hcqeg dcdc d#h.d
@qcceg hcqeg dcd#d wc
z01
@-wc b! a a!
@-wc b! haqa!g w-c</textarea><p>
<button onclick='
s=`@`+f.value.toLowerCase();
f.A?.close();
A=f.A=new AudioContext;
// per-channel data
T=[];L=[];
// tempo in beats per minute
u=137;
// volume. 40 is default, 35 is half, 30 is half again.
v=40;
for(
c=t=k=Z=0;D=s[c+1],E=D+s[c+2],C=s[c++];
// no match
z<0?0:
// comments
z<2?k=z:k?0:
// - + octave
z<4?o+=C+1|0:
// dots and triplets
z<5?l*=3/2:
z<6?l/=3:
// note or rest
z<14?(
z-=6,
O=new OscillatorNode(A,{
type:`square`,
detune:100*(z*2-(z>2)-(D==`!`)+(D==`#`)+12*o-9)
}),
G=new GainNode(A,{gain:z<7&&.5**(12-v/5)}),
O.connect(G),
G.connect(A.destination),
O.start(t),
O.stop(t+=7.5*l/u)
):
// percussion
z<23?(
// insert micronotes to match ZZTs drums. random numbers chosen in advance.
s=`z99o06${[
"+g",
"-b+c#deff#gg#aa#b+cc#d",
"g++ddb--g++ddb--g++ddb--g++ddb",
"+ea-g--b++g++c#----b++g+cd#-g+cec",
"a#ga#g-a#+g-b+g",
"-a++c#-aae+e--a++c#-aae+e--a++c#",
"-fffeeed#d#dddc#c#c#",
"dddddedc#ded#fee",
"--ba#bb-a+a#-baaa+a#bb-a",
][z-14]}z${Z} o${o+4} x`+s.slice(c),c=0,T[99]=t,L[99]=u/2**12
):
// reset octave and duration
z<24?(o=0,l=1):
// set note length
z<31?l=2**(z-25):
// change channel
z<32?(
T[Z]=t,L[Z]=l,
t=T[Z=E|0]||0,l=L[Z]||l,
c+=2
):
// change tempo
z<33?u=E+s[c+=2,c++]:
// change volume
z<34?(v=E,c+=2):
// set octave
(o=E-4,c+=2)
)z=`\n;-+.3cdefgabx012456789@jtsiqhwzuvo`.indexOf(C)
'>#play</button> <button onclick=f.A.close()>#stop</button>
<!-- stop snipping here -->
</center>
<script>
// set default value for input
f.value=(
window.location.search.slice(1)
.replaceAll('%20', ' ')
.replaceAll('%0a','\n')
.replaceAll('%0A','\n')
.replaceAll('%23', '#')
.replaceAll('%27', "'")
|| f.value
);
// remove redundant title
document.querySelector('center p').setAttribute('hidden', 'true');
// add save button
document.querySelector('center p:last-child').innerHTML+=`
<button onclick='
window.location.search = "?"+f.value
.replaceAll(" ","%20")
.replaceAll("\\n","%0a")
.replaceAll("#","%23")
.replaceAll("\\x27","%27")
'>#save</button>
`;
</script>
<h2>qr codes</h2>
<p>for security reasons, your phone probably won't open these as links. you'll have to copy and paste the text into your browser by yourself. it requires no permissions or internet access to run.</p>
<details open>
<summary>qrplay v2: added percussion, chords, volume control, sample song.</summary>
<div style="text-align: center;">
<p><strong>qrplay v2</strong></p>
<img src="qrplay-v2-nupas-theme.png">
<p><textarea>data:text/html,&lt;meta%20name=viewport%20content=initial-scale=1&gt;&lt;center&gt;&lt;p&gt;&lt;a%20href=http://nycki.net/qrplay&gt;qrplay%20v2c&lt;/a&gt;&lt;/p&gt;&lt;textarea%20id=f%20style=width:min(40em,99%);height:9lh&gt;;%20%23title%20nupa's%20theme%0A;%20%23authors%20nycki%20bsp%0Az00%0A@qcceg%20hcqeg%20dcdc%20d%23h.d%0A@qcceg%20hcqeg%20dcd%23d%20wc%0Az01%0A@-wc%20b!%20a%20a!%0A@-wc%20b!%20haqa!g%20w-c&lt;/textarea&gt;&lt;p&gt;&lt;button%20onclick='s=`@`+f.value.toLowerCase();f.A?.close();A=f.A=new%20AudioContext;T=[];L=[];u=137;v=40;for(c=t=k=Z=0;D=s[c+1],E=D+s[c+2],C=s[c++];z&lt;0?0:z&lt;2?k=z:k?0:z&lt;4?o+=C+1|0:z&lt;5?l*=3/2:z&lt;6?l/=3:z&lt;14?(z-=6,O=new%20OscillatorNode(A,{type:`square`,detune:100*(z*2-(z&gt;2)-(D==`!`)+(D==`%23`)+12*o-9)}),G=new%20GainNode(A,{gain:z&lt;7&&.5**(12-v/5)}),O.connect(G),G.connect(A.destination),O.start(t),O.stop(t+=7.5*l/u)):z&lt;23?(s=`z99o06${["+g","-b+c%23deff%23gg%23aa%23b+cc%23d","g++ddb--g++ddb--g++ddb--g++ddb","+ea-g--b++g++c%23----b++g+cd%23-g+cec","a%23ga%23g-a%23+g-b+g","-a++c%23-aae+e--a++c%23-aae+e--a++c%23","-fffeeed%23d%23dddc%23c%23c%23","dddddedc%23ded%23fee","--ba%23bb-a+a%23-baaa+a%23bb-a",][z-14]}z${Z}%20o${o+4}%20x`+s.slice(c),c=0,T[99]=t,L[99]=u/2**12):z&lt;24?(o=0,l=1):z&lt;31?l=2**(z-25):z&lt;32?(T[Z]=t,L[Z]=l,t=T[Z=E|0]||0,l=L[Z]||l,c+=2):z&lt;33?u=E+s[c+=2,c++]:z&lt;34?(v=E,c+=2):(o=E-4,c+=2))z=`\n;-+.3cdefgabx012456789@jtsiqhwzuvo`.indexOf(C)'&gt;%23play&lt;/button&gt;%20&lt;button%20onclick=f.A.close()&gt;%23stop&lt;/button&gt;</textarea></p>
</div>
</details>
<details>
<summary>qrplay v1: proof of concept. musical notes only.</summary>
<div style="text-align: center;">
<p><strong>qrplay v1</strong></p>
<img src="qrplay-v1.png">
<p><textarea>data:text/html,&lt;meta%20name="viewport"%20content="width=device-width%20initial-scale=1.0"&gt;&lt;textarea%20id=f&gt;&lt;/textarea&gt;&lt;br&gt;&lt;button%20onclick='/*qrplay%20v1,%20nycki%20&%20SArpnt,%202025*/v=f.value;T=[];Z=0;f.A=A=f.A||new%20AudioContext;B=new%20GainNode(A,{gain:0.4});B.connect(A.destination);for(i=o=k=t=0,l=1;c=v[i++],d=v[i],c;z&lt;0?0:z&gt;29?k=1:z&gt;28?k=0:k?0:z&gt;27?(T[Z]=t,Z=+v.slice(i,i+=2),t=T[Z]||0):z&gt;17?t+=l:z&lt;3?z?o+=g:(o=0,l=1/8):(console.log(z),z)&1?console.log(l=g):(a=new%20OscillatorNode(A,{type:`square`,detune:100*(g+o+(d==`%23`)-(d==`!`))}),a.connect(B),a.start(t*.4),t+=l,a.stop(t*.4)))g=[,-12,12,4,-9,2,-7,1,-5,.5,-4,.25,-2,1/8,0,l/3,2,l*1.5][z=`@-+wchdqeifsgta3b.x012456789z\npovukr\x27`.indexOf(c.toLowerCase())]'&gt;%23play&lt;/button&gt;&lt;button%20onclick='f.A.close();f.A=0'&gt;%23stop&lt;/button&gt;</textarea></p>
</div>
</details>
<details>
<summary>song: Solfeggettio in C Minor</summary>
<div style="text-align: center;">
<p><textarea>;; title Solfeggettio
;; artist JS Bach
@i-e!ce!g+ce!dc-bgb+dgfe!de!
@ice!g+ce!dcdc-bagfe!d
@ie!ce!g+ce!dc-bgb+dgfe!d
@i+e!ce!g+ce!dcdc-bagfe!d
@i+e!c-ge!c++c-ge!a!--fa!+cfa!+ce!
@i+d-b!fd-b!++b!fdg--e!gb!+dgb!+d
@i+ge!dc-ge!dh.c
</textarea></p>
<img src="qrplay-solfeggettio.png">
</div>
</details>
<details>
<summary>song: We Will Rock You</summary>
<div style="text-align: center;">
<p><textarea>U137V40
@s.9x9x6xxx9x9x6xxx9x9x6xxx9x9x6xxx
@s.deedi.es.eei.des.de
@s.edi.es.eeedi.eags.g-b+dx
@s.ei.es.dexxdexexe
@s.xxxdddxdd-bxage+eq.x
@q.gf#eds.exex6xxx9x9x6xxx
@q.gf#eds.exex6xxx9x9x6xxx
@s.deedi.es.eei.des.de
@s.edi.es.eeedi.eags.g-b+dx
@s.ei.es.dexxdexexe
@s.xxxdddxdd-bxage+eq.x
@q.gf#eds.exex6xxx9x9x6xxx
@q.gf#eds.exex6xxx9x9x6xxx
@q.gf#eds.exex6xxx9x9x6xxx
@q.gf#eds.exex6xxx9x9x6xxx
</textarea></p>
</div>
</details>
<h2>command reference</h2>
basic commands:
<ul>
<li>cdefgab: play a note in the current octave.</li>
<li>012456789: play one of various percussion effects. note that there is no effect 3.</li>
<li>x: rest for the length of a note.</li>
<li>-+ change octaves. default is the 4th octave, the one containing middle C.</li>
<li>whqistj: change note length to whole, half, quarter, eigth, sixteenth, thirtytwoth, or sixtyfourth. default is t.</li>
<li>3: cut the current note length into triplets. for instance, <em>q3ceg</em> would play three notes that are each 1/3 the length of a quarter note.</li>
<li>.: dotted note. increases the current note length by 50%. for instance, <em>q.c</em> would play a note that lasts 1.5 times a quarter note.</li>
<li>@: reset octave and note length to defaults.</li>
</ul>
extended commands: this stuff wasn't in ZZT!
<ul>
<li>;: comment. everything from a semicolon until the end of the line is ignored. you can put the name of your song in here!</li>
<li>znn: select track. you can use this to play two or more tracks at once! so for instance, <em>z00c z01+c</em> would play an octave chord. tracks greater than 15 are reserved for internal use.</li>
<li>unnn: set tempo in beats per minute. for historic reasons, u137 is the default.</li>
<li>vnn: set volume, from 00 to 99. this scales exponentially: 40 is the default, then 45 is double that, then 50 is double that, etc.</li>
<li>onn: set octave, from -9 to 99, but you should really only use 00 thru 08. default is 04.</li>
</ul>
not implemented:
<ul>
<li>loops! I'm thinking of implementing something brainfuck-esque so you can make turing machine music... we'll see.</li>
<li>there's a known bug where you can't play two percussion effects on different channels at the same time. but like, if I hadn't told you, I don't think you would have noticed.</li>
<li>change tuning! currently this is tuned to A440 but ZZT is actually slightly out of tune and it would be cool to replicate that.</li>
<li>slides and bends. ZZT Ultra has these and I could probably figure them out but I'd have to rework it to fit in the qr code.</li>
<li>compression: maybe I could rewrite this whole thing to use uppercase letters only so it fits in the QR alphanumeric mode? I'd have to escape every punctuation mark though. might experiment with this later.</li>
</ul>
</body>
</html>