JOAL v2.6.0-rc-20250712
JOAL, OpenAL® API Binding for Java™ (public API).
SimpleSineSynth.java
Go to the documentation of this file.
1/**
2 * Copyright 2023 JogAmp Community. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification, are
5 * permitted provided that the following conditions are met:
6 *
7 * 1. Redistributions of source code must retain the above copyright notice, this list of
8 * conditions and the following disclaimer.
9 *
10 * 2. Redistributions in binary form must reproduce the above copyright notice, this list
11 * of conditions and the following disclaimer in the documentation and/or other materials
12 * provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
15 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
16 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
21 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
22 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 *
24 * The views and conclusions contained in the software and documentation are those of the
25 * authors and should not be interpreted as representing official policies, either expressed
26 * or implied, of JogAmp Community.
27 */
28package com.jogamp.openal.util;
29
30import java.nio.ByteBuffer;
31import java.nio.FloatBuffer;
32
33import com.jogamp.common.av.AudioFormat;
34import com.jogamp.common.av.AudioSink;
35import com.jogamp.common.av.PTS;
36import com.jogamp.common.nio.Buffers;
37import com.jogamp.common.os.Platform;
38import com.jogamp.common.util.InterruptSource;
39import com.jogamp.common.util.InterruptedRuntimeException;
40import com.jogamp.common.util.SourcedInterruptedException;
41import com.jogamp.common.util.WorkerThread;
42import com.jogamp.openal.sound3d.Context;
43import com.jogamp.openal.sound3d.Device;
44import com.jogamp.openal.sound3d.Source;
45
46/**
47 * A continuous simple off-thread mutable sine wave synthesizer.
48 * <p>
49 * Implementation utilizes an off-thread worker thread streaming the generated wave to OpenAL,
50 * allowing to change frequency and amplitude without disturbance.
51 * </p>
52 */
53public final class SimpleSineSynth {
54 private static final boolean DEBUG = false;
55
56 /** The value PI, i.e. 180 degrees in radians. */
57 private static final float PI = 3.14159265358979323846f;
58
59 /** The value 2PI, i.e. 360 degrees in radians. */
60 private static final float TWO_PI = 2f * PI;
61
62 private static final float EPSILON = 1.1920929E-7f; // Float.MIN_VALUE == 1.4e-45f ; double EPSILON 2.220446049250313E-16d
63
64 private static final float SHORT_MAX = 32767.0f; // == Short.MAX_VALUE
65
66 public static final float MIDDLE_C = 261.625f;
67
68 private final ALAudioSink audioSink;
69 private final Object stateLock = new Object();
70 private volatile float audioAmplitude = 1.0f;
71 private volatile float audioFreq = MIDDLE_C;
72 private volatile int nextAudioPTS = 0;
73 private SynthWorker streamWorker;
74
75 public SimpleSineSynth() {
76 this(null);
77 }
78 public SimpleSineSynth(final Device device) {
79 audioSink = new ALAudioSink(device);
80 streamWorker = new SynthWorker();
81 }
82
83 public ALAudioSink getSink() { return audioSink; }
84 /** Return this instance's OpenAL {@link Device}. */
85 public final Device getDevice() { return audioSink.getDevice(); }
86 /** Return this instance's OpenAL {@link Context}. */
87 public final Context getContext() { return audioSink.getContext(); }
88 /** Return this instance's OpenAL {@link Source}. */
89 public final Source getSource() { return audioSink.getSource(); }
90
91 public void setFreq(final float f) {
92 audioFreq = f;
93 }
94 public float getFreq() { return audioFreq; }
95
96 public void setAmplitude(final float a) {
97 audioAmplitude = Math.min(1.0f, Math.max(0.0f, a)); // clip [0..1]
98 }
99 public float getAmplitude() { return audioAmplitude; }
100
101 /** Returns latency or frame-duration in milliseconds */
102 public int getLatency() { return null != streamWorker ? streamWorker.frameDuration : 2*AudioSink.DefaultFrameDuration; }
103
104 public void play() {
105 synchronized( stateLock ) {
106 if( null == streamWorker ) {
107 streamWorker = new SynthWorker();
108 }
109 streamWorker.doResume();
110 }
111 }
112
113 public void pause() {
114 synchronized( stateLock ) {
115 if( null != streamWorker ) {
116 streamWorker.doPause(true);
117 }
118 }
119 }
120
121 public void stop() {
122 synchronized( stateLock ) {
123 if( null != streamWorker ) {
124 streamWorker.doStop();
125 streamWorker = null;
126 } else {
127 audioSink.destroy();
128 }
129 }
130 }
131
132 public boolean isPlaying() {
133 synchronized( stateLock ) {
134 if( null != streamWorker ) {
135 return streamWorker.isPlaying();
136 }
137 }
138 return false;
139 }
140
141 public boolean isRunning() {
142 synchronized( stateLock ) {
143 if( null != streamWorker ) {
144 return streamWorker.isRunning();
145 }
146 }
147 return false;
148 }
149
150 public int getNextPTS() { return nextAudioPTS; }
151
152 public PTS getPTS() { return audioSink.getPTS(); }
153
154 @Override
155 public final String toString() {
156 synchronized( stateLock ) {
157 final int pts = getPTS().getLast();
158 final int lag = getNextPTS() - pts;
159 return getClass().getSimpleName()+"[f "+audioFreq+", a "+audioAmplitude+", latency "+getLatency()+
160 ", state[running "+isRunning()+", playing "+isPlaying()+"], pts[next "+getNextPTS()+", play "+pts+", lag "+lag+"], "+audioSink.toString()+"]";
161 }
162 }
163
164 private static ByteBuffer allocate(final int size) {
165 // return ByteBuffer.allocate(size);
166 return Buffers.newDirectByteBuffer(size);
167 }
168
169 class SynthWorker {
170 private final boolean useFloat32SampleType;
171 private final int bytesPerSample;
172 private final AudioFormat audioFormat;
173 private ByteBuffer sampleBuffer;
174 private int frameDuration;
175 private int audioQueueLimit;
176
177 private float lastFreq;
178 private float nextSin;
179 private boolean upSin;
180 private int nextStep;
181
182 private final WorkerThread.StateCallback stateCB = (final WorkerThread self, final WorkerThread.StateCallback.State cause) -> {
183 switch( cause ) {
184 case INIT:
185 nextAudioPTS = (int)Platform.currentMillis();
186 break;
187 case PAUSED:
188 audioSink.pause();
189 break;
190 case RESUMED:
191 nextAudioPTS = (int)Platform.currentMillis();
192 audioSink.play();
193 break;
194 case END:
195 break;
196 default:
197 break;
198 }
199 };
200 private final WorkerThread.Callback action = (final WorkerThread aaa) -> {
201 enqueueWave();
202 };
203 final WorkerThread wt =new WorkerThread(null, null, true /* daemonThread */, action, stateCB);
204
205 /**
206 * Starts this daemon thread,
207 * <p>
208 * This thread pauses after it's started!
209 * </p>
210 **/
211 SynthWorker() {
212 synchronized(this) {
213 nextAudioPTS = 0;
214
215 // Note: float32 is OpenAL-Soft's internally used format to mix samples etc.
216 final AudioFormat f32 = new AudioFormat(audioSink.getPreferredFormat().sampleRate, 4<<3, 1, true /* signed */,
217 false /* fixed point */, false /* planar */, true /* littleEndian */);
218 if( audioSink.isSupported(f32) ) {
219 useFloat32SampleType = true;
220 bytesPerSample = 4;
221 audioFormat = f32;
222 } else {
223 useFloat32SampleType = false;
224 bytesPerSample = 2;
225 audioFormat = new AudioFormat(audioSink.getPreferredFormat().sampleRate, bytesPerSample<<3, 1, true /* signed */,
226 true /* fixed point */, false /* planar */, true /* littleEndian */);
227 }
228 System.err.println("OpenAL float32 supported: "+useFloat32SampleType);
229
230 sampleBuffer = allocate( audioFormat.getDurationsByteSize(30/1000f) ); // pre-allocate buffer for 30ms
231
232 // clip [16 .. 2*AudioSink.DefaultFrameDuration]
233 frameDuration = 10; // let's try for the best ..
234 audioQueueLimit = Math.max( 16, Math.min(3*AudioSink.DefaultFrameDuration, 3*Math.round( 1000f*audioSink.getDefaultLatency() ) ) ); // ms
235
236 audioSink.init(audioFormat, frameDuration, audioQueueLimit);
237 frameDuration = Math.round( 1000f*audioSink.getLatency() ); // actual number
238 lastFreq = 0;
239 nextSin = 0;
240 upSin = true;
241 nextStep = 0;
242
243 wt.start( true );
244 }
245 }
246
247 private final int findNextStep(final boolean upSin, final float nextSin, final float freq, final int sampleRate, final int sampleCount) {
248 final float sample_step = ( TWO_PI * freq ) / sampleRate;
249
250 float s_diff = Float.MAX_VALUE;
251 float s_best = 0;
252 int i_best = -1;
253 float s0 = 0;
254 for(int i=0; i < sampleCount && s_diff >= EPSILON ; ++i) {
255 final float s1 = (float) Math.sin( sample_step * i );
256 final float s_d = Math.abs(nextSin - s1);
257 if( s_d < s_diff && ( ( upSin && s1 >= s0 ) || ( !upSin && s1 < s0 ) ) ) {
258 s_best = s1;
259 s_diff = s_d;
260 i_best = i;
261 }
262 s0 = s1;
263 }
264 if( DEBUG ) {
265 System.err.printf("%nBest: %d/[%d..%d]: s %f / %f (up %b), s_diff %f%n", i_best, 0, sampleCount, s_best, nextSin, upSin, s_diff);
266 }
267 return i_best;
268 }
269
270 private final void enqueueWave() {
271 // use local cache of volatiles, stable values
272 final float freq = audioFreq;
273 final float amp = audioAmplitude;
274
275 final float period = 1.0f / freq; // [s]
276 final float sample_step = ( TWO_PI * freq ) / audioFormat.sampleRate;
277
278 final float duration = frameDuration / 1000.0f; // [s]
279 final int sample_count = (int)( duration * audioFormat.sampleRate ); // [n]
280
281 final boolean overflow;
282 final boolean changedFreq;
283 if( Math.abs( freq - lastFreq ) >= EPSILON ) {
284 changedFreq = true;
285 overflow = false;
286 lastFreq = freq;
287 nextStep = findNextStep(upSin, nextSin, freq, audioFormat.sampleRate, sample_count);
288 } else {
289 changedFreq = false;
290 if( nextStep + sample_count >= Integer.MAX_VALUE/1000 ) {
291 nextStep = findNextStep(upSin, nextSin, freq, audioFormat.sampleRate, sample_count);
292 overflow = true;
293 } else {
294 overflow = false;
295 }
296 }
297
298 if( DEBUG ) {
299 if( changedFreq || overflow ) {
300 final float wave_count = duration / period;
301 System.err.printf("%nFreq %f Hz, period %f [ms], waves %.2f, duration %f [ms], sample[count %d, rate %d, step %f, next[up %b, sin %f, step %d]]%n", freq,
302 1000.0*period, wave_count, 1000.0*duration, sample_count, audioFormat.sampleRate, sample_step, upSin, nextSin, nextStep);
303 // System.err.println(Synth02AL.this.toString());
304 }
305 }
306
307 if( sampleBuffer.capacity() < bytesPerSample*sample_count ) {
308 if( DEBUG ) {
309 System.err.printf("SampleBuffer grow: %d -> %d%n", sampleBuffer.capacity(), bytesPerSample*sample_count);
310 }
311 sampleBuffer = allocate(bytesPerSample*sample_count);
312 }
313
314 {
315 int i;
316 float s = 0;
317 if( useFloat32SampleType ) {
318 final FloatBuffer f32sb = sampleBuffer.asFloatBuffer();
319 final int l = nextStep;
320 for(i=l; i<l+sample_count; ++i) {
321 s = (float) Math.sin( sample_step * i );
322 f32sb.put(s * amp);
323 }
324 } else {
325 final int l = nextStep;
326 for(i=l; i<l+sample_count; ++i) {
327 s = (float) Math.sin( sample_step * i );
328 final short s16 = (short)( SHORT_MAX * s * amp );
329 sampleBuffer.put( (byte) ( s16 & 0xff ) );
330 sampleBuffer.put( (byte) ( ( s16 >>> 8 ) & 0xff ) );
331 }
332 }
333 nextStep = i;
334 nextSin = (float) Math.sin( sample_step * nextStep );
335 upSin = nextSin >= s;
336 }
337 sampleBuffer.rewind();
338 audioSink.enqueueData(nextAudioPTS, sampleBuffer, sample_count*bytesPerSample);
339 sampleBuffer.clear();
340 nextAudioPTS += frameDuration;
341 }
342
343 public final synchronized void doPause(final boolean waitUntilDone) {
344 wt.pause(waitUntilDone);;
345 }
346 public final synchronized void doResume() {
347 wt.resume();
348 }
349 public final synchronized void doStop() {
350 wt.stop(true);
351 audioSink.destroy();
352 }
353 public final boolean isRunning() { return wt.isRunning(); }
354 public final boolean isPlaying() { return wt.isActive(); }
355 }
356}
This class provides a Sound3D Context associated with a specified device.
Definition: Context.java:49
This class provides a handle to a specific audio device.
Definition: Device.java:46
This class is used to represent sound-producing objects in the Sound3D environment.
Definition: Source.java:48
final AudioFrame enqueueData(final int pts, final ByteBuffer bytes, final int byteCount)
final boolean init(final AudioFormat requestedFormat, final int frameDurationHint, final int queueSize)
final boolean isSupported(final AudioFormat format)
final Context getContext()
Return this instance's OpenAL Context.
final AudioFormat getPreferredFormat()
final Device getDevice()
Return this instance's OpenAL Device.
final Source getSource()
Return this instance's OpenAL Source.
A continuous simple off-thread mutable sine wave synthesizer.
int getLatency()
Returns latency or frame-duration in milliseconds.
final Source getSource()
Return this instance's OpenAL Source.
final Device getDevice()
Return this instance's OpenAL Device.
final Context getContext()
Return this instance's OpenAL Context.