laser500-wav
Advanced tools
Comparing version 1.0.3 to 1.1.0
@@ -12,9 +12,5 @@ #!/usr/bin/env node | ||
const fs_1 = __importDefault(require("fs")); | ||
const wav_encoder_1 = __importDefault(require("wav-encoder")); | ||
const options_1 = require("./options"); | ||
const turbo_loader_1 = require("./turbo_loader"); | ||
const bytes_1 = require("./bytes"); | ||
const checksum_1 = require("./checksum"); | ||
const turbo_encoder_1 = require("./turbo_encoder"); | ||
const vz_1 = require("./vz"); | ||
const tape_creator_1 = require("./tape_creator"); | ||
function main() { | ||
@@ -29,8 +25,7 @@ if (options_1.options.input === undefined || options_1.options.output === undefined || (options_1.options.l310 === undefined && options_1.options.l500 === undefined)) { | ||
console.log(" -v or --volume number volume between 0 and 1 (1.0 default)"); | ||
console.log(" --stereoboost boost volume for stereo cables by inverting the RIGHT channel"); | ||
console.log(" --invert inverts the polarity of the audio"); | ||
console.log(" --stereoboost boost volume for stereo cables by inverting the RIGHT channel (default off)"); | ||
console.log(" --invert inverts the polarity of the audio (default off)"); | ||
console.log(" --header number of header bytes (128 default as in ROM loader)"); | ||
console.log(" --pulsems n ROM loader pulse width in microseconds (277 default)"); | ||
console.log(" -x or --turbo generates a turbo tape loadable file"); | ||
console.log(" --turbo-speed speed speed 1,2,3,4 defaults to 4 (fastest)"); | ||
console.log(" -x or --turbo speed 0=normal ROM loader WAV, 1-4 turbo tape (4 fastest)"); | ||
process.exit(0); | ||
@@ -42,254 +37,17 @@ } | ||
} | ||
const laser500 = options_1.options.l500 && !options_1.options.l310; | ||
// note on SAMPLE_RATE: when using turbo tape 48000 Hz is the minimum to work | ||
// on the real machine. One the emulator the minimum is 18000 Hz | ||
const SAMPLE_RATE = options_1.options.samplerate || 96000; | ||
const VOLUME = options_1.options.volume || 1.0; | ||
const HEADER_LEN = options_1.options.header || 128; | ||
const TAIL_LEN = 4; // 128; // TODO make it option? | ||
const pulsems = (options_1.options.pulsems || 277) / 1000000; // for a total of 277 microseconds | ||
const PULSE_SHORT = pulsems * SAMPLE_RATE; | ||
const PULSE_LONG = PULSE_SHORT * 2; | ||
const turboparams = (0, turbo_encoder_1.decodeBitSize)(options_1.options['turbo-speed'], SAMPLE_RATE, laser500); | ||
const turbo = { | ||
THRESHOLD: turboparams.THRESHOLD, | ||
TURBO_HALFPULSE_SIZE: turboparams.TURBO_HALFPULSE_SIZE, | ||
TURBO_INVERT: turboparams.TURBO_INVERT | ||
}; | ||
const fileName = options_1.options.input; | ||
const outputName = options_1.options.output; | ||
const wavName = outputName + ".wav"; | ||
if (!fs_1.default.existsSync(fileName)) { | ||
console.log(`file "${fileName}" not found`); | ||
const VZ_file_name = options_1.options.input; | ||
const WAV_file_name = options_1.options.output + ".wav"; | ||
if (!fs_1.default.existsSync(VZ_file_name)) { | ||
console.log(`file "${VZ_file_name}" not found`); | ||
process.exit(0); | ||
} | ||
const VZ = (0, vz_1.unpackvz)(fs_1.default.readFileSync(fileName)); | ||
const fileType = VZ.type; | ||
const tape = { | ||
tapeName: VZ.filename, | ||
fileType, | ||
startAddress: VZ.start, | ||
program: Buffer.from(VZ.data), | ||
headerLen: HEADER_LEN, | ||
tailLen: TAIL_LEN, | ||
PULSE_SHORT: PULSE_SHORT, | ||
PULSE_LONG: PULSE_LONG, | ||
SAMPLE_RATE: SAMPLE_RATE, | ||
VOLUME: VOLUME, | ||
laser500 | ||
}; | ||
console.log(`target is ${tape.laser500 ? 'Laser 500' : 'Laser 310'} `); | ||
console.log(`SAVING ${fileType === vz_1.VZ_BASIC ? "T" : "B"}: '${VZ.filename}' from $${(0, bytes_1.hex)(VZ.start, 4)}, ${VZ.data.length} bytes`); | ||
let samples; | ||
// normal tape | ||
if (options_1.options.turbo === undefined) { | ||
samples = getNormalSamples(tape); | ||
} | ||
else { | ||
samples = getTurboSamples(tape, turbo); | ||
} | ||
// invert audio samples if --invert option was given | ||
if (options_1.options.invert) | ||
samples = invertSamples(samples); | ||
// fix_cassette_port(samples, SAMPLE_RATE); | ||
const f_samples = new Float32Array(samples); | ||
const f_samples_inv = new Float32Array(invertSamples(samples)); | ||
const wavData = { | ||
sampleRate: SAMPLE_RATE, | ||
channelData: !options_1.options.stereoboost ? [f_samples] : [f_samples, f_samples_inv] | ||
}; | ||
const wavoptions = { bitDepth: 16, float: false, symmetric: false }; | ||
const buffer = wav_encoder_1.default.encode.sync(wavData, wavoptions); | ||
fs_1.default.writeFileSync(wavName, Buffer.from(buffer)); | ||
let gentype = fileType === vz_1.VZ_BINARY ? "B: standard file" : "T: standard file"; | ||
if (options_1.options.turbo !== undefined) | ||
const VZ_file = fs_1.default.readFileSync(VZ_file_name); | ||
const VZ = (0, vz_1.unpackvz)(VZ_file); | ||
const buffer = (0, tape_creator_1.VZ_to_WAV)(VZ, options_1.options); | ||
fs_1.default.writeFileSync(WAV_file_name, Buffer.from(buffer)); | ||
let gentype = VZ.type === vz_1.VZ_BINARY ? "B: standard file" : "T: standard file"; | ||
if (options_1.options.turbo !== 0) | ||
gentype = "TURBO tape"; | ||
console.log(`file "${wavName}" generated as ${gentype}`); | ||
/* | ||
// write inverted FFT | ||
{ | ||
const { fft, ifft, util } = require('fft-js'); | ||
const FFTSIZE = 256; | ||
let in_samples = Array.from(f_samples); | ||
let out_samples: number[] = []; | ||
for(let i=0; i<in_samples.length-FFTSIZE; i+=FFTSIZE) { | ||
const realInput = in_samples.slice(i,i+FFTSIZE); | ||
const phasors = fft(realInput); | ||
for(let j=32; j<FFTSIZE; j++) { | ||
phasors[j][0] = 0; | ||
phasors[j][1] = 0; | ||
} | ||
const signal = ifft(phasors) as [number, number][]; | ||
const real = signal.map(e=>e[0]); | ||
out_samples.push(...real); | ||
} | ||
const wavData = { | ||
sampleRate: SAMPLE_RATE, | ||
channelData: [ new Float32Array(out_samples) ] | ||
}; | ||
const wavoptions: WavEncoder.Options = { bitDepth: 16, float: false, symmetric: false }; | ||
const buffer = WavEncoder.encode.sync(wavData, wavoptions); | ||
fs.writeFileSync("testfft.wav", Buffer.from(buffer)); | ||
} | ||
*/ | ||
console.log(`file "${WAV_file_name}" generated as ${gentype}`); | ||
} | ||
// *************************************************************************************** | ||
function getNormalSamples(tape) { | ||
const { header_bytes, body_bytes } = tapeStructure(tape); | ||
// header | ||
const header_bits = bytesToBits(header_bytes); | ||
const header_pulses = bitsToPulses(header_bits); | ||
const header_samples = pulsesToSamples(header_pulses, tape); | ||
// gap between header and body in Laser310 | ||
let gap = tape.laser500 ? [] : getGapSamples(tape); | ||
// body | ||
const body_bits = bytesToBits(body_bytes); | ||
const body_pulses = bitsToPulses(body_bits); | ||
const body_samples = pulsesToSamples(body_pulses, tape); | ||
const samples = header_samples.concat(gap).concat(body_samples); | ||
return samples; | ||
} | ||
function getTurboSamples(tape, turbo) { | ||
const { startAddress, program } = tape; | ||
const turbo_address = startAddress + program.length; | ||
const loader_program = (0, turbo_loader_1.getTurboLoader)(tape.laser500, turbo.THRESHOLD, turbo_address, tape.fileType); | ||
tape.fileType = vz_1.VZ_BINARY; | ||
tape.startAddress = turbo_address; | ||
tape.program = loader_program; | ||
const { header_bytes, body_bytes } = tapeStructure(tape); | ||
const header_bits = bytesToBits(header_bytes); | ||
const header_pulses = bitsToPulses(header_bits); | ||
const header_samples = pulsesToSamples(header_pulses, tape); | ||
// gap between header and body in Laser310 | ||
let gap = tape.laser500 ? [] : getGapSamples(tape); | ||
// body | ||
const body_bits = bytesToBits(body_bytes); | ||
const body_pulses = bitsToPulses(body_bits); | ||
const body_samples = pulsesToSamples(body_pulses, tape); | ||
const turbo_bytes = (0, turbo_encoder_1.getTurboBytes)(startAddress, program); | ||
// patch | ||
//for(let t=0; t<turbo_bytes.length; t++) turbo_bytes[t] = t % 2 + 2; | ||
const turbo_bits = bytesToBits(turbo_bytes); | ||
const turbo_samples = (0, turbo_encoder_1.TT_bitsToSamples)(turbo_bits, tape, turbo); | ||
const samples = header_samples.concat(gap).concat(body_samples).concat(turbo_samples); | ||
return samples; | ||
} | ||
function invertSamples(samples) { | ||
return samples.map(e => -e); | ||
} | ||
function tapeStructure(tape) { | ||
const header_bytes = []; | ||
const body_bytes = []; | ||
const { tapeName, fileType, startAddress, program, headerLen, tailLen, laser500 } = tape; | ||
// header | ||
for (let t = 0; t < headerLen; t++) | ||
header_bytes.push(0x80); | ||
for (let t = 0; t < 5; t++) | ||
header_bytes.push(0xfe); | ||
// file type | ||
header_bytes.push(fileType); | ||
// file name | ||
for (let t = 0; t < tapeName.length; t++) | ||
header_bytes.push(tapeName.charCodeAt(t)); | ||
header_bytes.push(0x00); | ||
if (laser500) { | ||
// laser 500 has additional bytes and a marker to allow print of file name | ||
// laser 310 elongates the last pulse of the "0" bit to allow print of file name | ||
for (let t = 0; t < 5; t++) | ||
header_bytes.push(0x80); // additional header to allow print of file name | ||
for (let t = 0; t < 10; t++) | ||
header_bytes.push(0x80); | ||
header_bytes.push(0xff); // end of header | ||
} | ||
// start address | ||
body_bytes.push((0, bytes_1.lo)(startAddress)); | ||
body_bytes.push((0, bytes_1.hi)(startAddress)); | ||
// end address | ||
const endAddress = startAddress + program.length; | ||
body_bytes.push((0, bytes_1.lo)(endAddress)); | ||
body_bytes.push((0, bytes_1.hi)(endAddress)); | ||
// program | ||
for (let t = 0; t < program.length; t++) | ||
body_bytes.push(program[t]); | ||
// checksum | ||
const checksum = (0, checksum_1.calculate_checksum)(program, startAddress, endAddress); | ||
body_bytes.push((0, bytes_1.lo)(checksum)); | ||
body_bytes.push((0, bytes_1.hi)(checksum)); | ||
// terminator | ||
for (let t = 0; t < tailLen; t++) | ||
body_bytes.push(0x00); | ||
return { header_bytes, body_bytes }; | ||
} | ||
function bytesToBits(bytes) { | ||
const bits = []; | ||
for (let t = 0; t < bytes.length; t++) { | ||
const b = bytes[t] & 0xFF; | ||
bits.push((b & 128) >> 7); | ||
bits.push((b & 64) >> 6); | ||
bits.push((b & 32) >> 5); | ||
bits.push((b & 16) >> 4); | ||
bits.push((b & 8) >> 3); | ||
bits.push((b & 4) >> 2); | ||
bits.push((b & 2) >> 1); | ||
bits.push((b & 1) >> 0); | ||
} | ||
return bits; | ||
} | ||
function bitsToPulses(bits) { | ||
const pulses = []; | ||
for (let t = 0; t < bits.length; t++) { | ||
const b = bits[t]; | ||
if (b == 0) { | ||
pulses.push("S"); | ||
pulses.push("L"); /*tapeBits.push(1); tapeBits.push(0); tapeBits.push(1); tapeBits.push(1); tapeBits.push(0); tapeBits.push(0);*/ | ||
} | ||
else { | ||
pulses.push("S"); | ||
pulses.push("S"); | ||
pulses.push("S"); /*tapeBits.push(1); tapeBits.push(0); tapeBits.push(1); tapeBits.push(0); tapeBits.push(1); tapeBits.push(0);*/ | ||
} | ||
} | ||
return pulses; | ||
} | ||
function pulsesToSamples(tapeBits, tape) { | ||
const samples = []; | ||
let ptr = 0; | ||
const { SAMPLE_RATE, VOLUME, PULSE_LONG, PULSE_SHORT } = tape; | ||
for (let t = 0; t < tapeBits.length; t++) { | ||
if (tapeBits[t] === "S") { | ||
for (; ptr < PULSE_SHORT; ptr++) | ||
samples.push(VOLUME); | ||
ptr -= PULSE_SHORT; | ||
for (; ptr < PULSE_SHORT; ptr++) | ||
samples.push(-VOLUME); | ||
ptr -= PULSE_SHORT; | ||
} | ||
else { | ||
for (; ptr < PULSE_LONG; ptr++) | ||
samples.push(VOLUME); | ||
ptr -= PULSE_LONG; | ||
for (; ptr < PULSE_LONG; ptr++) | ||
samples.push(-VOLUME); | ||
ptr -= PULSE_LONG; | ||
} | ||
} | ||
return samples; | ||
} | ||
function getGapSamples(tape) { | ||
const samples = []; | ||
let ptr = 0; | ||
const { VOLUME, PULSE_SHORT } = tape; | ||
for (let t = 0; t < 11; t++) { | ||
for (; ptr < PULSE_SHORT; ptr++) | ||
samples.push(-VOLUME); | ||
ptr -= PULSE_SHORT; | ||
} | ||
return samples; | ||
} | ||
main(); |
@@ -9,12 +9,11 @@ "use strict"; | ||
exports.options = parseOptions([ | ||
{ name: 'input', alias: 'i', type: String, defaultOption: true }, | ||
{ name: 'input', alias: 'i', type: String, }, | ||
{ name: 'output', alias: 'o', type: String }, | ||
{ name: 'samplerate', alias: 's', type: Number }, | ||
{ name: 'volume', alias: 'v', type: Number }, | ||
{ name: 'stereoboost', type: Boolean }, | ||
{ name: 'invert', type: Boolean }, | ||
{ name: 'header', type: Number }, | ||
{ name: 'pulsems', type: Number }, | ||
{ name: 'turbo', alias: 'x', type: Boolean }, | ||
{ name: 'turbo-speed', type: Number }, | ||
{ name: 'samplerate', alias: 's', type: Number, defaultValue: 96000 }, | ||
{ name: 'volume', alias: 'v', type: Number, defaultValue: 1 }, | ||
{ name: 'stereoboost', type: Boolean, defaultValue: false }, | ||
{ name: 'invert', type: Boolean, defaultValue: false }, | ||
{ name: 'header', type: Number, defaultValue: 128 }, | ||
{ name: 'pulsems', type: Number, defaultValue: 277 }, | ||
{ name: 'turbo', alias: 'x', type: Number, defaultValue: 0 }, | ||
{ name: 'l500', type: Boolean }, | ||
@@ -21,0 +20,0 @@ { name: 'l310', type: Boolean } |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getTurboLoader = void 0; | ||
const fs_1 = __importDefault(require("fs")); | ||
const path_1 = __importDefault(require("path")); | ||
const bytes_1 = require("./bytes"); | ||
const turbo_L310_asm_info_1 = require("./turbo_L310_asm_info"); | ||
const turbo_L500_asm_info_1 = require("./turbo_L500_asm_info"); | ||
// get the Z80 turbo loader routine, patching two bytes and | ||
// relocating it at the desidered address | ||
function getTurboLoader(laser500, THRESHOLD, relocate_address, fileType) { | ||
const rootname = path_1.default.resolve(__dirname, laser500 ? "../turbo_tape/turbo_L500" : "../turbo_tape/turbo_L310"); | ||
const loader_program = fs_1.default.readFileSync(`${rootname}.bin`); | ||
patch_bytes(loader_program, THRESHOLD, fileType, rootname); | ||
relocate(loader_program, relocate_address, rootname); | ||
const asm_info = laser500 ? turbo_L500_asm_info_1.turbo_L500_asm_info : turbo_L310_asm_info_1.turbo_L310_asm_info; | ||
const loader_program = asm_info.code; | ||
patch_bytes(loader_program, THRESHOLD, fileType, asm_info); | ||
const relocated = relocate(loader_program, relocate_address, asm_info); | ||
// for debug purposes | ||
{ | ||
const symbols = fs_1.default.readFileSync(`${rootname}.sym`).toString(); | ||
let set_threshold = getSymbolAddress(symbols, "set_threshold") + relocate_address; | ||
let set_threshold = asm_info.symbols["set_threshold"] + relocate_address; | ||
console.log(`const label_set_threshold = 0x${(0, bytes_1.hex)(set_threshold, 4)};`); | ||
let loop_file = getSymbolAddress(symbols, "loop_file") + relocate_address; | ||
let loop_file = asm_info.symbols["loop_file"] + relocate_address; | ||
console.log(`const label_loop_file = 0x${(0, bytes_1.hex)(loop_file, 4)};`); | ||
let autorun = getSymbolAddress(symbols, "autorun") + relocate_address; | ||
let autorun = asm_info.symbols["autorun"] + relocate_address; | ||
console.log(`const label_autorun = 0x${(0, bytes_1.hex)(autorun, 4)};`); | ||
} | ||
return loader_program; | ||
return relocated; | ||
} | ||
exports.getTurboLoader = getTurboLoader; | ||
function patch_bytes(loader_program, THRESHOLD, fileType, rootname) { | ||
// read turbo.sym symbol files | ||
const symbols = fs_1.default.readFileSync(`${rootname}.sym`).toString(); | ||
// do the needed byte patches | ||
loader_program[getSymbolAddress(symbols, "turbo_load") + 1] = fileType; | ||
loader_program[getSymbolAddress(symbols, "set_threshold") + 1] = THRESHOLD; | ||
function patch_bytes(loader_program, THRESHOLD, fileType, info) { | ||
loader_program[info.symbols.turbo_load + 1] = fileType; // patches the file type B: or T: | ||
loader_program[info.symbols.set_threshold + 1] = THRESHOLD; // patches the turbo speed threshold | ||
} | ||
function getSymbolAddress(file, symbolname) { | ||
const regex = new RegExp(symbolname + "\\s*=\\s*\\$(?<address>[0-9a-fA-F]{4})", "g"); | ||
const match = regex.exec(file); | ||
if (match === null || match.groups === undefined) | ||
throw `can't find ${symbolname} label in .sym file`; | ||
// get label address from hex format | ||
const address = Number.parseInt(match.groups["address"], 16); | ||
return address; | ||
} | ||
// relocate the Z80 turbo loader routine at the desidered | ||
@@ -50,11 +34,12 @@ // destination by changing all the interested addresses. | ||
// of offsets (16 bits) that needs to be changed | ||
function relocate(loader_program, relocate_address, rootname) { | ||
const reloc_info = fs_1.default.readFileSync(`${rootname}.reloc`); | ||
for (let t = 0; t < reloc_info.length; t += 2) { | ||
const patch_address = reloc_info.readUInt16LE(t); | ||
const offset_value = loader_program.readUInt16LE(patch_address); | ||
const relocated_value = offset_value + relocate_address; | ||
loader_program.writeUInt16LE(relocated_value, patch_address); | ||
//console.log(`${t}: [${patch_address}] = ${offset_value.toString(16)} -> ${relocated_value.toString(16)}`); | ||
function relocate(loader_program, relocate_address, asm_info) { | ||
const reloc_table = asm_info.reloc; | ||
const lp = Buffer.from(loader_program); | ||
for (let t = 0; t < reloc_table.length; t++) { | ||
const index = reloc_table[t]; | ||
const address = lp.readUInt16LE(index); | ||
const relocated_value = address + relocate_address; | ||
lp.writeUInt16LE(relocated_value, index); | ||
} | ||
return lp; | ||
} |
{ | ||
"name": "laser500-wav", | ||
"version": "1.0.3", | ||
"version": "1.1.0", | ||
"description": "Laser310/500 program to WAV (tape) converter", | ||
@@ -5,0 +5,0 @@ "bin": { |
@@ -9,35 +9,7 @@ #!/usr/bin/env node | ||
import fs from "fs"; | ||
import path from 'path'; | ||
import WavEncoder from "wav-encoder"; | ||
import { options } from './options'; | ||
import { getTurboLoader } from "./turbo_loader"; | ||
import { hex, hi, lo } from "./bytes"; | ||
import { calculate_checksum } from "./checksum"; | ||
import { TT_bitsToSamples, decodeBitSize, getTurboBytes } from "./turbo_encoder"; | ||
import { VZ_BASIC, VZ_BINARY, VZFILETYPE, unpackvz } from "./vz"; | ||
import { fix_cassette_port } from "./fix_tape"; | ||
import { VZ_BINARY, unpackvz } from "./vz"; | ||
import { VZ_to_WAV } from "./tape_creator"; | ||
type Pulse = "S" | "L"; | ||
export interface Tape { | ||
tapeName: string; | ||
fileType: VZFILETYPE; | ||
startAddress: number; | ||
program: Buffer; | ||
headerLen: number; | ||
tailLen: number; | ||
PULSE_SHORT: number; | ||
PULSE_LONG: number; | ||
SAMPLE_RATE: number; | ||
VOLUME: number; | ||
laser500: boolean; | ||
} | ||
export interface TurboTape { | ||
THRESHOLD: number; | ||
TURBO_HALFPULSE_SIZE: number; | ||
TURBO_INVERT: boolean; | ||
} | ||
function main() { | ||
@@ -52,8 +24,7 @@ if(options.input === undefined || options.output === undefined || (options.l310===undefined && options.l500===undefined)) { | ||
console.log(" -v or --volume number volume between 0 and 1 (1.0 default)"); | ||
console.log(" --stereoboost boost volume for stereo cables by inverting the RIGHT channel"); | ||
console.log(" --invert inverts the polarity of the audio"); | ||
console.log(" --stereoboost boost volume for stereo cables by inverting the RIGHT channel (default off)"); | ||
console.log(" --invert inverts the polarity of the audio (default off)"); | ||
console.log(" --header number of header bytes (128 default as in ROM loader)"); | ||
console.log(" --pulsems n ROM loader pulse width in microseconds (277 default)"); | ||
console.log(" -x or --turbo generates a turbo tape loadable file"); | ||
console.log(" --turbo-speed speed speed 1,2,3,4 defaults to 4 (fastest)"); | ||
console.log(" -x or --turbo speed 0=normal ROM loader WAV, 1-4 turbo tape (4 fastest)"); | ||
process.exit(0); | ||
@@ -67,295 +38,21 @@ } | ||
const laser500 = options.l500 && !options.l310; | ||
const VZ_file_name = options.input; | ||
const WAV_file_name = options.output + ".wav"; | ||
// note on SAMPLE_RATE: when using turbo tape 48000 Hz is the minimum to work | ||
// on the real machine. One the emulator the minimum is 18000 Hz | ||
const SAMPLE_RATE = options.samplerate || 96000; | ||
const VOLUME = options.volume || 1.0; | ||
const HEADER_LEN = options.header || 128; | ||
const TAIL_LEN = 4; // 128; // TODO make it option? | ||
const pulsems = (options.pulsems || 277)/1000000; // for a total of 277 microseconds | ||
const PULSE_SHORT = pulsems * SAMPLE_RATE; | ||
const PULSE_LONG = PULSE_SHORT * 2; | ||
const turboparams = decodeBitSize(options['turbo-speed'], SAMPLE_RATE, laser500); | ||
const turbo: TurboTape = { | ||
THRESHOLD: turboparams.THRESHOLD, | ||
TURBO_HALFPULSE_SIZE: turboparams.TURBO_HALFPULSE_SIZE, | ||
TURBO_INVERT: turboparams.TURBO_INVERT | ||
}; | ||
const fileName = options.input; | ||
const outputName = options.output; | ||
const wavName = outputName + ".wav"; | ||
if(!fs.existsSync(fileName)) { | ||
console.log(`file "${fileName}" not found`); | ||
if(!fs.existsSync(VZ_file_name)) { | ||
console.log(`file "${VZ_file_name}" not found`); | ||
process.exit(0); | ||
} | ||
const VZ = unpackvz(fs.readFileSync(fileName)); | ||
const VZ_file = fs.readFileSync(VZ_file_name); | ||
const VZ = unpackvz(VZ_file); | ||
const buffer = VZ_to_WAV(VZ, options); | ||
const fileType = VZ.type; | ||
const tape: Tape = { | ||
tapeName: VZ.filename, | ||
fileType, | ||
startAddress: VZ.start, | ||
program: Buffer.from(VZ.data), | ||
headerLen: HEADER_LEN, | ||
tailLen: TAIL_LEN, | ||
PULSE_SHORT: PULSE_SHORT, | ||
PULSE_LONG: PULSE_LONG, | ||
SAMPLE_RATE: SAMPLE_RATE, | ||
VOLUME: VOLUME, | ||
laser500 | ||
}; | ||
fs.writeFileSync(WAV_file_name, Buffer.from(buffer)); | ||
console.log(`target is ${tape.laser500 ? 'Laser 500' : 'Laser 310'} `); | ||
console.log(`SAVING ${fileType === VZ_BASIC ? "T" : "B"}: '${VZ.filename}' from $${hex(VZ.start,4)}, ${VZ.data.length} bytes`); | ||
let samples: number[]; | ||
// normal tape | ||
if(options.turbo === undefined) { | ||
samples = getNormalSamples(tape); | ||
} | ||
else { | ||
samples = getTurboSamples(tape, turbo); | ||
} | ||
// invert audio samples if --invert option was given | ||
if(options.invert) samples = invertSamples(samples); | ||
// fix_cassette_port(samples, SAMPLE_RATE); | ||
const f_samples = new Float32Array(samples); | ||
const f_samples_inv = new Float32Array(invertSamples(samples)); | ||
const wavData = { | ||
sampleRate: SAMPLE_RATE, | ||
channelData: !options.stereoboost ? [ f_samples ] : [ f_samples, f_samples_inv ] | ||
}; | ||
const wavoptions: WavEncoder.Options = { bitDepth: 16, float: false, symmetric: false }; | ||
const buffer = WavEncoder.encode.sync(wavData, wavoptions); | ||
fs.writeFileSync(wavName, Buffer.from(buffer)); | ||
let gentype = fileType === VZ_BINARY ? "B: standard file" : "T: standard file"; | ||
if(options.turbo !== undefined) gentype = "TURBO tape"; | ||
console.log(`file "${wavName}" generated as ${gentype}`); | ||
/* | ||
// write inverted FFT | ||
{ | ||
const { fft, ifft, util } = require('fft-js'); | ||
const FFTSIZE = 256; | ||
let in_samples = Array.from(f_samples); | ||
let out_samples: number[] = []; | ||
for(let i=0; i<in_samples.length-FFTSIZE; i+=FFTSIZE) { | ||
const realInput = in_samples.slice(i,i+FFTSIZE); | ||
const phasors = fft(realInput); | ||
for(let j=32; j<FFTSIZE; j++) { | ||
phasors[j][0] = 0; | ||
phasors[j][1] = 0; | ||
} | ||
const signal = ifft(phasors) as [number, number][]; | ||
const real = signal.map(e=>e[0]); | ||
out_samples.push(...real); | ||
} | ||
const wavData = { | ||
sampleRate: SAMPLE_RATE, | ||
channelData: [ new Float32Array(out_samples) ] | ||
}; | ||
const wavoptions: WavEncoder.Options = { bitDepth: 16, float: false, symmetric: false }; | ||
const buffer = WavEncoder.encode.sync(wavData, wavoptions); | ||
fs.writeFileSync("testfft.wav", Buffer.from(buffer)); | ||
} | ||
*/ | ||
let gentype = VZ.type === VZ_BINARY ? "B: standard file" : "T: standard file"; | ||
if(options.turbo !== 0) gentype = "TURBO tape"; | ||
console.log(`file "${WAV_file_name}" generated as ${gentype}`); | ||
} | ||
// *************************************************************************************** | ||
function getNormalSamples(tape: Tape) { | ||
const { header_bytes, body_bytes } = tapeStructure(tape); | ||
// header | ||
const header_bits = bytesToBits(header_bytes); | ||
const header_pulses = bitsToPulses(header_bits); | ||
const header_samples = pulsesToSamples(header_pulses, tape); | ||
// gap between header and body in Laser310 | ||
let gap: number[] = tape.laser500 ? [] : getGapSamples(tape); | ||
// body | ||
const body_bits = bytesToBits(body_bytes); | ||
const body_pulses = bitsToPulses(body_bits); | ||
const body_samples = pulsesToSamples(body_pulses, tape); | ||
const samples = header_samples.concat(gap).concat(body_samples); | ||
return samples; | ||
} | ||
function getTurboSamples(tape: Tape, turbo: TurboTape) { | ||
const { startAddress, program } = tape; | ||
const turbo_address = startAddress + program.length; | ||
const loader_program = getTurboLoader(tape.laser500, turbo.THRESHOLD, turbo_address, tape.fileType); | ||
tape.fileType = VZ_BINARY; | ||
tape.startAddress = turbo_address; | ||
tape.program = loader_program; | ||
const { header_bytes, body_bytes } = tapeStructure(tape); | ||
const header_bits = bytesToBits(header_bytes); | ||
const header_pulses = bitsToPulses(header_bits); | ||
const header_samples = pulsesToSamples(header_pulses, tape); | ||
// gap between header and body in Laser310 | ||
let gap: number[] = tape.laser500 ? [] : getGapSamples(tape); | ||
// body | ||
const body_bits = bytesToBits(body_bytes); | ||
const body_pulses = bitsToPulses(body_bits); | ||
const body_samples = pulsesToSamples(body_pulses, tape); | ||
const turbo_bytes = getTurboBytes(startAddress, program); | ||
// patch | ||
//for(let t=0; t<turbo_bytes.length; t++) turbo_bytes[t] = t % 2 + 2; | ||
const turbo_bits = bytesToBits(turbo_bytes); | ||
const turbo_samples = TT_bitsToSamples(turbo_bits, tape, turbo); | ||
const samples = header_samples.concat(gap).concat(body_samples).concat(turbo_samples); | ||
return samples; | ||
} | ||
function invertSamples(samples: number[]) | ||
{ | ||
return samples.map(e=>-e); | ||
} | ||
function tapeStructure(tape: Tape) { | ||
const header_bytes = []; | ||
const body_bytes = []; | ||
const { tapeName, fileType, startAddress, program, headerLen, tailLen, laser500 } = tape; | ||
// header | ||
for(let t=0; t<headerLen; t++) header_bytes.push(0x80); | ||
for(let t=0; t<5; t++) header_bytes.push(0xfe); | ||
// file type | ||
header_bytes.push(fileType); | ||
// file name | ||
for(let t=0; t<tapeName.length; t++) header_bytes.push(tapeName.charCodeAt(t)); | ||
header_bytes.push(0x00); | ||
if(laser500) { | ||
// laser 500 has additional bytes and a marker to allow print of file name | ||
// laser 310 elongates the last pulse of the "0" bit to allow print of file name | ||
for(let t=0; t<5; t++) header_bytes.push(0x80); // additional header to allow print of file name | ||
for(let t=0; t<10; t++) header_bytes.push(0x80); | ||
header_bytes.push(0xff); // end of header | ||
} | ||
// start address | ||
body_bytes.push(lo(startAddress)); | ||
body_bytes.push(hi(startAddress)); | ||
// end address | ||
const endAddress = startAddress + program.length; | ||
body_bytes.push(lo(endAddress)); | ||
body_bytes.push(hi(endAddress)); | ||
// program | ||
for(let t=0; t<program.length; t++) body_bytes.push(program[t]); | ||
// checksum | ||
const checksum = calculate_checksum(program, startAddress, endAddress); | ||
body_bytes.push(lo(checksum)); | ||
body_bytes.push(hi(checksum)); | ||
// terminator | ||
for(let t=0; t<tailLen; t++) body_bytes.push(0x00); | ||
return { header_bytes, body_bytes }; | ||
} | ||
function bytesToBits(bytes: number[]): number[] { | ||
const bits = []; | ||
for(let t=0; t<bytes.length; t++) { | ||
const b = bytes[t] & 0xFF; | ||
bits.push((b & 128) >> 7); | ||
bits.push((b & 64) >> 6); | ||
bits.push((b & 32) >> 5); | ||
bits.push((b & 16) >> 4); | ||
bits.push((b & 8) >> 3); | ||
bits.push((b & 4) >> 2); | ||
bits.push((b & 2) >> 1); | ||
bits.push((b & 1) >> 0); | ||
} | ||
return bits; | ||
} | ||
function bitsToPulses(bits: number[]): Pulse[] { | ||
const pulses: Pulse[] = []; | ||
for(let t=0; t<bits.length; t++) { | ||
const b = bits[t]; | ||
if(b == 0) { pulses.push("S"); pulses.push("L"); /*tapeBits.push(1); tapeBits.push(0); tapeBits.push(1); tapeBits.push(1); tapeBits.push(0); tapeBits.push(0);*/ } | ||
else { pulses.push("S"); pulses.push("S"); pulses.push("S"); /*tapeBits.push(1); tapeBits.push(0); tapeBits.push(1); tapeBits.push(0); tapeBits.push(1); tapeBits.push(0);*/ } | ||
} | ||
return pulses; | ||
} | ||
function pulsesToSamples(tapeBits: Pulse[], tape: Tape): number[] { | ||
const samples = []; | ||
let ptr = 0; | ||
const { SAMPLE_RATE, VOLUME, PULSE_LONG, PULSE_SHORT } = tape; | ||
for(let t=0; t<tapeBits.length; t++) { | ||
if(tapeBits[t]==="S") { | ||
for(;ptr<PULSE_SHORT; ptr++) samples.push(VOLUME); ptr -= PULSE_SHORT; | ||
for(;ptr<PULSE_SHORT; ptr++) samples.push(-VOLUME); ptr -= PULSE_SHORT; | ||
} | ||
else { | ||
for(;ptr<PULSE_LONG; ptr++) samples.push(VOLUME); ptr -= PULSE_LONG; | ||
for(;ptr<PULSE_LONG; ptr++) samples.push(-VOLUME); ptr -= PULSE_LONG; | ||
} | ||
} | ||
return samples; | ||
} | ||
function getGapSamples(tape: Tape): number[] { | ||
const samples = []; | ||
let ptr = 0; | ||
const { VOLUME, PULSE_SHORT } = tape; | ||
for(let t=0; t<11; t++) { | ||
for(;ptr<PULSE_SHORT; ptr++) samples.push(-VOLUME); ptr -= PULSE_SHORT; | ||
} | ||
return samples; | ||
} | ||
main(); |
import commandLineArgs from 'command-line-args'; | ||
export interface CommandLineOptions { | ||
input: string; | ||
output: string; | ||
samplerate: number; | ||
volume: number; | ||
stereoboost: boolean; | ||
invert: boolean; | ||
header: number; | ||
pulsems: number; | ||
turbo: number; | ||
l500: boolean; | ||
l310: boolean; | ||
} | ||
export const options = parseOptions([ | ||
{ name: 'input', alias: 'i', type: String, defaultOption: true }, | ||
{ name: 'input', alias: 'i', type: String, }, | ||
{ name: 'output', alias: 'o', type: String }, | ||
{ name: 'samplerate', alias: 's', type: Number }, | ||
{ name: 'volume', alias: 'v', type: Number }, | ||
{ name: 'stereoboost', type: Boolean }, | ||
{ name: 'invert', type: Boolean }, | ||
{ name: 'header', type: Number }, | ||
{ name: 'pulsems', type: Number }, | ||
{ name: 'turbo', alias: 'x', type: Boolean }, | ||
{ name: 'turbo-speed', type: Number }, | ||
{ name: 'samplerate', alias: 's', type: Number, defaultValue: 96000 }, | ||
{ name: 'volume', alias: 'v', type: Number, defaultValue: 1 }, | ||
{ name: 'stereoboost', type: Boolean, defaultValue: false }, | ||
{ name: 'invert', type: Boolean, defaultValue: false }, | ||
{ name: 'header', type: Number, defaultValue: 128 }, | ||
{ name: 'pulsems', type: Number, defaultValue: 277 }, | ||
{ name: 'turbo', alias: 'x', type: Number, defaultValue: 0 }, | ||
{ name: 'l500', type: Boolean }, | ||
{ name: 'l310', type: Boolean } | ||
]); | ||
]) as CommandLineOptions; | ||
@@ -27,3 +40,1 @@ function parseOptions(optionDefinitions: commandLineArgs.OptionDefinition[]) { | ||
} | ||
@@ -6,4 +6,4 @@ // ************************************************************************ | ||
import { calculate_checksum } from "./checksum"; | ||
import { Tape, TurboTape } from "./laser500wav"; | ||
import { hi, lo } from "./bytes"; | ||
import { Tape, TurboTape } from "./tape_creator"; | ||
@@ -10,0 +10,0 @@ export function decodeBitSize(speed: number, SAMPLE_RATE: number, laser500: boolean) { |
@@ -1,53 +0,36 @@ | ||
import fs from "fs"; | ||
import path from "path"; | ||
import { hex } from "./bytes"; | ||
import { turbo_L310_asm_info } from "./turbo_L310_asm_info"; | ||
import { turbo_L500_asm_info } from "./turbo_L500_asm_info"; | ||
type AsmInfo = typeof turbo_L310_asm_info | typeof turbo_L500_asm_info; | ||
// get the Z80 turbo loader routine, patching two bytes and | ||
// relocating it at the desidered address | ||
export function getTurboLoader(laser500: boolean, THRESHOLD: number, relocate_address: number, fileType: number) { | ||
const rootname = path.resolve(__dirname, laser500 ? "../turbo_tape/turbo_L500" : "../turbo_tape/turbo_L310"); | ||
const loader_program = fs.readFileSync(`${rootname}.bin`); | ||
patch_bytes(loader_program, THRESHOLD, fileType, rootname); | ||
relocate(loader_program, relocate_address, rootname); | ||
const asm_info = laser500 ? turbo_L500_asm_info : turbo_L310_asm_info; | ||
const loader_program = asm_info.code; | ||
patch_bytes(loader_program, THRESHOLD, fileType, asm_info); | ||
const relocated = relocate(loader_program, relocate_address, asm_info); | ||
// for debug purposes | ||
{ | ||
const symbols = fs.readFileSync(`${rootname}.sym`).toString(); | ||
let set_threshold = getSymbolAddress(symbols, "set_threshold") + relocate_address; | ||
{ | ||
let set_threshold = asm_info.symbols["set_threshold"] + relocate_address; | ||
console.log(`const label_set_threshold = 0x${hex(set_threshold,4)};`); | ||
let loop_file = getSymbolAddress(symbols, "loop_file") + relocate_address; | ||
let loop_file = asm_info.symbols["loop_file"] + relocate_address; | ||
console.log(`const label_loop_file = 0x${hex(loop_file,4)};`); | ||
let autorun = getSymbolAddress(symbols, "autorun") + relocate_address; | ||
let autorun = asm_info.symbols["autorun"] + relocate_address; | ||
console.log(`const label_autorun = 0x${hex(autorun,4)};`); | ||
} | ||
return loader_program; | ||
return relocated; | ||
} | ||
function patch_bytes(loader_program: Buffer, THRESHOLD: number, fileType: number, rootname: string) { | ||
// read turbo.sym symbol files | ||
const symbols = fs.readFileSync(`${rootname}.sym`).toString(); | ||
// do the needed byte patches | ||
loader_program[getSymbolAddress(symbols, "turbo_load" ) + 1] = fileType; | ||
loader_program[getSymbolAddress(symbols, "set_threshold") + 1] = THRESHOLD; | ||
function patch_bytes(loader_program: number[], THRESHOLD: number, fileType: number, info: AsmInfo ) { | ||
loader_program[info.symbols.turbo_load + 1] = fileType; // patches the file type B: or T: | ||
loader_program[info.symbols.set_threshold + 1] = THRESHOLD; // patches the turbo speed threshold | ||
} | ||
function getSymbolAddress(file: string, symbolname: string) { | ||
const regex = new RegExp(symbolname + "\\s*=\\s*\\$(?<address>[0-9a-fA-F]{4})", "g"); | ||
const match = regex.exec(file); | ||
if(match === null || match.groups === undefined) throw `can't find ${symbolname} label in .sym file`; | ||
// get label address from hex format | ||
const address = Number.parseInt(match.groups["address"], 16); | ||
return address; | ||
} | ||
// relocate the Z80 turbo loader routine at the desidered | ||
@@ -58,12 +41,12 @@ // destination by changing all the interested addresses. | ||
function relocate(loader_program: Buffer, relocate_address: number, rootname: string) { | ||
const reloc_info = fs.readFileSync(`${rootname}.reloc`); | ||
for(let t=0; t<reloc_info.length; t+=2) { | ||
const patch_address = reloc_info.readUInt16LE(t); | ||
const offset_value = loader_program.readUInt16LE(patch_address); | ||
const relocated_value = offset_value + relocate_address; | ||
loader_program.writeUInt16LE(relocated_value, patch_address); | ||
//console.log(`${t}: [${patch_address}] = ${offset_value.toString(16)} -> ${relocated_value.toString(16)}`); | ||
} | ||
function relocate(loader_program: number[], relocate_address: number, asm_info: AsmInfo) { | ||
const reloc_table = asm_info.reloc; | ||
const lp = Buffer.from(loader_program); | ||
for(let t=0; t<reloc_table.length; t++) { | ||
const index = reloc_table[t]; | ||
const address = lp.readUInt16LE(index); | ||
const relocated_value = address + relocate_address; | ||
lp.writeUInt16LE(relocated_value, index); | ||
} | ||
return lp; | ||
} |
@@ -43,3 +43,3 @@ /* | ||
interface VZInfo { | ||
export interface VZInfo { | ||
filename: string; | ||
@@ -46,0 +46,0 @@ type: VZFILETYPE; |
@@ -52,3 +52,3 @@ { | ||
/* Emit */ | ||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ | ||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ | ||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ | ||
@@ -55,0 +55,0 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ |
Sorry, the diff of this file is not supported yet
105451
50
2871