From fdc41611cf52dcc6651bab35cb4b65c346da7a34 Mon Sep 17 00:00:00 2001 From: Tim Allen Date: Wed, 1 Jun 2016 21:23:22 +1000 Subject: [PATCH] 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. --- higan/audio/audio.cpp | 75 +++++++++---------- higan/audio/audio.hpp | 37 +++++---- higan/audio/stream.cpp | 52 +++++-------- higan/emulator/emulator.hpp | 2 +- higan/emulator/interface.hpp | 4 +- higan/fc/apu/apu.cpp | 2 +- higan/fc/interface/interface.cpp | 4 +- higan/gb/apu/apu.cpp | 5 +- higan/gba/apu/apu.cpp | 2 +- higan/sfc/coprocessor/icd2/icd2.cpp | 2 +- .../coprocessor/icd2/interface/interface.cpp | 4 +- .../coprocessor/icd2/interface/interface.hpp | 2 +- higan/sfc/coprocessor/msu1/msu1.cpp | 2 +- higan/sfc/dsp/echo.cpp | 2 +- higan/target-tomoko/program/interface.cpp | 4 +- higan/target-tomoko/program/medium.cpp | 3 +- higan/target-tomoko/program/program.hpp | 2 +- higan/target-tomoko/settings/audio.cpp | 4 +- higan/ws/apu/apu.cpp | 2 +- nall/dsp/iir/biquad.hpp | 15 +++- nall/interpolation.hpp | 2 +- 21 files changed, 107 insertions(+), 120 deletions(-) diff --git a/higan/audio/audio.cpp b/higan/audio/audio.cpp index cc441d0b..9a7fcaa2 100644 --- a/higan/audio/audio.cpp +++ b/higan/audio/audio.cpp @@ -5,11 +5,15 @@ namespace Emulator { #include "stream.cpp" Audio audio; -auto Audio::reset() -> void { +auto Audio::reset(maybe channels_, maybe 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 { - shared_pointer stream = new Stream{channels, frequency}; - stream->setFrequency(this->frequency); + shared_pointer 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); } } diff --git a/higan/audio/audio.hpp b/higan/audio/audio.hpp index eb22049c..7b0e0007 100644 --- a/higan/audio/audio.hpp +++ b/higan/audio/audio.hpp @@ -10,55 +10,52 @@ struct Audio; struct Stream; struct Audio { - auto reset() -> void; + auto reset(maybe channels = nothing, maybe 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; - auto poll() -> void; - private: + auto process() -> void; + Interface* interface = nullptr; vector> streams; + + uint channels = 0; double frequency = 0.0; + double volume = 1.0; double balance = 0.0; bool reverbEnable = false; - vector>> reverb; + vector>> 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 auto sample(P&&... p) -> void { - int16 samples[sizeof...(P)] = {forward

(p)...}; + double samples[sizeof...(P)] = {forward

(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> iir; - vector resampler; + const uint order = 6; //Nth-order filter (must be an even number) + struct Channel { + vector iir; + DSP::Resampler::Cubic resampler; + }; + vector channels; friend class Audio; }; diff --git a/higan/audio/stream.cpp b/higan/audio/stream.cpp index 3363126f..dc39be22 100644 --- a/higan/audio/stream.cpp +++ b/higan/audio/stream.cpp @@ -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(); } diff --git a/higan/emulator/emulator.hpp b/higan/emulator/emulator.hpp index be02c33e..15761d2b 100644 --- a/higan/emulator/emulator.hpp +++ b/higan/emulator/emulator.hpp @@ -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/"; diff --git a/higan/emulator/interface.hpp b/higan/emulator/interface.hpp index 8ae94725..a3c9faac 100644 --- a/higan/emulator/interface.hpp +++ b/higan/emulator/interface.hpp @@ -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); } diff --git a/higan/fc/apu/apu.cpp b/higan/fc/apu/apu.cpp index 71a99408..c8091b2d 100644 --- a/higan/fc/apu/apu.cpp +++ b/higan/fc/apu/apu.cpp @@ -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(); } diff --git a/higan/fc/interface/interface.cpp b/higan/fc/interface/interface.cpp index b77615cf..345ee3ac 100644 --- a/higan/fc/interface/interface.cpp +++ b/higan/fc/interface/interface.cpp @@ -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; diff --git a/higan/gb/apu/apu.cpp b/higan/gb/apu/apu.cpp index 3aae8ca4..f7491ecc 100644 --- a/higan/gb/apu/apu.cpp +++ b/higan/gb/apu/apu.cpp @@ -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 diff --git a/higan/gba/apu/apu.cpp b/higan/gba/apu/apu.cpp index 247d34d1..3daecacd 100644 --- a/higan/gba/apu/apu.cpp +++ b/higan/gba/apu/apu.cpp @@ -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); } diff --git a/higan/sfc/coprocessor/icd2/icd2.cpp b/higan/sfc/coprocessor/icd2/icd2.cpp index e1b2a006..56334fef 100644 --- a/higan/sfc/coprocessor/icd2/icd2.cpp +++ b/higan/sfc/coprocessor/icd2/icd2.cpp @@ -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(); diff --git a/higan/sfc/coprocessor/icd2/interface/interface.cpp b/higan/sfc/coprocessor/icd2/interface/interface.cpp index 6440abef..3d901e49 100644 --- a/higan/sfc/coprocessor/icd2/interface/interface.cpp +++ b/higan/sfc/coprocessor/icd2/interface/interface.cpp @@ -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 { diff --git a/higan/sfc/coprocessor/icd2/interface/interface.hpp b/higan/sfc/coprocessor/icd2/interface/interface.hpp index f6dcde6c..b9064f9a 100644 --- a/higan/sfc/coprocessor/icd2/interface/interface.hpp +++ b/higan/sfc/coprocessor/icd2/interface/interface.hpp @@ -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 { diff --git a/higan/sfc/coprocessor/msu1/msu1.cpp b/higan/sfc/coprocessor/msu1/msu1.cpp index 0f2442b0..6d8b4622 100644 --- a/higan/sfc/coprocessor/msu1/msu1.cpp +++ b/higan/sfc/coprocessor/msu1/msu1.cpp @@ -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(); } diff --git a/higan/sfc/dsp/echo.cpp b/higan/sfc/dsp/echo.cpp index b8f3e751..301e4ae0 100644 --- a/higan/sfc/dsp/echo.cpp +++ b/higan/sfc/dsp/echo.cpp @@ -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 { diff --git a/higan/target-tomoko/program/interface.cpp b/higan/target-tomoko/program/interface.cpp index 8a3808e5..ae26f7f6 100644 --- a/higan/target-tomoko/program/interface.cpp +++ b/higan/target-tomoko/program/interface.cpp @@ -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); } diff --git a/higan/target-tomoko/program/medium.cpp b/higan/target-tomoko/program/medium.cpp index 07a2ce62..511bb2de 100644 --- a/higan/target-tomoko/program/medium.cpp +++ b/higan/target-tomoko/program/medium.cpp @@ -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()); + Emulator::audio.reset(2, audio->get(Audio::Frequency).get(44100)); emulator = &interface; connectDevices(); emulator->load(medium.id); diff --git a/higan/target-tomoko/program/program.hpp b/higan/target-tomoko/program/program.hpp index b49140dc..2c80d1da 100644 --- a/higan/target-tomoko/program/program.hpp +++ b/higan/target-tomoko/program/program.hpp @@ -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; diff --git a/higan/target-tomoko/settings/audio.cpp b/higan/target-tomoko/settings/audio.cpp index 5c59207f..f43faaa7 100644 --- a/higan/target-tomoko/settings/audio.cpp +++ b/higan/target-tomoko/settings/audio.cpp @@ -24,7 +24,7 @@ AudioSettings::AudioSettings(TabFrame* parent) : TabFrameItem(parent) { latencyCombo.onChange([&] { updateDriver(); }); frequencyLabel.setText("Frequency:"); - auto frequencyValue = audio->get(Audio::Frequency).get(); + auto frequencyValue = audio->get(Audio::Frequency).get(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); diff --git a/higan/ws/apu/apu.cpp b/higan/ws/apu/apu.cpp index fd106a1a..317ae454 100644 --- a/higan/ws/apu/apu.cpp +++ b/higan/ws/apu/apu.cpp @@ -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 { diff --git a/nall/dsp/iir/biquad.hpp b/nall/dsp/iir/biquad.hpp index f20cedfa..b73dcb57 100644 --- a/nall/dsp/iir/biquad.hpp +++ b/nall/dsp/iir/biquad.hpp @@ -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))); +} + }}} diff --git a/nall/interpolation.hpp b/nall/interpolation.hpp index 587a9b2b..007f2ec2 100644 --- a/nall/interpolation.hpp +++ b/nall/interpolation.hpp @@ -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; }