Reorganize emu folder, update S-SMP system infra
This commit is contained in:
@@ -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
|
||||
@@ -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
125
src/app/emu/audio/apu.cc
Normal 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
192
src/app/emu/audio/apu.h
Normal 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
281
src/app/emu/audio/dsp.cc
Normal 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
315
src/app/emu/audio/dsp.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_};
|
||||
|
||||
@@ -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() {
|
||||
@@ -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_;
|
||||
@@ -15,12 +15,17 @@ add_executable(
|
||||
yaze_test.cc
|
||||
z3ed_test.cc
|
||||
cpu_test.cc
|
||||
spc700_test.cc
|
||||
../src/cli/patch.cc
|
||||
../src/cli/command_handler.cc
|
||||
compression_test.cc
|
||||
snes_palette_test.cc
|
||||
../src/app/rom.cc
|
||||
../src/app/emu/cpu.cc
|
||||
../src/app/emu/audio/apu.cc
|
||||
../src/app/emu/video/ppu.cc
|
||||
../src/app/emu/audio/dsp.cc
|
||||
../src/app/emu/audio/spc700.cc
|
||||
../src/app/gfx/bitmap.cc
|
||||
../src/app/gfx/snes_tile.cc
|
||||
../src/app/gfx/snes_palette.cc
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace yaze {
|
||||
namespace app {
|
||||
namespace emu {
|
||||
|
||||
class MockClock : public VirtualClock {
|
||||
class MockClock : public Clock {
|
||||
public:
|
||||
MOCK_METHOD(void, UpdateClock, (double delta), (override));
|
||||
MOCK_METHOD(unsigned long long, GetCycleCount, (), (const, override));
|
||||
@@ -150,6 +150,24 @@ class CPUTest : public ::testing::Test {
|
||||
using ::testing::_;
|
||||
using ::testing::Return;
|
||||
|
||||
TEST_F(CPUTest, CPURunTest) {
|
||||
// Arrange
|
||||
EXPECT_CALL(mock_clock, UpdateClock(_)).Times(1);
|
||||
EXPECT_CALL(mock_clock, GetCycleCount()).WillOnce(Return(1));
|
||||
EXPECT_CALL(mock_clock, ResetAccumulatedTime()).Times(1);
|
||||
|
||||
mock_memory.InsertMemory(0x00, {0x69, 0x01, 0xEA, 0xEA});
|
||||
|
||||
// Act
|
||||
mock_clock.UpdateClock(1.0);
|
||||
cpu.Update();
|
||||
mock_clock.ResetAccumulatedTime();
|
||||
|
||||
// Assert
|
||||
// Google Mock will automatically check the expectations
|
||||
ASSERT_THAT(cpu.A, ::testing::Eq(0x01));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Infrastructure
|
||||
// ============================================================================
|
||||
|
||||
26
test/spc700_test.cc
Normal file
26
test/spc700_test.cc
Normal file
@@ -0,0 +1,26 @@
|
||||
#include "app/emu/audio/spc700.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace yaze {
|
||||
namespace app {
|
||||
namespace emu {
|
||||
|
||||
class MockAudioRAM : public AudioRam {
|
||||
public:
|
||||
MOCK_METHOD(uint8_t, read, (uint16_t address), (const, override));
|
||||
MOCK_METHOD(void, write, (uint16_t address, uint8_t value), (override));
|
||||
};
|
||||
|
||||
class SPC700Test : public ::testing::Test {
|
||||
protected:
|
||||
SPC700Test() = default;
|
||||
|
||||
MockAudioRAM audioRAM;
|
||||
SPC700 spc700{audioRAM};
|
||||
};
|
||||
|
||||
} // namespace emu
|
||||
} // namespace app
|
||||
} // namespace yaze
|
||||
Reference in New Issue
Block a user