index.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  6. <meta name="description" content="cassette, mp3, wav, audio">
  7. <meta name="author" content="bzt">
  8. <title>Cassette</title>
  9. <link href="logo.png" rel="shortcut icon">
  10. <link href="bootstrap.min.css" rel="stylesheet">
  11. <style>
  12. body { margin:10px; }
  13. input, textarea { font-family: monospace,fixed; }
  14. table, button, input[type="text"], input[type="file"] { width:100%; }
  15. input[type="number"] { text-align:right; width:100px;}
  16. small { opacity:50%; }
  17. small[id^="help"] { display:none; }
  18. a[download] { display:none; }
  19. #loading { position:absolute;top:0px;left:0px;width:100%;height:100%;z-index:1024;background:rgba(0,0,0,0.5);cursor:wait; }
  20. #jserr { color:white; background:red; font-weight:bold; font-size: 150%; text-align:center; padding: 32px; }
  21. .nav-tabs li {position: relative;bottom: -1px;cursor:pointer;}
  22. .nav-tabs li label { margin-bottom: 0px !important; cursor: inherit; }
  23. input[name="tab"], div[rel^="tab"] { display: none; }
  24. #tab0:checked ~ ul li[rel="tab0"], #tab1:checked ~ ul li[rel="tab1"] { border-color: #dee2e6 #dee2e6 #fff; cursor: default; }
  25. #tab0:checked ~ div[rel="tab0"], #tab1:checked ~ div[rel="tab1"] { display: block; }
  26. </style>
  27. </head>
  28. <body>
  29. <h1>Cassette</h1>
  30. <div id="jserr">Please enable JavaScript</div>
  31. <script>document.getElementById("jserr").style.display = "none";</script>
  32. <p>This is a quick'n'dirty tool that can convert digitalized retro computer cassettes (C64, Spectrum, Homelab etc.) into binary files and vice versa. Also available as a CLI tool, <a href="https://gitlab.com/bztsrc/cassette">Open Source</a>.</p>
  33. <input type="radio" name="tab" id="tab0" checked onchange="help()" value="0">
  34. <input type="radio" name="tab" id="tab1" onchange="help()" value="1">
  35. <ul class="nav nav-tabs">
  36. <li class="nav-link" rel="tab0" style="margin-left:20px">
  37. <label for="tab0">Audio to binary</label>
  38. </li>
  39. <li class="nav-link" rel="tab1">
  40. <label for="tab1">Binary to audio</label>
  41. </li>
  42. </ul>
  43. <div>
  44. <br><table>
  45. <tr><td width="10%">Input</td><td width="90%"><input type="file" id="input"></td></tr>
  46. <tr><td width="10%">Output</td><td width="90%"><input type="text" id="output" placeholder="filename"></td></tr>
  47. <tr><td width="10%">Presets</td><td width="90%"><select id="preset" onchange="loadpreset(this.value)"></select></td></tr>
  48. <tr><td width="10%">Data</td><td width="90%"><input type="checkbox" id="bit7">&nbsp;<label for="bit7">7 bits per byte</label>&nbsp;&nbsp;&nbsp; <input type="checkbox" id="end">&nbsp;<label for="end">little endian (least significant bit first)</label>&nbsp;&nbsp;&nbsp; <select id="fm" onchange="help()"><option value="0" selected>AM</option><option value="1">FM, down-edge</option><option value="2">FM, up-edge</option><option value="3">FM, both edges</option></select>&nbsp;&nbsp;&nbsp; <select id="sy" onchange="help()"><option value="0">no sync bit</option><option value="1">leading sync bit</option><option value="2">trailing sync bit</option><option value="3">two sync bits</option></select></td></tr>
  49. </table>
  50. </div>
  51. <div rel="tab0">
  52. <table>
  53. <tr><td width="10%">Sample</td><td width="90%"><input type="number" id="len1" min="0" max="4096" value="0" title="in μsec (millionth of a sec), 0 = autodetect">&nbsp;bit's length&nbsp;&nbsp;&nbsp; <input type="number" id="gap1" min="0" max="4096" value="0" title="in μsec">&nbsp;gap&nbsp;&nbsp;&nbsp; <input type="checkbox" id="signed">&nbsp;<label for="signed">input is signed 8-bit PCM</label></td></tr>
  54. <tr><td width="10%"></td><td width="90%"><input type="number" id="skip" min="0" max="1073741824" value="0" title="in samples">&nbsp;skip samples at beginning&nbsp;&nbsp;&nbsp; <input type="checkbox" id="hdr1">&nbsp;<label for="hdr">don't detect Homelab header</label></td></tr>
  55. <tr><td colspan="2"><button class="btn btn-primary" onclick="dothething(0)">Convert</button></td></tr>
  56. </table>
  57. </div>
  58. <div rel="tab1">
  59. <table>
  60. <tr><td width="10%">Sample</td><td width="90%"><input type="number" id="len2" min="1" max="4096" value="200" title="in μsec">&nbsp;bit's length&nbsp;&nbsp;&nbsp; <input type="number" id="gap2" min="0" max="4096" value="0" title="in μsec">&nbsp;gap&nbsp;&nbsp;&nbsp; <input type="number" id="ol" min="-127" max="127" value="127" title="127 = highest, -127 = smallest">&nbsp;one (bit set) level&nbsp;&nbsp;&nbsp; <input type="number" id="zl" min="-127" max="127" value="-64" title="127 = highest, -127 = smallest">&nbsp;zero (bit clear) level</td></tr>
  61. <tr><td width="10%"></td><td width="90%"><select id="rate" title="in Hz"><option value="22050">22050</option><option value="32000">32000</option><option value="44100" selected>44100</option><option value="48000">48000</option></select>&nbsp;sample rate&nbsp;&nbsp;&nbsp; <input type="checkbox" id="hdr2">&nbsp;<label for="hdr">generate Homelab header</label></td></tr>
  62. <tr><td width="10%">Format</td><td width="90%"><input type="checkbox" id="mp3">&nbsp;<label for="mp3">MP3 (WAV if unset)</label>&nbsp;&nbsp;&nbsp; <select id="bitrate" title="MP3 compression"><option value="96">96</option><option value="128" selected>128</option><option value="192">192</option><option value="256">256</option></select>&nbsp;bit rate&nbsp;&nbsp;&nbsp; <input type="number" id="quality" min="0" max="9" value="0" title="MP3 compression">&nbsp;quality (0 = best, 9 = worst)</td></tr>
  63. <tr><td colspan="2"><button class="btn btn-primary" onclick="dothething(1)">Convert</button></td></tr>
  64. </table>
  65. </div>
  66. <p><small><br><b>Help</b><br><img id="helpimg"><br></small>
  67. <small id="help00">Bit length is detected from the up-edges. If you set gap, that many samples will be skipped after each byte, aka after every 7 or 8 bits.</small>
  68. <small id="help01">Only samples considered where wave crosses the base line from above. If bit length isn't set, the average of distances is used as treshold.</small>
  69. <small id="help02">Only samples considered where wave crosses the base line from below. If bit length isn't set, the average of distances is used as treshold.</small>
  70. <small id="help03">Only samples considered where wave crosses the base line. If bit length isn't set, the average of distances is used as treshold.</small>
  71. <small id="help10">Bit's length samples will be generated with either one level or zero level. If you set gap, that many zero level samples will be added after each byte, aka after every 7 or 8 bits.</small>
  72. <small id="help11">Shifted phase sine wave will be generated for each bit, either bit's length (zero) or bit's length + gap (one) long. Sign of one level and zero level must be different.</small>
  73. <small id="help12">Sine wave will be generated for each bit, either bit's length (zero) or bit's length + gap (one) long. Sign of one level and zero level must be different.</small>
  74. <small id="help13">Altering half sine wave will be generated for each bit, either bit's length (zero) or bit's length + gap (one) long. Sign of one level and zero level must be different.</small>
  75. <small id="helps0">No sync bit, 0 = 0, 1 = 1</small>
  76. <small id="helps1">A sync bit prefixes all data bits, 10 = 0, 11 = 1</small>
  77. <small id="helps2">A sync bit follows all data bits, 01 = 0, 11 = 1</small>
  78. <small id="helps3">Data bit between two sync bits, 101 = 0, 111 = 1</small>
  79. <small>Verbose output of conversion details can be found on the JavaScript console.</small>
  80. </p>
  81. <div id="loading" style="display:none">&nbsp;</div>
  82. <script src="cassette.js"></script>
  83. <script>
  84. var presets = {
  85. "": {},
  86. "Commodore": { fm: 3, end: 0, bit7: 0, len1: 0, len2: 245, gap1: 0, gap2: 122, sy: 0, rate: 44100, ol: 127, zl: -127 },
  87. "Homelab": { fm: 0, end: 0, bit7: 0, len1: 0, len2: 771, gap1: 0, gap2: 0, sy: 2, rate: 44100, ol: 127, zl: -64, mp3: 1, bitrate: 96 }
  88. };
  89. var preset = document.getElementById("preset");
  90. function dothething(dir) {
  91. var loading = document.getElementById("loading");
  92. var input=document.getElementById("input");
  93. var output=document.getElementById("output");
  94. loading.style.display="block";
  95. /* stupid firefox, force redraw */
  96. setTimeout(()=>{
  97. if(output.value.length<1) {
  98. alert("No output filename.");
  99. loading.style.display="none";
  100. } else
  101. if(input.files.length<1) {
  102. alert("No input file given.");
  103. loading.style.display="none";
  104. } else {
  105. var reader = new FileReader();
  106. reader.onloadend = function() {
  107. var len = 0, btns = document.querySelectorAll("button");
  108. var casdata = new Uint8Array(reader.result);
  109. if(casdata.length < 1) alert("No input data.");
  110. else {
  111. const inbuf = Module._malloc(casdata.length);
  112. const outbuf = Module._malloc(128*1024*1024);
  113. Module.HEAPU8.set(casdata, inbuf);
  114. if(dir) {
  115. var encode = Module.cwrap("doencode", "number", [ "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number" ]);
  116. len = encode(inbuf, casdata.length, document.getElementById("mp3").checked, document.getElementById("end").checked, document.getElementById("bit7").checked ? 7 : 8, parseInt(document.getElementById("sy").value), parseInt(document.getElementById("fm").value), parseInt(document.getElementById("gap2").value), parseInt(document.getElementById("len2").value), document.getElementById("hdr2").checked, parseInt(document.getElementById("rate").value), parseInt(document.getElementById("ol").value), parseInt(document.getElementById("zl").value), parseInt(document.getElementById("bitrate").value), parseInt(document.getElementById("quality").value), outbuf,128*1024*1024);
  117. } else {
  118. var decode = Module.cwrap("dodecode", "number", [ "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number", "number" ]);
  119. len = decode(inbuf, casdata.length, document.getElementById("signed").checked, document.getElementById("end").checked, document.getElementById("bit7").checked ? 7 : 8, parseInt(document.getElementById("sy").value), parseInt(document.getElementById("fm").value), parseInt(document.getElementById("gap1").value), parseInt(document.getElementById("len1").value), document.getElementById("hdr1").checked, parseInt(document.getElementById("skip").value), outbuf,128*1024*1024);
  120. }
  121. if(len > 0) {
  122. const view = new Uint8Array(Module.HEAPU8.buffer,outbuf,len);
  123. var blob = new Blob([view], { type: "application/octet-stream" });
  124. var url = window.URL.createObjectURL(blob);
  125. var a = document.createElement("A");
  126. a.href = url;
  127. a.download = output.value;
  128. document.body.appendChild(a);
  129. a.click();
  130. a.remove();
  131. window.URL.revokeObjectURL(url);
  132. } else
  133. alert("Unable to generate output file.");
  134. Module._free(inbuf);
  135. Module._free(outbuf);
  136. }
  137. loading.style.display="none";
  138. };
  139. reader.readAsArrayBuffer(input.files[0]);
  140. }
  141. }, 100);
  142. }
  143. function help() {
  144. var imgs = [ "am.png", "fm_d.png", "fm_u.png", "fm_b.png" ], txts = document.querySelectorAll('small[id^="help"]');
  145. document.getElementById("helpimg").src = imgs[document.getElementById("fm").value];
  146. for(var i = 0; i < txts.length; i++) { txts[i].style.display="none"; }
  147. document.getElementById("help"+document.querySelector("input[name=tab]:checked").value+document.getElementById("fm").value).style.display="block";
  148. document.getElementById("helps"+document.getElementById("sy").value).style.display="block";
  149. }
  150. function loadpreset(p) {
  151. for(var id in presets[p]) {
  152. var fld = document.getElementById(id);
  153. var val = parseInt(presets[p][id]);
  154. if(fld.type == "checkbox") fld.checked = val;
  155. else fld.value = val;
  156. }
  157. help();
  158. }
  159. for(var p in presets) { var opt = document.createElement("option"); opt.value = p; opt.innerText = p; preset.add(opt); }
  160. help();
  161. </script>
  162. </body>
  163. </html>