Reorganize emu folder, update S-SMP system infra

This commit is contained in:
scawful
2023-08-26 01:59:57 -04:00
parent 758056dc98
commit 3d793c452d
19 changed files with 1054 additions and 238 deletions

View File

@@ -1,75 +0,0 @@
#include "app/emu/apu.h"
#include <cstdint>
#include <iostream>
#include <vector>
#include "app/emu/mem.h"
namespace yaze {
namespace app {
namespace emu {
void APU::Init() {
// Set the clock frequency
clock_.SetFrequency(kApuClockSpeed);
// Initialize registers
// ...
}
void APU::Reset() {
// Reset the clock
clock_.ResetAccumulatedTime();
// Reset the SPC700
// ...
}
void APU::Update() {
auto cycles_to_run = clock_.GetCycleCount();
for (auto i = 0; i < cycles_to_run; ++i) {
// Update the APU
// ...
// Update the SPC700
// ...
}
}
uint8_t APU::ReadRegister(uint16_t address) {
// ...
}
void APU::WriteRegister(uint16_t address, uint8_t value) {
// ...
}
const std::vector<int16_t>& APU::GetAudioSamples() const {
// ...
}
void APU::UpdateChannelSettings() {
// ...
}
int16_t APU::GenerateSample(int channel) {
// ...
}
void APU::ApplyEnvelope(int channel) {
// ...
}
uint8_t APU::ReadDSPMemory(uint16_t address) {
// ...
}
void APU::WriteDSPMemory(uint16_t address, uint8_t value) {
// ...
}
} // namespace emu
} // namespace app
} // namespace yaze

View File

@@ -1,79 +0,0 @@
#ifndef YAZE_APP_EMU_APU_H_
#define YAZE_APP_EMU_APU_H_
#include <cstdint>
#include "app/emu/clock.h"
#include "app/emu/mem.h"
#include "app/emu/spc700.h"
namespace yaze {
namespace app {
namespace emu {
const int kApuClockSpeed = 1024000; // 1.024 MHz
const int apuSampleRate = 32000; // 32 KHz
const int apuClocksPerSample = 64; // 64 clocks per sample
class APU : public Observer {
public:
// Initializes the APU with the necessary resources and dependencies
APU(Memory &memory, VirtualAudioRAM &aram, VirtualClock &clock)
: memory_(memory), clock_(clock), aram_(aram) {}
void Init();
// Resets the APU to its initial state
void Reset();
// Runs the APU for one frame
void Update();
void Notify(uint32_t address, uint8_t data) override {
if (address >= 0x2140 && address <= 0x2143) {
// Handle communication with the APU
}
}
void UpdateClock(int delta_time) { clock_.UpdateClock(delta_time); }
// Reads a byte from the specified APU register
uint8_t ReadRegister(uint16_t address);
// Writes a byte to the specified APU register
void WriteRegister(uint16_t address, uint8_t value);
// Returns the audio samples for the current frame
const std::vector<int16_t> &GetAudioSamples() const;
private:
// Internal methods to handle APU operations and sound generation
// Updates internal state based on APU register settings
void UpdateChannelSettings();
// Generates a sample for an audio channel
int16_t GenerateSample(int channel);
// Applies an envelope to an audio channel
void ApplyEnvelope(int channel);
// Handles DSP (Digital Signal Processor) memory reads and writes
uint8_t ReadDSPMemory(uint16_t address);
void WriteDSPMemory(uint16_t address, uint8_t value);
// Member variables to store internal APU state and resources
Memory &memory_;
VirtualClock &clock_;
VirtualAudioRAM &aram_;
SPC700 spc700_{aram_};
std::vector<int16_t> audioSamples_;
// Other state variables (registers, counters, channel settings, etc.)
};
} // namespace emu
} // namespace app
} // namespace yaze
#endif

125
src/app/emu/audio/apu.cc Normal file
View File

@@ -0,0 +1,125 @@
#include "app/emu/audio/apu.h"
#include <cstdint>
#include <functional>
#include <iostream>
#include <vector>
#include "app/emu/audio/dsp.h"
#include "app/emu/audio/spc700.h"
#include "app/emu/clock.h"
#include "app/emu/mem.h"
namespace yaze {
namespace app {
namespace emu {
void APU::Init() {
// Set the clock frequency
clock_.SetFrequency(kApuClockSpeed);
// Initialize Digital Signal Processor Callbacks
dsp_.SetSampleFetcher([this](uint16_t address) -> uint8_t {
return this->FetchSampleFromRam(address);
});
dsp_.SetSamplePusher(
[this](int16_t sample) { this->PushToAudioBuffer(sample); });
// Initialize registers
// ...
}
void APU::Reset() {
// Reset the clock
clock_.ResetAccumulatedTime();
// Reset the SPC700
// ...
}
void APU::Update() {
auto cycles_to_run = clock_.GetCycleCount();
for (auto i = 0; i < cycles_to_run; ++i) {
// Update the APU
// ...
// Update the SPC700
// ...
}
}
void APU::ProcessSamples() {
// Fetch sample data from AudioRam
// Iterate over all voices
for (uint8_t voice_num = 0; voice_num < 8; voice_num++) {
// Fetch the sample data for the current voice from AudioRam
uint8_t sample = FetchSampleForVoice(voice_num);
// Process the sample through DSP
int16_t processed_sample = dsp_.ProcessSample(voice_num, sample);
// Add the processed sample to the audio buffer
audioSamples_.push_back(processed_sample);
}
}
uint8_t APU::FetchSampleForVoice(uint8_t voice_num) {
// Define how you determine the address based on the voice_num
uint16_t address = CalculateAddressForVoice(voice_num);
return aram_.read(address);
}
uint16_t APU::CalculateAddressForVoice(uint8_t voice_num) {
// Placeholder logic to calculate the address in the AudioRam
// based on the voice number.
return voice_num; // Assuming each voice has a fixed size
}
int16_t APU::GetNextSample() {
// This method fetches the next sample. If there's no sample available, it can
// return 0 or the last sample.
if (!audioSamples_.empty()) {
int16_t sample = audioSamples_.front();
audioSamples_.erase(audioSamples_.begin());
return sample;
}
return 0; // or return the last sample
}
uint8_t APU::ReadRegister(uint16_t address) {
// ...
}
void APU::WriteRegister(uint16_t address, uint8_t value) {
// ...
}
const std::vector<int16_t>& APU::GetAudioSamples() const {
// ...
}
void APU::UpdateChannelSettings() {
// ...
}
int16_t APU::GenerateSample(int channel) {
// ...
}
void APU::ApplyEnvelope(int channel) {
// ...
}
uint8_t APU::ReadDSPMemory(uint16_t address) {
// ...
}
void APU::WriteDSPMemory(uint16_t address, uint8_t value) {
// ...
}
} // namespace emu
} // namespace app
} // namespace yaze

192
src/app/emu/audio/apu.h Normal file
View File

@@ -0,0 +1,192 @@
#ifndef YAZE_APP_EMU_APU_H_
#define YAZE_APP_EMU_APU_H_
#include <cstdint>
#include <iostream>
#include <vector>
#include "app/emu/audio/dsp.h"
#include "app/emu/audio/spc700.h"
#include "app/emu/clock.h"
#include "app/emu/mem.h"
namespace yaze {
namespace app {
namespace emu {
/**
*
* 64 kilobytes of RAM are mapped across the 16-bit memory space of the SPC-700.
* Some regions of this space are overlaid with special hardware functions.
*
* Range Note
* $0000-00EF Zero Page RAM
* $00F0-00FF Sound CPU Registers
* $0100-01FF Stack Page RAM
* $0200-FFBF RAM
* $FFC0-FFFF IPL ROM or RAM
*
* The region at $FFC0-FFFF will normally read from the 64-byte IPL ROM, but the
* underlying RAM can always be written to, and the high bit of the Control
* register $F1 can be cleared to unmap the IPL ROM and allow read access to
* this RAM.
*
*/
const int kApuClockSpeed = 1024000; // 1.024 MHz
const int apuSampleRate = 32000; // 32 KHz
const int apuClocksPerSample = 64; // 64 clocks per sample
class APU : public Observer {
public:
// Initializes the APU with the necessary resources and dependencies
APU(Memory &memory, AudioRam &aram, Clock &clock)
: memory_(memory), aram_(aram), clock_(clock) {}
void Init();
// Resets the APU to its initial state
void Reset();
// Runs the APU for one frame
void Update();
void ProcessSamples();
uint8_t FetchSampleForVoice(uint8_t voice_num);
uint16_t CalculateAddressForVoice(uint8_t voice_num);
int16_t GetNextSample();
void Notify(uint32_t address, uint8_t data) override {
if (address >= 0x2140 && address <= 0x2143) {
// Handle communication with the APU
}
}
void UpdateClock(int delta_time) { clock_.UpdateClock(delta_time); }
// Method to fetch a sample from AudioRam
uint8_t FetchSampleFromRam(uint16_t address) { return aram_.read(address); }
// Method to push a processed sample to the audio buffer
void PushToAudioBuffer(int16_t sample) { audioSamples_.push_back(sample); }
// Reads a byte from the specified APU register
uint8_t ReadRegister(uint16_t address);
// Writes a byte to the specified APU register
void WriteRegister(uint16_t address, uint8_t value);
// Returns the audio samples for the current frame
const std::vector<int16_t> &GetAudioSamples() const;
// Called upon a reset
void Initialize() {
spc700_.Reset();
dsp_.Reset();
// Set stack pointer, zero-page values, etc. for the SPC700
SignalReady();
}
void SignalReady() {
// Set Port 0 = $AA and Port 1 = $BB
ports_[0] = READY_SIGNAL_0;
ports_[1] = READY_SIGNAL_1;
}
bool IsReadySignalReceived() const {
return ports_[0] == READY_SIGNAL_0 && ports_[1] == READY_SIGNAL_1;
}
void WaitForSignal() {
// This might be an active wait or a passive state where APU does nothing
// until it's externally triggered by the main CPU writing to its ports.
while (ports_[0] != BEGIN_SIGNAL)
;
}
uint16_t ReadAddressFromPorts() const {
// Read 2 byte address from port 2 (low) and 3 (high)
return static_cast<uint16_t>(ports_[2]) |
(static_cast<uint16_t>(ports_[3]) << 8);
}
void AcknowledgeSignal() {
// Read value from Port 0 and write it back to Port 0
ports_[0] = ports_[0];
}
void BeginTransfer() {
uint16_t destAddr = ReadAddressFromPorts();
uint8_t counter = 0;
// Port 1 determines whether to execute or transfer
while (ports_[1] != 0) {
uint8_t data = ports_[1];
aram_.write(destAddr, data);
AcknowledgeSignal();
destAddr++;
counter++;
// Synchronize with the counter from the main CPU
while (ports_[0] != counter)
;
}
}
void ExecuteProgram() {
// For now, this is a placeholder. Actual execution would involve running
// the SPC700's instruction at the specified address.
spc700_.ExecuteInstructions(ReadAddressFromPorts());
}
// This method will be called by the main CPU to write to the APU's ports.
void WriteToPort(uint8_t portNum, uint8_t value) {
if (portNum < 4) {
ports_[portNum] = value;
if (portNum == 0 && value == BEGIN_SIGNAL) {
BeginTransfer();
}
}
}
private:
// Constants for communication
static const uint8_t READY_SIGNAL_0 = 0xAA;
static const uint8_t READY_SIGNAL_1 = 0xBB;
static const uint8_t BEGIN_SIGNAL = 0xCC;
// Port buffers (equivalent to $2140 to $2143 for the main CPU)
uint8_t ports_[4] = {0};
// Updates internal state based on APU register settings
void UpdateChannelSettings();
// Generates a sample for an audio channel
int16_t GenerateSample(int channel);
// Applies an envelope to an audio channel
void ApplyEnvelope(int channel);
// Handles DSP (Digital Signal Processor) memory reads and writes
uint8_t ReadDSPMemory(uint16_t address);
void WriteDSPMemory(uint16_t address, uint8_t value);
// Member variables to store internal APU state and resources
Memory &memory_;
AudioRam &aram_;
Clock &clock_;
SPC700 spc700_{aram_};
Dsp dsp_;
std::vector<int16_t> audioSamples_;
};
} // namespace emu
} // namespace app
} // namespace yaze
#endif

281
src/app/emu/audio/dsp.cc Normal file
View File

@@ -0,0 +1,281 @@
#include "app/emu/audio/dsp.h"
#include "app/emu/mem.h"
namespace yaze {
namespace app {
namespace emu {
void Dsp::Reset() {}
uint8_t Dsp::ReadVoiceReg(uint8_t voice, uint8_t reg) const {
voice %= kNumVoices;
switch (reg % kNumVoiceRegs) {
case 0:
return voices_[voice].vol_left;
case 1:
return voices_[voice].vol_right;
case 2:
return voices_[voice].pitch_low;
case 3:
return voices_[voice].pitch_high;
case 4:
return voices_[voice].source_number;
case 5:
return voices_[voice].adsr1;
case 6:
return voices_[voice].adsr2;
case 7:
return voices_[voice].gain;
case 8:
return voices_[voice].envx;
case 9:
return voices_[voice].outx;
default:
return 0; // This shouldn't happen, but it's good to have a default
// case
}
}
void Dsp::WriteVoiceReg(uint8_t voice, uint8_t reg, uint8_t value) {
voice %= kNumVoices;
switch (reg % kNumVoiceRegs) {
case 0:
voices_[voice].vol_left = static_cast<int8_t>(value);
break;
case 1:
voices_[voice].vol_right = static_cast<int8_t>(value);
break;
case 2:
voices_[voice].pitch_low = value;
break;
case 3:
voices_[voice].pitch_high = value;
break;
case 4:
voices_[voice].source_number = value;
break;
case 5:
voices_[voice].adsr1 = value;
break;
case 6:
voices_[voice].adsr2 = value;
break;
case 7:
voices_[voice].gain = value;
break;
// Note: envx and outx are read-only, so they don't have cases here
}
}
// Set the callbacks
void Dsp::SetSampleFetcher(SampleFetcher fetcher) { sample_fetcher_ = fetcher; }
void Dsp::SetSamplePusher(SamplePusher pusher) { sample_pusher_ = pusher; }
int16_t Dsp::DecodeSample(uint8_t voice_num) {
Voice const& voice = voices_[voice_num];
uint16_t sample_address = voice.source_number;
// Use the callback to fetch the sample
int16_t sample = static_cast<int16_t>(sample_fetcher_(sample_address) << 8);
return sample;
}
int16_t Dsp::ProcessSample(uint8_t voice_num, int16_t sample) {
Voice const& voice = voices_[voice_num];
// Adjust the pitch (for simplicity, we're just adjusting the sample value)
sample += voice.pitch_low + (voice.pitch_high << 8);
// Apply volume (separate for left and right for stereo sound)
int16_t left_sample = (sample * voice.vol_left) / 255;
int16_t right_sample = (sample * voice.vol_right) / 255;
// Combine stereo samples into a single 16-bit value
return (left_sample + right_sample) / 2;
}
void Dsp::MixSamples() {
int16_t mixed_sample = 0;
for (uint8_t i = 0; i < kNumVoices; i++) {
int16_t decoded_sample = DecodeSample(i);
int16_t processed_sample = ProcessSample(i, decoded_sample);
mixed_sample += processed_sample;
}
// Clamp the mixed sample to 16-bit range
if (mixed_sample > 32767) {
mixed_sample = 32767;
} else if (mixed_sample < -32768) {
mixed_sample = -32768;
}
// Use the callback to push the mixed sample
sample_pusher_(mixed_sample);
}
void Dsp::UpdateEnvelope(uint8_t voice) {
uint8_t adsr1 = ReadVoiceReg(voice, 0x05);
uint8_t adsr2 = ReadVoiceReg(voice, 0x06);
uint8_t gain = ReadVoiceReg(voice, 0x07);
uint8_t enableADSR = (adsr1 & 0x80) >> 7;
if (enableADSR) {
// Handle ADSR envelope
Voice& voice_obj = voices_[voice];
switch (voice_obj.state) {
case VoiceState::ATTACK:
// Update amplitude based on attack rate
voice_obj.current_amplitude += AttackRate(adsr1);
if (voice_obj.current_amplitude >= ENVELOPE_MAX) {
voice_obj.current_amplitude = ENVELOPE_MAX;
voice_obj.state = VoiceState::DECAY;
}
break;
case VoiceState::DECAY:
// Update amplitude based on decay rate
voice_obj.current_amplitude -= DecayRate(adsr2);
if (voice_obj.current_amplitude <= voice_obj.decay_level) {
voice_obj.current_amplitude = voice_obj.decay_level;
voice_obj.state = VoiceState::SUSTAIN;
}
break;
case VoiceState::SUSTAIN:
// Keep amplitude at the calculated decay level
voice_obj.current_amplitude = voice_obj.decay_level;
break;
case VoiceState::RELEASE:
// Update amplitude based on release rate
voice_obj.current_amplitude -= ReleaseRate(adsr2);
if (voice_obj.current_amplitude <= 0) {
voice_obj.current_amplitude = 0;
voice_obj.state = VoiceState::OFF;
}
break;
default:
break;
}
} else {
// Handle Gain envelope
// Extract mode from the gain byte
uint8_t mode = (gain & 0xE0) >> 5;
uint8_t rate = gain & 0x1F;
Voice& voice_obj = voices_[voice];
switch (mode) {
case 0: // Direct Designation
case 1:
case 2:
case 3:
voice_obj.current_amplitude =
rate << 3; // Multiplying by 8 to scale to 0-255
break;
case 6: // Increase Mode (Linear)
voice_obj.current_amplitude += gainTimings[0][rate];
if (voice_obj.current_amplitude > ENVELOPE_MAX) {
voice_obj.current_amplitude = ENVELOPE_MAX;
}
break;
case 7: // Increase Mode (Bent Line)
// Hypothetical behavior: Increase linearly at first, then increase
// more slowly You'll likely need to adjust this based on your
// specific requirements
if (voice_obj.current_amplitude < (ENVELOPE_MAX / 2)) {
voice_obj.current_amplitude += gainTimings[1][rate];
} else {
voice_obj.current_amplitude += gainTimings[1][rate] / 2;
}
if (voice_obj.current_amplitude > ENVELOPE_MAX) {
voice_obj.current_amplitude = ENVELOPE_MAX;
}
break;
case 4: // Decrease Mode (Linear)
if (voice_obj.current_amplitude < gainTimings[2][rate]) {
voice_obj.current_amplitude = 0;
} else {
voice_obj.current_amplitude -= gainTimings[2][rate];
}
break;
case 5: // Decrease Mode (Exponential)
voice_obj.current_amplitude -=
(voice_obj.current_amplitude * gainTimings[3][rate]) / ENVELOPE_MAX;
break;
default:
// Default behavior can be handled here if necessary
break;
}
}
}
void Dsp::update_voice_state(uint8_t voice_num) {
if (voice_num >= kNumVoices) return;
Voice& voice = voices_[voice_num];
switch (voice.state) {
case VoiceState::OFF:
// Reset current amplitude
voice.current_amplitude = 0;
break;
case VoiceState::ATTACK:
// Increase the current amplitude at a rate defined by the ATTACK
// setting
voice.current_amplitude += AttackRate(voice.adsr1);
if (voice.current_amplitude >= ENVELOPE_MAX) {
voice.current_amplitude = ENVELOPE_MAX;
voice.state = VoiceState::DECAY;
voice.decay_level = CalculateDecayLevel(voice.adsr2);
}
break;
case VoiceState::DECAY:
// Decrease the current amplitude at a rate defined by the DECAY setting
voice.current_amplitude -= DecayRate(voice.adsr2);
if (voice.current_amplitude <= voice.decay_level) {
voice.current_amplitude = voice.decay_level;
voice.state = VoiceState::SUSTAIN;
}
break;
case VoiceState::SUSTAIN:
// Keep the current amplitude at the decay level
break;
case VoiceState::RELEASE:
// Decrease the current amplitude at a rate defined by the RELEASE
// setting
voice.current_amplitude -= ReleaseRate(voice.adsr2);
if (voice.current_amplitude == 0) {
voice.state = VoiceState::OFF;
}
break;
}
}
void Dsp::process_envelope(uint8_t voice_num) {
if (voice_num >= kNumVoices) return;
Voice& voice = voices_[voice_num];
// Update the voice state first (based on keys, etc.)
update_voice_state(voice_num);
// Calculate the envelope value based on the current amplitude
voice.envx = calculate_envelope_value(voice.current_amplitude);
// Apply the envelope value to the audio output
apply_envelope_to_output(voice_num);
}
} // namespace emu
} // namespace app
} // namespace yaze

315
src/app/emu/audio/dsp.h Normal file
View File

@@ -0,0 +1,315 @@
#ifndef YAZE_APP_EMU_AUDIO_S_DSP_H
#define YAZE_APP_EMU_AUDIO_S_DSP_H
#include <cstdint>
#include <functional>
#include <vector>
#include "app/emu/mem.h"
namespace yaze {
namespace app {
namespace emu {
using SampleFetcher = std::function<uint8_t(uint16_t)>;
using SamplePusher = std::function<void(int16_t)>;
/**
*
* The S-DSP is a digital signal processor generating the sound data.
*
* A DSP register can be selected with $F2, after which it can be read or
* written at $F3. Often it is useful to load the register address into A, and
* the value to send in Y, so that MOV $F2, YA can be used to do both in one
* 16-bit instruction.
*
* The DSP register address space only has 7 bits. The high bit of $F2, if set,
* will make the selected register read-only via $F3.
*
* When initializing the DSP registers for the first time, take care not to
* accidentally enable echo writeback via FLG, because it will immediately begin
* overwriting values in RAM.
*
* Voices
* There are 8 voices, numbered 0 to 7.
* Each voice X has 10 registers in the range $X0-$X9.
*
* Name Address Bits Notes
* VOL (L) $X0 SVVV VVVV Left channel volume, signed.
* VOL (R) $X1 SVVV VVVV Right channel volume, signed.
* P (L) $X2 LLLL LLLL Low 8 bits of sample pitch.
* P (H) $X3 --HH HHHH High 6 bits of sample pitch.
* SCRN $X4 SSSS SSSS Selects a sample source entry from the
* directory ADSR (1) $X5 EDDD AAAA ADSR enable (E), decay rate (D),
* attack rate (A).
* ADSR (2) $X6 SSSR RRRR Sustain level (S), release rate (R).
* GAIN $X7 0VVV VVVV 1MMV VVVV Mode (M), value (V).
* ENVX $X8 0VVV VVVV Reads current 7-bit value of ADSR/GAIN
* envelope.
* OUTX $X9 SVVV VVVV Reads signed 8-bit value of current
* sample wave multiplied by ENVX, before applying VOL.
*
*/
class Dsp {
private:
static const size_t kNumVoices = 8;
static const size_t kNumVoiceRegs = 10;
static const size_t kNumGlobalRegs = 15;
enum class VoiceState { OFF, ATTACK, DECAY, SUSTAIN, RELEASE };
struct Voice {
int8_t vol_left; // x0
int8_t vol_right; // x1
uint8_t pitch_low; // x2
uint8_t pitch_high; // x3
uint8_t source_number; // x4
uint8_t adsr1; // x5
uint8_t adsr2; // x6
uint8_t gain; // x7
uint8_t envx; // x8 (read-only)
int8_t outx; // x9 (read-only)
VoiceState state = VoiceState::OFF;
uint16_t current_amplitude = 0; // Current amplitude value used for ADSR
uint16_t decay_level; // Calculated decay level based on ADSR settings
};
Voice voices_[8];
// Global DSP registers
uint8_t mvol_left; // 0C
uint8_t mvol_right; // 0D
uint8_t evol_left; // 0E
uint8_t evol_right; // 0F
uint8_t kon; // 10
uint8_t koff; // 11
uint8_t flags; // 12
uint8_t endx; // 13 (read-only)
// Global registers
std::vector<uint8_t> globalRegs = std::vector<uint8_t>(kNumGlobalRegs, 0x00);
static const uint16_t ENVELOPE_MAX = 2047; // $7FF
// Attack times in ms
const std::vector<uint32_t> attackTimes = {
4100, 2600, 1500, 1000, 640, 380, 260, 160, 96, 64, 40, 24, 16, 10, 6, 0};
// Decay times in ms
const std::vector<uint32_t> decayTimes = {1200, 740, 440, 290,
180, 110, 74, 37};
// Release times in ms
const std::vector<uint32_t> releaseTimes = {
// "Infinite" is represented by a large value, e.g., UINT32_MAX
UINT32_MAX, 38000, 28000, 24000, 19000, 14000, 12000, 9400,
7100, 5900, 4700, 3500, 2900, 2400, 1800, 1500,
1200, 880, 740, 590, 440, 370, 290, 220,
180, 150, 110, 92, 74, 55, 37, 18};
// Gain timings for decrease linear, decrease exponential, etc.
// Organized by mode: [Linear Increase, Bentline Increase, Linear Decrease,
// Exponential Decrease]
const std::vector<std::vector<uint32_t>> gainTimings = {
{UINT32_MAX, 3100, 2600, 2000, 1500, 1300, 1000, 770, 640, 510, 380,
320, 260, 190, 160, 130, 96, 80, 64, 48, 40, 32,
24, 20, 16, 12, 10, 8, 6, 4, 2},
{UINT32_MAX, 5400, 4600, 3500, 2600, 2300, 1800, 1300, 1100, 900,
670, 560, 450, 340, 280, 220, 170, 140, 110, 84,
70, 56, 42, 35, 28, 21, 18, 14, 11, 7,
/*3.5=*/3},
// Repeating the Linear Increase timings for Linear Decrease, since they
// are the same.
{UINT32_MAX, 3100, 2600, 2000, 1500, 1300, 1000, 770, 640, 510, 380,
320, 260, 190, 160, 130, 96, 80, 64, 48, 40, 32,
24, 20, 16, 12, 10, 8, 6, 4, 2},
{UINT32_MAX, 38000, 28000, 24000, 19000, 14000, 12000, 9400,
7100, 5900, 4700, 3500, 2900, 2400, 1800, 1500,
1200, 880, 740, 590, 440, 370, 290, 220,
180, 150, 110, 92, 55, 37, 18}};
// DSP Period Table
const std::vector<std::vector<uint16_t>> DspPeriodTable = {
// ... Your DSP period table here ...
};
// DSP Period Offset
const std::vector<uint16_t> DspPeriodOffset = {
// ... Your DSP period offsets here ...
};
uint8_t calculate_envelope_value(uint16_t amplitude) const {
// Convert the 16-bit amplitude to an 8-bit envelope value
return amplitude >> 8;
}
void apply_envelope_to_output(uint8_t voice_num) {
Voice& voice = voices_[voice_num];
// Scale the OUTX by the envelope value
// This might be a linear scaling, or more complex operations can be used
voice.outx = (voice.outx * voice.envx) / 255;
}
SampleFetcher sample_fetcher_;
SamplePusher sample_pusher_;
public:
Dsp() = default;
void Reset();
void SetSampleFetcher(std::function<uint8_t(uint16_t)> fetcher);
void SetSamplePusher(std::function<void(int16_t)> pusher);
// Read a byte from a voice register
uint8_t ReadVoiceReg(uint8_t voice, uint8_t reg) const;
// Write a byte to a voice register
void WriteVoiceReg(uint8_t voice, uint8_t reg, uint8_t value);
// Read a byte from a global register
uint8_t ReadGlobalReg(uint8_t reg) const {
return globalRegs[reg % kNumGlobalRegs];
}
// Write a byte to a global register
void WriteGlobalReg(uint8_t reg, uint8_t value) {
globalRegs[reg % kNumGlobalRegs] = value;
}
int16_t DecodeSample(uint8_t voice_num);
int16_t ProcessSample(uint8_t voice_num, int16_t sample);
void MixSamples();
// Trigger a voice to start playing
void trigger_voice(uint8_t voice_num) {
if (voice_num >= kNumVoices) return;
Voice& voice = voices_[voice_num];
voice.state = VoiceState::ATTACK;
// Initialize other state management variables if needed
}
// Release a voice (e.g., note release in ADSR)
void release_voice(uint8_t voice_num) {
if (voice_num >= kNumVoices) return;
Voice& voice = voices_[voice_num];
if (voice.state != VoiceState::OFF) {
voice.state = VoiceState::RELEASE;
}
// Update other state management variables if needed
}
// Calculate envelope for a given voice
void UpdateEnvelope(uint8_t voice);
// Voice-related functions (implementations)
void set_voice_volume(int voice_num, int8_t left, int8_t right) {
voices_[voice_num].vol_left = left;
voices_[voice_num].vol_right = right;
}
void set_voice_pitch(int voice_num, uint16_t pitch) {
voices_[voice_num].pitch_low = pitch & 0xFF;
voices_[voice_num].pitch_high = (pitch >> 8) & 0xFF;
}
void set_voice_source_number(int voice_num, uint8_t srcn) {
voices_[voice_num].source_number = srcn;
}
void set_voice_adsr(int voice_num, uint8_t adsr1, uint8_t adsr2) {
voices_[voice_num].adsr1 = adsr1;
voices_[voice_num].adsr2 = adsr2;
}
void set_voice_gain(int voice_num, uint8_t gain) {
voices_[voice_num].gain = gain;
}
uint8_t read_voice_envx(int voice_num) { return voices_[voice_num].envx; }
int8_t read_voice_outx(int voice_num) { return voices_[voice_num].outx; }
// Global DSP functions
void set_master_volume(int8_t left, int8_t right) {
mvol_left = left;
mvol_right = right;
}
void set_echo_volume(int8_t left, int8_t right) {
evol_left = left;
evol_right = right;
}
void update_voice_state(uint8_t voice_num);
// Override the key_on and key_off methods to utilize the new state management
void key_on(uint8_t value) {
for (uint8_t i = 0; i < kNumVoices; i++) {
if (value & (1 << i)) {
trigger_voice(i);
}
}
}
void key_off(uint8_t value) {
for (uint8_t i = 0; i < kNumVoices; i++) {
if (value & (1 << i)) {
release_voice(i);
}
}
}
void set_flags(uint8_t value) {
flags = value;
// More logic may be needed here depending on flag behaviors
}
uint8_t read_endx() { return endx; }
uint16_t AttackRate(uint8_t adsr1) {
// Convert the ATTACK portion of adsr1 into a rate of amplitude change
// You might need to adjust this logic based on the exact ADSR
// implementation details
return (adsr1 & 0x0F) * 16; // Just a hypothetical conversion
}
uint16_t DecayRate(uint8_t adsr2) {
// Convert the DECAY portion of adsr2 into a rate of amplitude change
return ((adsr2 >> 4) & 0x07) * 8; // Hypothetical conversion
}
uint16_t ReleaseRate(uint8_t adsr2) {
// Convert the RELEASE portion of adsr2 into a rate of amplitude change
return (adsr2 & 0x0F) * 16; // Hypothetical conversion
}
uint16_t CalculateDecayLevel(uint8_t adsr2) {
// Calculate the decay level based on the SUSTAIN portion of adsr2
// This is the level the amplitude will decay to before entering the SUSTAIN
// phase Again, adjust based on your implementation details
return ((adsr2 >> 4) & 0x07) * 256; // Hypothetical conversion
}
// Envelope processing for all voices
// Goes through each voice and processes its envelope.
void process_envelopes() {
for (size_t i = 0; i < kNumVoices; ++i) {
process_envelope(i);
}
}
// Envelope processing for a specific voice
// For a given voice, update its state (ADSR), calculate the envelope value,
// and apply the envelope to the audio output.
void process_envelope(uint8_t voice_num);
};
} // namespace emu
} // namespace app
} // namespace yaze
#endif // YAZE_APP_EMU_AUDIO_S_DSP_H

View File

@@ -1,4 +1,4 @@
#include "app/emu/spc700.h"
#include "app/emu/audio/spc700.h"
#include <iostream>
#include <vector>
@@ -7,6 +7,8 @@ namespace yaze {
namespace app {
namespace emu {
void SPC700::Reset() {}
void SPC700::ExecuteInstructions(uint8_t opcode) {
switch (opcode) {
// 8-bit Move Memory to Register

View File

@@ -9,9 +9,9 @@ namespace yaze {
namespace app {
namespace emu {
class VirtualAudioRAM {
class AudioRam {
public:
virtual ~VirtualAudioRAM() = default;
virtual ~AudioRam() = default;
// Read a byte from ARAM at the given address
virtual uint8_t read(uint16_t address) const = 0;
@@ -20,12 +20,12 @@ class VirtualAudioRAM {
virtual void write(uint16_t address, uint8_t value) = 0;
};
class AudioRAM : public VirtualAudioRAM {
class AudioRamImpl : public AudioRam {
static const size_t ARAM_SIZE = 64 * 1024; // 64 KB
std::vector<uint8_t> ram = std::vector<uint8_t>(ARAM_SIZE, 0);
public:
AudioRAM() = default;
AudioRamImpl() = default;
// Read a byte from ARAM at the given address
uint8_t read(uint16_t address) const override {
@@ -38,51 +38,12 @@ class AudioRAM : public VirtualAudioRAM {
}
};
// Digital Signal Processor
class DigitalSignalProcessor {
private:
static const size_t NUM_VOICES = 8;
static const size_t NUM_VOICE_REGS = 10;
static const size_t NUM_GLOBAL_REGS = 15;
// Each voice has 10 registers
std::vector<std::vector<uint8_t>> voices = std::vector<std::vector<uint8_t>>(
NUM_VOICES, std::vector<uint8_t>(NUM_VOICE_REGS, 0));
// Global registers
std::vector<uint8_t> globalRegs = std::vector<uint8_t>(NUM_GLOBAL_REGS, 0x00);
public:
DigitalSignalProcessor() = default;
// Read a byte from a voice register
uint8_t ReadVoiceReg(uint8_t voice, uint8_t reg) const {
return voices[voice % NUM_VOICES][reg % NUM_VOICE_REGS];
}
// Write a byte to a voice register
void WriteVoiceReg(uint8_t voice, uint8_t reg, uint8_t value) {
voices[voice % NUM_VOICES][reg % NUM_VOICE_REGS] = value;
}
// Read a byte from a global register
uint8_t ReadGlobalReg(uint8_t reg) const {
return globalRegs[reg % NUM_GLOBAL_REGS];
}
// Write a byte to a global register
void WriteGlobalReg(uint8_t reg, uint8_t value) {
globalRegs[reg % NUM_GLOBAL_REGS] = value;
}
};
class SPC700 {
private:
VirtualAudioRAM& aram_;
AudioRam& aram_;
public:
explicit SPC700(VirtualAudioRAM& aram) : aram_(aram) {}
DigitalSignalProcessor sdsp;
explicit SPC700(AudioRam& aram) : aram_(aram) {}
uint8_t test_register_;
uint8_t control_register_;
uint8_t dsp_address_register_;
@@ -107,6 +68,8 @@ class SPC700 {
};
Flags PSW; // Processor status word
void Reset();
void ExecuteInstructions(uint8_t opcode);
// Read a byte from the memory-mapped registers
@@ -118,8 +81,6 @@ class SPC700 {
return control_register_;
case 0xF2:
return dsp_address_register_;
case 0xF3:
return sdsp.ReadGlobalReg(dsp_address_register_);
default:
if (address < 0xFFC0) {
return aram_.read(address);
@@ -142,9 +103,6 @@ class SPC700 {
case 0xF2:
dsp_address_register_ = value;
break;
case 0xF3:
sdsp.WriteGlobalReg(dsp_address_register_, value);
break;
default:
if (address < 0xFFC0) {
aram_.write(address, value);

View File

@@ -7,9 +7,9 @@ namespace yaze {
namespace app {
namespace emu {
class VirtualClock {
class Clock {
public:
virtual ~VirtualClock() = default;
virtual ~Clock() = default;
virtual void UpdateClock(double delta) = 0;
virtual unsigned long long GetCycleCount() const = 0;
virtual void ResetAccumulatedTime() = 0;
@@ -17,10 +17,10 @@ class VirtualClock {
virtual float GetFrequency() const = 0;
};
class Clock : public VirtualClock {
class ClockImpl : public Clock {
public:
Clock() = default;
virtual ~Clock() = default;
ClockImpl() = default;
virtual ~ClockImpl() = default;
void UpdateCycleCount(double deltaTime) {
accumulatedTime += deltaTime;

View File

@@ -1176,6 +1176,7 @@ void CPU::ExecuteInstruction(uint8_t opcode) {
void CPU::HandleInterrupts() {}
// ADC: Add with carry
void CPU::ADC(uint8_t operand) {
bool C = GetCarryFlag();
if (GetAccumulatorSize()) { // 8-bit mode
@@ -1209,6 +1210,7 @@ void CPU::ADC(uint8_t operand) {
}
}
// AND: Logical AND
void CPU::AND(uint16_t value, bool isImmediate) {
uint16_t operand;
if (E == 0) { // 16-bit mode
@@ -1232,6 +1234,7 @@ void CPU::ANDAbsoluteLong(uint32_t address) {
SetNegativeFlag(A & 0x80000000);
}
// ASL: Arithmetic shift left
void CPU::ASL(uint16_t address) {
uint8_t value = memory.ReadByte(address);
SetCarryFlag(!(value & 0x80)); // Set carry flag if bit 7 is set
@@ -1242,6 +1245,7 @@ void CPU::ASL(uint16_t address) {
SetZeroFlag(value);
}
// BCC: Branch if carry clear
void CPU::BCC(int8_t offset) {
if (!GetCarryFlag()) { // If the carry flag is clear
PC += offset; // Add the offset to the program counter
@@ -1520,7 +1524,7 @@ void CPU::JSL(uint32_t address) {
}
// LDA: Load accumulator
void CPU::LDA(uint16_t address, bool isImmediate ) {
void CPU::LDA(uint16_t address, bool isImmediate) {
if (GetAccumulatorSize()) {
A = isImmediate ? address : memory.ReadByte(address);
SetZeroFlag(A == 0);
@@ -1533,7 +1537,7 @@ void CPU::LDA(uint16_t address, bool isImmediate ) {
}
// LDX: Load X register
void CPU::LDX(uint16_t address, bool isImmediate ) {
void CPU::LDX(uint16_t address, bool isImmediate) {
if (GetIndexSize()) {
X = isImmediate ? address : memory.ReadByte(address);
SetZeroFlag(X == 0);
@@ -1546,7 +1550,7 @@ void CPU::LDX(uint16_t address, bool isImmediate ) {
}
// LDY: Load Y register
void CPU::LDY(uint16_t address, bool isImmediate ) {
void CPU::LDY(uint16_t address, bool isImmediate) {
if (GetIndexSize()) {
Y = isImmediate ? address : memory.ReadByte(address);
SetZeroFlag(Y == 0);
@@ -1577,7 +1581,7 @@ void CPU::NOP() {
}
// ORA: Logical OR
void CPU::ORA(uint16_t address, bool isImmediate ) {
void CPU::ORA(uint16_t address, bool isImmediate) {
if (GetAccumulatorSize()) {
A |= isImmediate ? address : memory.ReadByte(address);
SetZeroFlag(A == 0);

View File

@@ -74,8 +74,7 @@ const int kCpuClockSpeed = 21477272; // 21.477272 MHz
class CPU : public Memory, public Loggable {
public:
explicit CPU(Memory& mem, VirtualClock& vclock)
: memory(mem), clock(vclock) {}
explicit CPU(Memory& mem, Clock& vclock) : memory(mem), clock(vclock) {}
void Init() {
clock.SetFrequency(kCpuClockSpeed);
@@ -698,7 +697,7 @@ class CPU : public Memory, public Loggable {
uint8_t at(int i) const override { return 0; }
Memory& memory;
VirtualClock& clock;
Clock& clock;
};
} // namespace emu

View File

@@ -1,9 +1,9 @@
#ifndef YAZE_APP_EMU_DBG_H_
#define YAZE_APP_EMU_DBG_H_
#ifndef YAZE_APP_EMU_DEBUG_DEBUGGER_H_
#define YAZE_APP_EMU_DEBUG_DEBUGGER_H_
#include "app/emu/apu.h"
#include "app/emu/audio/apu.h"
#include "app/emu/cpu.h"
#include "app/emu/ppu.h"
#include "app/emu/video/ppu.h"
namespace yaze {
namespace app {
@@ -11,7 +11,7 @@ namespace emu {
class Debugger {
public:
Debugger()=default;
Debugger() = default;
// Attach the debugger to the emulator
// Debugger(CPU &cpu, PPU &ppu, APU &apu);

View File

@@ -1,14 +1,20 @@
#include "snes.h"
#include "app/emu/snes.h"
#include <SDL_mixer.h>
#include <cstdint>
#include <memory>
#include <string>
#include <thread>
#include "app/emu/apu.h"
#include "app/emu/audio/apu.h"
#include "app/emu/audio/spc700.h"
#include "app/emu/clock.h"
#include "app/emu/cpu.h"
#include "app/emu/debug/debugger.h"
#include "app/emu/mem.h"
#include "app/emu/ppu.h"
#include "app/emu/video/ppu.h"
#include "app/rom.h"
namespace yaze {
namespace app {
@@ -40,6 +46,16 @@ uint16_t GetHeaderOffset(const Memory& memory) {
return offset;
}
void audio_callback(void* userdata, uint8_t* stream, int len) {
auto* apu = static_cast<APU*>(userdata);
auto* buffer = reinterpret_cast<int16_t*>(stream);
for (int i = 0; i < len / 2; i++) { // Assuming 16-bit samples
buffer[i] = apu->GetNextSample(); // This function should be implemented in
// APU to fetch the next sample
}
}
} // namespace
void DMA::StartDMATransfer(uint8_t channelMask) {
@@ -180,6 +196,9 @@ void SNES::Init(ROM& rom) {
// Initialize APU
apu.Init();
// Initialize SDL_Mixer to play the audio samples
Mix_HookMusic(audio_callback, &apu);
// Disable interrupts and rendering
memory_.WriteByte(0x4200, 0x00); // NMITIMEN
memory_.WriteByte(0x420C, 0x00); // HDMAEN

View File

@@ -1,12 +1,12 @@
#include <cstdint>
#include <string>
#include "app/emu/apu.h"
#include "app/emu/audio/apu.h"
#include "app/emu/audio/spc700.h"
#include "app/emu/clock.h"
#include "app/emu/cpu.h"
#include "app/emu/dbg.h"
#include "app/emu/ppu.h"
#include "app/emu/spc700.h"
#include "app/emu/debug/debugger.h"
#include "app/emu/video/ppu.h"
#include "app/rom.h"
namespace yaze {
@@ -104,8 +104,8 @@ class SNES : public DMA {
// Components of the SNES
MemoryImpl memory_;
Clock clock_;
AudioRAM audio_ram_;
ClockImpl clock_;
AudioRamImpl audio_ram_;
CPU cpu{memory_, clock_};
PPU ppu{memory_, clock_};

View File

@@ -1,4 +1,4 @@
#include "app/emu/ppu.h"
#include "app/emu/video/ppu.h"
#include <cstdint>
#include <iostream>
@@ -155,7 +155,31 @@ void PPU::UpdateModeSettings() {
}
void PPU::RenderBackground(int layer) {
// ...
switch (layer) {
case 1:
// // Render the first background layer
// auto bg1_tilemap_info =
// PPURegisters::BGSC(ReadVRAM(PPURegisters::BG1SC)); auto bg1_chr_data =
// PPURegisters::BGNBA(ReadVRAM(PPURegisters::BG12NBA)); break;
// case 2:
// // Render the second background layer
// auto bg2_tilemap_info =
// PPURegisters::BGSC(ReadVRAM(PPURegisters::BG2SC)); auto bg2_chr_data =
// PPURegisters::BGNBA(ReadVRAM(PPURegisters::BG12NBA)); break;
// case 3:
// // Render the third background layer
// auto bg3_tilemap_info =
// PPURegisters::BGSC(ReadVRAM(PPURegisters::BG3SC)); auto bg3_chr_data =
// PPURegisters::BGNBA(ReadVRAM(PPURegisters::BG34NBA)); break;
// case 4:
// // Render the fourth background layer
// auto bg4_tilemap_info =
// PPURegisters::BGSC(ReadVRAM(PPURegisters::BG4SC)); auto bg4_chr_data =
// PPURegisters::BGNBA(ReadVRAM(PPURegisters::BG34NBA)); break;
default:
// Invalid layer, do nothing
break;
}
}
void PPU::RenderSprites() {

View File

@@ -635,18 +635,20 @@ struct BackgroundLayer {
const int kPpuClockSpeed = 5369318; // 5.369318 MHz
class PPU : public Clock, public Observer {
class PPU : public Observer {
public:
// Initializes the PPU with the necessary resources and dependencies
PPU(Memory& memory, VirtualClock& clock) : memory_(memory), clock_(clock) {}
PPU(Memory& memory, Clock& clock) : memory_(memory), clock_(clock) {}
void Init() {
// Initialize the frame buffer with a size that corresponds to the
// screen resolution
SetFrequency(kPpuClockSpeed);
clock_.SetFrequency(kPpuClockSpeed);
frame_buffer_.resize(256 * 240, 0);
}
void UpdateClock(double delta_time) { clock_.UpdateClock(delta_time); }
// Resets the PPU to its initial state
void Reset() { std::fill(frame_buffer_.begin(), frame_buffer_.end(), 0); }
@@ -725,7 +727,7 @@ class PPU : public Clock, public Observer {
// ===========================================================
// Member variables to store internal PPU state and resources
Memory& memory_;
VirtualClock& clock_;
Clock& clock_;
std::vector<uint8_t> frame_buffer_;
Tilemap tilemap_;