Refactor and enhance documentation across the Wolf3D project

- Updated library imports to use the correct package paths for consistency.
- Added detailed documentation comments to various classes and methods, improving code readability and maintainability.
- Refined the GameSelectScreen, SpriteGallery, and VgaGallery classes with clearer descriptions of their functionality.
- Enhanced the CliInput class to better explain the input handling process and its interaction with the engine.
- Improved the SixelRasterizer and Opl2Emulator classes with comprehensive comments on their operations and state management.
- Removed the deprecated wolf_3d.dart file and consolidated its functionality into wolf_3d_flutter.dart for a cleaner architecture.
- Updated the Wolf3dFlutterInput class to clarify its role in merging keyboard and pointer events.
- Enhanced the rendering classes to provide better context on their purpose and usage within the Flutter framework.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-18 10:01:12 +01:00
parent 28938f7301
commit 3c6a4672f7
23 changed files with 404 additions and 183 deletions

View File

@@ -1,7 +1,9 @@
import 'dart:math' as math;
/// States used by the simplified ADSR envelope inside the OPL2 emulator.
enum EnvelopeState { off, attack, decay, sustain, release }
/// One OPL2 operator, combining waveform generation and envelope progression.
class Opl2Operator {
double phase = 0.0;
double phaseIncrement = 0.0;
@@ -16,24 +18,27 @@ class Opl2Operator {
double multiplier = 1.0;
double volume = 1.0;
// Waveform Selection (0-3)
/// Selected waveform index as exposed by the OPL2 register set.
int waveform = 0;
/// Recomputes oscillator increment from the shared channel base frequency.
void updateFrequency(double baseFreq) {
phaseIncrement =
(baseFreq * multiplier * 2 * math.pi) / Opl2Emulator.sampleRate;
}
/// Starts a new note by resetting the phase and entering attack.
void triggerOn() {
phase = 0.0;
envState = EnvelopeState.attack;
}
/// Releases the note so the envelope decays back to silence.
void triggerOff() {
envState = EnvelopeState.release;
}
// Applies the OPL2 hardware waveform math
// Waveform handling mirrors the small set of shapes exposed by the OPL2 chip.
double _getOscillatorOutput(double currentPhase) {
// Normalize phase between 0 and 2*pi
double p = currentPhase % (2 * math.pi);
@@ -52,6 +57,7 @@ class Opl2Operator {
}
}
/// Produces one sample for this operator using [phaseOffset] modulation.
double getSample(double phaseOffset) {
switch (envState) {
case EnvelopeState.attack:
@@ -83,7 +89,8 @@ class Opl2Operator {
return 0.0;
}
// Pass the phase + modulation offset into our waveform generator!
// Modulation is expressed as a phase offset, which is how the carrier is
// driven by the modulator in two-operator FM synthesis.
double out = _getOscillatorOutput(phase + phaseOffset);
phase += phaseIncrement;
@@ -93,6 +100,7 @@ class Opl2Operator {
}
}
/// Two-operator OPL2 channel with optional additive mode and self-feedback.
class Opl2Channel {
Opl2Operator modulator = Opl2Operator();
Opl2Operator carrier = Opl2Operator();
@@ -107,12 +115,14 @@ class Opl2Channel {
double _prevModOutput1 = 0.0;
double _prevModOutput2 = 0.0;
/// Updates both operators after frequency register changes.
void updateFrequency() {
double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
modulator.updateFrequency(baseFreq);
carrier.updateFrequency(baseFreq);
}
/// Mixes one audio sample from the channel's current operator state.
double getSample() {
if (!keyOn &&
carrier.envState == EnvelopeState.off &&
@@ -122,6 +132,8 @@ class Opl2Channel {
double feedbackPhase = 0.0;
if (feedbackStrength > 0) {
// Feedback reuses the previous modulator outputs to create the harsher
// timbres that classic OPL instruments rely on.
double averageMod = (_prevModOutput1 + _prevModOutput2) / 2.0;
double feedbackFactor =
math.pow(2, feedbackStrength - 1) * (math.pi / 16.0);
@@ -136,9 +148,11 @@ class Opl2Channel {
double channelOutput = 0.0;
if (isAdditive) {
// Additive mode mixes both operators as audible oscillators.
double carOutput = carrier.getSample(0.0);
channelOutput = modOutput + carOutput;
} else {
// Standard FM mode feeds the modulator into the carrier's phase.
double carOutput = carrier.getSample(modOutput * 2.0);
channelOutput = carOutput;
}
@@ -147,6 +161,7 @@ class Opl2Channel {
}
}
/// Lightweight pseudo-random noise source for percussion voices.
class Opl2Noise {
int _seed = 0xFFFF;
@@ -158,12 +173,17 @@ class Opl2Noise {
}
}
/// Simplified register-driven OPL2 emulator used for IMF playback.
///
/// The implementation focuses on the subset of FM behavior needed by the game
/// assets: melodic channels, rhythm mode, waveform selection, and a practical
/// ADSR envelope approximation.
class Opl2Emulator {
static const int sampleRate = 44100;
bool rhythmMode = false;
// Key states for the 5 drums
// Rhythm mode steals the final three channels and exposes them as drum bits.
bool bassDrumKey = false;
bool snareDrumKey = false;
bool tomTomKey = false;
@@ -174,7 +194,7 @@ class Opl2Emulator {
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());
// The master lock for waveforms
// The chip only honors waveform writes after the global enable bit is set.
bool _waveformSelectionEnabled = false;
static const List<int> _operatorMap = [
@@ -202,6 +222,7 @@ class Opl2Emulator {
8,
];
/// Resolves a register offset to the affected operator, if any.
Opl2Operator? _getOperator(int offset) {
if (offset < 0 ||
offset >= _operatorMap.length ||
@@ -215,6 +236,7 @@ class Opl2Emulator {
: channels[channelIdx].modulator;
}
/// Applies a single OPL2 register write.
void writeRegister(int reg, int data) {
// --- 0x01: Test / Waveform Enable ---
if (reg == 0x01) {
@@ -310,6 +332,7 @@ class Opl2Emulator {
}
}
/// Generates one normalized mono sample from the current register state.
double generateSample() {
double mixedOutput = 0.0;
@@ -319,12 +342,12 @@ class Opl2Emulator {
}
if (!rhythmMode) {
// Standard mode: play channels 6, 7, and 8 normally
// Standard mode keeps the final channels melodic.
for (int i = 6; i < 9; i++) {
mixedOutput += channels[i].getSample();
}
} else {
// RHYTHM MODE: The last 3 channels are re-routed
// Rhythm mode repurposes the last three channels into drum voices.
mixedOutput += _generateBassDrum();
mixedOutput += _generateSnareAndHiHat();
mixedOutput += _generateTomAndCymbal();
@@ -333,13 +356,14 @@ class Opl2Emulator {
return mixedOutput.clamp(-1.0, 1.0);
}
// Example of Bass Drum logic (Channel 6)
/// Generates the bass drum voice routed through channel 6.
double _generateBassDrum() {
if (!bassDrumKey) return 0.0;
// Bass drum uses standard FM (Mod -> Car) but usually with very low frequency
return channels[6].getSample();
}
/// Generates the combined snare and hi-hat voices from channel 7.
double _generateSnareAndHiHat() {
double snareOut = 0.0;
double hiHatOut = 0.0;
@@ -363,6 +387,7 @@ class Opl2Emulator {
return (snareOut + hiHatOut) * 0.1;
}
/// Generates the combined tom and cymbal voices from channel 8.
double _generateTomAndCymbal() {
double tomOut = 0.0;
double cymbalOut = 0.0;