mirror of
https://github.com/libretro/bsnes-libretro.git
synced 2024-11-23 08:59:40 +00:00
Update to v098r14 release.
byuu says: Changelog: - improved attenuation of biquad filter by computing butterworth Q coefficients correctly (instead of using the same constant) - adding 1e-25 to each input sample into the biquad filters to try and prevent denormalization - updated normalization from [0.0 to 1.0] to [-1.0 to +1.0]; volume/reverb happen in floating-point mode now - good amount of work to make the base Emulator::Audio support any number of output channels - so that we don't have to do separate work on left/right channels; and can instead share the code for each channel - Emulator::Interface::audioSample(int16 left, int16 right); changed to: - Emulator::Interface::audioSample(double* samples, uint channels); - samples are normalized [-1.0 to +1.0] - for now at least, channels will be the value given to Emulator::Audio::reset() - fixed GUI crash on startup when audio driver is set to None I'm probably going to be updating ruby to accept normalized doubles as well; but I'm not sure if I will try and support anything other 2-channel audio output. It'll depend on how easy it is to do so; perhaps it'll be a per-driver setting. The denormalization thing is fierce. If that happens, it drops the emulator framerate from 220fps to about 20fps for Game Boy emulation. And that happens basically whenever audio output is silent. I'm probably also going to make a nall/denormal.hpp file at some point with platform-specific functionality to set the CPU state to "denormals as zero" where applicable. I'll still add the 1e-25 offset (inaudible) as another fallback.
This commit is contained in:
parent
839813d0f1
commit
fdc41611cf
@ -5,11 +5,15 @@ namespace Emulator {
|
||||
#include "stream.cpp"
|
||||
Audio audio;
|
||||
|
||||
auto Audio::reset() -> void {
|
||||
auto Audio::reset(maybe<uint> channels_, maybe<double> frequency_) -> void {
|
||||
if(channels_) channels = channels_();
|
||||
if(frequency_) frequency = frequency_();
|
||||
|
||||
streams.reset();
|
||||
reverb.reset();
|
||||
reverb.resize(2);
|
||||
for(auto c : range(2)) {
|
||||
|
||||
reverb.resize(channels);
|
||||
for(auto c : range(channels)) {
|
||||
reverb[c].resize(7);
|
||||
reverb[c][0].resize(1229);
|
||||
reverb[c][1].resize(1559);
|
||||
@ -25,11 +29,6 @@ auto Audio::setInterface(Interface* interface) -> void {
|
||||
this->interface = interface;
|
||||
}
|
||||
|
||||
auto Audio::setFrequency(double frequency) -> void {
|
||||
this->frequency = frequency;
|
||||
for(auto& stream : streams) stream->setFrequency(frequency);
|
||||
}
|
||||
|
||||
auto Audio::setVolume(double volume) -> void {
|
||||
this->volume = volume;
|
||||
}
|
||||
@ -43,52 +42,48 @@ auto Audio::setReverb(bool enabled) -> void {
|
||||
}
|
||||
|
||||
auto Audio::createStream(uint channels, double frequency) -> shared_pointer<Stream> {
|
||||
shared_pointer<Stream> stream = new Stream{channels, frequency};
|
||||
stream->setFrequency(this->frequency);
|
||||
shared_pointer<Stream> stream = new Stream;
|
||||
stream->reset(channels, frequency, this->frequency);
|
||||
streams.append(stream);
|
||||
return stream;
|
||||
}
|
||||
|
||||
//audio mixer
|
||||
auto Audio::poll() -> void {
|
||||
auto Audio::process() -> void {
|
||||
while(true) {
|
||||
for(auto& stream : streams) {
|
||||
if(!stream->pending()) return;
|
||||
}
|
||||
|
||||
double left = 0.0, right = 0.0;
|
||||
double samples[channels] = {0};
|
||||
for(auto& stream : streams) {
|
||||
double samples[2];
|
||||
stream->read(samples);
|
||||
left += samples[0];
|
||||
right += samples[1];
|
||||
}
|
||||
left /= streams.size();
|
||||
right /= streams.size();
|
||||
double buffer[16];
|
||||
uint length = stream->read(buffer), offset = 0;
|
||||
|
||||
if(balance < 0.0) right *= 1.0 + balance;
|
||||
if(balance > 0.0) left *= 1.0 - balance;
|
||||
|
||||
//todo: apply volume, reverb before denormalization?
|
||||
int ileft = (left * 65535.0) - 32768.0;
|
||||
int iright = (right * 65535.0) - 32768.0;
|
||||
|
||||
if(reverbEnable) {
|
||||
ileft *= 0.125;
|
||||
for(auto n : range(7)) ileft += 0.125 * reverb[0][n].last();
|
||||
for(auto n : range(7)) reverb[0][n].write(ileft);
|
||||
ileft *= 8.000;
|
||||
|
||||
iright *= 0.125;
|
||||
for(auto n : range(7)) iright += 0.125 * reverb[1][n].last();
|
||||
for(auto n : range(7)) reverb[1][n].write(iright);
|
||||
iright *= 8.000;
|
||||
for(auto c : range(channels)) {
|
||||
samples[c] += buffer[offset];
|
||||
if(++offset >= length) offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ileft *= volume;
|
||||
iright *= volume;
|
||||
for(auto c : range(channels)) {
|
||||
samples[c] /= streams.size();
|
||||
|
||||
interface->audioSample(sclamp<16>(ileft), sclamp<16>(iright));
|
||||
if(reverbEnable) {
|
||||
samples[c] *= 0.125;
|
||||
for(auto n : range(7)) samples[c] += 0.125 * reverb[c][n].last();
|
||||
for(auto n : range(7)) reverb[c][n].write(samples[c]);
|
||||
samples[c] *= 8.000;
|
||||
}
|
||||
|
||||
samples[c] *= volume;
|
||||
}
|
||||
|
||||
if(channels == 2) {
|
||||
if(balance < 0.0) samples[1] *= 1.0 + balance;
|
||||
if(balance > 0.0) samples[0] *= 1.0 - balance;
|
||||
}
|
||||
|
||||
interface->audioSample(samples, channels);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,55 +10,52 @@ struct Audio;
|
||||
struct Stream;
|
||||
|
||||
struct Audio {
|
||||
auto reset() -> void;
|
||||
auto reset(maybe<uint> channels = nothing, maybe<double> frequency = nothing) -> void;
|
||||
auto setInterface(Interface*) -> void;
|
||||
|
||||
auto setFrequency(double frequency) -> void;
|
||||
auto setVolume(double volume) -> void;
|
||||
auto setBalance(double balance) -> void;
|
||||
auto setReverb(bool enabled) -> void;
|
||||
|
||||
auto createStream(uint channels, double frequency) -> shared_pointer<Stream>;
|
||||
|
||||
auto poll() -> void;
|
||||
|
||||
private:
|
||||
auto process() -> void;
|
||||
|
||||
Interface* interface = nullptr;
|
||||
vector<shared_pointer<Stream>> streams;
|
||||
|
||||
uint channels = 0;
|
||||
double frequency = 0.0;
|
||||
|
||||
double volume = 1.0;
|
||||
double balance = 0.0;
|
||||
|
||||
bool reverbEnable = false;
|
||||
vector<vector<queue<int16>>> reverb;
|
||||
vector<vector<queue<double>>> reverb;
|
||||
|
||||
friend class Stream;
|
||||
};
|
||||
|
||||
struct Stream {
|
||||
Stream(uint channels, double inputFrequency);
|
||||
|
||||
auto reset() -> void;
|
||||
auto setFrequency(double outputFrequency) -> void;
|
||||
auto reset(uint channels, double inputFrequency, double outputFrequency) -> void;
|
||||
|
||||
auto pending() const -> bool;
|
||||
auto read(double* samples) -> void;
|
||||
auto write(int16* samples) -> void;
|
||||
auto read(double* samples) -> uint;
|
||||
auto write(const double* samples) -> void;
|
||||
|
||||
template<typename... P> auto sample(P&&... p) -> void {
|
||||
int16 samples[sizeof...(P)] = {forward<P>(p)...};
|
||||
double samples[sizeof...(P)] = {forward<P>(p)...};
|
||||
write(samples);
|
||||
}
|
||||
|
||||
private:
|
||||
const uint channels;
|
||||
const double inputFrequency;
|
||||
double outputFrequency = 0.0;
|
||||
double cutoffFrequency = 0.0;
|
||||
|
||||
const uint iirPasses = 3; //6th-order filter
|
||||
vector<vector<DSP::IIR::Biquad>> iir;
|
||||
vector<DSP::Resampler::Cubic> resampler;
|
||||
const uint order = 6; //Nth-order filter (must be an even number)
|
||||
struct Channel {
|
||||
vector<DSP::IIR::Biquad> iir;
|
||||
DSP::Resampler::Cubic resampler;
|
||||
};
|
||||
vector<Channel> channels;
|
||||
|
||||
friend class Audio;
|
||||
};
|
||||
|
@ -1,49 +1,35 @@
|
||||
Stream::Stream(uint channels, double inputFrequency) : channels(channels), inputFrequency(inputFrequency) {
|
||||
}
|
||||
auto Stream::reset(uint channels_, double inputFrequency, double outputFrequency) -> void {
|
||||
channels.reset();
|
||||
channels.resize(channels_);
|
||||
|
||||
auto Stream::reset() -> void {
|
||||
iir.reset();
|
||||
resampler.reset();
|
||||
}
|
||||
|
||||
auto Stream::setFrequency(double outputFrequency_) -> void {
|
||||
reset();
|
||||
|
||||
outputFrequency = outputFrequency_;
|
||||
cutoffFrequency = outputFrequency / inputFrequency;
|
||||
iir.resize(channels);
|
||||
|
||||
if(cutoffFrequency <= 0.5) {
|
||||
for(auto c : range(channels)) {
|
||||
iir[c].resize(iirPasses);
|
||||
for(auto p : range(iirPasses)) {
|
||||
//attenuates frequencies that exceed the limits of human hearing (20KHz) to prevent aliasing
|
||||
iir[c][p].reset(DSP::IIR::Biquad::Type::LowPass, 20000.0 / inputFrequency);
|
||||
for(auto& channel : channels) {
|
||||
if(outputFrequency / inputFrequency <= 0.5) {
|
||||
channel.iir.resize(order / 2);
|
||||
for(auto phase : range(order / 2)) {
|
||||
double q = DSP::IIR::Biquad::butterworth(order, phase);
|
||||
channel.iir[phase].reset(DSP::IIR::Biquad::Type::LowPass, 20000.0 / inputFrequency, q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resampler.resize(channels);
|
||||
for(auto c : range(channels)) {
|
||||
resampler[c].reset(inputFrequency, outputFrequency);
|
||||
channel.resampler.reset(inputFrequency, outputFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
auto Stream::pending() const -> bool {
|
||||
return resampler && resampler[0].pending();
|
||||
return channels && channels[0].resampler.pending();
|
||||
}
|
||||
|
||||
auto Stream::read(double* samples) -> void {
|
||||
for(auto c : range(channels)) samples[c] = resampler[c].read();
|
||||
if(channels == 1) samples[1] = samples[0]; //monaural->stereo hack
|
||||
auto Stream::read(double* samples) -> uint {
|
||||
for(auto c : range(channels)) samples[c] = channels[c].resampler.read();
|
||||
return channels.size();
|
||||
}
|
||||
|
||||
auto Stream::write(int16* samples) -> void {
|
||||
auto Stream::write(const double* samples) -> void {
|
||||
for(auto c : range(channels)) {
|
||||
double sample = (samples[c] + 32768.0) / 65535.0; //normalize
|
||||
for(auto& p : iir[c]) sample = p.process(sample);
|
||||
resampler[c].write(sample);
|
||||
double sample = samples[c] + 1e-25; //constant offset used to suppress denormals
|
||||
for(auto& iir : channels[c].iir) sample = iir.process(sample);
|
||||
channels[c].resampler.write(sample);
|
||||
}
|
||||
|
||||
audio.poll();
|
||||
audio.process();
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ using namespace nall;
|
||||
|
||||
namespace Emulator {
|
||||
static const string Name = "higan";
|
||||
static const string Version = "098.13";
|
||||
static const string Version = "098.14";
|
||||
static const string Author = "byuu";
|
||||
static const string License = "GPLv3";
|
||||
static const string Website = "http://byuu.org/";
|
||||
|
@ -50,7 +50,7 @@ struct Interface {
|
||||
virtual auto loadRequest(uint, string, bool) -> void {}
|
||||
virtual auto saveRequest(uint, string) -> void {}
|
||||
virtual auto videoRefresh(const uint32*, uint, uint, uint) -> void {}
|
||||
virtual auto audioSample(int16, int16) -> void {}
|
||||
virtual auto audioSample(const double*, uint) -> void {}
|
||||
virtual auto inputPoll(uint, uint, uint) -> int16 { return 0; }
|
||||
virtual auto inputRumble(uint, uint, uint, bool) -> void {}
|
||||
virtual auto dipSettings(const Markup::Node&) -> uint { return 0; }
|
||||
@ -64,7 +64,7 @@ struct Interface {
|
||||
auto loadRequest(uint id, string path, bool required) -> void { return bind->loadRequest(id, path, required); }
|
||||
auto saveRequest(uint id, string path) -> void { return bind->saveRequest(id, path); }
|
||||
auto videoRefresh(const uint32* data, uint pitch, uint width, uint height) -> void { return bind->videoRefresh(data, pitch, width, height); }
|
||||
auto audioSample(int16 lsample, int16 rsample) -> void { return bind->audioSample(lsample, rsample); }
|
||||
auto audioSample(const double* samples, uint channels) -> void { return bind->audioSample(samples, channels); }
|
||||
auto inputPoll(uint port, uint device, uint input) -> int16 { return bind->inputPoll(port, device, input); }
|
||||
auto inputRumble(uint port, uint device, uint input, bool enable) -> void { return bind->inputRumble(port, device, input, enable); }
|
||||
auto dipSettings(const Markup::Node& node) -> uint { return bind->dipSettings(node); }
|
||||
|
@ -57,7 +57,7 @@ auto APU::main() -> void {
|
||||
//output = filter.run_lopass(output);
|
||||
output = sclamp<16>(output);
|
||||
|
||||
stream->sample(output);
|
||||
stream->sample(output / 32768.0);
|
||||
|
||||
tick();
|
||||
}
|
||||
|
@ -97,8 +97,8 @@ auto Interface::videoColor(uint32 n) -> uint64 {
|
||||
v *= brightness / 12.0;
|
||||
|
||||
y += v;
|
||||
i += v * cos((3.141592653 / 6.0) * (p + hue));
|
||||
q += v * sin((3.141592653 / 6.0) * (p + hue));
|
||||
i += v * cos((Math::Pi / 6.0) * (p + hue));
|
||||
q += v * sin((Math::Pi / 6.0) * (p + hue));
|
||||
}
|
||||
|
||||
i *= saturation;
|
||||
|
@ -26,9 +26,10 @@ auto APU::main() -> void {
|
||||
hipass(sequencer.right, sequencer.rightBias);
|
||||
|
||||
if(!system.sgb()) {
|
||||
stream->sample(sequencer.left, sequencer.right);
|
||||
stream->sample(sequencer.left / 32768.0, sequencer.right / 32768.0);
|
||||
} else {
|
||||
interface->audioSample(sequencer.left, sequencer.right);
|
||||
double samples[] = {sequencer.left / 32768.0, sequencer.right / 32768.0};
|
||||
interface->audioSample(samples, 2);
|
||||
}
|
||||
|
||||
if(cycle == 0) { //512hz
|
||||
|
@ -63,7 +63,7 @@ auto APU::main() -> void {
|
||||
if(regs.bias.amplitude == 3) lsample &= ~15, rsample &= ~15;
|
||||
|
||||
if(cpu.regs.mode == CPU::Registers::Mode::Stop) lsample = 0, rsample = 0;
|
||||
stream->sample(sclamp<16>(lsample << 6), sclamp<16>(rsample << 6)); //should be <<5; use <<6 for added volume
|
||||
stream->sample(sclamp<16>(lsample << 6) / 32768.0, sclamp<16>(rsample << 6) / 32768.0); //should be <<5; use <<6 for added volume
|
||||
step(512);
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ auto ICD2::main() -> void {
|
||||
step(GameBoy::system._clocksExecuted);
|
||||
GameBoy::system._clocksExecuted = 0;
|
||||
} else { //DMG halted
|
||||
stream->sample(0, 0);
|
||||
stream->sample(0.0, 0.0);
|
||||
step(2); //two clocks per audio sample
|
||||
}
|
||||
synchronizeCPU();
|
||||
|
@ -119,8 +119,8 @@ auto ICD2::saveRequest(uint id, string name) -> void {
|
||||
auto ICD2::videoRefresh(const uint32* data, uint pitch, uint width, uint height) -> void {
|
||||
}
|
||||
|
||||
auto ICD2::audioSample(int16 left, int16 right) -> void {
|
||||
stream->sample(left, right);
|
||||
auto ICD2::audioSample(const double* samples, uint channels) -> void {
|
||||
stream->write(samples);
|
||||
}
|
||||
|
||||
auto ICD2::inputPoll(uint port, uint device, uint id) -> int16 {
|
||||
|
@ -7,7 +7,7 @@ auto loadRequest(uint id, string name, bool required) -> void override;
|
||||
auto saveRequest(uint id, string name) -> void override;
|
||||
|
||||
auto videoRefresh(const uint32* data, uint pitch, uint width, uint height) -> void override;
|
||||
auto audioSample(int16 lsample, int16 rsample) -> void override;
|
||||
auto audioSample(const double* samples, uint channels) -> void override;
|
||||
auto inputPoll(uint port, uint device, uint id) -> int16 override;
|
||||
|
||||
struct Packet {
|
||||
|
@ -38,7 +38,7 @@ auto MSU1::main() -> void {
|
||||
right = sclamp<16>(rchannel);
|
||||
if(dsp.mute()) left = 0, right = 0;
|
||||
|
||||
stream->sample(left, right);
|
||||
stream->sample(left / 32768.0, right / 32768.0);
|
||||
step(1);
|
||||
synchronizeCPU();
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ auto DSP::echo27() -> void {
|
||||
}
|
||||
|
||||
//output sample to DAC
|
||||
stream->sample(outl, outr);
|
||||
stream->sample(outl / 32768.0, outr / 32768.0);
|
||||
}
|
||||
|
||||
auto DSP::echo28() -> void {
|
||||
|
@ -89,7 +89,9 @@ auto Program::videoRefresh(const uint32* data, uint pitch, uint width, uint heig
|
||||
}
|
||||
}
|
||||
|
||||
auto Program::audioSample(int16 left, int16 right) -> void {
|
||||
auto Program::audioSample(const double* samples, uint channels) -> void {
|
||||
int16 left = sclamp<16>(samples[0] * 32768.0);
|
||||
int16 right = sclamp<16>(samples[1] * 32768.0);
|
||||
audio->sample(left, right);
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,7 @@ auto Program::loadMedium(Emulator::Interface& interface, Emulator::Interface::Me
|
||||
folderPaths.append(location);
|
||||
|
||||
//note: the order of operations in this block of code is critical
|
||||
Emulator::audio.reset();
|
||||
Emulator::audio.setFrequency(audio->get(Audio::Frequency).get<uint>());
|
||||
Emulator::audio.reset(2, audio->get(Audio::Frequency).get<uint>(44100));
|
||||
emulator = &interface;
|
||||
connectDevices();
|
||||
emulator->load(medium.id);
|
||||
|
@ -10,7 +10,7 @@ struct Program : Emulator::Interface::Bind {
|
||||
auto loadRequest(uint id, string path, bool required) -> void override;
|
||||
auto saveRequest(uint id, string path) -> void override;
|
||||
auto videoRefresh(const uint32* data, uint pitch, uint width, uint height) -> void override;
|
||||
auto audioSample(int16 lsample, int16 rsample) -> void override;
|
||||
auto audioSample(const double* samples, uint channels) -> void override;
|
||||
auto inputPoll(uint port, uint device, uint input) -> int16 override;
|
||||
auto inputRumble(uint port, uint device, uint input, bool enable) -> void override;
|
||||
auto dipSettings(const Markup::Node& node) -> uint override;
|
||||
|
@ -24,7 +24,7 @@ AudioSettings::AudioSettings(TabFrame* parent) : TabFrameItem(parent) {
|
||||
latencyCombo.onChange([&] { updateDriver(); });
|
||||
|
||||
frequencyLabel.setText("Frequency:");
|
||||
auto frequencyValue = audio->get(Audio::Frequency).get<uint>();
|
||||
auto frequencyValue = audio->get(Audio::Frequency).get<uint>(44100);
|
||||
frequencyCombo.append(ComboButtonItem().setText({frequencyValue, "hz"}));
|
||||
frequencyCombo.setEnabled(false);
|
||||
|
||||
@ -40,7 +40,7 @@ AudioSettings::AudioSettings(TabFrame* parent) : TabFrameItem(parent) {
|
||||
|
||||
volumeLabel.setText("Volume:");
|
||||
volumeValue.setAlignment(0.5);
|
||||
volumeSlider.setLength(201).setPosition(settings["Audio/Volume"].natural()).onChange([&] { updateEffects(); });
|
||||
volumeSlider.setLength(501).setPosition(settings["Audio/Volume"].natural()).onChange([&] { updateEffects(); });
|
||||
|
||||
balanceLabel.setText("Balance:");
|
||||
balanceValue.setAlignment(0.5);
|
||||
|
@ -56,7 +56,7 @@ auto APU::dacRun() -> void {
|
||||
right = 0;
|
||||
}
|
||||
|
||||
stream->sample(left, right);
|
||||
stream->sample(left / 32768.0, right / 32768.0);
|
||||
}
|
||||
|
||||
auto APU::step(uint clocks) -> void {
|
||||
|
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
//transposed direct form II biquadratic second-order IIR filter (butterworth)
|
||||
//transposed direct form II biquadratic second-order IIR filter
|
||||
|
||||
namespace nall { namespace DSP { namespace IIR {
|
||||
|
||||
@ -15,8 +15,10 @@ struct Biquad {
|
||||
HighShelf,
|
||||
};
|
||||
|
||||
inline auto reset(Type type, double cutoff, double quality = 0.707107, double gain = 0.0) -> void;
|
||||
inline auto process(double in) -> double; //normalized sample
|
||||
inline auto reset(Type type, double cutoff, double quality, double gain = 0.0) -> void;
|
||||
inline auto process(double in) -> double; //normalized sample (-1.0 to +1.0)
|
||||
|
||||
inline static auto butterworth(uint order, uint phase) -> double;
|
||||
|
||||
private:
|
||||
Type type; //filter type
|
||||
@ -37,7 +39,7 @@ auto Biquad::reset(Type type, double cutoff, double quality, double gain) -> voi
|
||||
z2 = 0.0;
|
||||
|
||||
double v = pow(10, fabs(gain) / 20.0);
|
||||
double k = tan(3.141592 * cutoff);
|
||||
double k = tan(Math::Pi * cutoff);
|
||||
double q = quality;
|
||||
double n = 0.0;
|
||||
|
||||
@ -143,4 +145,9 @@ auto Biquad::process(double in) -> double {
|
||||
return out;
|
||||
}
|
||||
|
||||
//compute Q values for N-order butterworth filtering
|
||||
auto Biquad::butterworth(uint order, uint phase) -> double {
|
||||
return -0.5 / cos(Math::Pi / 2.0 * (1.0 + (1.0 + (2.0 * phase + 1.0) / order)));
|
||||
}
|
||||
|
||||
}}}
|
||||
|
@ -19,7 +19,7 @@ struct Interpolation {
|
||||
}
|
||||
|
||||
static inline auto Cosine(double mu, double a, double b, double c, double d) -> double {
|
||||
mu = (1.0 - cos(mu * 3.14159265)) / 2.0;
|
||||
mu = (1.0 - cos(mu * Math::Pi)) / 2.0;
|
||||
return b * (1.0 - mu) + c * mu;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user