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

Feat: add online NES emulator #180

Merged
merged 18 commits into from
Aug 9, 2024
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
Binary file added public/emu/TetrisGYM-6.0.0.bps
Binary file not shown.
101 changes: 101 additions & 0 deletions public/emu/addresses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// offsets extracted from
// https://github.com/kirjavascript/TetrisGYM/blob/master/src/ram.asm
// and
// https://github.com/zohassadar/TetrisGYM/blob/ed2ntc/src/nmi/ed2ntc.asm
// id to [address, num_bytes]
const gym6_data_maps = {
gameMode: [0xc0, 1], // gameMode
playState: [0x48, 1], // gameModeState
completedRowXClear: [0x52, 1], // rowY (rowY is a terrible name in Rom)
completedRows: [0x4a, 4], // completedRow
lines: [0x50, 2], // lines
level: [0x44, 1], // levelNumber
score: [0x8, 4], // binScore
nextPieceOrientation: [0xbf, 1], // nextPiece
tetriminoOrientation: [0x42, 1], // currentPiece
tetriminoX: [0x40, 1], // tetriminoX
tetriminoY: [0x41, 1], // tetriminoY
frameCounter: [0xb1, 2], // frameCounter
autoRepeatX: [0x46, 1], // autorepeatX
stats: [0x3f0, 7 * 2], // statsByType
field: [0x400, 200], // playfield
};

export const address_maps = {
gym6: gym6_data_maps,
};

// The 2 functions below work because Javascript guarantees iteration order for objects
export function getDataAddresses(definition) {
const values = Object.values(definition);
const res = new Uint16Array(values.length * 2);

values.forEach(([addr, size], idx) => {
res[idx * 2] = addr;
res[idx * 2 + 1] = size;
});

return res;
}

export function assignData(rawData, definition) {
const entries = Object.entries(definition);
const result = { ...definition };

// raw assignments first
let offset = 0;
entries.forEach(([key, [_addr, size]]) => {
if (size === 1) {
result[key] = rawData[offset];
} else {
result[key] = rawData.slice(offset, offset + size);
}
offset += size;
});

// data transformation
result.score =
(result.score[3] << 24) |
(result.score[2] << 16) |
(result.score[1] << 8) |
result.score[0];
result.frameCounter = (result.frameCounter[1] << 8) | result.frameCounter[0];
result.lines = _bcdToDecimal(result.lines[0], result.lines[1]);

let statsByteIndex = 0;
result.T = _bcdToDecimal(
result.stats[statsByteIndex++],
result.stats[statsByteIndex++]
);
result.J = _bcdToDecimal(
result.stats[statsByteIndex++],
result.stats[statsByteIndex++]
);
result.Z = _bcdToDecimal(
result.stats[statsByteIndex++],
result.stats[statsByteIndex++]
);
result.O = _bcdToDecimal(
result.stats[statsByteIndex++],
result.stats[statsByteIndex++]
);
result.S = _bcdToDecimal(
result.stats[statsByteIndex++],
result.stats[statsByteIndex++]
);
result.L = _bcdToDecimal(
result.stats[statsByteIndex++],
result.stats[statsByteIndex++]
);
result.I = _bcdToDecimal(
result.stats[statsByteIndex++],
result.stats[statsByteIndex++]
);
delete result.stats;

result.field = result.field.map(
tileId => TILE_ID_TO_NTC_BLOCK_ID.get(tileId) ?? tileId
);

return result;
}
54 changes: 54 additions & 0 deletions public/emu/audio_processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// An example thing, obviously fix this
class NesAudioProcessor extends AudioWorkletProcessor {
constructor(...args) {
super(...args);
this.sampleBuffer = new Int16Array(0);
this.lastPlayedSample = 0;
this.port.onmessage = e => {
if (e.data.type == 'samples') {
let mergedBuffer = new Int16Array(
this.sampleBuffer.length + e.data.samples.length
);
mergedBuffer.set(this.sampleBuffer);
mergedBuffer.set(e.data.samples, this.sampleBuffer.length);
this.sampleBuffer = mergedBuffer;
}
};
}
process(inputs, outputs, parameters) {
const output = outputs[0];
const desired_length = output[0].length;
//console.log("Want to play: ", desired_length);
//console.log("Actual size: ", this.sampleBuffer.length);
if (desired_length <= this.sampleBuffer.length) {
// Queue up the buffer contents. Note that NES audio is in mono, so we'll replicate that
// to every channel on the output. (I'm guessing usually 2?)
output.forEach(channel => {
for (let i = 0; i < channel.length; i++) {
// Convert from i16 to float, ranging from -1 to 1
channel[i] = this.sampleBuffer[i] / 32768;
}
});
// Set the new last played sample, this will be our hold value if we have an underrun
this.lastPlayedSample = this.sampleBuffer[desired_length - 1];
// Remove those contents from the buffer
this.sampleBuffer = this.sampleBuffer.slice(desired_length);
// Finally, tell the main thread so it can adjust its totals
this.port.postMessage({ type: 'samplesPlayed', count: desired_length });
} else {
// Queue up nothing! Specifically, *repeat* the last sample, to hold the level; this won't
// avoid a break in the audio, but it avoids ugly pops
output.forEach(channel => {
for (let i = 0; i < channel.length; i++) {
channel[i] = this.lastPlayedSample / 37268;
}
});
// Tell the main thread that we've run behind
this.port.postMessage({ type: 'audioUnderrun', count: output[0].length });
}

return true;
}
}

registerProcessor('nes-audio-processor', NesAudioProcessor);
Loading
Loading