Skip to content
Closed
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
6 changes: 3 additions & 3 deletions build-wasm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ $emccArgs = "-O3",
"-s", "ALLOW_MEMORY_GROWTH=1", "-s", "INITIAL_MEMORY=16777216", "-s", "MAXIMUM_MEMORY=16777216",
"-s", "ENVIRONMENT=web", "-s", "STANDALONE_WASM=1", "--no-entry"

$ayumiArgs = "external/ayumi/ayumi.c", "-o", "public/ayumi.wasm",
$ayumiArgs = "external/ayumi/ayumi.c", "-o", "public/ay/ayumi.wasm",
"-s", "EXPORTED_FUNCTIONS=`"[\`"_ayumi_configure\`", \`"_ayumi_set_pan\`", \`"_ayumi_set_tone\`", \`"_ayumi_set_noise\`", \`"_ayumi_set_mixer\`", \`"_ayumi_set_volume\`", \`"_ayumi_set_timer_effect\`", \`"_ayumi_set_timer_effect_slot\`", \`"_ayumi_set_timer_effect_waveform\`", \`"_ayumi_timer_effect_reset\`", \`"_ayumi_get_timer_effect_active_period\`", \`"_ayumi_get_registers\`", \`"_ayumi_struct_size\`", \`"_ayumi_set_envelope\`", \`"_ayumi_set_envelope_shape\`", \`"_ayumi_process\`", \`"_ayumi_remove_dc\`", \`"_malloc\`", \`"_free\`"]`"",

$nesApuArgs = "external/nsfplug/nes_apu.c", "external/nsfplug/nes_dmc.c", "-o", "public/nes_apu.wasm",
$nesApuArgs = "external/nsfplug/nes_apu.c", "external/nsfplug/nes_dmc.c", "-o", "public/nes/nes_apu.wasm",
"-s", "EXPORTED_FUNCTIONS=`"[\`"_nes_apu_Init\`", \`"_nes_apu_Reset\`", \`"_nes_apu_Tick\`", \`"_nes_apu_Render\`", \`"_nes_apu_Write\`", \`"_nes_apu_SetMask\`", \`"_nes_apu_SetStereoMix\`", \`"_nes_dmc_Init\`", \`"_nes_dmc_Reset\`", \`"_nes_dmc_Tick\`", \`"_nes_dmc_Render\`", \`"_nes_dmc_Write\`", \`"_nes_dmc_SetMask\`", \`"_nes_dmc_SetStereoMix\`", \`"_nes_dmc_SetPal\`", \`"_nes_dmc_SetAPU\`", \`"_nes_dmc_SetMemory_Read\`", \`"_nes_dmc_TickFrameSequence\`"]`""

$nesMmc5Args = "external/nsfplug/nes_mmc5.c", "-o", "public/nes_mmc5.wasm",
$nesMmc5Args = "external/nsfplug/nes_mmc5.c", "-o", "public/nes/nes_mmc5.wasm",
"-s", "EXPORTED_FUNCTIONS=`"[\`"_nes_mmc5_Init\`", \`"_nes_mmc5_Reset\`", \`"_nes_mmc5_Tick\`", \`"_nes_mmc5_Render\`", \`"_nes_mmc5_Write\`", \`"_nes_mmc5_SetMask\`", \`"_nes_mmc5_SetStereoMix\`", \`"_nes_mmc5_TickFrameSequence\`"]`""

$python = $null
Expand Down
9 changes: 5 additions & 4 deletions build-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fi

EMCC_ARGS="\
-O3 \
-DNDEBUG \
-s WASM=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s INITIAL_MEMORY=16777216 \
Expand All @@ -23,15 +24,15 @@ EMCC_ARGS="\

emcc ${EMCC_ARGS} \
external/ayumi/ayumi.c \
-o public/ayumi.wasm \
-o public/ay/ayumi.wasm \
-s EXPORTED_FUNCTIONS='["_ayumi_configure", "_ayumi_set_pan", "_ayumi_set_tone", "_ayumi_set_noise", "_ayumi_set_mixer", "_ayumi_set_volume", "_ayumi_set_timer_effect", "_ayumi_set_timer_effect_slot", "_ayumi_set_timer_effect_waveform", "_ayumi_timer_effect_reset", "_ayumi_get_timer_effect_active_period", "_ayumi_get_registers", "_ayumi_struct_size", "_ayumi_set_envelope", "_ayumi_set_envelope_shape", "_ayumi_process", "_ayumi_remove_dc", "_malloc", "_free"]'

emcc ${EMCC_ARGS} \
external/nsfplug/nes_apu.c external/nsfplug/nes_dmc.c \
-o public/nes_apu.wasm \
-s EXPORTED_FUNCTIONS='["_nes_apu_Init", "_nes_apu_Reset", "_nes_apu_Tick", "_nes_apu_Render", "_nes_apu_Write", "_nes_apu_SetMask", "_nes_apu_SetStereoMix", "_nes_dmc_Init", "_nes_dmc_Reset", "_nes_dmc_Tick", "_nes_dmc_Render", "_nes_dmc_Write", "_nes_dmc_SetMask", "_nes_dmc_SetStereoMix", "_nes_dmc_SetPal", "_nes_dmc_SetAPU", "_nes_dmc_SetMemory_Read", "_nes_dmc_TickFrameSequence"]'
-o public/nes/nes_apu.wasm \
-s EXPORTED_FUNCTIONS='["_nes_apu_Init", "_nes_apu_Reset", "_nes_apu_Tick", "_nes_apu_Render", "_nes_apu_Write", "_nes_apu_SetMask", "_nes_apu_SetStereoMix", "_nes_dmc_Init", "_nes_dmc_Reset", "_nes_dmc_Tick", "_nes_dmc_Render", "_nes_dmc_Write", "_nes_dmc_SetMask", "_nes_dmc_SetStereoMix", "_nes_dmc_SetPal", "_nes_dmc_SetAPU", "_nes_dmc_SetMemory_Read", "_nes_dmc_TickFrameSequence", "_malloc", "_free"]'

emcc ${EMCC_ARGS} \
external/nsfplug/nes_mmc5.c \
-o public/nes_mmc5.wasm \
-o public/nes/nes_mmc5.wasm \
-s EXPORTED_FUNCTIONS='["_nes_mmc5_Init", "_nes_mmc5_Reset", "_nes_mmc5_Tick", "_nes_mmc5_Render", "_nes_mmc5_Write", "_nes_mmc5_SetMask", "_nes_mmc5_SetStereoMix", "_nes_mmc5_TickFrameSequence"]'
20 changes: 10 additions & 10 deletions external/nsfplug/nes_dmc.c
Original file line number Diff line number Diff line change
Expand Up @@ -484,16 +484,16 @@ void nes_dmc_Reset (nes_dmc_t* s)
s->noise = 1;
s->noise_tap = (1<<1);

if (s->option[NES_DMC_OPT_RANDOMIZE_NOISE])
{
s->noise |= rand();
s->counter[1] = -(rand() & 511);
}
if (s->option[NES_DMC_OPT_RANDOMIZE_TRI])
{
s->tphase = rand() & 31;
s->counter[0] = -(rand() & 2047);
}
// if (s->option[NES_DMC_OPT_RANDOMIZE_NOISE])
// {
// s->noise |= rand();
// s->counter[1] = -(rand() & 511);
// }
// if (s->option[NES_DMC_OPT_RANDOMIZE_TRI])
// {
// s->tphase = rand() & 31;
// s->counter[0] = -(rand() & 2047);
// }
}

void nes_dmc_SetMemory_Read (nes_dmc_t* s, read_func * r)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import SongTimeline from './song-timeline.js';
import SongTimeline from '../tracker/song-timeline.js';
import { createAudioSlot } from './audio-slot-registry.js';
import './builtin-audio-slots.js';

Expand Down
15 changes: 15 additions & 0 deletions public/audio/builtin-audio-slots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { registerAudioSlotKind } from './audio-slot-registry.js';
import { AyumiSlot } from '../ay/ayumi-slot.js';
import { NesWorkletSlot } from '../nes/nes-worklet-slot.js';

registerAudioSlotKind('ayumi', (port, chipIndex, sharedTimeline, initData) => {
const slot = new AyumiSlot(port, chipIndex, sharedTimeline);
void slot.handleMessage({ type: 'init', wasmBuffer: initData.wasmBuffer });
return slot;
});

registerAudioSlotKind('nes', (port, chipIndex, sharedTimeline, initData) => {
const slot = new NesWorkletSlot(port, chipIndex, sharedTimeline);
void slot.handleMessage({ type: 'init', wasmBuffer: initData.wasmBuffer });
return slot;
});
41 changes: 29 additions & 12 deletions public/ay-audio-driver.js → public/ay/ay-audio-driver.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
import AYChipRegisterState from './ay-chip-register-state.js';
import EffectAlgorithms from './effect-algorithms.js';
import { PT3VolumeTable } from './pt3-volume-table.js';
import { normalizeAyInstrumentFields, getAySidBaseVolume, computeTimerEffectPeriod, computeTimerPwmPeriods, effectiveRowTimerWaveform, effectiveRowFmWaveform, effectiveRowFmWaveformLoop, effectiveRowEnvFmWaveform, effectiveRowEnvFmWaveformLoop, resolveAyFmOffsetMode, effectiveRowTimerWaveformLoop, effectiveRowTimerPwmDuty, effectiveRowTimerPwmSweep, effectiveRowTimerPwmSweepMin, rowSupportsSidTimerPwm, rowSupportsSyncbuzzerTimerPwm, rowSupportsFmTimerPwm, rowSupportsEnvFmTimerPwm, rowSupportsTimerPwm, rowUsesSyncbuzzerPwmDuty, resolveSyncbuzzerWaveform, isPatternEnvelopeShapeSet, advanceTimerPwmSweep, DEFAULT_AY_TIMER_PWM_DUTY } from './ay-instrument-utils.js';
import EffectAlgorithms from '../tracker/effect-algorithms.js';
import { PT3VolumeTable } from '../tracker/pt3-volume-table.js';
import { advanceInstrumentRowPosition } from '../tracker/tracker-audio-utils.js';
import {
normalizeAyInstrumentFields,
getAySidBaseVolume,
computeTimerEffectPeriod,
computeTimerPwmPeriods,
effectiveRowTimerWaveform,
effectiveRowFmWaveform,
effectiveRowFmWaveformLoop,
effectiveRowEnvFmWaveform,
effectiveRowEnvFmWaveformLoop,
resolveAyFmOffsetMode,
effectiveRowTimerWaveformLoop,
effectiveRowTimerPwmDuty,
effectiveRowTimerPwmSweep,
effectiveRowTimerPwmSweepMin,
rowSupportsSidTimerPwm,
rowSupportsSyncbuzzerTimerPwm,
rowSupportsFmTimerPwm,
rowSupportsEnvFmTimerPwm,
rowSupportsTimerPwm,
rowUsesSyncbuzzerPwmDuty,
resolveSyncbuzzerWaveform,
isPatternEnvelopeShapeSet,
advanceTimerPwmSweep,
DEFAULT_AY_TIMER_PWM_DUTY
} from './ay-instrument-utils.js';
import {
instrumentHasSample,
computeSampleSidPeriod,
Expand All @@ -23,15 +49,6 @@ import {
disableTimerEffect
} from './ay-timer-effect-constants.js';

function advanceInstrumentRowPosition(position, rowsLength, loop) {
const length = rowsLength > 0 ? rowsLength : 1;
let next = position + 1;
if (next >= length) {
next = loop > 0 && loop < length ? loop : 0;
}
return next;
}

class AYAudioDriver {
constructor(channelCount = 3) {
this.channelMixerState = [];
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
200 changes: 200 additions & 0 deletions public/ay/ay8910-worklet-slot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import AyumiState from './ayumi-state.js';
import TrackerPatternProcessor from '../tracker/tracker-pattern-processor.js';
import AYAudioDriver from './ay-audio-driver.js';
import AyumiEngine from './ayumi-engine.js';
import AYChipRegisterState from './ay-chip-register-state.js';
import VirtualChannelMixer from './virtual-channel-mixer.js';
import { disableAllChannelTimerEffects, ensureChannelTimerEffects } from './ay-timer-effect-constants.js';
import { TrackerWorkletSlot } from '../tracker/tracker-worklet-slot.js';

export class Ay8910WorkletSlot extends TrackerWorkletSlot {
constructor(port, chipIndex, sharedTimeline) {
super(port, chipIndex);
this.state = new AyumiState(3, sharedTimeline);
this.initialized = false;
this.audioDriver = null;
this.patternProcessor = null;
this.ayumiEngine = null;
this.registerState = new AYChipRegisterState();
this.virtualChannelMixer = new VirtualChannelMixer();
this.virtualChannelMap = {};
this.hwChannelCount = 3;
}

_slotState() {
return this.state;
}

_isReadyForPlayback() {
return this.initialized && this.state.wasmModule && this.state.ayumiPtr;
}

_resizeForPatternChannels(channelCount) {
this._ensureChannelCapacity(channelCount);
}

_applyPlaybackSpeed(speed) {
if (!(speed > 0)) return;
this.state.publishPlaybackSpeed(speed);
}

_chipEngineReady() {
return this._playbackWorkersReady() && this.ayumiEngine;
}

_onTransportStop() {
this.registerState.reset();
if (this.audioDriver) {
this.audioDriver.resetChannelMixerState();
}
if (this.ayumiEngine) {
this.ayumiEngine.reset();
}
this._applyRegisterStateToEngine();
}

_dispatchChipPortMessage(type, data) {
if (type !== 'set_virtual_channel_config') {
return false;
}
this.handleSetVirtualChannelConfig(data);
return true;
}

applyChannelSilent(registerState, channelIndex) {
registerState.channels[channelIndex].volume = 0;
registerState.channels[channelIndex].mixer = {
tone: false,
noise: false,
envelope: false
};
disableAllChannelTimerEffects(ensureChannelTimerEffects(registerState.channels[channelIndex]));
}

handleSetVirtualChannelConfig({ virtualChannelMap, hwChannelCount }) {
this.virtualChannelMap = virtualChannelMap || {};
this.hwChannelCount = hwChannelCount || 3;
this.virtualChannelMixer.configure(this.virtualChannelMap, this.hwChannelCount);

const totalChannels = this.virtualChannelMixer.getTotalVirtualChannelCount();
this.state.resizeChannels(totalChannels);
this.registerState.resize(totalChannels);
if (this.audioDriver) {
this.audioDriver.resizeChannels(totalChannels);
}
if (this.ayumiEngine) {
this.registerState.reset();
this.audioDriver?.resetChannelMixerState();
this.ayumiEngine.reset();
this._applyRegisterStateToEngine();
}
}

handleSetChannelMute({ channelIndex, muted }) {
const totalChannels = this.virtualChannelMixer.getTotalVirtualChannelCount();
if (channelIndex >= 0 && channelIndex < totalChannels) {
this.state.channelMuted[channelIndex] = muted;
if (muted) {
this.applyChannelSilent(this.registerState, channelIndex);
this.state.channelEnvelopeEnabled[channelIndex] = false;
if (this.ayumiEngine) {
this._applyRegisterStateToEngine();
}
}
}
}

_getEngineRegisterState() {
if (this.virtualChannelMixer.hasVirtualChannels()) {
return this.virtualChannelMixer.merge(this.registerState, this.state);
}
return this.registerState;
}

_collectHardwareRegisters() {
const wasmModule = this.state?.wasmModule;
const ayumiPtr = this.state?.ayumiPtr;
const getRegisters = wasmModule?.ayumi_get_registers;
if (typeof getRegisters === 'function' && ayumiPtr) {
if (!this._hardwareRegistersBufferPtr) {
this._hardwareRegistersBufferPtr = wasmModule.malloc(14);
}
getRegisters(ayumiPtr, this._hardwareRegistersBufferPtr);
return Array.from(
new Uint8Array(wasmModule.memory.buffer, this._hardwareRegistersBufferPtr, 14)
);
}
return this._getEngineRegisterState().toHardwareRegisters();
}

_applyRegisterStateToEngine() {
if (!this.ayumiEngine) return;
if (this.virtualChannelMixer.hasVirtualChannels()) {
const hwState = this._getEngineRegisterState();
this.ayumiEngine.applyRegisterState(hwState);
this.registerState.forceEnvelopeShapeWrite = false;
} else {
this.ayumiEngine.applyRegisterState(this.registerState);
}
}

_applyVirtualChannelResize() {
if (!this.virtualChannelMixer.hasVirtualChannels()) return;
const totalChannels = this.virtualChannelMixer.getTotalVirtualChannelCount();
this.state.resizeChannels(totalChannels);
this.registerState.resize(totalChannels);
if (this.audioDriver) {
this.audioDriver.resizeChannels(totalChannels);
}
}

_ensureChannelCapacity(channelCount) {
if (channelCount <= this.registerState.channelCount) return;
this.state.resizeChannels(channelCount);
this.registerState.resize(channelCount);
if (this.audioDriver) {
this.audioDriver.resizeChannels(channelCount);
}
}

enforceMuteState() {
const totalChannels = this.registerState.channelCount;
for (let ch = 0; ch < totalChannels; ch++) {
if (this.state.channelMuted[ch]) {
this.applyChannelSilent(this.registerState, ch);
this.state.channelEnvelopeEnabled[ch] = false;
}
}
this._applyRegisterStateToEngine();
}

ensurePlaybackWorkers() {
if (!this.audioDriver || !this.patternProcessor || !this.ayumiEngine) {
this.audioDriver = new AYAudioDriver();
this.ayumiEngine = new AyumiEngine(this.state.wasmModule, this.state.ayumiPtr);
this.patternProcessor = new TrackerPatternProcessor(
this.state,
this.audioDriver,
this.port
);
this._applyVirtualChannelResize();
}
}

_resetEnginesForPreview() {
if (this.audioDriver) {
this.audioDriver.resetChannelMixerState();
}
if (this.ayumiEngine) {
this.ayumiEngine.reset();
}
}

_silencePreviewChannel(channelIndex) {
this.applyChannelSilent(this.registerState, channelIndex);
}

canRender() {
return this.initialized && this.state.wasmModule && this.state.ayumiPtr;
}
}
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion public/ayumi-slot.js → public/ay/ayumi-slot.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from './ay-timer-effect-constants.js';
import AYAudioDriver from './ay-audio-driver.js';
import AyumiEngine from './ayumi-engine.js';
import TrackerPatternProcessor from './tracker-pattern-processor.js';
import TrackerPatternProcessor from '../tracker/tracker-pattern-processor.js';
import { Ay8910WorkletSlot } from './ay8910-worklet-slot.js';

export class AyumiSlot extends Ay8910WorkletSlot {
Expand Down
2 changes: 1 addition & 1 deletion public/ayumi-state.js → public/ay/ayumi-state.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DEFAULT_AYM_FREQUENCY } from './ayumi-constants.js';
import TrackerState from './tracker-state.js';
import TrackerState from '../tracker/tracker-state.js';

const AY_CHANNEL_ARRAY_SPECS = [
['channelInstruments', -1],
Expand Down
File renamed without changes.
Loading
Loading