Skip to content

Instantly share code, notes, and snippets.

@Alikberov
Last active April 24, 2024 13:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Alikberov/ad8f341bb2537179dcc3554d1271fea6 to your computer and use it in GitHub Desktop.
Save Alikberov/ad8f341bb2537179dcc3554d1271fea6 to your computer and use it in GitHub Desktop.
Audio to ROM-Disk for RADIO-86RK (Paguo-86PK)
<html>
<head>
<meta charset="UTF-8">
<script>
var hVideo;
var hAudio;
var fileName;
const ticksPerBit = (101.0 + 39.0 / 256.0) / 8.0;
const romAudio1_0 = [
0x41, 0x55, 0x44, 0x49, 0x4F, 0x3A, 0x38, 0x36, 0x52, 0x4B, 0x2F, 0x56, 0x31, 0x2E, 0x30, 0xFF,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x2D, 0x2D, 0x3E, 0x52, 0x2C, 0x42, 0x46,
0xE1, 0xCD, 0x18, 0xF8, 0x21, 0x00, 0x80, 0x75, 0x23, 0x7E, 0x3C, 0xCA, 0x49, 0x00, 0x7E, 0x3C,
0xC2, 0x4E, 0x00, 0x7C, 0x32, 0x08, 0xE0, 0xC6, 0x10, 0x32, 0x03, 0xA0, 0x21, 0xBF, 0x00, 0x11,
0x00, 0x34, 0x01, 0x02, 0x0F, 0x3E, 0x10, 0x36, 0x1A, 0x23, 0x71, 0x23, 0x70, 0x07, 0xD2, 0x73,
0x00, 0x23, 0x72, 0xFE, 0x10, 0xC2, 0x69, 0x00, 0x1D, 0xC2, 0x67, 0x00, 0x36, 0x23, 0x23, 0x36,
0xC3, 0x23, 0x36, 0xB8, 0x23, 0x73, 0x01, 0x02, 0x80, 0x16, 0xA0, 0x60, 0x6B, 0xC3, 0x93, 0x00,
0x21, 0x00, 0x80, 0x22, 0x01, 0xA0, 0x2C, 0xCA, 0x00, 0xF8, 0x22, 0x91, 0x00, 0x78, 0xD3, 0xC1,
0x7D, 0x0F, 0x0F, 0xE6, 0x3F, 0xC6, 0x08, 0xD3, 0xC0, 0x3E, 0x0E, 0xD3, 0xC0, 0x21, 0x01, 0xA0,
0x73, 0x23, 0x73, 0x1A, 0xB7, 0xFA, 0x00, 0xF8, 0x7E, 0x3C, 0xFA, 0x90, 0x00, 0x77, 0x2B, 0xC7,
0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F,
0x49, 0x38, 0x30, 0x38, 0x30, 0x41, 0x20, 0x53, 0x4F, 0x46, 0x54, 0x57, 0x41, 0x52, 0x45, 0x21,
0x46, 0x4F, 0x52, 0x20, 0x22, 0x72, 0x41, 0x64, 0x69, 0x4F, 0x2D, 0x38, 0x36, 0x72, 0x4B, 0x22,
0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E, 0x5E
];
const romAudio1_1 = [
0x41, 0x55, 0x44, 0x49, 0x4F, 0x3A, 0x38, 0x36, 0x52, 0x4B, 0x2F, 0x56, 0x31, 0x2E, 0x31, 0xFF,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x2D, 0x2D, 0x3E, 0x52, 0x2C, 0x42, 0x46,
0xE1, 0xCD, 0x18, 0xF8, 0x21, 0x00, 0x80, 0x75, 0x23, 0x7E, 0x3C, 0xCA, 0x49, 0x00, 0x7E, 0x3C,
0xC2, 0x4E, 0x00, 0x7C, 0x32, 0x08, 0xE0, 0xC6, 0x10, 0x32, 0x03, 0xA0, 0x21, 0xE9, 0x00, 0x11,
0xFF, 0x34, 0x01, 0x02, 0x0F, 0x3E, 0x10, 0x36, 0x1A, 0x23, 0x71, 0x23, 0x70, 0x07, 0xD2, 0x73,
0x00, 0x23, 0x72, 0xFE, 0x10, 0xC2, 0x69, 0x00, 0x1D, 0xC2, 0x67, 0x00, 0x36, 0x1A, 0x23, 0x71,
0x23, 0x70, 0x23, 0x36, 0x73, 0x23, 0x70, 0x23, 0x71, 0x23, 0x70, 0x23, 0x71, 0x23, 0x70, 0x23,
0x36, 0x23, 0x23, 0x36, 0x34, 0x23, 0x70, 0x23, 0x71, 0x23, 0x70, 0x23, 0x71, 0x23, 0x36, 0xF2,
0x23, 0x36, 0xE8, 0x23, 0x73, 0x23, 0x36, 0x73, 0x23, 0x36, 0xC3, 0x23, 0x36, 0xBA, 0x23, 0x73,
0x01, 0x02, 0x80, 0x16, 0xA0, 0x60, 0x6B, 0xC3, 0xBD, 0x00, 0x21, 0x00, 0x80, 0x22, 0x01, 0xA0,
0x2C, 0xCA, 0x00, 0xF8, 0x22, 0xBB, 0x00, 0x78, 0xD3, 0xC1, 0x7D, 0x0F, 0x0F, 0xE6, 0x3F, 0xC6,
0x08, 0xD3, 0xC0, 0x3E, 0x0E, 0xD3, 0xC0, 0x21, 0x01, 0xA0, 0x73, 0x23, 0x73, 0x1A, 0xB7, 0xFA,
0x00, 0xF8, 0x7E, 0x3C, 0xFA, 0xBA, 0x00, 0x77, 0x2B, 0xC7, 0x25, 0x32, 0x30, 0x32, 0x34, 0x20,
0x28, 0x43, 0x29, 0x20, 0x41, 0x4C, 0x49, 0x4B, 0x42, 0x45, 0x52, 0x4F, 0x56, 0x31, 0x2E, 0x31
];
var romArray = [];
var ampArray = [];
var bitCount = 0;
var bitStack = 0;
var pwmLevels = 5;
var pwmFreq;
var averageTicks;
var cpuFreq;
var hCpuFreq;
var hCpuOsc;
var hOscDiv;
var hTitle;
var hArtist;
var hCopyright;
var header;
var samples = 0;
</script>
<script>
function saving() {
var streamAmp = streamMax - streamMin;
var average = streamMin + (streamMax - streamMin) / 2;
var bipolar = hPwmPolar.checked;
//
for(var amp, i = 0; i < ampArray.length; ++ i) {
amp = ampArray[i];
if(bipolar)
amp = Math.floor(((amp - streamMin) / streamAmp) * (pwmLevels - 1));
//amp = Math.floor((0.51 + amp / 2) * (pwmLevels - 1));
else
if(amp > 0.0) {
amp = Math.floor(amp * pwmLevels / streamMax);
} else
amp = 0;
bitStack |= ((1 << amp) - 1) << bitCount;
while(bitCount > 7) {
if(romArray.length % 32768 == 0) {
romArray.push(...header);
document.title = `${romArray.length >> 10} kb`;
}
romArray.push(bitStack & 255);
bitCount -= 8;
bitStack >>= 8;
}
bitCount += pwmLevels - 1
}
//
saveFile(fileName.replace(/\..+/, `@${pwmLevels}${bipolar ? "int" : "ord"}.rom`), romArray);
}
function koi7(s) {
const koi = "ЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧЪ";
var r = "";
for(var i = 0, c; i < s.length; ++ i) {
c = s.charAt(i);
if(c.match(/[А-Я]/i))
c = String.fromCharCode(96 + koi.indexOf(c.toUpperCase()));
r += c;
}
return r;
}
</script>
<script>
let min = +2;
let max = -2;
let streamMin = +2;
let streamMax = -2;
let sampleMin = +2;
let sampleMax = -2;
let avg;
function Infos(e) {
e.preventDefault();
window.localStorage.pwmLevels = pwmLevels;
window.localStorage.Title = hTitle.value;
window.localStorage.Artist = hArtist.value;
window.localStorage.Copyright = hCopyright.value;
window.localStorage.cpuOsc = hCpuOsc.value;
window.localStorage.oscDiv = hOscDiv.value;
window.localStorage.cpuFreq = hCpuFreq.value;
window.localStorage.pwmPolar = String(hPwmPolar.checked);
window.localStorage.pwmPlayer = String(hPwmPlayer.checked);
const Title = hTitle.value.substr(0, 16).padEnd(16);
const Artist = hArtist.value.substr(0, 16).padEnd(16);
const Copyright = hCopyright.value.substr(0, 5).padEnd(5);
const Extra = ((hPwmPolar.checked ? "@" : "&") + String(pwmLevels)).padEnd(3);
const Infos = koi7(Title + Artist + Copyright + Extra);
header = [].concat(hPwmPlayer.checked ? romAudio1_1 : romAudio1_0);
var i = 16;
for(const chr of Infos)
header[i ++] = chr.charCodeAt(0);
document.querySelector("input[type=submit]").style.display = "none";
hVideo.style.visibility = "visible";
hVideo.style.position = "relative";
document.querySelector("form").disabled = "disable";
const source = hAudio.createMediaElementSource(hVideo);
const scriptNode = hAudio.createScriptProcessor(4096, 1, 1);
// Give the node a function to process audio events
scriptNode.addEventListener("audioprocess", (audioProcessingEvent) => {
// The input buffer is the song we loaded earlier
let inputBuffer = audioProcessingEvent.inputBuffer;
let inputLength = inputBuffer.length;
// The output buffer contains the samples that will be modified and played
let outputBuffer = audioProcessingEvent.outputBuffer;
// Loop through the output channels (in this case there is only one)
let inputData = inputBuffer.getChannelData(0);
let outputData = outputBuffer.getChannelData(0);
let playbackRate = inputBuffer.sampleRate;
for(let amp, sample = 0; sample < inputLength; ++ sample) {
amp = inputData[sample];
outputData[sample] = amp;
//
streamMin = Math.min(streamMin, amp);
streamMax = Math.max(amp, streamMax);
//
sampleMin = Math.min(sampleMin, amp);
sampleMax = Math.max(amp, sampleMax);
//
samples += pwmFreq / playbackRate;
//
if(samples >= 1.0) {
//ampArray.push(sampleMin + (sampleMax - sampleMin) / 2.0);
ampArray.push(sampleMax);
sampleMin = +2.0;
sampleMax = -2.0;
samples -= 1.0;
}
}
document.title = `${ampArray.length >> 10} kb`;
/* for (let channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
let inputData = inputBuffer.getChannelData(channel);
let outputData = outputBuffer.getChannelData(channel);
let playbackRate = inputBuffer.sampleRate;
// Loop through the 4096 samples
for (let sample = 0, amp; sample < inputBuffer.length; sample++) {
// make output equal to the same as the input
amp = inputData[sample];
outputData[sample] = amp;
min = Math.min(min, amp);
max = Math.max(1/256, max, amp);
//
samples += pwmFreq / playbackRate;
if(hVideo.currentTime > 0 && samples >= 1.0) {
samples -= 1.0;
//amp = Math.floor((0.51 + amp / 2) * (pwmLevels - 1));
avg = min + (max - min) / 2;
amp = amp / max;
if(amp < 0)
amp = 0;
else
amp = Math.floor(amp * pwmLevels);
//amp = Math.floor(((amp - min) / 2) * (pwmLevels - 1));
//amp = Math.floor((0.51 + amp / 2) * (pwmLevels - 1));
if(bitStack < 0 || amp < 0 || amp >= (1 << pwmLevels)) {
amp = 0;
document.body.style.backgroundColor = "red";
}
bitStack |= ((1 << amp) - 1) << bitCount;
max = -2, min = +2;
while(bitCount > 7) {
if(romArray.length % 32768 == 0) {
//min = +2,
//max = -2;
romArray.push(...header);
document.title = `${romArray.length >> 10} kb`;
}
romArray.push(bitStack & 255);
bitCount -= 8;
bitStack >>= 8;
}
bitCount += pwmLevels - 1
}
}
}*/
});
source.connect(scriptNode);
scriptNode.connect(hAudio.destination);
// When the buffer source stops playing, disconnect everything
hVideo.addEventListener("ended", () => {
source.disconnect(scriptNode);
scriptNode.disconnect(hAudio.destination);
saving();
}, false);
hVideo.play();
}
</script>
<script>
function saveFile(fileName, stream) {
function base64ToArrayBuffer(base64) {
var binaryString = base64;
var binaryLen = binaryString.length;
var bytes = new Uint8Array(binaryLen);
for(var i = 0; i < binaryLen; ++ i)
bytes[i] = binaryString[i];
return bytes;
}
//
function saveByteArray(fileName, bytes) {
var blob = new Blob([bytes], {type: "application/octet-stream"});
var link = document.createElement("A");
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
}
//
saveByteArray(fileName, base64ToArrayBuffer(stream));
}
</script>
<script>
function load(e) {
if(e.files.length) {
fileName = e.files[0].name;
hVideo.setAttribute("src", URL.createObjectURL(e.files[0]));
return e.files[0].name;
}
document.body.style.backgroundColor = "red";
window.location.href = "";
}
</script>
<script>
/********************************************************************************
Interactive calculator for resampling constants
********************************************************************************/
function calc() {
pwmLevels = Number(hPwmLevels.value);
averageTicks = ticksPerBit * (pwmLevels - 1);
pwmFreq = Number(hCpuOsc.value) * 1000000 / Number(hOscDiv.value) / averageTicks;
hCpuFreq.value = Number(hCpuOsc.value) / Number(hOscDiv.value);
hPwmFreq.value = pwmFreq.toPrecision(8);
}
</script>
<script>
function main() {
document.querySelector("form").addEventListener("submit", Infos);
document
.querySelectorAll("input")
.forEach(el => {
window[`h${el.id.charAt(0).toUpperCase()}${el.id.substr(1)}`] = el;
if(el.id in window.localStorage) {
if(el.type == "checkbox")
el.checked = window.localStorage[el.id] == "true";
else
el.value = window.localStorage[el.id];
}
});
hAudio = new AudioContext();
hVideo = document.querySelector("video");
hVideo.addEventListener("canplay", function(e) {
var submit = document.querySelector("input[type=submit]");
submit.style.width = hVideo.videoWidth;
submit.style.height = hVideo.videoHeight;
submit.style.visibility = "visible";
});
hCpuOsc.addEventListener("change", calc);
hOscDiv.addEventListener("change", calc);
hCpuFreq.addEventListener("change", calc);
hPwmLevels.addEventListener("change", calc);
calc();
hFiles.click();
}
</script>
<style>
button.starter {
background-color:silver;
color :grey;
font-size :48px;
position :absolute;
left :0px;
top :0px;
width :100%;
height :100%;
}
input#Files {
display :none;
}
input[type=submit] {
visibility :hidden;
}
label {
border :grey thin ridge;
}
label input {
display :none;
}
label input + span {
display :inline;
}
label input + span + span {
display :none;
}
label input:checked + span {
display :none;
}
label input:checked + span + span {
display :inline;
}
video {
position :absolute;
visibility :hidden;
}
</style>
</head>
<body>
<input id=Files type=file onchange='load(this)' />
<form>
<table>
<tr><td>XTAL Frequency</td><td><input id=cpuOsc type=number value=16 min=8 max=27 />MHz</td></tr>
<tr><td>i8224 Divisor</td><td><input id=oscDiv type=number value=9 min=1 max=16 /></td></tr>
<tr><td>CPU Frequency</td><td><input id=cpuFreq type=text disabled />MHz</td></tr>
<tr><td>PWM Levels</td><td><input id=pwmLevels type=number value=5 min=2 max=13 /><label><input id=pwmPlayer type=checkbox /><span>V1.0</span><span>V1.1</span></label><label><input id=pwmPolar type=checkbox /><span>Unipolar</span><span>Bipolar</span></label></td></tr>
<tr><td>Sampling Rate</td><td><input id=pwmFreq type=text disabled />Hz</td></tr>
<tr><td>Title</td><td><input id=Title type=text placeholder=Title value='' /></td></tr>
<tr><td>Artist</td><td><input id=Artist type=text placeholder=Artist value='' /></td></tr>
<tr><td>Copyright</td><td><input id=Copyright type=text placeholder=Copyright value='' /></td></tr>
</table>
<input type=submit value='Play & Translate' /><video controls></video>
</form>
<button class=starter onclick='this.style.display="none"; main()'>START</button>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment