From 7682c16831e16b8cfe043f068df6b66060e4361a Mon Sep 17 00:00:00 2001 From: nino-porcino Date: Thu, 13 Jan 2022 14:11:03 +0100 Subject: [PATCH] add wav converter utilities --- README.md | 5 +- package-lock.json | 24 +++++- package.json | 4 +- tools/wavconv/aci.js | 6 ++ tools/wavconv/bits2samples.js | 26 ++++++ tools/wavconv/bytes2bits.js | 12 +++ tools/wavconv/checksum.js | 9 +++ tools/wavconv/hex.js | 7 ++ tools/wavconv/parseOptions.js | 12 +++ tools/wavconv/prg2wav.js | 65 +++++++++++++++ tools/wavconv/samples2wav.js | 15 ++++ tools/wavconv/wav2prg.js | 145 ++++++++++++++++++++++++++++++++++ 12 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 tools/wavconv/aci.js create mode 100644 tools/wavconv/bits2samples.js create mode 100644 tools/wavconv/bytes2bits.js create mode 100644 tools/wavconv/checksum.js create mode 100644 tools/wavconv/hex.js create mode 100644 tools/wavconv/parseOptions.js create mode 100644 tools/wavconv/prg2wav.js create mode 100644 tools/wavconv/samples2wav.js create mode 100644 tools/wavconv/wav2prg.js diff --git a/README.md b/README.md index 8152a44..cd53457 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 006cb93..06d5af0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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==" } } } diff --git a/package.json b/package.json index c7fd0cb..47d58db 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/tools/wavconv/aci.js b/tools/wavconv/aci.js new file mode 100644 index 0000000..21d2e87 --- /dev/null +++ b/tools/wavconv/aci.js @@ -0,0 +1,6 @@ +let ACI = { + zero: 2000, // frequency for "0" bit + one: 800 // frequency for "1" bit +}; + +module.exports = { ACI }; \ No newline at end of file diff --git a/tools/wavconv/bits2samples.js b/tools/wavconv/bits2samples.js new file mode 100644 index 0000000..6ff5422 --- /dev/null +++ b/tools/wavconv/bits2samples.js @@ -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 { + for(let t=7;t>=0;t--) { + let bit = (byte >> t) & 1; + bits.push(bit); + } + }); + return bits; +} + +module.exports = { bytesToBits }; \ No newline at end of file diff --git a/tools/wavconv/checksum.js b/tools/wavconv/checksum.js new file mode 100644 index 0000000..e5d9f85 --- /dev/null +++ b/tools/wavconv/checksum.js @@ -0,0 +1,9 @@ +function checksum_byte(bytes) { + let checksum = 0xFF; + for(let t=0; t 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(``); + diff --git a/tools/wavconv/samples2wav.js b/tools/wavconv/samples2wav.js new file mode 100644 index 0000000..37eea41 --- /dev/null +++ b/tools/wavconv/samples2wav.js @@ -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 }; diff --git a/tools/wavconv/wav2prg.js b/tools/wavconv/wav2prg.js new file mode 100644 index 0000000..dabeb3d --- /dev/null +++ b/tools/wavconv/wav2prg.js @@ -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 mid_point) bits.push(1); + else bits.push(0); + } + + return bits; +} + +function bits2bytes(bits) { + let bytes = []; + for(let t=0; t> 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`); +} +