From 39adcd740d51826c165bcf83d257c65d2ecde912 Mon Sep 17 00:00:00 2001 From: Igor Maznitsa Date: Sun, 8 Dec 2024 13:05:42 +0200 Subject: [PATCH] added Low Pass Filter for Sound output --- changelog.txt | 4 + .../zxpoly/components/Motherboard.java | 9 +- .../zxpoly/components/sound/Beeper.java | 58 ++--- .../components/sound/MixerUtilsABC.java | 96 +++++---- .../components/sound/MixerUtilsACB.java | 24 ++- .../sound/SoundChannelLowPassFilter.java | 47 ++++ .../sound/SoundChannelValueFilter.java | 16 -- .../igormaznitsa/zxpoly/ui/OptionsPanel.java | 203 ++++++++++++------ .../igormaznitsa/zxpoly/utils/AppOptions.java | 38 ++++ .../zxpspritecorrector/AboutDialog.java | 2 +- 10 files changed, 329 insertions(+), 168 deletions(-) create mode 100644 zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelLowPassFilter.java delete mode 100644 zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelValueFilter.java diff --git a/changelog.txt b/changelog.txt index 0767e927..2d1da45d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +__2.3.5 (SNAPSHOT)__ + +- added Low Pass Filter for sound output + __2.3.4 (07-dec-2024)__ - the minimum required JDK version has been raised to 21 diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/Motherboard.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/Motherboard.java index 7f0716e0..3ca978a7 100644 --- a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/Motherboard.java +++ b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/Motherboard.java @@ -39,6 +39,7 @@ import com.igormaznitsa.zxpoly.components.video.VideoController; import com.igormaznitsa.zxpoly.components.video.VirtualKeyboardDecoration; import com.igormaznitsa.zxpoly.components.video.timings.TimingProfile; +import com.igormaznitsa.zxpoly.utils.AppOptions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -125,7 +126,13 @@ public Motherboard( this.memoryTimings = timingProfile.makeUlaFrame(); this.boardMode = boardMode; - this.beeper = new Beeper(timingProfile, useAcbSoundScheme, enableCovoxFb, useTurboSound, + + final float lowPassFilter = AppOptions.getInstance().isLpfActive() ? AppOptions.getInstance() + .getLpfValue() / 100.0f : -1.0f; + LOGGER.info("Low Pass Sound Filter is " + (lowPassFilter < 0 ? "OFF" : lowPassFilter)); + + this.beeper = + new Beeper(timingProfile, lowPassFilter, useAcbSoundScheme, enableCovoxFb, useTurboSound, tryConsumeLessSystemResources); if (rom.isTrdosPresented()) { LOGGER.info("TR-DOS presented in ROM, creating BetaDiskInterface"); diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/Beeper.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/Beeper.java index 452b5b15..ee0f32fd 100644 --- a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/Beeper.java +++ b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/Beeper.java @@ -52,7 +52,7 @@ public final class Beeper { private static final IWavWriter NULL_WAV = new IWavWriter() { @Override - public void updateState(boolean tstatesInt, boolean wallclockInt, int spentTstates, + public void updateState(boolean tiStatesInt, boolean wallClockInt, int spentTiStates, int levelLeft, int levelRight) { } @@ -65,7 +65,7 @@ public void dispose() { private static final IBeeper NULL_BEEPER = new IBeeper() { @Override - public void updateState(boolean tstatesInt, boolean wallclockInt, int spentTstates, + public void updateState(boolean tiStatesInt, boolean wallClockInt, int spentTiStates, int levelLeft, int levelRight) { } @@ -89,9 +89,7 @@ public void start() { }; private final AtomicReference activeInternalBeeper = new AtomicReference<>(NULL_BEEPER); - private final SoundChannelValueFilter[] soundChannelFilters = - IntStream.range(0, 8).mapToObj(i -> new SoundChannelValueFilter()) - .toArray(SoundChannelValueFilter[]::new); + private final SoundChannelLowPassFilter[] soundChannelLowPassFilters; private final int[] channels = new int[8]; private final MixerFunction mixerLeft; private final MixerFunction mixerRight; @@ -100,10 +98,14 @@ public void start() { public Beeper( final TimingProfile timingProfile, + final float lowPassFilterValue, final boolean useAcbSoundScheme, final boolean covoxPresented, final boolean turboSoundPresented, final boolean tryConsumeLessSystemResources) { + this.soundChannelLowPassFilters = + IntStream.range(0, 8).mapToObj(i -> new SoundChannelLowPassFilter(i, lowPassFilterValue)) + .toArray(SoundChannelLowPassFilter[]::new); this.tryConsumeLessSystemResources = tryConsumeLessSystemResources; this.timingProfile = timingProfile; if (useAcbSoundScheme) { @@ -195,25 +197,25 @@ public boolean isNullBeeper() { return this.activeInternalBeeper.get() == NULL_BEEPER; } - public void updateState(final boolean tstatesInt, final boolean wallclockInt, - final int spentTstates) { + public void updateState(final boolean tiStatesInt, final boolean wallClockInt, + final int spentTiStates) { final int leftChannel = - this.mixerLeft.mix(this.channels, this.soundChannelFilters, spentTstates); + this.mixerLeft.mix(this.channels, this.soundChannelLowPassFilters, spentTiStates); final int rightChannel = - this.mixerRight.mix(this.channels, this.soundChannelFilters, spentTstates); + this.mixerRight.mix(this.channels, this.soundChannelLowPassFilters, spentTiStates); this.activeInternalBeeper.get() - .updateState(tstatesInt, - wallclockInt, - spentTstates, + .updateState(tiStatesInt, + wallClockInt, + spentTiStates, leftChannel, rightChannel ); this.activeWavWriter.get() - .updateState(tstatesInt, - wallclockInt, - spentTstates, + .updateState(tiStatesInt, + wallClockInt, + spentTiStates, leftChannel, rightChannel ); @@ -226,7 +228,7 @@ public void reset() { public void clearChannels() { Arrays.fill(this.channels, 0); - for (final SoundChannelValueFilter f : this.soundChannelFilters) { + for (final SoundChannelLowPassFilter f : this.soundChannelLowPassFilters) { f.reset(); } } @@ -246,11 +248,11 @@ public AudioFormat getAudioFormat() { @FunctionalInterface private interface MixerFunction { - int mix(int[] values, SoundChannelValueFilter[] filters, int spentTstates); + int mix(int[] values, SoundChannelLowPassFilter[] filters, int spentTiStates); } private interface IWavWriter { - void updateState(boolean tstatesInt, boolean wallclockInt, int spentTstates, int levelLeft, + void updateState(boolean tiStatesInt, boolean wallClockInt, int spentTiStates, int levelLeft, int levelRight); void dispose(); @@ -262,7 +264,7 @@ private interface IBeeper { void start(); - void updateState(boolean tstatesInt, boolean wallclockInt, int spentTstates, int levelLeft, + void updateState(boolean tiStatesInt, boolean wallClockInt, int spentTiStates, int levelLeft, int levelRight); void dispose(); @@ -310,13 +312,13 @@ private WavWriterImpl(final TimingProfile timingProfile, final File wavFile) @Override public void updateState( - final boolean tstatesInt, - final boolean wallclockInt, - final int spentTstates, + final boolean tiStatesInt, + final boolean wallClockInt, + final int spentTiStates, final int levelLeft, final int levelRight ) { - final double frameOffset = spentTstates * this.framesPerTick; + final double frameOffset = spentTiStates * this.framesPerTick; long currentFrame = (long) this.frameCounter; this.frameCounter += frameOffset; @@ -399,18 +401,18 @@ public void start() { @Override public void updateState( - boolean tstatesIntReached, - boolean wallclockInt, - int spentTstates, + boolean tiStatesInt, + boolean wallClockInt, + int spentTiStates, final int levelLeft, final int levelRight ) { if (this.working) { - if (wallclockInt) { + if (wallClockInt) { this.soundDataQueue.offer(sndBuffer.nextBuffer(levelLeft, levelRight)); sndBuffer.resetPosition(); } else { - sndBuffer.setValue(spentTstates, levelLeft, levelRight); + sndBuffer.setValue(spentTiStates, levelLeft, levelRight); } } } diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsABC.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsABC.java index dd7daac9..dc1f6eec 100644 --- a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsABC.java +++ b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsABC.java @@ -17,86 +17,94 @@ private MixerUtilsABC() { super(); } - public static int mixLeft_TS_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) - + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]) - + filters[CHANNEL_TS_B].update(spentTstates, values[CHANNEL_TS_B]); + public static int mixLeft_TS_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_COVOX].update(spentTiStates, values[CHANNEL_COVOX]) + + filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]) + + filters[CHANNEL_TS_B].update(spentTiStates, values[CHANNEL_TS_B]); - final int left = filters[CHANNEL_TS_A].update(spentTstates, values[CHANNEL_TS_A]) - + filters[CHANNEL_AY_A].update(spentTstates, values[CHANNEL_AY_A]); + final int left = filters[CHANNEL_TS_A].update(spentTiStates, values[CHANNEL_TS_A]) + + filters[CHANNEL_AY_A].update(spentTiStates, values[CHANNEL_AY_A]); return scaleLeft6(left, middle); } - public static int mixRight_TS_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) - + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]) - + filters[CHANNEL_TS_B].update(spentTstates, values[CHANNEL_TS_B]); + public static int mixRight_TS_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_COVOX].update(spentTiStates, values[CHANNEL_COVOX]) + + filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]) + + filters[CHANNEL_TS_B].update(spentTiStates, values[CHANNEL_TS_B]); - final int right = filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]) - + filters[CHANNEL_TS_C].update(spentTstates, values[CHANNEL_TS_C]); + final int right = filters[CHANNEL_AY_C].update(spentTiStates, values[CHANNEL_AY_C]) + + filters[CHANNEL_TS_C].update(spentTiStates, values[CHANNEL_TS_C]); return scaleRight6(right, middle); } - public static int mixLeft_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) - + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]); + public static int mixLeft_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_COVOX].update(spentTiStates, values[CHANNEL_COVOX]) + + filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]); - final int left = filters[CHANNEL_AY_A].update(spentTstates, values[CHANNEL_AY_A]); + final int left = filters[CHANNEL_AY_A].update(spentTiStates, values[CHANNEL_AY_A]); return scaleLeft4(left, middle); } - public static int mixRight_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) - + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]); + public static int mixRight_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_COVOX].update(spentTiStates, values[CHANNEL_COVOX]) + + filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]); - final int right = filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]); + final int right = filters[CHANNEL_AY_C].update(spentTiStates, values[CHANNEL_AY_C]); return scaleRight4(right, middle); } - public static int mixLeft_TS(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]) - + filters[CHANNEL_TS_B].update(spentTstates, values[CHANNEL_TS_B]); + public static int mixLeft_TS(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]) + + filters[CHANNEL_TS_B].update(spentTiStates, values[CHANNEL_TS_B]); - final int left = filters[CHANNEL_TS_A].update(spentTstates, values[CHANNEL_TS_A]) - + filters[CHANNEL_AY_A].update(spentTstates, values[CHANNEL_AY_A]); + final int left = filters[CHANNEL_TS_A].update(spentTiStates, values[CHANNEL_TS_A]) + + filters[CHANNEL_AY_A].update(spentTiStates, values[CHANNEL_AY_A]); return scaleLeft5(left, middle); } - public static int mixRight_TS(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]) - + filters[CHANNEL_TS_B].update(spentTstates, values[CHANNEL_TS_B]); + public static int mixRight_TS(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]) + + filters[CHANNEL_TS_B].update(spentTiStates, values[CHANNEL_TS_B]); - final int right = filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]) - + filters[CHANNEL_TS_C].update(spentTstates, values[CHANNEL_TS_C]); + final int right = filters[CHANNEL_AY_C].update(spentTiStates, values[CHANNEL_AY_C]) + + filters[CHANNEL_TS_C].update(spentTiStates, values[CHANNEL_TS_C]); return scaleRight5(right, middle); } - public static int mixLeft(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]); + public static int mixLeft(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]); - final int left = filters[CHANNEL_AY_A].update(spentTstates, values[CHANNEL_AY_A]); + final int left = filters[CHANNEL_AY_A].update(spentTiStates, values[CHANNEL_AY_A]); return scaleLeft3(left, middle); } - public static int mixRight(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { - final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) - + filters[CHANNEL_AY_B].update(spentTstates, values[CHANNEL_AY_B]); + public static int mixRight(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTiStates) { + final int middle = filters[CHANNEL_BEEPER].update(spentTiStates, values[CHANNEL_BEEPER]) + + filters[CHANNEL_AY_B].update(spentTiStates, values[CHANNEL_AY_B]); - final int right = filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]); + final int right = filters[CHANNEL_AY_C].update(spentTiStates, values[CHANNEL_AY_C]); return scaleRight3(right, middle); } diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsACB.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsACB.java index bdd6528f..3f92a911 100644 --- a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsACB.java +++ b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/MixerUtilsACB.java @@ -17,7 +17,8 @@ private MixerUtilsACB() { super(); } - public static int mixLeft_TS_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixLeft_TS_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]) @@ -29,7 +30,8 @@ public static int mixLeft_TS_CVX(final int[] values, final SoundChannelValueFilt return scaleLeft6(left, middle); } - public static int mixRight_TS_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixRight_TS_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]) @@ -41,7 +43,8 @@ public static int mixRight_TS_CVX(final int[] values, final SoundChannelValueFil return scaleRight6(right, middle); } - public static int mixLeft_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixLeft_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]); @@ -51,7 +54,8 @@ public static int mixLeft_CVX(final int[] values, final SoundChannelValueFilter[ return scaleLeft4(left, middle); } - public static int mixRight_CVX(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixRight_CVX(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_COVOX].update(spentTstates, values[CHANNEL_COVOX]) + filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]); @@ -61,7 +65,8 @@ public static int mixRight_CVX(final int[] values, final SoundChannelValueFilter return scaleRight4(right, middle); } - public static int mixLeft_TS(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixLeft_TS(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]) + filters[CHANNEL_TS_C].update(spentTstates, values[CHANNEL_TS_C]); @@ -72,7 +77,8 @@ public static int mixLeft_TS(final int[] values, final SoundChannelValueFilter[] return scaleLeft5(left, middle); } - public static int mixRight_TS(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixRight_TS(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]) + filters[CHANNEL_TS_C].update(spentTstates, values[CHANNEL_TS_C]); @@ -83,7 +89,8 @@ public static int mixRight_TS(final int[] values, final SoundChannelValueFilter[ return scaleRight5(right, middle); } - public static int mixLeft(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixLeft(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]); final int left = filters[CHANNEL_AY_A].update(spentTstates, values[CHANNEL_AY_A]); @@ -91,7 +98,8 @@ public static int mixLeft(final int[] values, final SoundChannelValueFilter[] fi return scaleLeft3(left, middle); } - public static int mixRight(final int[] values, final SoundChannelValueFilter[] filters, final int spentTstates) { + public static int mixRight(final int[] values, final SoundChannelLowPassFilter[] filters, + final int spentTstates) { final int middle = filters[CHANNEL_BEEPER].update(spentTstates, values[CHANNEL_BEEPER]) + filters[CHANNEL_AY_C].update(spentTstates, values[CHANNEL_AY_C]); diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelLowPassFilter.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelLowPassFilter.java new file mode 100644 index 00000000..21245966 --- /dev/null +++ b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelLowPassFilter.java @@ -0,0 +1,47 @@ +package com.igormaznitsa.zxpoly.components.sound; + +public final class SoundChannelLowPassFilter { + + private final float alpha1; + private final float alpha2; + private final int channelIndex; + private final boolean active; + private float previousFilteredValue; + + public SoundChannelLowPassFilter(final int channelIndex, final float level) { + this.channelIndex = channelIndex; + if (level < 0.0f) { + this.active = false; + this.alpha1 = 0.0f; + this.alpha2 = 0.0f; + } else { + this.active = true; + this.alpha1 = level; + this.alpha2 = 1.0f - level; + } + this.reset(); + } + + public boolean isActive() { + return this.active; + } + + public int getChannelIndex() { + return this.channelIndex; + } + + public void reset() { + this.previousFilteredValue = 0.0f; + } + + public int update(final int spentTiStates, final int nextLevel) { + if (this.active) { + float filteredValue = alpha1 * (float) nextLevel + this.alpha2 * previousFilteredValue; + this.previousFilteredValue = filteredValue; + return Math.max(0, Math.min(255, Math.round(filteredValue))); + } else { + return nextLevel; + } + } + +} diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelValueFilter.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelValueFilter.java deleted file mode 100644 index 1b61845b..00000000 --- a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/components/sound/SoundChannelValueFilter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.igormaznitsa.zxpoly.components.sound; - -final class SoundChannelValueFilter { - - SoundChannelValueFilter() { - this.reset(); - } - - void reset() { - } - - int update(final int spentTstates, final int nextLevel) { - return nextLevel; - } - -} diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/ui/OptionsPanel.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/ui/OptionsPanel.java index 4f67eb55..fa96253b 100644 --- a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/ui/OptionsPanel.java +++ b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/ui/OptionsPanel.java @@ -27,6 +27,8 @@ import com.igormaznitsa.zxpoly.components.video.timings.TimingProfile; import com.igormaznitsa.zxpoly.utils.AppOptions; import com.igormaznitsa.zxpoly.utils.RomSource; +import java.awt.BorderLayout; +import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; @@ -43,9 +45,12 @@ import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.JTabbedPane; import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; public class OptionsPanel extends JTabbedPane { @@ -75,6 +80,8 @@ public class OptionsPanel extends JTabbedPane { private JLabel labelTryLessResources; private JLabel labelBorderWidth; private JLabel labelEmulateFFport; + private JCheckBox checkboxActivateLowPassFilter; + private JSlider sliderLowPassFilterValue; private JCheckBox checkGrabSound; private JCheckBox checkTryLessResources; private JCheckBox checkInterlacedScan; @@ -156,6 +163,15 @@ public OptionsPanel(final DataContainer dataContainer) { } private void fillByDataContainer(final DataContainer data) { + this.sliderLowPassFilterValue.setValue(data.lpfValue); + if (data.lpfActive) { + this.sliderLowPassFilterValue.setEnabled(true); + this.checkboxActivateLowPassFilter.setSelected(true); + } else { + this.sliderLowPassFilterValue.setEnabled(false); + this.checkboxActivateLowPassFilter.setSelected(false); + } + this.comboTimingProfile.setSelectedItem(data.timingProfile); this.checkEmulateFFport.setSelected(data.emulateFFport); this.checkInterlacedScan.setSelected(data.interlacedScan); @@ -249,6 +265,12 @@ private void initComponents() { textCustomRomPath.setToolTipText("Provided file path overrides selected ROM, if empty then inactive"); labelEmulateFFport = new JLabel(); checkEmulateFFport = new JCheckBox(); + checkboxActivateLowPassFilter = new JCheckBox(); + sliderLowPassFilterValue = new JSlider(0, 100); + sliderLowPassFilterValue.setMajorTickSpacing(10); + sliderLowPassFilterValue.setPaintLabels(true); + sliderLowPassFilterValue.setPaintTicks(true); + sliderLowPassFilterValue.setPaintTrack(true); keySelectorKempstonDown = new KeyCodeChooser(); keySelectorKempstonLeft = new KeyCodeChooser(); @@ -262,8 +284,13 @@ private void initComponents() { keySelectorProtekJoystickFire = new KeyCodeChooser(); keySelectorProtekJoystickUp = new KeyCodeChooser(); + checkboxActivateLowPassFilter.addChangeListener(e -> { + this.sliderLowPassFilterValue.setEnabled(checkboxActivateLowPassFilter.isSelected()); + }); + final JPanel panelGeneral = new JPanel(); final JPanel panelStreaming = new JPanel(); + final JPanel panelSound = new JPanel(); final JPanel panelScreen = new JPanel(); panelScreen.setLayout(new GridBagLayout()); @@ -376,6 +403,82 @@ private void initComponents() { gridBagConstraints.anchor = GridBagConstraints.WEST; panelScreen.add(checkUlaPlus, gridBagConstraints); + panelSound.setLayout(new GridBagLayout()); + + labelTurboSound.setHorizontalAlignment(RIGHT); + labelTurboSound.setText("TurboSound (NedoPC):"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + panelSound.add(labelTurboSound, gridBagConstraints); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.WEST; + panelSound.add(checkTurboSound, gridBagConstraints); + + labelCovoxFb.setHorizontalAlignment(RIGHT); + labelCovoxFb.setText("Covox (#FB):"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + panelSound.add(labelCovoxFb, gridBagConstraints); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.WEST; + panelSound.add(checkCovoxFb, gridBagConstraints); + + labelSoundSchemeACB.setHorizontalAlignment(RIGHT); + labelSoundSchemeACB.setText("Sound channels ACB:"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + panelSound.add(labelSoundSchemeACB, gridBagConstraints); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.WEST; + panelSound.add(checkSoundSchemeACB, gridBagConstraints); + + labelVolumeProfile.setHorizontalAlignment(RIGHT); + labelVolumeProfile.setText("Volume profile:"); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + panelSound.add(labelVolumeProfile, gridBagConstraints); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.WEST; + panelSound.add(comboVolumeProfile, gridBagConstraints); + + final JPanel panelLowPassFilter = new JPanel(new BorderLayout()); + panelLowPassFilter.setBorder(new TitledBorder("Low Pass Filter")); + + panelLowPassFilter.add(sliderLowPassFilterValue, BorderLayout.CENTER); + + checkboxActivateLowPassFilter.setText("Active"); + final JPanel flow = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + flow.add(checkboxActivateLowPassFilter); + panelLowPassFilter.add(flow, BorderLayout.SOUTH); + + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + + panelSound.add(panelLowPassFilter, gridBagConstraints); + panelStreaming.setLayout(new GridBagLayout()); labelFfMpegPath.setHorizontalAlignment(RIGHT); @@ -488,72 +591,16 @@ private void initComponents() { textCustomRomPath.setColumns(24); panelGeneral.add(textCustomRomPath, gridBagConstraints); - labelTurboSound.setHorizontalAlignment(RIGHT); - labelTurboSound.setText("TurboSound (NedoPC):"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 2; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - panelGeneral.add(labelTurboSound, gridBagConstraints); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 2; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - gridBagConstraints.anchor = GridBagConstraints.WEST; - panelGeneral.add(checkTurboSound, gridBagConstraints); - - labelCovoxFb.setHorizontalAlignment(RIGHT); - labelCovoxFb.setText("Covox (#FB):"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 3; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - panelGeneral.add(labelCovoxFb, gridBagConstraints); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 3; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - gridBagConstraints.anchor = GridBagConstraints.WEST; - panelGeneral.add(checkCovoxFb, gridBagConstraints); - - labelSoundSchemeACB.setHorizontalAlignment(RIGHT); - labelSoundSchemeACB.setText("Sound channels ACB:"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 4; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - panelGeneral.add(labelSoundSchemeACB, gridBagConstraints); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 4; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - gridBagConstraints.anchor = GridBagConstraints.WEST; - panelGeneral.add(checkSoundSchemeACB, gridBagConstraints); - - labelVolumeProfile.setHorizontalAlignment(RIGHT); - labelVolumeProfile.setText("Volume profile:"); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 5; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - panelGeneral.add(labelVolumeProfile, gridBagConstraints); - gridBagConstraints = new GridBagConstraints(); - gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 5; - gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; - gridBagConstraints.anchor = GridBagConstraints.WEST; - panelGeneral.add(comboVolumeProfile, gridBagConstraints); - labelZx128ByDefault.setHorizontalAlignment(RIGHT); labelZx128ByDefault.setText("Default ZX Mode:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 6; + gridBagConstraints.gridy = 2; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; panelGeneral.add(labelZx128ByDefault, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 6; + gridBagConstraints.gridy = 2; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.WEST; panelGeneral.add(checkZx128ByDefault, gridBagConstraints); @@ -562,12 +609,12 @@ private void initComponents() { labelKempstonMouseAllowed.setText("Kempston mouse allowed:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 7; + gridBagConstraints.gridy = 3; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; panelGeneral.add(labelKempstonMouseAllowed, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 7; + gridBagConstraints.gridy = 3; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.WEST; panelGeneral.add(checkKempstonMouseAllowed, gridBagConstraints); @@ -576,12 +623,12 @@ private void initComponents() { labelVirtualKbdApart.setText("Virtual keyboard apart:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 8; + gridBagConstraints.gridy = 4; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; panelGeneral.add(labelVirtualKbdApart, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 8; + gridBagConstraints.gridy = 4; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.WEST; panelGeneral.add(checkVkbdApart, gridBagConstraints); @@ -590,12 +637,12 @@ private void initComponents() { labelVirtualKbdLook.setText("Keyboard decoration:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 9; + gridBagConstraints.gridy = 5; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; panelGeneral.add(labelVirtualKbdLook, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 9; + gridBagConstraints.gridy = 5; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.WEST; panelGeneral.add(comboKeyboardLook, gridBagConstraints); @@ -604,12 +651,12 @@ private void initComponents() { labelMacroCursorKeys.setText("Auto-CS for cursor keys:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 10; + gridBagConstraints.gridy = 6; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; panelGeneral.add(labelMacroCursorKeys, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 10; + gridBagConstraints.gridy = 6; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.WEST; panelGeneral.add(checkAutoiCsForCursorKeys, gridBagConstraints); @@ -618,12 +665,12 @@ private void initComponents() { labelTimingProfile.setText("Timing:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 11; + gridBagConstraints.gridy = 7; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; panelGeneral.add(labelTimingProfile, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 11; + gridBagConstraints.gridy = 7; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.WEST; panelGeneral.add(comboTimingProfile, gridBagConstraints); @@ -632,12 +679,12 @@ private void initComponents() { labelTryLessResources.setText("Try use less resources:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 12; + gridBagConstraints.gridy = 8; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; panelGeneral.add(labelTryLessResources, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; - gridBagConstraints.gridy = 12; + gridBagConstraints.gridy = 8; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.WEST; panelGeneral.add(checkTryLessResources, gridBagConstraints); @@ -740,8 +787,15 @@ private void initComponents() { joysticksPanel.add(panelKempston); joysticksPanel.add(panelCursor); + panelGeneral.setBorder(new EmptyBorder(8, 8, 8, 8)); + panelScreen.setBorder(new EmptyBorder(8, 8, 8, 8)); + panelSound.setBorder(new EmptyBorder(8, 8, 8, 8)); + joysticksPanel.setBorder(new EmptyBorder(8, 8, 8, 8)); + panelStreaming.setBorder(new EmptyBorder(8, 8, 8, 8)); + this.addTab("General", panelGeneral); this.addTab("Screen", panelScreen); + this.addTab("Sound", panelSound); this.addTab("Joystick", joysticksPanel); this.addTab("Streaming", panelStreaming); } @@ -776,6 +830,8 @@ public static final class DataContainer { public final boolean syncPaint; public final boolean oldTvFilter; public final boolean emulateFFport; + public final boolean lpfActive; + public final int lpfValue; public final int kempstonKeyUp; public final int kempstonKeyDown; @@ -793,6 +849,8 @@ public static final class DataContainer { public DataContainer() { final String customRomPath = AppOptions.getInstance().getCustomRomPath(); + this.lpfActive = AppOptions.getInstance().isLpfActive(); + this.lpfValue = AppOptions.getInstance().getLpfValue(); this.borderWidth = AppOptions.getInstance().getBorderWidth(); this.syncPaint = AppOptions.getInstance().isSyncPaint(); this.emulateFFport = AppOptions.getInstance().isAttributePortFf(); @@ -844,6 +902,9 @@ public DataContainer(final OptionsPanel optionsPanel) { this.tryLessResources = optionsPanel.checkTryLessResources.isSelected(); this.oldTvFilter = optionsPanel.checkOldTvFilter.isSelected(); + this.lpfValue = optionsPanel.sliderLowPassFilterValue.getValue(); + this.lpfActive = optionsPanel.checkboxActivateLowPassFilter.isSelected(); + this.syncPaint = optionsPanel.checkSyncPaint.isSelected(); this.emulateFFport = optionsPanel.checkEmulateFFport.isSelected(); @@ -878,6 +939,8 @@ public DataContainer(final OptionsPanel optionsPanel) { } public void store() { + AppOptions.getInstance().setLpfActive(this.lpfActive); + AppOptions.getInstance().setLpfValue(this.lpfValue); AppOptions.getInstance().setSyncPaint(this.syncPaint); AppOptions.getInstance().setTimingProfile(this.timingProfile); AppOptions.getInstance().setBorderWidth(this.borderWidth); diff --git a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/utils/AppOptions.java b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/utils/AppOptions.java index 78b5b129..0e3fb984 100644 --- a/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/utils/AppOptions.java +++ b/zxpoly-emul/src/main/java/com/igormaznitsa/zxpoly/utils/AppOptions.java @@ -100,6 +100,42 @@ public void setActiveRom(final String romPath) { } } + public boolean isLpfActive() { + this.locker.lock(); + try { + return preferences.getBoolean(Option.LPF_ACTIVE.name(), false); + } finally { + this.locker.unlock(); + } + } + + public void setLpfActive(final boolean value) { + this.locker.lock(); + try { + preferences.putBoolean(Option.LPF_ACTIVE.name(), value); + } finally { + this.locker.unlock(); + } + } + + public int getLpfValue() { + this.locker.lock(); + try { + return Math.min(100, Math.max(0, preferences.getInt(Option.LPF_VALUE.name(), 20))); + } finally { + this.locker.unlock(); + } + } + + public void setLpfValue(final int value) { + this.locker.lock(); + try { + preferences.putInt(Option.LPF_VALUE.name(), value); + } finally { + this.locker.unlock(); + } + } + public boolean isTestRomActive() { this.locker.lock(); try { @@ -934,6 +970,8 @@ public File getRomCacheFolder() { } public enum Option { + LPF_ACTIVE, + LPF_VALUE, BORDER_WIDTH, TIMING_PROFILE, SYNC_PAINT, diff --git a/zxpoly-sprite-corrector/src/main/java/com/igormaznitsa/zxpspritecorrector/AboutDialog.java b/zxpoly-sprite-corrector/src/main/java/com/igormaznitsa/zxpspritecorrector/AboutDialog.java index 0c1aed22..1b8b1374 100755 --- a/zxpoly-sprite-corrector/src/main/java/com/igormaznitsa/zxpspritecorrector/AboutDialog.java +++ b/zxpoly-sprite-corrector/src/main/java/com/igormaznitsa/zxpspritecorrector/AboutDialog.java @@ -31,7 +31,7 @@ public AboutDialog(final JFrame parent) { super(parent, true); initComponents(); - this.infoText.setText(this.infoText.getText().replace("${project.version}", "2.3.1")); + this.infoText.setText(this.infoText.getText().replace("${project.version}", "2.3.5")); this.setLocationRelativeTo(null); }