/* This file is part of jpcsp. Jpcsp is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Jpcsp 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 General Public License for more details. You should have received a copy of the GNU General Public License along with Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.sound; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_OUTPUTMODE_STEREO; import static jpcsp.sound.SoundChannel.MAX_VOLUME; import java.util.Arrays; import org.apache.log4j.Logger; import jpcsp.HLE.Modules; import jpcsp.hardware.Audio; import jpcsp.memory.IMemoryReader; import jpcsp.memory.IMemoryWriter; import jpcsp.memory.MemoryReader; import jpcsp.memory.MemoryWriter; public class SoundMixer { private static Logger log = SoftwareSynthesizer.log; private SoundVoice[] voices; private SoftwareSynthesizer[] synthesizers; public SoundMixer(SoundVoice[] voices) { this.voices = voices; synthesizers = new SoftwareSynthesizer[voices.length]; for (int i = 0; i < voices.length; i++) { synthesizers[i] = new SoftwareSynthesizer(voices[i]); } } private static short clampSample(int sample) { if (sample < Short.MIN_VALUE) { return Short.MIN_VALUE; } else if (sample > Short.MAX_VALUE) { return Short.MAX_VALUE; } return (short) sample; } private void mixStereo(int[] stereoSamples, ISampleSource sampleSource, int startIndex, int length, int leftVol, int rightVol) { int endIndex = startIndex + length; if (startIndex == 0) { sampleSource.resetToStart(); } for (int i = startIndex, j = 0; i < endIndex; i++, j += 2) { int sample = sampleSource.getNextSample(); stereoSamples[j] += SoundChannel.adjustSample(getSampleLeft(sample), leftVol); stereoSamples[j + 1] += SoundChannel.adjustSample(getSampleRight(sample), rightVol); } } private void mixMono(int[] monoSamples, ISampleSource sampleSource, int startIndex, int length, int monoVol) { int endIndex = startIndex + length; if (startIndex == 0) { sampleSource.resetToStart(); } for (int i = startIndex, j = 0; i < endIndex; i++, j++) { int sample = sampleSource.getNextSample(); monoSamples[j] += SoundChannel.adjustSample(getSampleLeft(sample), monoVol); } } private void copyStereoSamplesToMem(int[] mixedSamples, int addr, int samples, int leftVol, int rightVol, boolean writeSamples) { // Adjust the volume according to the global volume settings leftVol = Audio.getVolume(leftVol); rightVol = Audio.getVolume(rightVol); if (!writeSamples) { // If the samples have not been changed and the volume settings // would also not adjust the samples, no need to copy them back to memory. if (leftVol == MAX_VOLUME && rightVol == MAX_VOLUME) { return; } } int lengthInBytes = mixedSamples.length << 1; IMemoryWriter memoryWriter = MemoryWriter.getMemoryWriter(addr, lengthInBytes, 4); for (int i = 0, j = 0; i < samples; i++, j += 2) { short sampleLeft = clampSample(mixedSamples[j]); short sampleRight = clampSample(mixedSamples[j + 1]); sampleLeft = SoundChannel.adjustSample(sampleLeft, leftVol); sampleRight = SoundChannel.adjustSample(sampleRight, rightVol); int sampleStereo = getSampleStereo(sampleLeft, sampleRight); memoryWriter.writeNext(sampleStereo); } memoryWriter.flush(); } private void copyMonoSamplesToMem(int[] mixedSamples, int addr, int samples, int monoVol, boolean writeSamples) { // Adjust the volume according to the global volume settings monoVol = Audio.getVolume(monoVol); if (!writeSamples) { // If the samples have not been changed and the volume settings // would also not adjust the samples, no need to copy them back to memory. if (monoVol == MAX_VOLUME) { return; } } int lengthInBytes = mixedSamples.length << 1; IMemoryWriter memoryWriter = MemoryWriter.getMemoryWriter(addr, lengthInBytes, 2); for (int i = 0, j = 0; i < samples; i++, j++) { short sampleMono = clampSample(mixedSamples[j]); sampleMono = SoundChannel.adjustSample(sampleMono, monoVol); memoryWriter.writeNext(sampleMono & 0xFFFF); } memoryWriter.flush(); } private void mix(int[] mixedSamples, int addr, int samples, int leftVol, int rightVol, boolean writeSamples) { boolean isStereo = Modules.sceSasCoreModule.getOutputMode() == PSP_SAS_OUTPUTMODE_STEREO; for (int i = 0; i < voices.length; i++) { SoundVoice voice = voices[i]; if (voice.isPlaying() && !voice.isPaused()) { ISampleSource sampleSource = synthesizers[i].getSampleSource(); int playSample = voice.getPlaySample(); if (sampleSource.isEnded()) { // End of voice sample reached if (log.isTraceEnabled()) { log.trace(String.format("Reaching end of sample source for voice %s", voice)); } voice.setPlaying(false); } else { if (isStereo) { mixStereo(mixedSamples, sampleSource, playSample, samples, voice.getLeftVolume(), voice.getRightVolume()); } else { mixMono(mixedSamples, sampleSource, playSample, samples, voice.getLeftVolume()); } writeSamples = true; voice.setPlaySample(1); } } } if (isStereo) { copyStereoSamplesToMem(mixedSamples, addr, samples, leftVol, rightVol, writeSamples); } else { copyMonoSamplesToMem(mixedSamples, addr, samples, leftVol, writeSamples); } } /** * Synthesizing audio function. * @param addr Output address for the PCM data (must be 64-byte aligned). * @param samples Number of samples returned. */ public void synthesize(int addr, int samples) { int[] mixedSamples = new int[samples * 2]; Arrays.fill(mixedSamples, 0); mix(mixedSamples, addr, samples, MAX_VOLUME, MAX_VOLUME, true); } /** * Synthesizing audio function with mix. * @param addr Input and output address for the PCM data (must be 64-byte aligned). * @param samples Number of samples returned. * @param leftVol the volume of the left channel for modulating the input PCM data. * This volume is not affecting the currently played samples. * @param rightVol the volume of the right channel for modulating the input PCM data. * This volume is not affecting the currently played samples. */ public void synthesizeWithMix(int addr, int samples, int leftVol, int rightVol) { int[] mixedSamples = new int[samples * 2]; // Read the input buffer into mixedSamples. // Check first for simple cases... if (leftVol == 0 && rightVol == 0) { // Do not mix with the input buffer Arrays.fill(mixedSamples, 0); } else if (leftVol == MAX_VOLUME && rightVol == MAX_VOLUME) { // Mix with the input buffer with no volume change int lengthInBytes = mixedSamples.length * 2; IMemoryReader memoryReader = MemoryReader.getMemoryReader(addr, lengthInBytes, 2); for (int i = 0; i < mixedSamples.length; i++) { mixedSamples[i] = (short) memoryReader.readNext(); } } else { // Mix with the input buffer with a volume adjustment int lengthInBytes = mixedSamples.length * 2; IMemoryReader memoryReader = MemoryReader.getMemoryReader(addr, lengthInBytes, 2); for (int i = 0; i < samples; i++) { short sampleLeft = (short) memoryReader.readNext(); short sampleRight = (short) memoryReader.readNext(); sampleLeft = SoundChannel.adjustSample(sampleLeft, leftVol); sampleRight = SoundChannel.adjustSample(sampleRight, rightVol); mixedSamples[i * 2] = sampleLeft; mixedSamples[i * 2 + 1] = sampleRight; } } mix(mixedSamples, addr, samples, MAX_VOLUME, MAX_VOLUME, false); } public static short getSampleLeft(int sample) { return (short) (sample & 0x0000FFFF); } public static short getSampleRight(int sample) { return (short) (sample >> 16); } public static int getSampleStereo(short left, short right) { return (left & 0x0000FFFF) | ((right & 0x0000FFFF) << 16); } }