Last active
April 24, 2024 13:05
-
-
Save Alikberov/ad8f341bb2537179dcc3554d1271fea6 to your computer and use it in GitHub Desktop.
Audio to ROM-Disk for RADIO-86RK (Paguo-86PK)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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