/* 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_ADSR_CURVE_MODE_DIRECT; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_ADSR_CURVE_MODE_EXPONENT_DECREASE; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_ADSR_CURVE_MODE_EXPONENT_INCREASE; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_ADSR_CURVE_MODE_LINEAR_BENT; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_ADSR_CURVE_MODE_LINEAR_DECREASE; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_ADSR_CURVE_MODE_LINEAR_INCREASE; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_ENVELOPE_FREQ_MAX; import static jpcsp.HLE.modules.sceSasCore.PSP_SAS_ENVELOPE_HEIGHT_MAX; import static jpcsp.sound.SoundMixer.getSampleLeft; import static jpcsp.sound.SoundMixer.getSampleRight; import org.apache.log4j.Logger; import jpcsp.HLE.modules.sceSasCore; import jpcsp.sound.SoundVoice.VoiceADSREnvelope; /** * @author gid15 * */ public class SampleSourceWithADSR implements ISampleSource { private static Logger log = SoftwareSynthesizer.log; private ISampleSource sampleSource; private SoundVoice voice; private EnvelopeState envelopeState; private final boolean tracing; private static final int ATTACK_CURVE_STATE = 0; private static final int DECAY_CURVE_STATE = 1; private static final int SUSTAIN_CURVE_STATE = 2; private static final int RELEASE_CURVE_STATE = 3; /** * Keep track of an envelope state: * - the 4 defined curve types (attack, decay, sustain, release) * - the current active curve * - the current envelope height */ private static class EnvelopeState { private VoiceADSREnvelope envelope; // It really makes life easier to use a "long" value and not have to // handle with overflows while doing "int" arithmetic. private long envelopeHeight; private int curveState; private int indexExp; private final boolean tracing; public EnvelopeState(VoiceADSREnvelope envelope) { this.envelope = envelope; tracing = sceSasCore.log.isTraceEnabled(); } public void resetToStart() { indexExp = 0; envelopeHeight = 0; curveState = ATTACK_CURVE_STATE; } private static final short[] expCurve = new short[] { 0x0000, 0x0380, 0x06E4, 0x0A2D, 0x0D5B, 0x1072, 0x136F, 0x1653, 0x1921, 0x1BD9, 0x1E7B, 0x2106, 0x237F, 0x25E4, 0x2835, 0x2A73, 0x2CA0, 0x2EBB, 0x30C6, 0x32C0, 0x34AB, 0x3686, 0x3852, 0x3A10, 0x3BC0, 0x3D63, 0x3EF7, 0x4081, 0x41FC, 0x436E, 0x44D3, 0x462B, 0x477B, 0x48BF, 0x49FA, 0x4B2B, 0x4C51, 0x4D70, 0x4E84, 0x4F90, 0x5095, 0x5191, 0x5284, 0x5370, 0x5455, 0x5534, 0x5609, 0x56D9, 0x57A3, 0x5867, 0x5924, 0x59DB, 0x5A8C, 0x5B39, 0x5BE0, 0x5C81, 0x5D1C, 0x5DB5, 0x5E48, 0x5ED5, 0x5F60, 0x5FE5, 0x6066, 0x60E2, 0x615D, 0x61D2, 0x6244, 0x62B2, 0x631D, 0x6384, 0x63E8, 0x644A, 0x64A8, 0x6503, 0x655B, 0x65B1, 0x6605, 0x6653, 0x66A2, 0x66ED, 0x6737, 0x677D, 0x67C1, 0x6804, 0x6844, 0x6882, 0x68BF, 0x68F9, 0x6932, 0x6969, 0x699D, 0x69D2, 0x6A03, 0x6A34, 0x6A63, 0x6A8F, 0x6ABC, 0x6AE6, 0x6B0E, 0x6B37, 0x6B5D, 0x6B84, 0x6BA7, 0x6BCB, 0x6BED, 0x6C0E, 0x6C2D, 0x6C4D, 0x6C6B, 0x6C88, 0x6CA4, 0x6CBF, 0x6CD9, 0x6CF3, 0x6D0C, 0x6D24, 0x6D3B, 0x6D52, 0x6D68, 0x6D7D, 0x6D91, 0x6DA6, 0x6DB9, 0x6DCA, 0x6DDE, 0x6DEF, 0x6DFF, 0x6E10, 0x6E20, 0x6E30, 0x6E3E, 0x6E4C, 0x6E5A, 0x6E68, 0x6E76, 0x6E82, 0x6E8E, 0x6E9B, 0x6EA5, 0x6EB1, 0x6EBC, 0x6EC6, 0x6ED1, 0x6EDB, 0x6EE4, 0x6EED, 0x6EF6, 0x6EFE, 0x6F07, 0x6F10, 0x6F17, 0x6F20, 0x6F27, 0x6F2E, 0x6F35, 0x6F3C, 0x6F43, 0x6F48, 0x6F4F, 0x6F54, 0x6F5B, 0x6F60, 0x6F66, 0x6F6B, 0x6F70, 0x6F74, 0x6F79, 0x6F7E, 0x6F82, 0x6F87, 0x6F8A, 0x6F90, 0x6F93, 0x6F97, 0x6F9A, 0x6F9E, 0x6FA1, 0x6FA5, 0x6FA8, 0x6FAC, 0x6FAD, 0x6FB1, 0x6FB4, 0x6FB6, 0x6FBA, 0x6FBB, 0x6FBF, 0x6FC1, 0x6FC4, 0x6FC6, 0x6FC8, 0x6FC9, 0x6FCD, 0x6FCF, 0x6FD0, 0x6FD2, 0x6FD4, 0x6FD6, 0x6FD7, 0x6FD9, 0x6FDB, 0x6FDD, 0x6FDE, 0x6FDE, 0x6FE0, 0x6FE2, 0x6FE4, 0x6FE5, 0x6FE5, 0x6FE7, 0x6FE9, 0x6FE9, 0x6FEB, 0x6FEC, 0x6FEC, 0x6FEE, 0x6FEE, 0x6FF0, 0x6FF0, 0x6FF2, 0x6FF2, 0x6FF3, 0x6FF3, 0x6FF5, 0x6FF5, 0x6FF7, 0x6FF7, 0x6FF7, 0x6FF9, 0x6FF9, 0x6FF9, 0x6FFA, 0x6FFA, 0x6FFA, 0x6FFC, 0x6FFC, 0x6FFC, 0x6FFE, 0x6FFE, 0x6FFE, 0x7000 }; private static final short expCurveReference = 0x7000; private static short extrapolateSample(short[] curve, int index, int duration) { float curveIndex = (index * curve.length) / (float) duration; int curveIndex1 = (int) curveIndex; int curveIndex2 = curveIndex1 + 1; float curveIndexFraction = curveIndex - curveIndex1; if (curveIndex1 < 0) { return curve[0]; } else if (curveIndex2 >= curve.length || curveIndex2 < 0) { return curve[curve.length - 1]; } float sample = curve[curveIndex1] * (1.f - curveIndexFraction) + curve[curveIndex2] * curveIndexFraction; return (short) Math.round(sample); } private long stepCurveExp(int rate) { int duration; if (rate == 0) { duration = PSP_SAS_ENVELOPE_FREQ_MAX; } else { // From experimental tests on a PSP: // rate=0x7FFFFFFF => duration=0x10 // rate=0x3FFFFFFF => duration=0x22 // rate=0x1FFFFFFF => duration=0x44 // rate=0x0FFFFFFF => duration=0x81 // rate=0x07FFFFFF => duration=0xF1 // rate=0x03FFFFFF => duration=0x1B9 // // The correct curve model is still unknown. // We use the following approximation: // duration = 0x7FFFFFFF / rate * 0x10 duration = PSP_SAS_ENVELOPE_FREQ_MAX / rate * 0x10; } short expFactor = extrapolateSample(expCurve, indexExp, duration); indexExp++; return ((long) expFactor) * PSP_SAS_ENVELOPE_HEIGHT_MAX / expCurveReference; } private void setCurve(int curve) { if (this.curveState != curve) { this.curveState = curve; indexExp = 0; } } /** * Return the given envelope height as a 32-bit integer value by * cutting the value to the allowed range [0..0x40000000]. * * @param envelopeHeight 33-bit integer value * @return 32-bit integer value [0..0x40000000] */ private int getIntEnvelopeHeight(long envelopeHeight) { if (envelopeHeight <= 0) { return 0; } if (envelopeHeight >= PSP_SAS_ENVELOPE_HEIGHT_MAX) { return PSP_SAS_ENVELOPE_HEIGHT_MAX; } return (int) envelopeHeight; } private void stepCurve(int type, int rate) { switch (type) { case PSP_SAS_ADSR_CURVE_MODE_LINEAR_INCREASE: // The curve value will increase linearly according to its rate. envelopeHeight += rate; break; case PSP_SAS_ADSR_CURVE_MODE_LINEAR_DECREASE: // The curve value will decrease linearly according to its rate. envelopeHeight -= rate; break; case PSP_SAS_ADSR_CURVE_MODE_LINEAR_BENT: // The curve value will increase linearly according to its rate up to 75% // of the maximum envelope value. Over 75%, the curve value will // increase linearly according to 1/4th of its rate. // Between 0% and 75%: linear increase with given rate. // Between 75% and 100%: linear increase with 1/4th of the rate. if (envelopeHeight <= (PSP_SAS_ENVELOPE_HEIGHT_MAX / 4 * 3)) { envelopeHeight += rate; } else { envelopeHeight += rate >> 2; } break; case PSP_SAS_ADSR_CURVE_MODE_EXPONENT_DECREASE: // The curve value will decrease exponentially according to its rate. // The exact curve algorithm is still unknown. The same empirical approximation // as for the curve type PSP_SAS_ADSR_CURVE_MODE_EXPONENT_INCREASE is used. envelopeHeight = PSP_SAS_ENVELOPE_HEIGHT_MAX - stepCurveExp(rate); break; case PSP_SAS_ADSR_CURVE_MODE_EXPONENT_INCREASE: // The curve value will increase exponentially according to its rate. // The exact curve algorithm is still unknown. An empirical approximation // is used. envelopeHeight = stepCurveExp(rate); break; case PSP_SAS_ADSR_CURVE_MODE_DIRECT: // The curve value is directly set to its rate. envelopeHeight = rate; break; } } /** * Return the next envelope height. Switch to the next curve if required. * * @return the next envelope height in the range [0..0x40000000] */ public int getNextEnvelopeHeight() { long currentEnvelopeHeight = envelopeHeight; switch (curveState) { case ATTACK_CURVE_STATE: stepCurve(envelope.AttackCurveType, envelope.AttackRate); // Switch Attack to Decay: when the envelope height gets over the upper limit // or under the lower limit if (envelopeHeight >= PSP_SAS_ENVELOPE_HEIGHT_MAX || envelopeHeight < 0) { if (envelopeHeight >= PSP_SAS_ENVELOPE_HEIGHT_MAX) { envelopeHeight = PSP_SAS_ENVELOPE_HEIGHT_MAX; } setCurve(DECAY_CURVE_STATE); } break; case DECAY_CURVE_STATE: stepCurve(envelope.DecayCurveType, envelope.DecayRate); // Switch Decay to Sustain: when the envelope height gets under the sustain level if (envelopeHeight < envelope.SustainLevel) { setCurve(SUSTAIN_CURVE_STATE); } break; case SUSTAIN_CURVE_STATE: stepCurve(envelope.SustainCurveType, envelope.SustainRate); // Switch Sustain to Release: this switch only happens when setting the key off. break; case RELEASE_CURVE_STATE: stepCurve(envelope.ReleaseCurveType, envelope.ReleaseRate); break; } if (tracing) { sceSasCore.log.trace(String.format("getNextEnvelopeHeight curve=%d, current=0x%08X, next=0x%08X", curveState, currentEnvelopeHeight, envelopeHeight)); } return getIntEnvelopeHeight(currentEnvelopeHeight); } /** * This method has to be called when setting a key off. * It switches to the Release curve. */ public void setKeyOff() { // Switch to the release curve setCurve(RELEASE_CURVE_STATE); } /** * A voice is ended when the envelope reaches 0 in the Sustain * or Release curves. * * @return true if the voice is ended (envelope reached 0 * in the Sustain or Release curves) * false if the voice is not ended */ public boolean isEnded() { return curveState >= SUSTAIN_CURVE_STATE && envelopeHeight <= 0; } } public SampleSourceWithADSR(ISampleSource sampleSource, SoundVoice voice, VoiceADSREnvelope envelope) { this.sampleSource = sampleSource; this.voice = voice; envelopeState = new EnvelopeState(envelope); tracing = sceSasCore.log.isTraceEnabled(); } /** * Return the next sample value. * The next sample of the sampleSource is modulated by the next * ADSR envelope value. * The voice is ended if the envelope reaches 0 in the SR curves. * The current ADSR envelope height is stored in the voice envelope structure. */ @Override public int getNextSample() { if (log.isTraceEnabled()) { log.trace(String.format("SampleSourceWithADSR.getNextSample height=0x%X, state=%d", envelopeState.envelopeHeight, envelopeState.curveState)); } if (!voice.isOn()) { // The voice has been keyed Off, process the Release part of the wave envelopeState.setKeyOff(); } if (envelopeState.isEnded()) { // The Release/Sustain has ended, stop playing the voice voice.setPlaying(false); return 0; } int envelopeHeight = envelopeState.getNextEnvelopeHeight(); int sample = sampleSource.getNextSample(); // envelopeHeight: [0..0x40000000] // sample: [-0x8000..0x7FFF] // Modulate the sample by the envelope value, assuming the envelope value // is ranging from 0.0f to 1.0f (0x40000000). // // First reduce the envelope value to a 16 bit value, // with rounding: [0..0x8000]. int envelopeHeight16 = ((envelopeHeight >> 14) + 1) >> 1; // Multiply the sample by the envelope value with rounding short modulatedSampleLeft = modulate(getSampleLeft(sample), envelopeHeight16); short modulatedSampleRight = modulate(getSampleRight(sample), envelopeHeight16); int modulatedSample = SoundMixer.getSampleStereo(modulatedSampleLeft, modulatedSampleRight); if (tracing) { sceSasCore.log.trace(String.format("getNextSample voice=0x%X, sample=0x%08X, envelopeHeight=0x%08X, modulatedSample=0x%08X", voice.getIndex(), sample, envelopeHeight, modulatedSample)); } // Store the current envelope height // (can be retrieved by the application using __sceSasGetEnvelopeHeight) voice.getEnvelope().height = envelopeHeight; return modulatedSample; } private short modulate(short sample, int envelopeHeight16) { return (short) ((sample * envelopeHeight16 + 0x4000) >> 15); } @Override public void resetToStart() { sampleSource.resetToStart(); envelopeState.resetToStart(); } @Override public boolean isEnded() { return sampleSource.isEnded(); } }