Skip to content
Open
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,28 @@ outputProfile.saveSync('/path/to/my/new_snes_profile.rt4');

## Changelog

### Version 1.1.0 (2024-07-04)

- Added support for the following settings:
- `advanced.acquisition.audio_input.sampling.sample_rate`
- `advanced.acquisition.audio_input.sampling.preamp_gain`
- `advanced.acquisition.audio_input.source.input_override`
- `advanced.acquisition.audio_input.source.input_swap`

### Version 1.0.1 (2024-07-03)

- Documentation update

### Version 1.0.0 (2024-07-03)

- Support `Advanced -> System -> OSD/Firmware -> On Screen Display`
- Implement `header` as a `RetroTinkReadOnlySetting`
- Fixed Critical Bug https://github.com/boatmeme/rt4k-profile/issues/26 preventing files saved with this library from loading on RetroTink4k devices
- There are now **read-only** settings, such as `header`, which should be readable, but cannot be set
- Profiles saved with this library will now work on the actual device 🤦
- Added support for the following settings:
- `advanced.system.osd_firmware.banner_image.load_banner`
- `advanced.system.osd_firmware.on_screen_display.position`
- `advanced.system.osd_firmware.on_screen_display.auto_off`
- `advanced.system.osd_firmware.on_screen_display.hide_input_res`
- `advanced.system.osd_firmware.on_screen_display.enable_debug_osd`

### Version 0.1.0 (2024-06-29)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rt4k-profile",
"version": "1.0.1",
"version": "1.1.0",
"description": "A Typescript Library for Reading and Writing RetroTINK-4k .rt4 Profiles",
"main": "dist/index.js",
"exports": {
Expand Down
23 changes: 22 additions & 1 deletion src/profile/RetroTinkProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ export default class RetroTinkProfile {
return RetroTinkProfile.sliceBytes(setting, this._bytes);
}

private _setReadOnlyValues() {
const readOnlyValues = <RetroTinkReadOnlySetting[]>(
Array.from(RetroTinkProfile._settings.values()).filter((s) => s instanceof RetroTinkReadOnlySetting)
);
const byte_array = Array.from(this._bytes);
for (const setting of readOnlyValues) {
const derivedFromValues =
setting.derivedFrom.length > 0 ? Array.from(this.getValues(...setting.derivedFrom).values()) : [];
const value = setting.deriveValue(...derivedFromValues);

let offset = 0;
for (const byteRange of setting.byteRanges) {
byte_array.splice(byteRange.address, byteRange.length, ...value.slice(offset, offset + byteRange.length));
offset += byteRange.length;
}
}
this._bytes = new Uint8Array(byte_array);
}

private _setValueWithInstance(setting: RetroTinkSettingValue): void {
if (!RetroTinkProfile._settings.has(setting.name)) throw new SettingNotSupportedError(setting.name);
if (RetroTinkProfile._settings.get(setting.name) instanceof RetroTinkReadOnlySetting)
Expand Down Expand Up @@ -217,7 +236,6 @@ export default class RetroTinkProfile {
private static CRC_WRITE_INDEX = 32;
private _getCrc(): Uint8Array {
const START_INDEX = 128;

const crcValue = CRC16CCITT.calculate(this._bytes, START_INDEX);
return new Uint8Array([crcValue & 0xff, (crcValue >> 8) & 0xff]);
}
Expand All @@ -227,11 +245,13 @@ export default class RetroTinkProfile {
}

async save(filePath: string, opts: WriteFileOptions = { createDirectoryIfNotExist: true }) {
this._setReadOnlyValues();
this._writeCrc();
return writeFileBinary(filePath, this._bytes, opts);
}

saveSync(filePath: string, opts: WriteFileOptions = { createDirectoryIfNotExist: true }) {
this._setReadOnlyValues();
this._writeCrc();
return writeFileBinarySync(filePath, this._bytes, opts);
}
Expand All @@ -241,6 +261,7 @@ export default class RetroTinkProfile {
}

getCrcString(): string {
this._setReadOnlyValues();
const [lowByte, highByte] = this._getCrc();
const crcValue = (highByte << 8) | lowByte;
return `0x${crcValue.toString(16).toUpperCase().padStart(4, '0')}`;
Expand Down
14 changes: 13 additions & 1 deletion src/profile/__fixtures__/json_profiles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const unpretty_json_str = `{"advanced":{"effects":{"mask":{"enabled":true,"strength":-4,"path":"Mono Masks/A Grille Medium Mono.bmp"}},"system":{"osd_firmware":{"banner_image":{"load_banner":""},"on_screen_display":{"position":"Left","auto_off":"Off","hide_input_res":false,"enable_debug_osd":"Off"}}}},"input":"HDMI","output":{"resolution":"4K60","transmitter":{"hdr":"Off","colorimetry":"Auto-Rec.709","rgb_range":"Full","sync_lock":"Triple Buffer","vrr":"Off","deep_color":false}}}`;
export const unpretty_json_str = `{"advanced":{"effects":{"mask":{"enabled":true,"strength":-4,"path":"Mono Masks/A Grille Medium Mono.bmp"}},"acquisition":{"audio_input":{"sampling":{"sample_rate":"48 kHz","preamp_gain":"+0.0 dB"},"source":{"input_override":"Off","input_swap":"Off"}}},"system":{"osd_firmware":{"banner_image":{"load_banner":""},"on_screen_display":{"position":"Left","auto_off":"Off","hide_input_res":false,"enable_debug_osd":"Off"}}}},"input":"HDMI","output":{"resolution":"4K60","transmitter":{"hdr":"Off","colorimetry":"Auto-Rec.709","rgb_range":"Full","sync_lock":"Triple Buffer","vrr":"Off","deep_color":false}}}`;
export const pretty_json_str = `{
"advanced": {
"effects": {
Expand All @@ -8,6 +8,18 @@ export const pretty_json_str = `{
"path": "Mono Masks/A Grille Medium Mono.bmp"
}
},
"acquisition": {
"audio_input": {
"sampling": {
"sample_rate": "48 kHz",
"preamp_gain": "+0.0 dB"
},
"source": {
"input_override": "Off",
"input_swap": "Off"
}
}
},
"system": {
"osd_firmware": {
"banner_image": {
Expand Down
15 changes: 14 additions & 1 deletion src/settings/RetroTinkSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ interface RetroTinkSettingParams {
enums?: RetroTinkEnumValue[];
}

interface RetroTinkReadOnlySettingParams extends RetroTinkSettingParams {
derivedFrom: RetroTinkSettingName[];
deriveValue: (...values: RetroTinkSettingValue[]) => Uint8Array;
}

interface RetroTinkEnumValue {
name: string;
value: Uint8Array;
Expand Down Expand Up @@ -63,7 +68,15 @@ export class RetroTinkSetting {
}
}

export class RetroTinkReadOnlySetting extends RetroTinkSetting {}
export class RetroTinkReadOnlySetting extends RetroTinkSetting {
derivedFrom: RetroTinkSettingName[];
deriveValue: (...values: RetroTinkSettingValue[]) => Uint8Array;
constructor(params: RetroTinkReadOnlySettingParams) {
super(params);
this.derivedFrom = params.derivedFrom;
this.deriveValue = params.deriveValue;
}
}

export type RetroTinkSettingsValuesPlainObject = {
[key: string]: string | number | boolean | RetroTinkSettingsValuesPlainObject;
Expand Down
113 changes: 113 additions & 0 deletions src/settings/Schema.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { RetroTinkReadOnlySetting, RetroTinkSettingValue } from './RetroTinkSetting';
import { RetroTinkSettingName, RetroTinkSettingsVersion } from './Schema';
import { SettingValidationError } from '../exceptions/RetroTinkProfileException';

describe('Schema', () => {
describe('1.4.2', () => {
describe('derived settings', () => {
describe('input.audio', () => {
it('should vary with input', () => {
const settings = RetroTinkSettingsVersion['1.4.2'];
const inputs = settings.get('input').enums?.reduce((acc, e) => ({ ...acc, [e.name]: e.value }), {}) || {};
const audio_input_override = new RetroTinkSettingValue(
settings.get('advanced.acquisition.audio_input.source.input_override'),
new Uint8Array([0]),
);
const input_audio = <RetroTinkReadOnlySetting>settings.get('input.audio' as RetroTinkSettingName);

let input = new RetroTinkSettingValue(settings.get('input'), inputs['HDMI']);
let [value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(5);

input = new RetroTinkSettingValue(settings.get('input'), inputs['Front|Composite']);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(3);

input = new RetroTinkSettingValue(settings.get('input'), inputs['RCA|RGsB']);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(0);

input = new RetroTinkSettingValue(settings.get('input'), inputs['SCART|YPbPr']);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(2);

input = new RetroTinkSettingValue(settings.get('input'), inputs['HD-15|RGBHV']);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(1);
});
it('should vary with audio input override', () => {
const settings = RetroTinkSettingsVersion['1.4.2'];
const inputs = settings.get('input').enums?.reduce((acc, e) => ({ ...acc, [e.name]: e.value }), {}) || {};
const inputOverrides =
settings
.get('advanced.acquisition.audio_input.source.input_override')
.enums?.reduce((acc, e) => ({ ...acc, [e.name]: e.value }), {}) || {};
const input_audio = <RetroTinkReadOnlySetting>settings.get('input.audio' as RetroTinkSettingName);
const input = new RetroTinkSettingValue(settings.get('input'), inputs['HDMI']);

let audio_input_override = new RetroTinkSettingValue(
settings.get('advanced.acquisition.audio_input.source.input_override'),
inputOverrides['RCA'],
);
let [value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(0);

audio_input_override = new RetroTinkSettingValue(
settings.get('advanced.acquisition.audio_input.source.input_override'),
inputOverrides['HD-15'],
);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(1);

audio_input_override = new RetroTinkSettingValue(
settings.get('advanced.acquisition.audio_input.source.input_override'),
inputOverrides['SCART'],
);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(2);

audio_input_override = new RetroTinkSettingValue(
settings.get('advanced.acquisition.audio_input.source.input_override'),
inputOverrides['Front'],
);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(3);

audio_input_override = new RetroTinkSettingValue(
settings.get('advanced.acquisition.audio_input.source.input_override'),
inputOverrides['S/PDIF'],
);
[value] = input_audio.deriveValue(audio_input_override, input);
expect(value).toEqual(4);
});

it('should throw if there is a bad input value', () => {
const settings = RetroTinkSettingsVersion['1.4.2'];
const audio_input_override = new RetroTinkSettingValue(
settings.get('advanced.acquisition.audio_input.source.input_override'),
new Uint8Array([0]),
);
const input_audio = <RetroTinkReadOnlySetting>settings.get('input.audio' as RetroTinkSettingName);

expect(() =>
input_audio.deriveValue(audio_input_override, { asInt: () => 69 } as RetroTinkSettingValue),
).toThrow(SettingValidationError);
});
});
});
describe('dynamically generated values', () => {
describe('advanced.acquisition.audio_input.sampling.preamp_gain', () => {
const settings = RetroTinkSettingsVersion['1.4.2'];
const gain = settings.get('advanced.acquisition.audio_input.sampling.preamp_gain');
const first = gain.enums?.find(({ name }) => name == '-24.0 dB');
const last = gain.enums?.find(({ name }) => name == '+28.0 dB');
const middle = gain.enums?.find(({ name }) => name == '+0.0 dB');

expect(gain.enums?.length).toBe(105);
expect(first?.value).toEqual(new Uint8Array([208]));
expect(last?.value).toEqual(new Uint8Array([56]));
expect(middle?.value).toEqual(new Uint8Array([0]));
});
});
});
});
Loading