Skip to content

Commit

Permalink
Draft: Add Z1013 decoder
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanschramm committed Nov 12, 2023
1 parent 77869bd commit 176fb85
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 3 deletions.
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;
}
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;
66 changes: 66 additions & 0 deletions retroload-lib/src/decoding/writer/z1013/Z1013BlockProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {type BufferAccess} from '../../../common/BufferAccess.js';
import {type Position} from '../../../common/Positioning.js';
import {type BlockDecodingResult, BlockDecodingResultStatus, type Z1013BlockProvider} from './Z1013BlockProvider.js';

/**
* Minimal expected gap between files in seconds (from end of previous block to begin of next block)
*/
const maximalIntraFileBlockGap = 1;

export class Z1013BlockProcessor {
private blocks: BlockDecodingResult[] = [];
constructor(
private readonly blockProvider: Z1013BlockProvider,
) {}

* files(): Generator<FileDecodingResult> {
for (const bdr of this.blockProvider.blocks()) {
if (this.belongsToCurrentFile(bdr)) {
this.blocks.push(bdr);
} else {
yield new FileDecodingResult(
this.blocks.map((b) => b.data),
this.currentFileWasSuccessful() ? FileDecodingResultStatus.Success : FileDecodingResultStatus.Error,
this.blocks[0].blockBegin,
this.blocks[this.blocks.length - 1].blockEnd,
);
this.blocks = [];
}
}
}

private belongsToCurrentFile(bdr: BlockDecodingResult): boolean {
if (this.blocks.length === 0) {
return true;
}
if (bdr.blockEnd.seconds - this.blocks[this.blocks.length - 1].blockEnd.seconds < maximalIntraFileBlockGap) {
return true;
}

return false;
}

private currentFileWasSuccessful(): boolean {
for (const bdr of this.blocks) {
if (bdr.status !== BlockDecodingResultStatus.Complete) {
return false;
}
}

return true;
}
}

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

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

export type Z1013BlockProvider = {
blocks(): Generator<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,
}
37 changes: 37 additions & 0 deletions retroload-lib/src/decoding/writer/z1013/Z1013GenericWriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {BufferAccess} from '../../../common/BufferAccess.js';
import {type ConverterSettings, type OutputFile, type WriterDefinition} from '../../ConverterManager.js';
import {StreamingSampleToHalfPeriodConverter} from '../../half_period_provider/StreamingSampleToHalfPeriodConverter.js';
import {type SampleProvider} from '../../sample_provider/SampleProvider.js';
import {type FileDecodingResult, FileDecodingResultStatus, Z1013BlockProcessor} from './Z1013BlockProcessor.js';
import {Z1013HalfPeriodProcessor} from './Z1013HalfPeriodProcessor.js';

const definition: WriterDefinition = {
to: 'z1013generic',
convert,
};
export default definition;

function * convert(sampleProvider: SampleProvider, settings: ConverterSettings): Generator<OutputFile> {
const streamingHalfPeriodProvider = new StreamingSampleToHalfPeriodConverter(sampleProvider);
const bp = new Z1013BlockProcessor(new Z1013HalfPeriodProcessor(streamingHalfPeriodProvider));
for (const fdr of bp.files()) {
if (fdr.status !== FileDecodingResultStatus.Success || settings.onError !== 'skipfile') {
yield mapFileDecodingResult(fdr);
}
}
}

function mapFileDecodingResult(fdr: FileDecodingResult): OutputFile {
const data = BufferAccess.create(fdr.blocks.length * 32);
for (const blockBa of fdr.blocks) {
data.writeBa(blockBa.slice(2, 32));
}

return {
proposedName: undefined,
data,
proposedExtension: 'bin',
begin: fdr.begin,
end: fdr.end,
};
}
109 changes: 109 additions & 0 deletions retroload-lib/src/decoding/writer/z1013/Z1013HalfPeriodProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {BufferAccess} from '../../../common/BufferAccess.js';
import {formatPosition} from '../../../common/Positioning.js';
import {Logger} from '../../../common/logging/Logger.js';
import {BlockStartNotFound, DecodingError, EndOfInput} from '../../ConverterExceptions.js';
import {type FrequencyRange, bitByFrequency, oscillationIs} from '../../Frequency.js';
import {SyncFinder} from '../../SyncFinder.js';
import {type HalfPeriodProvider} from '../../half_period_provider/HalfPeriodProvider.js';
import {BlockDecodingResult, BlockDecodingResultStatus, type Z1013BlockProvider} from './Z1013BlockProvider.js';

const fOne: FrequencyRange = [1000, 1500];
const fZero: FrequencyRange = [2200, 2800];
const fSync: FrequencyRange = [300, 900];
const minIntroSyncPeriods = 5;
const rawBlockLength = 2 + 32 + 2;

export class Z1013HalfPeriodProcessor implements Z1013BlockProvider {
private readonly syncFinder: SyncFinder;
constructor(private readonly halfPeriodProvider: HalfPeriodProvider) {
this.syncFinder = new SyncFinder(this.halfPeriodProvider, fSync, minIntroSyncPeriods);
}

* blocks(): Generator<BlockDecodingResult> {
let keepGoing = true;
do {
try {
yield this.decodeFile();
} catch (e) {
if (e instanceof BlockStartNotFound) {
continue;
} else if (e instanceof EndOfInput) {
keepGoing = false;
} else {
throw e;
}
}
} while (keepGoing);
}

private decodeFile(): BlockDecodingResult {
if (!this.syncFinder.findSync()) {
throw new EndOfInput();
}

const blockBa = BufferAccess.create(rawBlockLength);
const fileBegin = this.halfPeriodProvider.getPosition();
if (!oscillationIs(this.halfPeriodProvider.getNext(), this.halfPeriodProvider.getNext(), fOne)) {
throw new BlockStartNotFound();
}
for (let i = 0; i < rawBlockLength; i++) {
blockBa.writeUint8(this.readByte());
}
const fileEnd = this.halfPeriodProvider.getPosition();

const readChecksum = blockBa.getUint16Le(34);
const calculatedChecksum = calculateChecksum(blockBa.slice(0, blockBa.length() - 2));
const checksumCorrect = calculatedChecksum === readChecksum;
if (!checksumCorrect) {
Logger.error(`${formatPosition(fileEnd)} Warning: Invalid checksum! Read checksum: ${readChecksum}, Calculated checksum: ${calculatedChecksum}.`);
}

return new BlockDecodingResult(
blockBa,
checksumCorrect ? BlockDecodingResultStatus.Complete : BlockDecodingResultStatus.InvalidChecksum,
fileBegin,
fileEnd,
);
}

private readByte(): number {
let byte = 0;
for (let i = 0; i < 8; i++) {
const bit = this.readBit();
if (bit === undefined) {
throw new DecodingError(`${formatPosition(this.halfPeriodProvider.getPosition())} Unable to detect bit.`);
}
byte |= ((bit ? 1 : 0) << i);
}

return byte;
}

private readBit(): boolean | undefined {
const halfPeriod = this.halfPeriodProvider.getNext();
const bitValue = bitByFrequency(halfPeriod, fZero, fOne);
if (bitValue === undefined) {
return undefined;
}
if (bitValue) {
// one bit consists of only one half period
return true;
}
// zero - read next half period
if (bitByFrequency(this.halfPeriodProvider.getNext(), fZero, fOne) !== false) {
return undefined; // next half period doesn't match
}

return false;
}
}

function calculateChecksum(ba: BufferAccess) {
let checkSum = 0;
for (let i = 0; i < ba.length(); i += 2) {
const word = ba.getUint16Le(i);
checkSum = (checkSum + word) & 0xffff;
}

return checkSum;
}
6 changes: 3 additions & 3 deletions retroload-lib/src/encoding/encoder/Z1013Encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Logger} from '../../common/logging/Logger.js';
const blockDataSize = 32;
const fOne = 1280;
const fZero = 2560;
const fDelimiter = 640;
const fSync = 640;

/**
* Encoder for Robotron Z 1013
Expand Down Expand Up @@ -55,11 +55,11 @@ export class Z1013Encoder extends AbstractEncoder {
}

recordFirstIntro() {
this.recordOscillations(fDelimiter, 2000);
this.recordOscillations(fSync, 2000);
}

recordIntro() {
this.recordOscillations(fDelimiter, 7);
this.recordOscillations(fSync, 7);
}

recordDelimiter() {
Expand Down

0 comments on commit 176fb85

Please sign in to comment.