add wav converter utilities

This commit is contained in:
nino-porcino 2022-01-13 14:11:03 +01:00
parent d2da4199e3
commit 7682c16831
12 changed files with 327 additions and 3 deletions

View File

@ -10,10 +10,13 @@ demos/
demo/ demo program that makes use of the library
picshow/ demo program that shows a picture in bitmap mode
tetris/ a game
montyr/ "Monty on the Run" SID tune by R. Hubbard
tapemon/ tape monitor utility
docs/ TMS9918 and Apple-1 manuals
kickc/ target configuration files for KickC
lib/ the library files to include in your project
tools/ some build tools
tools/
wavconv/ prg <-> WAV file converter
```
## Introduction

24
package-lock.json generated
View File

@ -9,7 +9,9 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"command-line-args": "^5.2.0"
"command-line-args": "^5.2.0",
"wav-decoder": "^1.3.0",
"wav-encoder": "^1.3.0"
}
},
"node_modules/array-back": {
@ -57,6 +59,16 @@
"engines": {
"node": ">=8"
}
},
"node_modules/wav-decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/wav-decoder/-/wav-decoder-1.3.0.tgz",
"integrity": "sha512-4U6O/JNb1dPO90CO2YMTQ5N2plJcntm39vNMvRq9VZ4Vy5FzS7Lnx95N2QcYUyKYcZfCbhI//W3dSHA8YnOQyQ=="
},
"node_modules/wav-encoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/wav-encoder/-/wav-encoder-1.3.0.tgz",
"integrity": "sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw=="
}
},
"dependencies": {
@ -93,6 +105,16 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
"integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw=="
},
"wav-decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/wav-decoder/-/wav-decoder-1.3.0.tgz",
"integrity": "sha512-4U6O/JNb1dPO90CO2YMTQ5N2plJcntm39vNMvRq9VZ4Vy5FzS7Lnx95N2QcYUyKYcZfCbhI//W3dSHA8YnOQyQ=="
},
"wav-encoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/wav-encoder/-/wav-encoder-1.3.0.tgz",
"integrity": "sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw=="
}
}
}

View File

@ -21,6 +21,8 @@
},
"homepage": "https://github.com/nippur72/apple1-videocard-lib#readme",
"dependencies": {
"command-line-args": "^5.2.0"
"command-line-args": "^5.2.0",
"wav-decoder": "^1.3.0",
"wav-encoder": "^1.3.0"
}
}

6
tools/wavconv/aci.js Normal file
View File

@ -0,0 +1,6 @@
let ACI = {
zero: 2000, // frequency for "0" bit
one: 800 // frequency for "1" bit
};
module.exports = { ACI };

View File

@ -0,0 +1,26 @@
function bitsToSamples(bits, samplerate, one_freq, zero_freq) {
let clock = 1000000;
let cycles = bits.map(b=>{
if(b==1) return samplerate / one_freq; // 1000 Hz
else return samplerate / zero_freq; // 2000 Hz
});
let samples = [];
let volume = 0.75;
let ptr = 0;
for(let i=0;i<cycles.length;i++) {
let nsamples = cycles[i];
while(ptr<nsamples) {
if(ptr<nsamples/2) samples.push(volume);
else samples.push(-volume);
ptr++;
}
ptr-=nsamples;
}
return samples;
}
module.exports = { bitsToSamples };

View File

@ -0,0 +1,12 @@
function bytesToBits(data) {
let bits = [];
data.forEach(byte => {
for(let t=7;t>=0;t--) {
let bit = (byte >> t) & 1;
bits.push(bit);
}
});
return bits;
}
module.exports = { bytesToBits };

View File

@ -0,0 +1,9 @@
function checksum_byte(bytes) {
let checksum = 0xFF;
for(let t=0; t<bytes.length; t++) {
checksum = (checksum ^ bytes[t]) & 0xFF;
}
return checksum;
}
module.exports = { checksum_byte };

7
tools/wavconv/hex.js Normal file
View File

@ -0,0 +1,7 @@
function hex(value, size) {
if(size === undefined) size = 2;
let s = "0".repeat(size) + value.toString(16);
return s.substr(s.length - size).toUpperCase();
}
module.exports = { hex };

View File

@ -0,0 +1,12 @@
const commandLineArgs = require('command-line-args');
function parseOptions(optionDefinitions) {
try {
return commandLineArgs(optionDefinitions);
} catch(ex) {
console.log(ex.message);
process.exit(-1);
}
}
module.exports = parseOptions;

65
tools/wavconv/prg2wav.js Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env node
const fs = require('fs');
const parseOptions = require("./parseOptions");
const { samples2wav } = require("./samples2wav");
const { bytesToBits } = require("./bytes2bits");
const { bitsToSamples } = require("./bits2samples");
const { checksum_byte } = require("./checksum");
const { hex } = require("./hex");
const { ACI } = require("./aci");
const options = parseOptions([
{ name: 'input', alias: 'i', type: String },
{ name: 'output', alias: 'o', type: String },
{ name: 'samplerate', alias: 's', type: Number },
{ name: 'binary', alias: 'b', type: String }
]);
if(options.input === undefined || options.output === undefined) {
console.log("usage: prg2wav -i inputfile.prg -o outputfile [-s samplerate] [-b hexaddress]");
console.log("");
console.log("The input is a binary file with two bytes start address header (usually with .prg extension).");
console.log("If -b <hexaddress> is specified, the input file is treated as a binary with no header");
console.log("and the start address must be specified (in hexadecimal format).");
console.log("Samplerate is the rate of the output WAV file (44100 Hz default)");
process.exit(-1);
}
let samplerate = options.samplerate == undefined ? 44100 : options.samplerate;
let binfile,startaddress;
if(options.binary) {
binfile = fs.readFileSync(options.input);
startaddress = parseInt(options.binary,16);
}
else {
let prgfile = fs.readFileSync(options.input);
startaddress = prgfile[0]+prgfile[1]*256;
binfile = prgfile.slice(2);
}
let endaddress = startaddress + binfile.length - 1;
// header is composed of a 10 seconds of long cycles ("1") ending with a short cyles ("0")
let header = new Uint8Array(1250).fill(255);
let startbyte = 254; // 7 long cycles and a short one as start bit
let checksum = checksum_byte(binfile);
let slipbytes = [ 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE ];
let data = [ ...header, startbyte, ...binfile, checksum, ...slipbytes ];
let bits = bytesToBits(data);
let samples = bitsToSamples(bits, samplerate, ACI.one, ACI.zero);
let wavfile = samples2wav(samples, samplerate);
let wavName = options.output;
let s_start = hex(startaddress,4);
let s_end = hex(endaddress,4);
wavName = `${wavName}_${s_start}.${s_end}R.wav`;
fs.writeFileSync(wavName, wavfile);
console.log(`file "${wavName}" generated, load it on the Apple-1 with:`);
console.log(`C100R (RETURN) ${s_start}.${s_end}R (RETURN)`);
console.log(``);

View File

@ -0,0 +1,15 @@
const WavEncoder = require("wav-encoder");
// turns the samples into an actual WAV file
// returns the array of bytes to be written to file
function samples2wav(samples, samplerate) {
const wavData = {
sampleRate: samplerate,
channelData: [ new Float32Array(samples) ]
};
const buffer = WavEncoder.encode.sync(wavData, { bitDepth: 16, float: false });
return Buffer.from(buffer)
}
module.exports = { samples2wav };

145
tools/wavconv/wav2prg.js Normal file
View File

@ -0,0 +1,145 @@
#!/usr/bin/env node
const fs = require('fs');
const WavDecoder = require("wav-decoder");
const parseOptions = require("./parseOptions");
const { ACI } = require("./aci");
function samples2phases(samples) {
let phases = [];
// quantize data
let s = samples.map(e=> e<0 ? 0 : 1);
let counter = 0;
let last_state = 0;
for(let i=0;i<s.length-1;i++) {
counter++;
if(last_state != s[i]) {
let phase_width = counter;
counter = 0;
last_state = s[i];
phases.push(phase_width);
}
}
return phases;
}
function getDataPhases(phases, mid_point) {
// how many header long phases before the short phase of the starting bit
let header_len = (8*2)*256;
for(let t=header_len;t<phases.length;t++) {
if(phases[t] <= mid_point) {
// if we found a starting bit half phase, check the header before
let start_found = true;
for(let i=1;i<header_len;i++) {
if(phases[t-i] <= mid_point) {
start_found = false;
break;
}
}
if(start_found) {
return phases.slice(t+2); // also skip the second phase of the start bit
}
}
}
throw "start bit not found";
}
function phases2bits(pulses, mid_point) {
let cycles = [];
// compat two consecutive half phases into one full cycle
for(let t=0; t<pulses.length; t+=2) {
cycles.push(pulses[t]+pulses[t+1]);
}
let bits = [];
for(let t=0; t<cycles.length; t++) {
let width = cycles[t];
if(width > mid_point) bits.push(1);
else bits.push(0);
}
return bits;
}
function bits2bytes(bits) {
let bytes = [];
for(let t=0; t<bits.length; t+=8) {
let byte =
(bits[t+0] << 7) +
(bits[t+1] << 6) +
(bits[t+2] << 5) +
(bits[t+3] << 4) +
(bits[t+4] << 3) +
(bits[t+5] << 2) +
(bits[t+6] << 1) +
(bits[t+7] << 0) ;
bytes.push(byte);
}
return bytes;
}
////////////////////////////////////////////////////////////////////////////////////////
const options = parseOptions([
{ name: 'input', alias: 'i', type: String },
{ name: 'output', alias: 'o', type: String },
{ name: 'start', alias: 's', type: String },
{ name: 'end', alias: 'e', type: String }
]);
if(options.input === undefined || options.output === undefined) {
console.log("usage: wav2prg -i inputfile.wav -o outputfile [-s hexstart] [-e hexend]");
process.exit(-1);
}
let wavfile = fs.readFileSync(options.input);
let audioData = WavDecoder.decode.sync(wavfile);
let samples = audioData.channelData[0];
let samplerate = audioData.sampleRate;
const long_cycle = samplerate / ACI.one;
const short_cyle = samplerate / ACI.zero;
const cycle_mid_point = (long_cycle + short_cyle) / 2;
const phase_mid_point = cycle_mid_point / 2;
let phases = samples2phases(samples);
let dataPhases = getDataPhases(phases, phase_mid_point);
let bits = phases2bits(dataPhases, cycle_mid_point);
let bytes = bits2bytes(bits);
if(options.start !== undefined && options.end !== undefined) {
let start_address = parseInt(options.start,16);
let end_address = parseInt(options.end,16);
let len = end_address - start_address + 1; // end address included
if(bytes.length<len) throw `decoded file (${bytes.length} bytes) smaller than specified (${len} bytes)`;
// cut file
bytes = bytes.slice(0,len);
// add prg header
let hi = (start_address >> 8) & 0xff;
let lo = (start_address >> 0) & 0xff;
bytes = [ lo, hi, ...bytes];
let prgFile = new Uint8Array(bytes);
let prgName = `${options.output}.prg`;
fs.writeFileSync(prgName, prgFile);
console.log(`file "${prgName}" generated (it has two bytes header start address)`);
}
else
{
let binFile = new Uint8Array(bytes);
let binName = `${options.output}.bin`;
fs.writeFileSync(binName, binFile);
console.log(`no address specified, writing raw binary`);
console.log(`file "${binName}" generated`);
}