mirror of
https://github.com/hexahigh/games.git
synced 2025-12-11 20:15:38 +01:00
242 lines
5.9 KiB
JavaScript
242 lines
5.9 KiB
JavaScript
|
|
function NsfPlayer() {
|
|
|
|
this.cpu = new Cpu(this);
|
|
this.apu = new Apu(this);
|
|
|
|
this.ram = new Uint8Array(0x800);
|
|
|
|
// internally used
|
|
this.callArea = new Uint8Array(0x10);
|
|
|
|
this.totalSongs = 0;
|
|
this.startSong = 0;
|
|
this.tags = {
|
|
name: "",
|
|
artist: "",
|
|
copyright: ""
|
|
}
|
|
|
|
this.playReturned = true;
|
|
|
|
this.frameIrqWanted = false;
|
|
this.dmcIrqWanted = false;
|
|
|
|
// this.mapper = new NsfMapper(this);
|
|
|
|
this.loadNsf = function(nsf) {
|
|
if(nsf.length < 0x80) {
|
|
log("Invalid NSF loaded");
|
|
return false;
|
|
}
|
|
if(
|
|
nsf[0] !== 0x4e || nsf[1] !== 0x45 || nsf[2] !== 0x53 ||
|
|
nsf[3] !== 0x4d || nsf[4] !== 0x1a
|
|
) {
|
|
log("Invalid NSF loaded");
|
|
return false;
|
|
}
|
|
if(nsf[5] !== 1) {
|
|
log("Unknown NSF version: " + nsf[5]);
|
|
return false;
|
|
}
|
|
this.totalSongs = nsf[6];
|
|
this.startSong = nsf[7];
|
|
let loadAdr = nsf[8] | (nsf[9] << 8);
|
|
if(loadAdr < 0x8000) {
|
|
log("Load address less than 0x8000 is not supported");
|
|
return false;
|
|
}
|
|
let initAdr = nsf[0xa] | (nsf[0xb] << 8);
|
|
let playAdr = nsf[0xc] | (nsf[0xd] << 8);
|
|
this.tags = {
|
|
name: "",
|
|
artist: "",
|
|
copyright: ""
|
|
}
|
|
for(let i = 0; i < 32; i++) {
|
|
if(nsf[0xe + i] === 0) {
|
|
break;
|
|
}
|
|
this.tags.name += String.fromCharCode(nsf[0xe + i]);
|
|
}
|
|
for(let i = 0; i < 32; i++) {
|
|
if(nsf[0x2e + i] === 0) {
|
|
break;
|
|
}
|
|
this.tags.artist += String.fromCharCode(nsf[0x2e + i]);
|
|
}
|
|
for(let i = 0; i < 32; i++) {
|
|
if(nsf[0x4e + i] === 0) {
|
|
break;
|
|
}
|
|
this.tags.copyright += String.fromCharCode(nsf[0x4e + i]);
|
|
}
|
|
// TODO: play speed, pal/ntsc, extra chips
|
|
let initBanks = [0, 0, 0, 0, 0, 0, 0, 0];
|
|
let total = 0;
|
|
for(let i = 0; i < 8; i++) {
|
|
initBanks[i] = nsf[0x70 + i];
|
|
total += nsf[0x70 + i];
|
|
}
|
|
banking = total > 0;
|
|
// set up the NSF mapper
|
|
this.mapper = new NsfMapper(nsf, loadAdr, banking, initBanks);
|
|
// set up the call area
|
|
this.callArea[0] = 0x20; // JSR
|
|
this.callArea[1] = initAdr & 0xff;
|
|
this.callArea[2] = initAdr >> 8;
|
|
this.callArea[3] = 0xea // NOP
|
|
this.callArea[4] = 0xea // NOP
|
|
this.callArea[5] = 0xea // NOP
|
|
this.callArea[6] = 0xea // NOP
|
|
this.callArea[7] = 0xea // NOP
|
|
this.callArea[8] = 0x20; // JSR
|
|
this.callArea[9] = playAdr & 0xff;
|
|
this.callArea[0xa] = playAdr >> 8;
|
|
this.callArea[0xb] = 0xea // NOP
|
|
this.callArea[0xc] = 0xea // NOP
|
|
this.callArea[0xd] = 0xea // NOP
|
|
this.callArea[0xe] = 0xea // NOP
|
|
this.callArea[0xf] = 0xea // NOP
|
|
|
|
this.playSong(this.startSong);
|
|
log("Loaded NSF file");
|
|
return true;
|
|
}
|
|
|
|
this.playSong = function(songNum) {
|
|
// also acts as a reset
|
|
for(let i = 0; i < this.ram.length; i++) {
|
|
this.ram[i] = 0;
|
|
}
|
|
this.playReturned = true;
|
|
this.apu.reset();
|
|
this.cpu.reset();
|
|
this.mapper.reset();
|
|
this.frameIrqWanted = false;
|
|
this.dmcIrqWanted = false;
|
|
for(let i = 0x4000; i <= 0x4013; i++) {
|
|
this.apu.write(i, 0);
|
|
}
|
|
this.apu.write(0x4015, 0);
|
|
this.apu.write(0x4015, 0xf);
|
|
this.apu.write(0x4017, 0x40);
|
|
|
|
// run the init routine
|
|
this.cpu.br[0] = 0x3ff0;
|
|
this.cpu.r[0] = songNum - 1;
|
|
this.cpu.r[1] = 0;
|
|
// don't allow init to take more than 10 frames
|
|
let cycleCount = 0;
|
|
let finished = false;
|
|
while(cycleCount < 297800) {
|
|
this.cpu.cycle();
|
|
this.apu.cycle();
|
|
if(this.cpu.br[0] === 0x3ff5) {
|
|
// we are in the nops after the init-routine, it finished
|
|
finished = true;
|
|
break;
|
|
}
|
|
cycleCount++;
|
|
}
|
|
if(!finished) {
|
|
log("Init did not finish within 10 frames");
|
|
}
|
|
}
|
|
|
|
this.runFrame = function() {
|
|
// run the cpu until either a frame has passed, or the play-routine returned
|
|
if(this.playReturned) {
|
|
this.cpu.br[0] = 0x3ff8;
|
|
}
|
|
this.playReturned = false;
|
|
let cycleCount = 0;
|
|
while(cycleCount < 29780) {
|
|
this.cpu.irqWanted = this.dmcIrqWanted || this.frameIrqWanted;
|
|
if(!this.playReturned) {
|
|
this.cpu.cycle();
|
|
}
|
|
this.apu.cycle();
|
|
if(this.cpu.br[0] === 0x3ffd) {
|
|
// we are in the nops after the play-routine, it finished
|
|
this.playReturned = true;
|
|
}
|
|
cycleCount++;
|
|
}
|
|
}
|
|
|
|
this.getSamples = function(data, count) {
|
|
// apu returns 29780 or 29781 samples (0 - 1) for a frame
|
|
// we need count values (0 - 1)
|
|
let samples = this.apu.getOutput();
|
|
let runAdd = (29780 / count);
|
|
let total = 0;
|
|
let inputPos = 0;
|
|
let running = 0;
|
|
for(let i = 0; i < count; i++) {
|
|
running += runAdd;
|
|
let total = 0;
|
|
let avgCount = running & 0xffff;
|
|
for(let j = inputPos; j < inputPos + avgCount; j++) {
|
|
total += samples[1][j];
|
|
}
|
|
data[i] = total / avgCount;
|
|
inputPos += avgCount;
|
|
running -= avgCount;
|
|
}
|
|
}
|
|
|
|
this.read = function(adr) {
|
|
adr &= 0xffff;
|
|
|
|
if(adr < 0x2000) {
|
|
// ram
|
|
return this.ram[adr & 0x7ff];
|
|
}
|
|
if(adr < 0x3ff0) {
|
|
// ppu ports, not readable in NSF
|
|
return 0;
|
|
}
|
|
if(adr < 0x4000) {
|
|
// special call area used internally by player
|
|
return this.callArea[adr & 0xf];
|
|
}
|
|
if(adr < 0x4020) {
|
|
// apu/misc ports
|
|
if(adr === 0x4014) {
|
|
return 0; // not readable
|
|
}
|
|
if(adr === 0x4016 || adr === 0x4017) {
|
|
return 0; // not readable in NSF
|
|
}
|
|
return this.apu.read(adr);
|
|
}
|
|
return this.mapper.read(adr);
|
|
}
|
|
|
|
this.write = function(adr, value) {
|
|
adr &= 0xffff;
|
|
|
|
if(adr < 0x2000) {
|
|
// ram
|
|
this.ram[adr & 0x7ff] = value;
|
|
return;
|
|
}
|
|
if(adr < 0x4000) {
|
|
// ppu ports, not writable in NSF
|
|
return;
|
|
}
|
|
if(adr < 0x4020) {
|
|
// apu/misc ports
|
|
if(adr === 0x4014 || adr === 0x4016) {
|
|
// not writable in NSF
|
|
return;
|
|
}
|
|
this.apu.write(adr, value);
|
|
return;
|
|
}
|
|
this.mapper.write(adr, value);
|
|
}
|
|
}
|