/* * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA */ package jace.hardware.mockingboard; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; /** * Implementation of the AY sound PSG chip. This class manages register values * and mixes the channels together (in the update method) The work of * maintaining the sound, envelope and noise generator states is provided by the * respective generator classes. * * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ public class PSG { int baseReg; /* register ids */ public enum Reg { AFine(0, 255), ACoarse(1, 15), BFine(2, 255), BCoarse(3, 15), CFine(4, 255), CCoarse(5, 15), NoisePeriod(6, 31), Enable(7, 255), AVol(8, 31), BVol(9, 31), CVol(10, 31), EnvFine(11, 255), EnvCoarse(12, 255), EnvShape(13, 15), PortA(14, 255), PortB(15, 255); public final int registerNumber; public final int max; Reg(int number, int maxValue) { registerNumber = number; max = maxValue; } static Reg get(int number) { for (Reg r : Reg.values()) { if (r.registerNumber == number) { return r; } } return null; } static public Reg[] preferredOrder = new Reg[]{ Enable, EnvShape, EnvCoarse, EnvFine, NoisePeriod, AVol, BVol, CVol, AFine, ACoarse, BFine, BCoarse, CFine, CCoarse}; } public enum BusControl { inactive(4), read(5), write(6), latch(7); int val; BusControl(int v) { val = v; } public static BusControl fromInt(int i) { for (BusControl b : BusControl.values()) { if (b.val == i) { return b; } } return null; } } List<SoundGenerator> channels; EnvelopeGenerator envelopeGenerator; NoiseGenerator noiseGenerator; int CLOCK; int SAMPLE_RATE; public int bus; int selectedReg; String name; Map<Reg, Integer> regValues; public int mask; public PSG(int base, int clock, int sample_rate, String name, int DDR_Mask) { this.name = name; this.mask = DDR_Mask; baseReg = base; channels = new ArrayList<>(); for (int i = 0; i < 3; i++) { channels.add(new SoundGenerator(clock, sample_rate)); } envelopeGenerator = new EnvelopeGenerator(clock, sample_rate); noiseGenerator = new NoiseGenerator(clock, sample_rate); regValues = Collections.synchronizedMap(new EnumMap<Reg, Integer>(Reg.class)); reset(); } public void setBus(int b) { bus = b; } public void setControl(int c) { BusControl cmd = BusControl.fromInt(c); if (cmd == null) { // System.out.println("Bad control param "+c); return; } switch (cmd) { case inactive: break; case latch: // System.out.println("PSG latched register "+selectedReg); selectedReg = bus & 0x0f; break; case read: bus = getReg(Reg.get(selectedReg)); // System.out.println("PSG read register "+selectedReg + " == "+bus); break; case write: // System.out.println("PSG wrote register "+selectedReg + " == "+bus); setReg(Reg.get(selectedReg), bus); break; } } public int getBaseReg() { return baseReg; } public void setRate(int clock, int sample_rate) { CLOCK = clock; SAMPLE_RATE = sample_rate; channels.stream().forEach((c) -> { c.setRate(clock, sample_rate); }); envelopeGenerator.setRate(clock, sample_rate); noiseGenerator.setRate(clock, sample_rate); reset(); } public final void reset() { for (Reg r : Reg.values()) { // Don't reset the enable register with 0, that will turn everything on! setReg(r, r == Reg.Enable ? 255 : 0); } envelopeGenerator.reset(); noiseGenerator.reset(); channels.parallelStream().forEach((c) -> { c.reset(); }); } public void setReg(Reg r, int value) { if (r != null) { value &= r.max; regValues.put(r, value); writeReg(r, value & 0x0ff); } } public int getReg(Reg r) { if (r == null) { return -1; } Integer value = regValues.get(r); if (value == null) { return -1; } return value & 0x0ff; } public void writeReg(Reg r, int value) { /* A note about the period of tones, noise and envelope: for speed reasons,*/ /* we count down from the period to 0, but careful studies of the chip */ /* output prove that it instead counts up from 0 until the counter becomes */ /* greater or equal to the period. This is an important difference when the*/ /* program is rapidly changing the period to modulate the sound. */ /* To compensate for the difference, when the period is changed we adjust */ /* our internal counter. */ /* Also, note that period = 0 is the same as period = 1. This is mentioned */ /* in the YM2203 data sheets. However, this does NOT apply to the Envelope */ /* period. In that case, period = 0 is half as period = 1. */ value = value & 0x0ff; switch (r) { case ACoarse: case AFine: channels.get(0).setPeriod(getReg(Reg.AFine) + (getReg(Reg.ACoarse) << 8)); break; case BCoarse: case BFine: channels.get(1).setPeriod(getReg(Reg.BFine) + (getReg(Reg.BCoarse) << 8)); break; case CCoarse: case CFine: channels.get(2).setPeriod(getReg(Reg.CFine) + (getReg(Reg.CCoarse) << 8)); break; case NoisePeriod: if (value == 0) value = 32; noiseGenerator.setPeriod(value+16); noiseGenerator.counter = 0; break; case Enable: channels.get(0).setActive((value & 1) == 0); channels.get(0).setNoiseActive((value & 8) == 0); channels.get(1).setActive((value & 2) == 0); channels.get(1).setNoiseActive((value & 16) == 0); channels.get(2).setActive((value & 4) == 0); channels.get(2).setNoiseActive((value & 32) == 0); break; case AVol: channels.get(0).setAmplitude(value); break; case BVol: channels.get(1).setAmplitude(value); break; case CVol: channels.get(2).setAmplitude(value); break; case EnvFine: case EnvCoarse: envelopeGenerator.setPeriod(getReg(Reg.EnvFine) + 256 * getReg(Reg.EnvCoarse)); break; case EnvShape: envelopeGenerator.setShape(value); break; case PortA: case PortB: break; } } public void update(int[] bufA, boolean clearA, int[] bufB, boolean clearB, int[] bufC, boolean clearC, int length) { for (int i = 0; i < length; i++) { noiseGenerator.step(); envelopeGenerator.step(); if (clearA) { bufA[i] = channels.get(0).step(noiseGenerator, envelopeGenerator); } else { bufA[i] += channels.get(0).step(noiseGenerator, envelopeGenerator); } if (clearB) { bufB[i] = channels.get(1).step(noiseGenerator, envelopeGenerator); } else { bufB[i] += channels.get(1).step(noiseGenerator, envelopeGenerator); } if (clearC) { bufC[i] = channels.get(2).step(noiseGenerator, envelopeGenerator); } else { bufC[i] += channels.get(2).step(noiseGenerator, envelopeGenerator); } } } };