mirror of
https://github.com/nippur72/apple1-videocard-lib.git
synced 2025-02-05 17:30:47 +00:00
commit
8ed0933a8e
4
.gitignore
vendored
4
.gitignore
vendored
@ -11,4 +11,6 @@ cc65/*
|
||||
.vscode/*
|
||||
!demos/montyr/*.asm
|
||||
*.sym
|
||||
|
||||
!demos/tapemon/out/tapemon.0280.bin
|
||||
!demos/tapemon/out/tapemon_apple1.prg
|
||||
node_modules/
|
||||
|
@ -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
|
||||
|
30
demos/tapemon/README.md
Normal file
30
demos/tapemon/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# TAPEMON
|
||||
|
||||
Tapemon is a Apple-I tool that helps finding the optimal playback volume to
|
||||
the ACI audio cassette interface.
|
||||
|
||||
## How to use it
|
||||
|
||||
- Load `tapemon.0280.bin` on the Apple-1 and execute it with `280R`.
|
||||
|
||||
- Connect your playback device (PC, Smartphone, iPod etc) to the ACI
|
||||
|
||||
- Play the file `packets.wav` contained in `packets_wav/packets.zip` archive. The file
|
||||
will send a continous stream of small data packets that are catched by the program
|
||||
running on the Apple-1. Each packet is 64 bytes long.
|
||||
|
||||
For each received packet a character will be displayed:
|
||||
|
||||
- `*` indicates a good packet
|
||||
- `.` indicates packet not received
|
||||
- an hex digit 0-F indicates the packet is partially corrupt
|
||||
|
||||
Adjust the volume level so that you have a lot of `*`.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
4
demos/tapemon/m.bat
Normal file
4
demos/tapemon/m.bat
Normal file
@ -0,0 +1,4 @@
|
||||
@call ..\..\tools\build tapemon
|
||||
@call node ..\..\tools\prg2bin -i out\tapemon_apple1.prg -o out\tapemon.0280.bin
|
||||
|
||||
|
BIN
demos/tapemon/out/tapemon.0280.bin
Normal file
BIN
demos/tapemon/out/tapemon.0280.bin
Normal file
Binary file not shown.
BIN
demos/tapemon/out/tapemon_apple1.prg
Normal file
BIN
demos/tapemon/out/tapemon_apple1.prg
Normal file
Binary file not shown.
24
demos/tapemon/packets_wav/mkpackets.js
Normal file
24
demos/tapemon/packets_wav/mkpackets.js
Normal file
@ -0,0 +1,24 @@
|
||||
const fs = require("fs");
|
||||
|
||||
const PACKETSIZE = 64;
|
||||
|
||||
let data = [];
|
||||
for(let t=0;t<PACKETSIZE;t++) data.push(t);
|
||||
|
||||
let packet = [
|
||||
0xff, 0xff, 0xff, 0xff, 0xfe, // header plus start bit
|
||||
...data, // the actual data block
|
||||
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, // filler
|
||||
]
|
||||
|
||||
let packets = [];
|
||||
|
||||
for(let t=0;t<1000;t++) {
|
||||
packets = [...packets, ...packet];
|
||||
}
|
||||
|
||||
let file = new Uint8Array(packets);
|
||||
|
||||
fs.writeFileSync("packets.bin", file);
|
||||
|
||||
|
2
demos/tapemon/packets_wav/mw.bat
Normal file
2
demos/tapemon/packets_wav/mw.bat
Normal file
@ -0,0 +1,2 @@
|
||||
call node mkpackets
|
||||
call node ..\..\..\..\apple1-wav\prg2wav.js -i packets.bin -o packets -b 00
|
BIN
demos/tapemon/packets_wav/packets.zip
Normal file
BIN
demos/tapemon/packets_wav/packets.zip
Normal file
Binary file not shown.
144
demos/tapemon/tapemon.c
Normal file
144
demos/tapemon/tapemon.c
Normal file
@ -0,0 +1,144 @@
|
||||
#include <utils.h>
|
||||
#include <apple1.h>
|
||||
|
||||
byte *const HEX1L = 0x24; // End address of dump block
|
||||
byte *const HEX1H = 0x25; //
|
||||
byte *const HEX2L = 0x26; // Begin address of dump block
|
||||
byte *const HEX2H = 0x27; //
|
||||
byte *const LASTSTATE = 0x29; // Last input state
|
||||
byte *const NUMPULSES = 0x30; // Number of long pulses to sync at the header
|
||||
byte *const TAPEIN = 0xC081; // Tape input
|
||||
byte *const DSP = 0xD012; // display data port
|
||||
|
||||
#define PACKETSIZE 64
|
||||
|
||||
const byte *RX_BUFFER = 0x200;
|
||||
const byte *RX_BUFFER_END = (0x200+PACKETSIZE-1);
|
||||
|
||||
void read_packet()
|
||||
{
|
||||
asm {
|
||||
// set READ buffer pointers to $0200-$021F (32 characters)
|
||||
lda #<RX_BUFFER_END
|
||||
sta HEX1L
|
||||
lda #>RX_BUFFER_END
|
||||
lda HEX1H
|
||||
|
||||
lda #<RX_BUFFER
|
||||
sta HEX2L
|
||||
lda #>RX_BUFFER
|
||||
lda HEX2H
|
||||
|
||||
// synchronizes with the short header
|
||||
|
||||
syncstart: lda #24 // 24 cycles (3 bytes of $ff)
|
||||
sta NUMPULSES // count 24 cycles pulses
|
||||
jsr fullcycle // skip the first full cycle (when looping)
|
||||
nextsync: ldy #58 // full cycle duration
|
||||
jsr fullcycle // read a full cycle
|
||||
bcc syncstart // if short cycle found (c=0), redo from start
|
||||
dec NUMPULSES // else long cycle found, decrease count
|
||||
bne nextsync // if not 24 cycles, get next cycle
|
||||
|
||||
// else read bit start and 32 bytes of data normally
|
||||
// the following routine was copied directly from the ACI ROM
|
||||
|
||||
notstart: ldy #31 // try to detect the much shorter start bit
|
||||
jsr cmplevel //
|
||||
bcs notstart // start bit not detected yet!
|
||||
jsr cmplevel // wait for 2nd phase of start bit
|
||||
ldy #58 // set threshold value in middle
|
||||
rdbyte: ldx #8 // receiver 8 bits
|
||||
rdbit: pha
|
||||
jsr fullcycle // detect a full cycle
|
||||
pla
|
||||
rol // roll new bit into result
|
||||
ldy #57 // set threshold value in middle
|
||||
dex // decrement bit counter
|
||||
bne rdbit // read next bit!
|
||||
sta ($26,x) // save new byte *** same as "STA (HEX2L,X)" see KickC bug #756 https://gitlab.com/camelot/kickc/-/issues/756
|
||||
jsr incaddr // increment address
|
||||
ldy #53 // compensate threshold with workload
|
||||
bcc rdbyte // do next byte if not done yet!
|
||||
bcs restidx // always taken! restore parse index
|
||||
fullcycle: jsr cmplevel // wait for two level changes
|
||||
cmplevel: dey // decrement time counter
|
||||
lda TAPEIN // get tape in data
|
||||
cmp LASTSTATE // same as before?
|
||||
beq cmplevel // yes!
|
||||
sta LASTSTATE // save new data
|
||||
cpy #128 // compare threshold
|
||||
rts
|
||||
// [...]
|
||||
incaddr: lda HEX2L // compare current address with
|
||||
cmp HEX1L // end address
|
||||
lda HEX2H
|
||||
sbc HEX1H
|
||||
inc HEX2L // and increment current address
|
||||
bne nocarry // no carry to msb!
|
||||
inc HEX2H
|
||||
nocarry: rts
|
||||
|
||||
// end of read routine "restidx" is the exit point
|
||||
|
||||
restidx: rts
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
byte reference_packet[PACKETSIZE];
|
||||
byte *vmeter = "0123456789ABCDEF";
|
||||
|
||||
void decode_packets() {
|
||||
|
||||
// fill the reference packet with the known values
|
||||
for(byte t=0;t<PACKETSIZE;t++) reference_packet[t] = t;
|
||||
|
||||
// tape monitor loop
|
||||
for(;;) {
|
||||
read_packet(); // attempt reading 1 packet
|
||||
|
||||
// compare received packet with reference
|
||||
byte i;
|
||||
for(i=0;i<PACKETSIZE;i++) {
|
||||
if(RX_BUFFER[i] != reference_packet[i]) break;
|
||||
}
|
||||
|
||||
// display result
|
||||
if(i==0) woz_putc('.');
|
||||
else if(i==PACKETSIZE) woz_putc('*');
|
||||
else woz_putc(vmeter[i>>2]);
|
||||
|
||||
// exit with "X"
|
||||
if(apple1_readkey()=='X') break;
|
||||
}
|
||||
}
|
||||
|
||||
void simple_toggle_monitor()
|
||||
{
|
||||
asm {
|
||||
simple_monitor: lda TAPEIN // read tape input
|
||||
cmp LASTSTATE // compare to previous state
|
||||
beq no_toggle // if same just skip
|
||||
sta LASTSTATE // else save new state
|
||||
ldx #35 // set "toggle detected" flag in X, 35 is also the char to print
|
||||
no_toggle: bit DSP // check if display is ready to accept a character
|
||||
bmi simple_monitor // if not, just keep reading tape
|
||||
stx DSP // else display the "toggle detected" flag character
|
||||
ldx #45 // resets the "toggle detected" flag to the "-" sign, sets also Z=0 flag
|
||||
bne simple_monitor // cheap jump because Z is also 0
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
woz_puts("\r\rTAPE MONITOR\r\r"
|
||||
"[1] DECODE PACKETS\r"
|
||||
"[2] SIMPLE TOGGLE MONITOR\r\r"
|
||||
);
|
||||
|
||||
while(1) {
|
||||
byte key = apple1_getkey();
|
||||
if(key == '1') decode_packets();
|
||||
if(key == '2') simple_toggle_monitor();
|
||||
}
|
||||
}
|
120
package-lock.json
generated
Normal file
120
package-lock.json
generated
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"name": "apple1-videocard-lib",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "apple1-videocard-lib",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"command-line-args": "^5.2.0",
|
||||
"wav-decoder": "^1.3.0",
|
||||
"wav-encoder": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-back": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
|
||||
"integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-args": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
|
||||
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
|
||||
"dependencies": {
|
||||
"array-back": "^3.1.0",
|
||||
"find-replace": "^3.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/find-replace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
|
||||
"integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
|
||||
"dependencies": {
|
||||
"array-back": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||
},
|
||||
"node_modules/typical": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
|
||||
"integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
|
||||
"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": {
|
||||
"array-back": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
|
||||
"integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q=="
|
||||
},
|
||||
"command-line-args": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
|
||||
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
|
||||
"requires": {
|
||||
"array-back": "^3.1.0",
|
||||
"find-replace": "^3.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"find-replace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
|
||||
"integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
|
||||
"requires": {
|
||||
"array-back": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||
},
|
||||
"typical": {
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "apple1-videocard-lib",
|
||||
"version": "1.0.0",
|
||||
"description": "Library and demos for the \"Apple-1 Graphic Card\" by P-LAB, \r featuring the TMS9918 Video Display Processor by Texas Instruments.",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"lib": "lib"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nippur72/apple1-videocard-lib.git"
|
||||
},
|
||||
"author": "Antonino Porcino <nino.porcino@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nippur72/apple1-videocard-lib/issues"
|
||||
},
|
||||
"homepage": "https://github.com/nippur72/apple1-videocard-lib#readme",
|
||||
"dependencies": {
|
||||
"command-line-args": "^5.2.0",
|
||||
"wav-decoder": "^1.3.0",
|
||||
"wav-encoder": "^1.3.0"
|
||||
}
|
||||
}
|
12
tools/parseOptions.js
Normal file
12
tools/parseOptions.js
Normal 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;
|
@ -1,9 +1,21 @@
|
||||
const fs = require('fs');
|
||||
const parseOptions = require("./parseOptions");
|
||||
|
||||
let prg = fs.readFileSync("test_apple1.prg");
|
||||
const options = parseOptions([
|
||||
{ name: 'input', alias: 'i', type: String },
|
||||
{ name: 'output', alias: 'o', type: String }
|
||||
]);
|
||||
|
||||
if(options.input === undefined || options.output === undefined) {
|
||||
console.log("usage: prg2bin -i inputfile.prg -o outputfile.bin");
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
let prg = fs.readFileSync(options.input);
|
||||
|
||||
prg = prg.slice(2);
|
||||
|
||||
fs.writeFileSync("test_apple1.bin",prg);
|
||||
fs.writeFileSync(options.output,prg);
|
||||
|
||||
console.log(`${options.output} written`);
|
||||
|
||||
console.log("bin written");
|
||||
|
6
tools/wavconv/aci.js
Normal file
6
tools/wavconv/aci.js
Normal file
@ -0,0 +1,6 @@
|
||||
let ACI = {
|
||||
zero: 2000, // frequency for "0" bit
|
||||
one: 800 // frequency for "1" bit
|
||||
};
|
||||
|
||||
module.exports = { ACI };
|
26
tools/wavconv/bits2samples.js
Normal file
26
tools/wavconv/bits2samples.js
Normal 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 };
|
12
tools/wavconv/bytes2bits.js
Normal file
12
tools/wavconv/bytes2bits.js
Normal 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 };
|
9
tools/wavconv/checksum.js
Normal file
9
tools/wavconv/checksum.js
Normal 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
7
tools/wavconv/hex.js
Normal 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 };
|
12
tools/wavconv/parseOptions.js
Normal file
12
tools/wavconv/parseOptions.js
Normal 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
65
tools/wavconv/prg2wav.js
Normal 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(``);
|
||||
|
15
tools/wavconv/samples2wav.js
Normal file
15
tools/wavconv/samples2wav.js
Normal 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
145
tools/wavconv/wav2prg.js
Normal 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`);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user