c64: .tap export

This commit is contained in:
Steven Hugg 2023-11-01 16:48:19 -05:00
parent 64ef7cc885
commit 483675fded
2 changed files with 335 additions and 6 deletions

View File

@ -0,0 +1,305 @@
/*
https://github.com/eightbitjim/commodore-tape-maker/blob/master/maketape.py
# MIT License
#
# Copyright (c) 2018 eightbitjim
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
*/
export class OutputSoundFile {
options: any;
sampleRate: number;
soundData: number[];
tapData: number[];
constructor(options: any) {
this.options = options;
this.sampleRate = 44100.0;
this.soundData = [];
//00000000 43 36 34 2d 54 41 50 45 2d 52 41 57 01 00 00 00 |C64-TAPE-RAW....|
//00000010 1e 62 03 00
this.tapData = [0x43,0x36,0x34,0x2d,0x54,0x41,0x50,0x45,0x2d,0x52,0x41,0x57,0x01,0x00,0x00,0x00,0,0,0,0];
}
getWAVHeader() {
const header = new Uint8Array(44);
const view = new DataView(header.buffer);
view.setUint32(0, 0x52494646, false); // "RIFF"
view.setUint32(4, 44 + this.soundData.length, true); // ChunkSize
view.setUint32(8, 0x57415645, false); // "WAVE"
view.setUint32(12, 0x666d7420, false); // "fmt "
view.setUint32(16, 16, true); // Subchunk1Size
view.setUint16(20, 1, true); // AudioFormat (PCM)
view.setUint16(22, 1, true); // NumChannels
view.setUint32(24, this.sampleRate, true); // SampleRate
view.setUint32(28, this.sampleRate * 2, true); // ByteRate
view.setUint16(32, 1, true); // BlockAlign
view.setUint16(34, 8, true); // BitsPerSample
view.setUint32(36, 0x64617461, false); // "data"
view.setUint32(40, this.soundData.length, true); // Subchunk2Size
return header;
}
addSilence(lengthInSeconds: number): void {
const numberOfSamples = Math.floor(this.sampleRate * lengthInSeconds);
for (let i = 0; i < numberOfSamples; i++) {
this.soundData.push(0);
}
//For v1 and v2 TAP files, a $00 value is followed by 3 bytes containing the actual duration measured in clock cycles (not divided by 8). These 3 bytes are in low-high format.
const numCycles = TAPFile.CLOCK_RATE * lengthInSeconds;
this.tapData.push(0);
this.tapData.push(numCycles & 0xff);
this.tapData.push((numCycles >> 8) & 0xff);
this.tapData.push((numCycles >> 16) & 0xff);
}
addCycle(cycles: number): void {
this.tapData.push(cycles);
const numberOfSamples = Math.floor(this.sampleRate * TAPFile.TAP_LENGTH_IN_SECONDS * cycles);
for (let i = 0; i < numberOfSamples; i++) {
let value;
if (this.options.sine_wave) {
value = - Math.sin((i / numberOfSamples) * 2.0 * Math.PI);
} else {
if (i < numberOfSamples / 2) {
value = -1;
} else {
value = 1;
}
}
if (this.options.invert_waveform) {
value = -value;
}
this.soundData.push(Math.round(128 + value * 127));
}
}
updateTAPHeader() {
let datalen = this.tapData.length - 0x14;
// set bytes 0x10-0x13 to length
this.tapData[0x10] = datalen & 0xff;
this.tapData[0x11] = (datalen >> 8) & 0xff;
this.tapData[0x12] = (datalen >> 16) & 0xff;
this.tapData[0x13] = (datalen >> 24) & 0xff;
}
getTAPData(): Uint8Array {
this.updateTAPHeader();
return new Uint8Array(this.tapData);
}
getSoundData(): Uint8Array {
let header = this.getWAVHeader();
let data = new Uint8Array(header.length + this.soundData.length);
data.set(header, 0);
data.set(new Uint8Array(this.soundData), header.length);
return data;
}
}
export class TAPFile {
static CLOCK_RATE = 985248.0;
static TAP_LENGTH_IN_SECONDS = 8.0 / this.CLOCK_RATE;
static FILENAME_BUFFER_SIZE = 0x10;
static FILE_TYPE_NONE = 0;
static FILE_TYPE_RELOCATABLE = 1;
static FILE_TYPE_SEQUENTIAL = 2;
static FILE_TYPE_NON_RELOCATABLE = 3;
static LEADER_TYPE_HEADER = 0;
static LEADER_TYPE_CONTENT = 1;
static LEADER_TYPE_REPEATED = 2;
static NUMBER_OF_PADDING_BYTES = 171;
static PADDING_CHARACTER = 0x20;
static SHORT_PULSE = 0x30;
static MEDIUM_PULSE = 0x42;
static LONG_PULSE = 0x56;
options: any;
checksum: number;
data: Uint8Array;
filenameData: number[];
startAddress: number;
endAddress: number;
fileType: number;
waveFile: OutputSoundFile;
constructor(filename: string, options?: any) {
this.options = options;
this.checksum = 0;
this.data = new Uint8Array(0);
this.filenameData = this.makeFilename(filename);
this.startAddress = 0;
this.endAddress = 0;
this.fileType = TAPFile.FILE_TYPE_NONE;
this.waveFile = null;
}
makeFilename(filename: string): number[] {
const filenameBuffer = [];
const space = 0x20;
filename = filename.toUpperCase(); // for PETSCII
for (let i = 0; i < TAPFile.FILENAME_BUFFER_SIZE; i++) {
if (filename.length <= i) {
filenameBuffer.push(space);
} else {
let ch = filename.charCodeAt(i);
filenameBuffer.push(ch);
}
}
return filenameBuffer;
}
setContent(inputFile: { data: Uint8Array, startAddress: number, type: number }): void {
this.data = inputFile.data;
this.startAddress = inputFile.startAddress;
this.endAddress = inputFile.startAddress + inputFile.data.length;
this.fileType = inputFile.type;
}
generateSound(outputWaveFile: OutputSoundFile): void {
this.waveFile = outputWaveFile;
this.addHeader(false);
this.addHeader(true);
outputWaveFile.addSilence(0.1);
this.addFile();
}
addTapCycle(tapValue: number): void {
this.waveFile.addCycle(tapValue);
}
addBit(value: number): void {
if (value === 0) {
this.addTapCycle(TAPFile.SHORT_PULSE);
this.addTapCycle(TAPFile.MEDIUM_PULSE);
} else {
this.addTapCycle(TAPFile.MEDIUM_PULSE);
this.addTapCycle(TAPFile.SHORT_PULSE);
}
}
addDataMarker(moreToFollow: boolean): void {
if (moreToFollow) {
this.addTapCycle(TAPFile.LONG_PULSE);
this.addTapCycle(TAPFile.MEDIUM_PULSE);
} else {
this.addTapCycle(TAPFile.LONG_PULSE);
this.addTapCycle(TAPFile.SHORT_PULSE);
}
}
resetChecksum(): void {
this.checksum = 0;
}
addByteFrame(value: number, moreToFollow: boolean): void {
let checkBit = 1;
for (let i = 0; i < 8; i++) {
const bit = (value & (1 << i)) !== 0 ? 1 : 0;
this.addBit(bit);
checkBit ^= bit;
}
this.addBit(checkBit);
this.addDataMarker(moreToFollow);
this.checksum ^= value;
}
addLeader(fileType: number): void {
let numberofPulses;
if (fileType === TAPFile.LEADER_TYPE_HEADER) {
numberofPulses = 0x6a00;
} else if (fileType === TAPFile.LEADER_TYPE_CONTENT) {
numberofPulses = 0x1a00;
} else {
numberofPulses = 0x4f;
}
for (let i = 0; i < numberofPulses; i++) {
this.addTapCycle(TAPFile.SHORT_PULSE);
}
}
addSyncChain(repeated: boolean): void {
let value;
if (repeated) {
value = 0x09;
} else {
value = 0x89;
}
let count = 9;
while (count > 0) {
this.addByteFrame(value, true);
value -= 1;
count -= 1;
}
}
addData(): void {
for (let i = 0; i < this.data.length; i++) {
this.addByteFrame(this.data[i], true);
}
}
addFilename(): void {
for (let i = 0; i < this.filenameData.length; i++) {
this.addByteFrame(this.filenameData[i], true);
}
}
addHeader(repeated: boolean): void {
if (repeated) {
this.addLeader(TAPFile.LEADER_TYPE_REPEATED);
} else {
this.addLeader(TAPFile.LEADER_TYPE_HEADER);
}
this.addDataMarker(true);
this.addSyncChain(repeated);
this.resetChecksum();
this.addByteFrame(this.fileType, true);
this.addByteFrame(this.startAddress & 0x00ff, true);
this.addByteFrame((this.startAddress & 0xff00) >> 8, true);
this.addByteFrame(this.endAddress & 0x00ff, true);
this.addByteFrame((this.endAddress & 0xff00) >> 8, true);
this.addFilename();
for (let i = 0; i < TAPFile.NUMBER_OF_PADDING_BYTES; i++) {
this.addByteFrame(TAPFile.PADDING_CHARACTER, true);
}
this.addByteFrame(this.checksum, false);
}
addFile(): void {
let repeated = false;
for (let i = 0; i < 2; i++) {
if (!repeated) {
this.addLeader(TAPFile.LEADER_TYPE_CONTENT);
} else {
this.addLeader(TAPFile.LEADER_TYPE_REPEATED);
}
this.addDataMarker(true);
this.addSyncChain(repeated);
this.resetChecksum();
this.addData();
this.addByteFrame(this.checksum, false);
repeated = true;
}
this.addLeader(1);
}
}

View File

@ -21,6 +21,7 @@ import { isMobileDevice } from "./views/baseviews";
import { CallStackView, DebugBrowserView } from "./views/treeviews";
import { saveAs } from "file-saver";
import DOMPurify = require("dompurify");
import { OutputSoundFile, TAPFile } from "../common/audio/CommodoreTape";
// external libs (TODO)
declare var Tour, GIF, Octokat;
@ -1027,16 +1028,39 @@ function _downloadCassetteFile_vcs(e) {
});
}
function _downloadCassetteFile_c64(e) {
var prefix = getFilenamePrefix(getCurrentMainFilename());
let audpath = prefix + ".tap";
let tapmaker = new TAPFile(prefix);
let outfile = new OutputSoundFile({sine_wave:true});
let data = current_output;
let startAddress = data[0] + data[1]*256;
data = data.slice(2); // remove header
tapmaker.setContent({ data, startAddress, type: TAPFile.FILE_TYPE_NON_RELOCATABLE });
tapmaker.generateSound(outfile);
let tapout = outfile.getTAPData();
//let audout = outfile.getSoundData();
if (tapout) {
//let blob = new Blob([audout], { type: "audio/wav" });
let blob = new Blob([tapout], { type: "application/octet-stream" });
saveAs(blob, audpath);
}
}
function _getCassetteFunction() {
switch (getBasePlatform(platform_id)) {
case 'vcs': return _downloadCassetteFile_vcs;
case 'apple2': return _downloadCassetteFile_apple2;
case 'c64': return _downloadCassetteFile_c64;
}
}
function _downloadCassetteFile(e) {
if (current_output == null) {
alertError("Please fix errors before exporting.");
return true;
}
var fn;
switch (getBasePlatform(platform_id)) {
case 'vcs': fn = _downloadCassetteFile_vcs; break;
case 'apple2': fn = _downloadCassetteFile_apple2; break;
}
var fn = _getCassetteFunction();
if (fn === undefined) {
alertError("Cassette export is not supported on this platform.");
return true;
@ -1949,7 +1973,7 @@ function setupDebugControls() {
}
$("#item_download_allzip").click(_downloadAllFilesZipFile);
$("#item_record_video").click(_recordVideo);
if (platform_id.startsWith('apple2') || platform_id.startsWith('vcs')) // TODO: look for function
if (_getCassetteFunction())
$("#item_export_cassette").click(_downloadCassetteFile);
else
$("#item_export_cassette").hide();