Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Z1013 decoder #41

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions retroload-lib/src/decoding/Frequency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,28 @@ export function is(value: number, range: FrequencyRange): boolean {
export function isNot(value: number, range: FrequencyRange): boolean {
return value < range[0] || value > range[1];
}

export function oscillationIs(
firstHalfValue: number | undefined,
secondHalfValue: number | undefined,
range: FrequencyRange,
): boolean | undefined {
if (firstHalfValue === undefined || secondHalfValue === undefined) {
return undefined;
}

return is((firstHalfValue + secondHalfValue) / 2, range);
}

export function bitByFrequency(value: number | undefined, rangeZero: FrequencyRange, rangeOne: FrequencyRange): boolean | undefined {
if (value === undefined) {
return undefined;
}
const isOne = is(value, rangeOne);
const isZero = is(value, rangeZero);
if (!isOne && !isZero) {
return undefined;
}

return isOne;
}
26 changes: 26 additions & 0 deletions retroload-lib/src/decoding/writer/BlockDecodingResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {type BufferAccess} from '../../common/BufferAccess.js';
import {type Position} from '../../common/Positioning.js';

export class BlockDecodingResult {
constructor(
readonly data: BufferAccess,
readonly status: BlockDecodingResultStatus,
readonly begin: Position,
readonly end: Position,
) {}
}

export enum BlockDecodingResultStatus {
/**
* A complete block has successfully been read.
*/
Complete,
/**
* A complete block has been read, but it's checksum was incorrect.
*/
InvalidChecksum,
/**
* Reading of a block was partial because of an encoding error.
*/
Partial,
}
16 changes: 16 additions & 0 deletions retroload-lib/src/decoding/writer/FileDecodingResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {type Position} from '../../common/Positioning.js';
import {type BlockDecodingResult} from './BlockDecodingResult.js';

export class FileDecodingResult {
constructor(
readonly blocks: BlockDecodingResult[],
readonly status: FileDecodingResultStatus,
readonly begin: Position,
readonly end: Position,
) {}
}

export enum FileDecodingResultStatus {
Success,
Error,
}
2 changes: 2 additions & 0 deletions retroload-lib/src/decoding/writer/WriterProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {type WriterDefinition} from '../ConverterManager.js';
import Apple2GenericWriter from './apple2/Apple2GenericWriter.js';
import KcTapWriter from './kc/KcTapWriter.js';
import Lc80GenericWriter from './lc80/Lc80GenericWriter.js';
import Z1013GenericWriter from './z1013/Z1013GenericWriter.js';

const writers: WriterDefinition[] = [
Apple2GenericWriter,
KcTapWriter,
Lc80GenericWriter,
Z1013GenericWriter,
];
export default writers;
11 changes: 9 additions & 2 deletions retroload-lib/src/decoding/writer/apple2/Apple2GenericWriter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {type SampleProvider} from '../../sample_provider/SampleProvider.js';
import {StreamingSampleToHalfPeriodConverter} from '../../half_period_provider/StreamingSampleToHalfPeriodConverter.js';
import {type WriterDefinition, type ConverterSettings, type OutputFile} from '../../ConverterManager.js';
import {Apple2HalfPeriodProcessor, type FileDecodingResult, FileDecodingResultStatus} from './Apple2HalfPeriodProcessor.js';
import {Apple2HalfPeriodProcessor} from './Apple2HalfPeriodProcessor.js';
import {type FileDecodingResult, FileDecodingResultStatus} from '../FileDecodingResult.js';

const definition: WriterDefinition = {
to: 'apple2generic',
Expand All @@ -22,5 +23,11 @@ function * convert(sampleProvider: SampleProvider, settings: ConverterSettings):
}

function bufferAccessListToOutputFile(fdr: FileDecodingResult): OutputFile {
return {proposedName: undefined, data: fdr.data, proposedExtension: 'bin', begin: fdr.begin, end: fdr.end};
return {
proposedName: undefined,
data: fdr.blocks[0].data, // files don't consist of blocks
proposedExtension: 'bin',
begin: fdr.begin,
end: fdr.end,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {BufferAccess} from '../../../common/BufferAccess.js';
import {type HalfPeriodProvider} from '../../half_period_provider/HalfPeriodProvider.js';
import {Logger} from '../../../common/logging/Logger.js';
import {BlockStartNotFound, DecodingError, EndOfInput} from '../../ConverterExceptions.js';
import {type Position, formatPosition} from '../../../common/Positioning.js';
import {formatPosition} from '../../../common/Positioning.js';
import {type FrequencyRange, is} from '../../Frequency.js';
import {calculateChecksum8Xor, hex8} from '../../../common/Utils.js';
import {SyncFinder} from '../../SyncFinder.js';
import {FileDecodingResult, FileDecodingResultStatus} from '../FileDecodingResult.js';
import {BlockDecodingResult, BlockDecodingResultStatus} from '../BlockDecodingResult.js';

const fSyncIntro: FrequencyRange = [680, 930]; // 770 Hz
// const fSyncEndFirstHalf: FrequencyRange = [1700, 2100]; // 2000 Hz
Expand All @@ -27,8 +29,8 @@ export class Apple2HalfPeriodProcessor {
do {
try {
const decodedFile = this.decodeRecord();
if (decodedFile.status === FileDecodingResultStatus.Success && decodedFile.data.length() === 2) {
const length = decodedFile.data.getUint16Le(0);
if (decodedFile.status === FileDecodingResultStatus.Success && decodedFile.blocks[0].data.length() === 2) {
const length = decodedFile.blocks[0].data.getUint16Le(0);
Logger.info(`Found a 2-byte long record that is probably a header for a basic record. Not outputting it. Recorded length value was: ${length}`);
continue;
}
Expand Down Expand Up @@ -71,15 +73,23 @@ export class Apple2HalfPeriodProcessor {
throw new EndOfInput();
}
const dataBa = BufferAccess.createFromUint8Array(new Uint8Array(bytesRead));
const calculatedChecksum = calculateChecksum8Xor(dataBa);
const calculatedChecksum = calculateChecksum8Xor(dataBa, 0xff);
const checksumCorrect = calculatedChecksum === readChecksum;

if (!checksumCorrect) {
Logger.error(`${formatPosition(recordEnd)} Warning: Invalid checksum! Read checksum: ${hex8(readChecksum)}, Calculated checksum: ${hex8(calculatedChecksum)}.`);
}

return new FileDecodingResult(
dataBa,
[
// Files are not made out of blocks. Just dummy-wrapping it.
new BlockDecodingResult(
dataBa,
checksumCorrect ? BlockDecodingResultStatus.Complete : BlockDecodingResultStatus.InvalidChecksum,
recordBegin,
recordEnd,
),
],
checksumCorrect ? FileDecodingResultStatus.Success : FileDecodingResultStatus.Error,
recordBegin,
recordEnd,
Expand Down Expand Up @@ -150,17 +160,3 @@ export class Apple2HalfPeriodProcessor {
return (firstHalf + secondHalf) / 2;
}
}

export class FileDecodingResult {
constructor(
readonly data: BufferAccess,
readonly status: FileDecodingResultStatus,
readonly begin: Position,
readonly end: Position,
) {}
}

export enum FileDecodingResultStatus {
Success,
Error,
}
24 changes: 13 additions & 11 deletions retroload-lib/src/decoding/writer/kc/KcBlockProcessor.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {BufferAccess} from '../../../common/BufferAccess.js';
import {DecodingError} from '../../ConverterExceptions.js';
import {FileDecodingResultStatus, KcBlockProcessor} from './KcBlockProcessor.js';
import {BlockDecodingResult, BlockDecodingResultStatus, type KcBlockProvider} from './KcBlockProvider.js';
import {BlockDecodingResult, BlockDecodingResultStatus} from '../BlockDecodingResult.js';
import {FileDecodingResultStatus} from '../FileDecodingResult.js';
import {KcBlockProcessor} from './KcBlockProcessor.js';
import {type KcBlockProvider} from './KcBlockProvider.js';

describe('KcBlockProcessor', () => {
test('Generates a single file from a single file dump', () => {
Expand All @@ -15,9 +17,9 @@ describe('KcBlockProcessor', () => {
expect(files.length).toBe(1);
expect(files[0].blocks.length).toBe(3);
expect(files[0].status).toBe(FileDecodingResultStatus.Success);
expect(files[0].blocks[0].getUint8(0)).toBe(1);
expect(files[0].blocks[1].getUint8(0)).toBe(2);
expect(files[0].blocks[2].getUint8(0)).toBe(0xff);
expect(files[0].blocks[0].data.getUint8(0)).toBe(1);
expect(files[0].blocks[1].data.getUint8(0)).toBe(2);
expect(files[0].blocks[2].data.getUint8(0)).toBe(0xff);
});

test('Generates two files from a multiple file dump', () => {
Expand Down Expand Up @@ -50,9 +52,9 @@ describe('KcBlockProcessor', () => {
expect(files.length).toBe(1);
expect(files[0].blocks.length).toBe(3);
expect(files[0].status).toBe(FileDecodingResultStatus.Error);
expect(files[0].blocks[0].getUint8(0)).toBe(1);
expect(files[0].blocks[1].getUint8(0)).toBe(2);
expect(files[0].blocks[2].getUint8(0)).toBe(0xff);
expect(files[0].blocks[0].data.getUint8(0)).toBe(1);
expect(files[0].blocks[1].data.getUint8(0)).toBe(2);
expect(files[0].blocks[2].data.getUint8(0)).toBe(0xff);
});

test('Marks a file as broken if one block had an invalid checksum', () => {
Expand All @@ -67,9 +69,9 @@ describe('KcBlockProcessor', () => {
expect(files.length).toBe(1);
expect(files[0].blocks.length).toBe(3);
expect(files[0].status).toBe(FileDecodingResultStatus.Error);
expect(files[0].blocks[0].getUint8(0)).toBe(1);
expect(files[0].blocks[1].getUint8(0)).toBe(2);
expect(files[0].blocks[2].getUint8(0)).toBe(0xff);
expect(files[0].blocks[0].data.getUint8(0)).toBe(1);
expect(files[0].blocks[1].data.getUint8(0)).toBe(2);
expect(files[0].blocks[2].data.getUint8(0)).toBe(0xff);
});

test('Stops if one block was only partialy loaded and stopping was requested', () => {
Expand Down
29 changes: 8 additions & 21 deletions retroload-lib/src/decoding/writer/kc/KcBlockProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {type BufferAccess} from '../../../common/BufferAccess.js';
import {hex8} from '../../../common/Utils.js';
import {Logger} from '../../../common/logging/Logger.js';
import {DecodingError} from '../../ConverterExceptions.js';
import {formatPosition, type Position} from '../../../common/Positioning.js';
import {type BlockDecodingResult, BlockDecodingResultStatus, type KcBlockProvider} from './KcBlockProvider.js';
import {type KcBlockProvider} from './KcBlockProvider.js';
import {FileDecodingResult, FileDecodingResultStatus} from '../FileDecodingResult.js';
import {type BlockDecodingResult, BlockDecodingResultStatus} from '../BlockDecodingResult.js';

/**
* Minimal expected gap between files in seconds (from end of previous block to begin of next block)
Expand Down Expand Up @@ -41,13 +42,13 @@ export class KcBlockProcessor {
}

const blockNumber = decodingResult.data.getUint8(0);
if (this.blockBelongsToNextFile(blockNumber, decodingResult.blockBegin)) {
if (this.blockBelongsToNextFile(blockNumber, decodingResult.begin)) {
yield this.finishFile();
}
this.errorOccured = this.errorOccured || errorInCurrentBlock;
this.blocks.push(decodingResult);
this.previousBlockNumber = blockNumber;
this.previousBlockEnd = decodingResult.blockEnd;
this.previousBlockEnd = decodingResult.end;
}

yield this.finishFile();
Expand All @@ -59,10 +60,10 @@ export class KcBlockProcessor {

private finishFile(): FileDecodingResult {
const result = new FileDecodingResult(
this.blocks.map((bdr) => bdr.data),
this.blocks,
this.errorOccured ? FileDecodingResultStatus.Error : FileDecodingResultStatus.Success,
this.blocks[0].blockBegin,
this.blocks[this.blocks.length - 1].blockEnd,
this.blocks[0].begin,
this.blocks[this.blocks.length - 1].end,
);
this.blocks = [];
this.errorOccured = false;
Expand Down Expand Up @@ -93,17 +94,3 @@ export class KcBlockProcessor {
}
}
}

export class FileDecodingResult {
constructor(
readonly blocks: BufferAccess[],
readonly status: FileDecodingResultStatus,
readonly begin: Position,
readonly end: Position,
) {}
}

export enum FileDecodingResultStatus {
Success,
Error,
}
27 changes: 2 additions & 25 deletions retroload-lib/src/decoding/writer/kc/KcBlockProvider.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,7 @@
import {type BufferAccess} from '../../../common/BufferAccess.js';
import {type Position} from '../../../common/Positioning.js';
import {type BlockDecodingResult} from '../BlockDecodingResult.js';

export type KcBlockProvider = {
blocks(): Generator<BlockDecodingResult>;
};
export {BlockDecodingResult};

export class BlockDecodingResult {
constructor(
readonly data: BufferAccess,
readonly status: BlockDecodingResultStatus,
readonly blockBegin: Position,
readonly blockEnd: Position,
) {}
}

export enum BlockDecodingResultStatus {
/**
* A complete block has successfully been read.
*/
Complete,
/**
* A complete block has been read, but it's checksum was incorrect.
*/
InvalidChecksum,
/**
* Reading of a block was partial because of an encoding error.
*/
Partial,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {Logger} from '../../../common/logging/Logger.js';
import {calculateChecksum8, hex8} from '../../../common/Utils.js';
import {BlockStartNotFound, DecodingError, EndOfInput} from '../../ConverterExceptions.js';
import {formatPosition} from '../../../common/Positioning.js';
import {BlockDecodingResult, BlockDecodingResultStatus, type KcBlockProvider} from './KcBlockProvider.js';
import {is, type FrequencyRange, isNot} from '../../Frequency.js';
import {SyncFinder} from '../../SyncFinder.js';
import {type KcBlockProvider} from './KcBlockProvider.js';
import {BlockDecodingResult, BlockDecodingResultStatus} from '../BlockDecodingResult.js';

const one: FrequencyRange = [770, 1300];
const delimiter: FrequencyRange = [500, 670];
Expand Down
10 changes: 6 additions & 4 deletions retroload-lib/src/decoding/writer/kc/KcTapWriter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {BufferAccess} from '../../../common/BufferAccess.js';
import {KcHalfPeriodProcessor} from './KcHalfPeriodProcessor.js';
import {type FileDecodingResult, FileDecodingResultStatus, KcBlockProcessor} from './KcBlockProcessor.js';
import {LowPassFilter} from '../../sample_provider/LowPassFilter.js';
import {Logger} from '../../../common/logging/Logger.js';
import {type SampleProvider} from '../../sample_provider/SampleProvider.js';
import {StreamingSampleToHalfPeriodConverter} from '../../half_period_provider/StreamingSampleToHalfPeriodConverter.js';
import {type WriterDefinition, type ConverterSettings, type OutputFile} from '../../ConverterManager.js';
import {KcBlockProcessor} from './KcBlockProcessor.js';
import {type FileDecodingResult, FileDecodingResultStatus} from '../FileDecodingResult.js';

const definition: WriterDefinition = {
to: 'kctap',
Expand Down Expand Up @@ -36,10 +37,11 @@ function bufferAccessListToOutputFile(fdr: FileDecodingResult): OutputFile {
const data = BufferAccess.create(fileHeader.length + 129 * blocks.length);
data.writeAsciiString(fileHeader);
for (const block of fdr.blocks) {
data.writeBa(block);
data.writeBa(block.data);
}
const isBasicProgram = blocks[0].containsDataAt(0, '\x01\xd3\xd3\xd3') || blocks[0].containsDataAt(0, '\x01\xd7\xd7\xd7');
const filename = isBasicProgram ? blocks[0].slice(4, 8).asAsciiString().trim() : blocks[0].slice(1, 8).asAsciiString().trim();
const firstBlockData = blocks[0].data;
const isBasicProgram = firstBlockData.containsDataAt(0, '\x01\xd3\xd3\xd3') || firstBlockData.containsDataAt(0, '\x01\xd7\xd7\xd7');
const filename = isBasicProgram ? firstBlockData.slice(4, 8).asAsciiString().trim() : firstBlockData.slice(1, 8).asAsciiString().trim();
const proposedName = restrictCharacters(filename);

return {proposedName, proposedExtension: 'tap', data, begin: fdr.begin, end: fdr.end};
Expand Down
Loading