From 9e04a0572625ec47d2e6d547470da25aa6a6be20 Mon Sep 17 00:00:00 2001 From: "brobert@adobe.com" Date: Thu, 1 Aug 2024 10:21:28 -0500 Subject: [PATCH] Add Mac ARM support --- .gitignore | 1 + pom.xml | 12 + src/main/java/jace/core/SoundMixer.java | 2 +- src/main/java/jace/core/TimedDevice.java | 4 +- src/main/java/jace/hardware/Joystick.java | 26 +- .../jace/hardware/mockingboard/Votrax.java | 318 ++++++++++++++++++ src/test/java/jace/AbstractFXTest.java | 16 + .../hardware/mockingboard/VotraxTest.java | 49 +++ 8 files changed, 417 insertions(+), 11 deletions(-) create mode 100644 src/main/java/jace/hardware/mockingboard/Votrax.java create mode 100644 src/test/java/jace/AbstractFXTest.java create mode 100644 src/test/java/jace/hardware/mockingboard/VotraxTest.java diff --git a/.gitignore b/.gitignore index a260071..2a83a75 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ hs_err_pid* *.DS_Store !/lib/nestedvm.jar _acme_tmp* +.vscode/settings.json diff --git a/pom.xml b/pom.xml index a01a5af..f5d7fba 100644 --- a/pom.xml +++ b/pom.xml @@ -336,6 +336,18 @@ natives-macos + + lwjgl-natives-macos-arm64 + + + mac + aarch64 + + + + natives-macos-arm64 + + lwjgl-natives-windows-amd64 diff --git a/src/main/java/jace/core/SoundMixer.java b/src/main/java/jace/core/SoundMixer.java index d710c66..538ccca 100644 --- a/src/main/java/jace/core/SoundMixer.java +++ b/src/main/java/jace/core/SoundMixer.java @@ -135,7 +135,7 @@ public class SoundMixer extends Device { soundThreadExecutor.submit(operation, action); } - protected static void initSound() { + public static void initSound() { if (Utility.isHeadlessMode()) { return; } diff --git a/src/main/java/jace/core/TimedDevice.java b/src/main/java/jace/core/TimedDevice.java index ddb4f6c..d5ad092 100644 --- a/src/main/java/jace/core/TimedDevice.java +++ b/src/main/java/jace/core/TimedDevice.java @@ -30,8 +30,8 @@ import jace.config.ConfigurableField; public abstract class TimedDevice extends Device { // From the holy word of Sather 3:5 (Table 3.1) :-) // This average speed averages in the "long" cycles - public static final long NTSC_1MHZ = 1020484L; - public static final long PAL_1MHZ = 1015625L; + public static final int NTSC_1MHZ = 1020484; + public static final int PAL_1MHZ = 1015625; public static final long SYNC_FREQ_HZ = 60; public static final double NANOS_PER_SECOND = 1000000000.0; public static final long NANOS_PER_MILLISECOND = 1000000L; diff --git a/src/main/java/jace/hardware/Joystick.java b/src/main/java/jace/hardware/Joystick.java index e2131b3..18489fe 100644 --- a/src/main/java/jace/hardware/Joystick.java +++ b/src/main/java/jace/hardware/Joystick.java @@ -321,6 +321,24 @@ public class Joystick extends Device { xSwitch = (MemorySoftSwitch) SoftSwitches.PDL2.getSwitch(); ySwitch = (MemorySoftSwitch) SoftSwitches.PDL3.getSwitch(); } + new Thread(()->{ + try { + // We have to wait for the the library to be loaded and the UI to be active + // Otherwise this will fail + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (port == 0 && glfwController.getSelections().keySet().size() == 2) { + // Get the entry that is not null + glfwController.setValue(glfwController.getSelections().keySet().stream().filter(s->s != null && !s.isBlank()).findFirst().get()); + System.out.println("Using device for joystick: " + glfwController.getValue()); + useKeyboard = false; + useDPad = true; + } else { + System.out.println("Using device for joystick: " + (useKeyboard ? "keyboard" : "mouse")); + } + }).start(); } public boolean leftPressed = false; public boolean rightPressed = false; @@ -579,9 +597,7 @@ public class Joystick extends Device { @InvokableAction(name = "Left", category = "joystick", defaultKeyMapping = "left", notifyOnRelease = true) public boolean joystickLeft(boolean pressed) { - System.out.println("LEFT "+pressed); if (!useKeyboard) { - System.out.println("(ignored)"); return false; } leftPressed = pressed; @@ -593,9 +609,7 @@ public class Joystick extends Device { @InvokableAction(name = "Right", category = "joystick", defaultKeyMapping = "right", notifyOnRelease = true) public boolean joystickRight(boolean pressed) { - System.out.println("RIGHT "+pressed); if (!useKeyboard) { - System.out.println("(ignored)"); return false; } rightPressed = pressed; @@ -607,9 +621,7 @@ public class Joystick extends Device { @InvokableAction(name = "Up", category = "joystick", defaultKeyMapping = "up", notifyOnRelease = true) public boolean joystickUp(boolean pressed) { - System.out.println("UP!"); if (!useKeyboard) { - System.out.println("(ignored)"); return false; } upPressed = pressed; @@ -621,9 +633,7 @@ public class Joystick extends Device { @InvokableAction(name = "Down", category = "joystick", defaultKeyMapping = "down", notifyOnRelease = true) public boolean joystickDown(boolean pressed) { - System.out.println("DOWN!"); if (!useKeyboard) { - System.out.println("(ignored)"); return false; } downPressed = pressed; diff --git a/src/main/java/jace/hardware/mockingboard/Votrax.java b/src/main/java/jace/hardware/mockingboard/Votrax.java new file mode 100644 index 0000000..5fd047c --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/Votrax.java @@ -0,0 +1,318 @@ +package jace.hardware.mockingboard; + +import static org.lwjgl.openal.AL10.AL_NO_ERROR; +import static org.lwjgl.openal.AL10.alGetError; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import org.lwjgl.openal.EXTEfx; + +import jace.core.SoundMixer; +import jace.core.SoundMixer.SoundBuffer; +import jace.core.SoundMixer.SoundError; +import jace.core.TimedDevice; + +public class Votrax extends TimedDevice { + // This is a speech synthesizer based on the Votrax SC-02 + // There are 2 sound generators, one for the voice and one for the noise + // The voice generator is a saw-tooth wave generator at a frequency determined by the voice frequency register + // The noise generator is a pseudo-random noise generator + + // The Votrax has 5 filters that can be applied to the voice generator, controlled by a filter frequency register + // The fifth filter takes both the voice and noise generators as input, but other filters only take the voice generator + // There is also a final high-pass filter that can be applied to the output of the voice and noise generators + + // There is a phoneme register which controls the phoneme to be spoken (0-63) + // There is a duration register which controls the duration of the phoneme (0-3) + // There is a rate register which controls the rate of speech (0-15) + // There is an inflection register which controls the inflection of the voice (0-15) + + // For each phoneme there are 8 bytes that control the filters and sound generator levels + byte[] phonemeData = new byte[64 * 8]; + // Phoneme chart: + // 00: PA (pause) + // 01: E (mEEt) + // 02: E1 (bEnt) + // 03: Y (bEfore) + // 04: Y1 (Year) + // 05: AY (plEAse) + // 06: IE (anY) + // 07: I (sIx) + // 08: A (mAde) + // 09: A1 (cAre) + // 0a: EH (nEst) + // 0b: EH1 (bElt) + // 0c: AE (dAd) + // 0d: AE1 (After) + // 0e: AH (gOt) + // 0f: AH1 (fAther) + // 10: AW (Office) + // 11: O (stOre) + // 12: OU (bOAt) + // 13: OO (lOOk) + // 14: IU (yOU) + // 15: IU1 (cOUld) + // 16: U (tUne) + // 17: U1 (cartOOn) + // 18: UH (wOnder) + // 19: UH1 (lOve) + // 1a: UH2 (whAt) + // 1b: UH3 (nUt) + // 1c: ER (bIRd) + // 1d: R (Roof) + // 1e: R1 (Rug) + // 1f: R2 (mutteR -- German) + // 20: L (Lift) + // 21: L1 (pLay) + // 22: LF (faLL) + // 23: W (Water) + // 24: B (Bag) + // 25: D (paiD) + // 26: KV (taG) + // 27: P (Pen) + // 28: T (Tart) + // 29: K (Kit) + // 2a: HV - Hold Vocal + // 2b: HVC - Hold Vocal Closure + // 2c: HF - (Heart) + // 2d: HFC - Hold Frictave Closure + // 2e: HN - Hold Nasal + // 2f: Z (Zero) + // 30: S (Same) + // 31: J (meaSure) + // 32: SCH (SHip) + // 33: V (Very) + // 34: F (Four) + // 35: THV (THere) + // 36: TH (wiTH) + // 37: M (More) + // 38: N (NiNe) + // 39: NG (raNG) + // 3a: :A (mAErchen -- German) + // 3b: :OH (lOwe - French) + // 3c: :U (fUEnf -- German) + // 3d: :UH (menU -- French) + // 3e: E2 (bittE -- German) + // 3f: LB (Lube) + + public void loadPhonemeData() { + InputStream romFile = Votrax.class.getResourceAsStream("jace/data/sc01a.bin"); + if (romFile == null) { + throw new RuntimeException("Cannot find Votrax SC-01A ROM"); + } + // Load into phonemeData + try { + if (romFile.read(phonemeData) != phonemeData.length) { + throw new RuntimeException("Bad Votrax SC-01A ROM size"); + } + } catch (Exception ex) { + throw new RuntimeException("Error loading Votrax SC-01A ROM", ex); + } + } + + public static abstract class Generator { + public int clockFrequency = 1; + public int sampleRate = 1; + public double samplesPerClock; + public double sampleCounter = 0.0; + public void setClockFrequency(int clockFrequency) { + this.clockFrequency = clockFrequency; + this.updateFrequency(); + } + public void setSampleRate(int sampleRate) { + this.sampleRate = sampleRate; + this.updateFrequency(); + } + public void updateFrequency() { + this.sampleCounter = 0.0; + this.samplesPerClock = clockFrequency / sampleRate; + } + public Optional tick() { + sampleCounter += samplesPerClock; + if (sampleCounter >= 1.0) { + sampleCounter -= 1.0; + + return Optional.of(doGenerate()); + } else { + return Optional.empty(); + } + } + + public abstract double doGenerate(); + } + + public static class Mixer extends Generator { + public List inputs = new ArrayList<>(); + public List gains = new ArrayList<>(); + double volume=0.0; + + public void addInput(Generator input) { + inputs.add(input); + gains.add(1.0); + } + + public void setGain(int index, double gain) { + gains.set(index, gain); + } + + public double doGenerate() { + double sample = 0.0; + for (int i = 0; i < inputs.size(); i++) { + sample += inputs.get(i).doGenerate() * gains.get(i); + } + return sample; + } + } + + public static class SawGenerator extends Generator { + double pitch=440.0; + double sample = 0.0; + double direction = 1.0; + double changePerSample = 0.0; + public void setPitch(double frequency) { + this.pitch = frequency; + this.updateFrequency(); + } + public void setDirection(double direction) { + this.direction = direction; + this.updateFrequency(); + } + public void updateFrequency() { + super.updateFrequency(); + changePerSample = direction * 2.0 * pitch / sampleRate; + } + + public double doGenerate() { + sample += changePerSample; + if (sample < -1.0) { + sample += 2.0; + } else if (sample > 1.0) { + sample -= 2.0; + } + return sample; + } + } + + public static class NoiseGenerator extends Generator { + double sample = 0.0; + public double doGenerate() { + return Math.random() * 2.0 - 1.0; + } + } + + public float mixerGain = 32767.0f; + public int[] filters = new int[5]; + public SawGenerator formantGenerator = new SawGenerator(); + public NoiseGenerator noiseGenerator = new NoiseGenerator(); + public Mixer mixer = new Mixer(); + public static int FORMANT = 0; + public static int NOISE = 1; + private Thread playbackThread = null; + + public Votrax() throws Exception { + // loadPhonemeData(); + formantGenerator.setSampleRate(44100); + formantGenerator.setPitch(100); + noiseGenerator.setSampleRate(44100); + mixer.addInput(formantGenerator); + mixer.addInput(noiseGenerator); + mixer.setGain(FORMANT, 0.5); + mixer.setGain(NOISE, 0.1); + } + + public void resume() { + try { + createFilters(); + } catch (Exception e) { + e.printStackTrace(); + suspend(); + } + super.resume(); + if (playbackThread != null && !playbackThread.isAlive()) { + return; + } + playbackThread = new Thread(() -> { + SoundBuffer soundBuffer = null; + try { + soundBuffer = SoundMixer.createBuffer(false); + while (isRunning()) { + try { + soundBuffer.playSample((short) (mixer.doGenerate() * mixerGain)); + } catch (Exception e) { + e.printStackTrace(); + suspend(); + } + } + } catch (InterruptedException | ExecutionException | SoundError e) { + e.printStackTrace(); + suspend(); + } finally { + try { + soundBuffer.shutdown(); + } catch (InterruptedException | ExecutionException | SoundError e) { + e.printStackTrace(); + } + } + }); + playbackThread.start(); + } + + private void createFilters() throws Exception { + // TODO: Consider filter values from here: https://modwiggler.com/forum/viewtopic.php?t=234128 + // Bark scale: 60, 150, 250, 350, 450, 570, 700, 840, 1000, 1170, 1370, 1600, 1850, 2150, 2500, 2900 + // Roland SVC-350: 150, 220, 350, 500, 760, 1100, 1600, 2200, 3600, 5200. 6000 highpass filter + // EMS Vocoder System 3000: 125, 185, 270, 350, 430, 530, 630, 780, 950, 1150, 1380, 2070, 2780, 3800, 6400 + + for (int i = 0; i < 5; i++) { + alGetError(); + filters[i] = EXTEfx.alGenFilters(); + if (alGetError() != AL_NO_ERROR) { + throw new Exception("Failed to create filter " + i); + } + if (EXTEfx.alIsFilter(filters[i])) { + // Set Filter type to Band-Pass and set parameters + EXTEfx.alFilteri(filters[i], EXTEfx.AL_FILTER_TYPE, EXTEfx.AL_FILTER_BANDPASS); + if (alGetError() != AL_NO_ERROR) { + System.out.println("Band pass filter not supported."); + } else { + EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAIN, 0.5f); + EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAINHF, 0.5f); + System.out.println("Band pass filter "+i+" created."); + } + } + } + } + + public boolean suspend() { + destroyFilters(); + + playbackThread = null; + return super.suspend(); + } + + private void destroyFilters() { + for (int i = 0; i < 5; i++) { + if (EXTEfx.alIsFilter(filters[i])) { + EXTEfx.alDeleteFilters(filters[i]); + } + } + } + + @Override + public String getShortName() { + return "Votrax"; + } + + @Override + protected String getDeviceName() { + return "Votrax SC-02 / SSI-263"; + } + + @Override + public void tick() { + } +} \ No newline at end of file diff --git a/src/test/java/jace/AbstractFXTest.java b/src/test/java/jace/AbstractFXTest.java new file mode 100644 index 0000000..9b99a0b --- /dev/null +++ b/src/test/java/jace/AbstractFXTest.java @@ -0,0 +1,16 @@ +package jace; + +import org.junit.BeforeClass; + +import javafx.application.Platform; + +public abstract class AbstractFXTest { + public static boolean fxInitialized = false; + @BeforeClass + public static void initJfxRuntime() { + if (!fxInitialized) { + fxInitialized = true; + Platform.startup(() -> {}); + } + } +} \ No newline at end of file diff --git a/src/test/java/jace/hardware/mockingboard/VotraxTest.java b/src/test/java/jace/hardware/mockingboard/VotraxTest.java new file mode 100644 index 0000000..b92ebc3 --- /dev/null +++ b/src/test/java/jace/hardware/mockingboard/VotraxTest.java @@ -0,0 +1,49 @@ +package jace.hardware.mockingboard; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import jace.AbstractFXTest; +import jace.core.SoundMixer; +import jace.core.Utility; + +public class VotraxTest extends AbstractFXTest { + @Before + public void setUp() { + System.out.println("Init sound"); + Utility.setHeadlessMode(false); + SoundMixer.PLAYBACK_ENABLED = true; + SoundMixer.initSound(); + } + + @Test + public void testVoicedSource() { + + } + + @Test + public void testFricativeSource() { + + } + + @Test + public void testMixer() throws Exception { + + Votrax vo = new Votrax(); + vo.resume(); + System.out.println("Sound: ON for 2sec"); + Thread.sleep(2000); + boolean stillRunning = vo.isRunning(); + vo.suspend(); + System.out.println("Sound: OFF"); + boolean overrun = vo.isRunning(); + + assertTrue("Playback was interrupted early", stillRunning); + assertFalse("Playback didn't stop when suspended", overrun); + } + + +}