/*
AudioGateSoundVoice.java
(c) 2009-2016 Edward Swartz
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/epl-v10.html
*/
package v9t9.audio.sound;
import java.util.Arrays;
import ejs.base.settings.ISettingSection;
import ejs.base.sound.IFlushableSoundVoice;
import ejs.base.sound.ISoundOutput;
/**
* Produce sound on the audio gate, which has two states: on and off.
* This gate can be toggled at the CPU cycle rate, leading to interesting
* issues:
* <ul>
* <li>the gate may be toggled much faster than the sound generation rate</li>
* <li>the gate's changes won't sync with the sound generation rate</li>
* <li>the gate may be toggled much slower than the generation window</li>
* </ul>
* <p>
* The first two issues mean we must antialias the gate's output: instead of
* containing pure 1's and 0's, it will contain up to two samples at the transition
* to approximate the proportion of the cycle count when the sample changed.
* </p>
* <p>The last issue means we must track transitions that span generation windows.
* This holds in general in any case, since transitions aren't expected to line
* up with the generation window either.</p>
* <p>
* The AudioGateVoice driver will invoke {@link #setState(float, boolean)} with
* each transition from the CPU side. The float is a delta from the last
* change, in terms of seconds. We store these transitions in 'deltas', scaled
* by the sound clock (e.g. moving them to frame lengths).
* @author ejs
*
*/
public class AudioGateSoundVoice extends SoundVoice implements IFlushableSoundVoice {
private boolean wasSet;
private boolean state;
private boolean origState;
private float prevCycleRemainder;
private float frac;
private float[] deltas = new float[0];
private int deltaIdx = 0;
private long timeout;
private float soundClock;
public AudioGateSoundVoice(String name) {
super("Audio Gate");
}
/* (non-Javadoc)
* @see v9t9.audio.sound.SoundVoice#setOutput(ejs.base.sound.ISoundOutput)
*/
@Override
public void setOutput(ISoundOutput output) {
super.setOutput(output);
soundClock = output.getSoundFormat().getFrameRate();
}
@Override
public void setupVoice() {
setVolume((byte) (state ? MAX_VOLUME : 0));
wasSet = true;
}
/* (non-Javadoc)
* @see org.ejs.emul.core.sound.ISoundVoice#reset()
*/
public synchronized void reset() {
wasSet = false;
origState = false;
deltaIdx = 0;
prevCycleRemainder = 0;
frac = 0;
}
public synchronized void setState(float seconds, boolean newState) {
if (state != newState) {
state = newState;
appendPos((newState ? seconds : -seconds) * soundClock);
}
}
/**
* @param offs
* @throws AssertionError
*/
protected void appendPos(float offs) throws AssertionError {
if (deltaIdx >= deltas.length) {
int newlen = Math.max(16, deltas.length * 2);
deltas = Arrays.copyOf(deltas, newlen);
}
// ignore long "off" periods when we've already been silent
if (offs < 0 && deltaIdx == 0 && prevCycleRemainder == 0 && timeout == 0)
return;
// System.out.println(offs);
deltas[deltaIdx++] = offs;
// don't keep a high audio gate on all the time
timeout = System.currentTimeMillis() + 1000;
}
public boolean generate(float[] soundGeneratorWorkBuffer, int from,
int to) {
//appendPos(state ? totalCount : -totalCount-1);
return wasSet;
}
/* (non-Javadoc)
* @see v9t9.base.sound.ITimeAdjustSoundVoice#flushAudio(float[], int, int)
*/
@Override
public synchronized boolean flushAudio(float[] soundGeneratorWorkBuffer, int from,
int to, int total_unused) {
boolean generated = false;
if (origState && System.currentTimeMillis() >= timeout) {
deltaIdx = 0;
prevCycleRemainder = 0;
frac = 0;
origState = false;
timeout = 0;
}
if (from < to && (deltaIdx > 0 || origState)) {
generated = true;
int ratio = 128 + balance;
float sampleL = ((256 - ratio) * 1f) / 128.f;
float sampleR = (ratio * 1f) / 128.f;
boolean on = origState;
// frame count
float fpos = from / 2 + frac;
float fto = to / 2;
int idx = 0;
while (fpos < fto) {
float flen;
float fnext;
// if (idx == -1) {
// if (prevCycleRemainder != 0) {
// // account for remainder of previous cycle
// flen = prevCycleRemainder;
// } else {
// idx++;
// continue;
// }
// }
// else
if (idx < deltaIdx) {
flen = Math.abs(deltas[idx]);
on = deltas[idx] >= 0;
if (origState != on && flen > 0 && frac != 0 && fpos < fto) {
// handle transition from previous
float alpha = frac;
if (on) {
alpha = 1 - alpha;
}
int spos = ((int) fpos) * 2;
soundGeneratorWorkBuffer[spos] += alpha * sampleL;
soundGeneratorWorkBuffer[spos + 1] += alpha * sampleR;
fpos++;
flen--;
}
origState = on;
} else {
// fill remainder of transition
flen = fto - fpos;
}
fnext = fpos + flen;
if (fnext > fto) {
// only getting part of this one --
// bite off what we can chew
//prevCycleRemainder = (fnext - fto);
fnext = fto;
flen = fnext - fpos;
//System.out.println(idx+": "+flen);
if (idx >= 0) {
if (on) {
deltas[idx] -= flen;
} else {
deltas[idx] += flen;
}
}
} else {
// prevCycleRemainder = 0;
// will get it all!
idx++;
}
frac = fnext - (int) fnext;
// fill integral positions
if (on) {
int spos = ((int) fpos) * 2;
int snext = ((int) fnext) * 2;
while (spos < snext) {
soundGeneratorWorkBuffer[spos] += sampleL;
soundGeneratorWorkBuffer[spos + 1] += sampleR;
spos += 2;
}
}
fpos = fnext;
}
if (idx < deltaIdx) {
System.arraycopy(deltas, idx, deltas, 0, deltaIdx - idx);
deltaIdx -= idx;
} else {
deltaIdx = 0;
}
}
return generated;
}
@Override
public void loadState(ISettingSection settings) {
if (settings == null) return;
super.loadState(settings);
setVolume((byte) (settings.getBoolean("State") ? MAX_VOLUME : 0));
}
@Override
public void saveState(ISettingSection settings) {
super.saveState(settings);
settings.put("State", Boolean.toString(getVolume() != 0));
}
}