View Javadoc

1   package net.sf.jack4j.examples;
2   
3   /*
4   Copyright (C) 2008 Ondrej Par
5   
6   This program is free software; you can redistribute it and/or modify
7   it under the terms of the GNU Lesser General Public License as published by
8   the Free Software Foundation; either version 2.1 of the License, or
9   (at your option) any later version.
10  
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  GNU Lesser General Public License for more details.
15  
16  You should have received a copy of the GNU Lesser General Public License
17  along with this program; if not, write to the Free Software
18  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19  
20  */
21  
22  import java.io.BufferedReader;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.nio.ByteBuffer;
26  import java.util.EnumSet;
27  
28  import net.sf.jack4j.AbstractJackTransportClient;
29  import net.sf.jack4j.JackException;
30  import net.sf.jack4j.JackLocalPort;
31  import net.sf.jack4j.JackPortFlag;
32  import net.sf.jack4j.JackPositionBit;
33  import net.sf.jack4j.JackTransportState;
34  import net.sf.jack4j.TransportPosition;
35  
36  /**
37   * Metronome that can cooperate with Jack transport infrastructure.
38   * 
39   * <p>
40   * The metronome can run in one of three modes:
41   * <ul>
42   * <li>standalone - can be started or stopped manually, clicks at manually
43   * specified tempo; first click occurs just after the metronome is started.</li>
44   * <li>transport aware - clicks at manually specified tempo, starts and stops
45   * with Jack transport; first click occurs at zero time of Jack transport.</li>
46   * <li>transport BBT aware - clicks at tempo contolled by Timebase master,
47   * click occurs on each beat. Also, higher click occurs on the first beat within
48   * bar.</li>
49   * </ul>
50   * 
51   * For transport-aware and transport-BBT-aware mode, you need a tool that can
52   * control Jack transport (qjackctl, for example). For transport-BBT-aware mode,
53   * you also need a Timebase master that controls bar/beat/tick and BBT-offset
54   * information (you can use {@link TimebaseMaster} example from Jack4j package).
55   * 
56   * <p>
57   * See comments in the code that describe the usage of Jack4j library.
58   * 
59   * <p>
60   * To run the examples, you need to have the native Jack4j library in your
61   * system load path (LD_LIBRARY_PATH under Linux).
62   * 
63   * <p>
64   * This example client expects single command line parameter - client name.
65   * 
66   * @author repa
67   */
68  public class Metronome extends AbstractJackTransportClient {
69  
70  	private enum Mode {
71  		STANDALONE,
72  		TRANSPORT_AWARE,
73  		TRANSPORT_BBT_AWARE
74  	};
75  
76  	private JackLocalPort audioOut;
77  
78  	private Mode currentMode;
79  	private boolean reset;
80  	private boolean started = false;
81  	private int standaloneSampleCounter;
82  	private double bpmTempo = 120.0;
83  
84  	private float soundFrequency = 880.0f;
85  	private int clickDurationMillis = 10;
86  	private float attackLength = 0.01f;
87  	private float decayLength = 0.1f;
88  
89  	private boolean runningClick = false;
90  	private int runningClickSampleIdx;
91  	private float runningClickFrequency;
92  
93  	/**
94  	 * This is callback method, invoked by Jack client thread during each
95  	 * process cycle.
96  	 * 
97  	 * <p>
98  	 * This method produces the sound.
99  	 * 
100 	 * <p>
101 	 * The method is expected to put <code>bufferSize</code> samples into
102 	 * <i>buffer</i> associated with the <i>output audio port</i>.
103 	 * 
104 	 * <p>
105 	 * Port was created earlier, during client initialization.
106 	 * 
107 	 * <p>
108 	 * Note that this method is not synchronized, but the native code that
109 	 * invokes this method always obtains a lock on this JackClient instance, so
110 	 * it actually behaves as a synchronized method.
111 	 */
112 	@Override
113 	public int process(int bufferSize) throws Exception {
114 		// obtain ByteBuffer associated with the output audio port
115 		ByteBuffer outBuffer = audioOut.getByteBuffer(bufferSize);
116 		// ALWAYS clear the buffer before use! Otherwise, limit and position would be undefined:
117 		outBuffer.clear();
118 
119 		TransportPosition transportPosition;
120 
121 		// compute following information first:
122 		boolean produceClicks = false; // should clicks be produced at all?
123 		int samplesSinceLastClick = 0; // how many samples passed since the beginning of the last click?
124 		int clickPeriodInSamples = 0; // how many samples are between two clicks?
125 		int beatsPerBar = 0; // how many beats are within bar (only in TRANSPORT_BBT_AWARE mode)
126 		int lastClickBeat = 0; // in which beat occured the last click (only in TRANSPORT_BBT_AWARE mode)
127 
128 		// in different modes, these information is computed differently:
129 		switch (currentMode) {
130 			case STANDALONE: // produce clicks only when started manually
131 				if (started) {
132 					produceClicks = true;
133 
134 					// in STANDALONE mode, the standaloneSampleCounter counts samples since last click
135 
136 					if (reset) {
137 						// if the mode was just set, or we were just started, count from zero
138 						standaloneSampleCounter = 0;
139 					}
140 
141 					clickPeriodInSamples = (int) (getSampleRate() / (bpmTempo / 60.0));
142 					samplesSinceLastClick = standaloneSampleCounter;
143 
144 					// update standaloneSampleCounter for the next invocation of process() method
145 					standaloneSampleCounter = (standaloneSampleCounter + bufferSize) % clickPeriodInSamples;
146 				}
147 				break;
148 			case TRANSPORT_AWARE: // produce clicks only when transport is rolling
149 				transportPosition = new TransportPosition();
150 				if (queryTransport(transportPosition) == JackTransportState.ROLLING) {
151 					produceClicks = true;
152 					clickPeriodInSamples = (int) (getSampleRate() / (bpmTempo / 60.0));
153 					// use transport position to compute click positions
154 					samplesSinceLastClick = transportPosition.getFrame() % clickPeriodInSamples;
155 				}
156 				break;
157 			case TRANSPORT_BBT_AWARE: // produce clicks only when transport is rolling and timebase master is available
158 				transportPosition = new TransportPosition();
159 				if (queryTransport(transportPosition) == JackTransportState.ROLLING) {
160 					if (transportPosition.getValidBitsSet().contains(JackPositionBit.POSITION_BBT)
161 					        && transportPosition.getValidBitsSet().contains(JackPositionBit.BBT_FRAME_OFFSET)) {
162 						produceClicks = true;
163 
164 						double transportBPM = transportPosition.getBeatsPerMinute();
165 
166 						clickPeriodInSamples = (int) (getSampleRate() / (transportBPM / 60.0));
167 
168 						// compute length (in seconds) of a single BBT "tick"
169 						double tickLength = 60.0 / (transportBPM * transportPosition.getTicksPerBeat());
170 						// compute time (in samples) between start of current beat and current tick
171 						int bbtSamplesSinceBeatStart = (int) (transportPosition.getTick() * tickLength * getSampleRate());
172 						// add BBT offset, because the tick might begin before the current process phase started
173 						samplesSinceLastClick = bbtSamplesSinceBeatStart + transportPosition.getBbtOffset();
174 
175 						beatsPerBar = (int) transportPosition.getBeatsPerBar();
176 						lastClickBeat = transportPosition.getBeat();
177 					}
178 				}
179 				break;
180 		}
181 
182 		reset = false;
183 
184 		// pre-fill the buffer with zero samples
185 		for (int i = 0; i < bufferSize; i++) {
186 			// remember to multiply start position in float size in bytes
187 			outBuffer.putFloat(i * (Float.SIZE / 8), 0.0f);
188 		}
189 
190 		// in case that the click begun in the previous process cycle and is still playing,
191 		// generate samples into outBuffer.
192 		continueClickSound(outBuffer, bufferSize, 0);
193 
194 		if (produceClicks) {
195 			// start new clicks
196 			int nextClickAt;
197 			int nextClickBeat;
198 			if (samplesSinceLastClick == 0) {
199 				nextClickAt = 0;
200 				nextClickBeat = lastClickBeat;
201 			} else {
202 				nextClickAt = clickPeriodInSamples - samplesSinceLastClick;
203 				nextClickBeat = lastClickBeat + 1;
204 			}
205 
206 			while (nextClickAt < bufferSize) {
207 				if (nextClickAt >= 0) {
208 					startClickSound(outBuffer, bufferSize, nextClickAt, nextClickBeat, beatsPerBar);
209 				}
210 				nextClickAt += clickPeriodInSamples;
211 				nextClickBeat++;
212 			}
213 		}
214 
215 		// return zero to indicate that we're finished successfully
216 		return 0;
217 	}
218 
219 	/**
220 	 * Computes the sound and stores it into given buffer.
221 	 */
222 	private void continueClickSound(ByteBuffer outBuffer, int bufferSize, int startAtSample) throws JackException {
223 		if (!runningClick) {
224 			return;
225 		}
226 
227 		int sampleRate = getSampleRate();
228 
229 		// compute durations of the whole click, the attack, and decay
230 		int clickDurationSamples = clickDurationMillis * sampleRate / 1000;
231 		int attackLengthSamples = (int) (attackLength * clickDurationSamples);
232 		int decayLengthSamples = (int) (decayLength * clickDurationSamples);
233 		int decayStartSample = clickDurationSamples - decayLengthSamples;
234 
235 		// compute how many samples will be generated
236 		int remainingClickSamples = clickDurationSamples - runningClickSampleIdx;
237 		int remainingBufferSize = bufferSize - startAtSample;
238 		int generateSamples = Math.min(remainingClickSamples, remainingBufferSize);
239 
240 		double phasePerSample = 2 * Math.PI * runningClickFrequency / sampleRate;
241 
242 		for (int sampleIdx = 0; sampleIdx < generateSamples; sampleIdx++) {
243 			int clickSampleIdx = sampleIdx + runningClickSampleIdx;
244 
245 			float volume;
246 			if (clickSampleIdx < attackLengthSamples) {
247 				volume = (((float) clickSampleIdx) / attackLengthSamples);
248 			} else if (clickSampleIdx >= decayStartSample) {
249 				volume = (((float) (clickDurationSamples - clickSampleIdx)) / decayLengthSamples);
250 			} else {
251 				volume = 1.0f;
252 			}
253 
254 			float sample = (float) (volume * Math.sin(clickSampleIdx * phasePerSample));
255 
256 			// remember to multiply start position in float size in bytes
257 			outBuffer.putFloat((startAtSample + sampleIdx) * (Float.SIZE / 8), sample);
258 		}
259 
260 		runningClickSampleIdx += generateSamples;
261 		if (runningClickSampleIdx >= clickDurationSamples) {
262 			runningClick = false;
263 		}
264 	}
265 
266 	private void startClickSound(ByteBuffer outBuffer, int bufferSize, int startAtSample, int clickBeat, int beatsPerBar)
267 	        throws JackException {
268 		runningClick = true;
269 		runningClickSampleIdx = 0;
270 
271 		boolean highPitch = (beatsPerBar != 0) && (clickBeat % beatsPerBar == 1);
272 		runningClickFrequency = highPitch ? 2 * soundFrequency : soundFrequency;
273 
274 		continueClickSound(outBuffer, bufferSize, startAtSample);
275 	}
276 
277 	public Metronome(String clientName, boolean useExactName, boolean canStartServer, String serverName)
278 	        throws JackException {
279 		// Call to super constructor will register us with Jack server
280 		super(clientName, useExactName, canStartServer, serverName);
281 
282 		// Setting default ThreadInit callback is necessary, 
283 		// otherwise other callbacks won't work properly (the Jack client thread
284 		// must be registered with JVM first)
285 		setDefaultThreadInitCallback();
286 
287 		// Set the native callback that will invoke our process(int) method.
288 		setDefaultProcessCallback();
289 
290 		// Add output audio port.
291 		audioOut = addAudioPort("out", EnumSet.of(JackPortFlag.IS_OUTPUT));
292 
293 		currentMode = Mode.STANDALONE;
294 		started = false;
295 		reset = true;
296 
297 		// we are not activated yet, thus the callbacks won't be invoked
298 	}
299 
300 	/**
301 	 * Activation and main event loop.
302 	 */
303 	public void run() throws JackException, IOException {
304 		// Activate the client; until now, callbacks weren't invoked
305 		activate();
306 
307 		System.out.println("CONNECT THE OUTPUT PORT to the soundcard or other client to hear the sound.");
308 
309 		BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
310 
311 		System.out.println("Issue commands (\"h\" for help)");
312 		boolean cont = true;
313 		while (cont) {
314 			System.out.print("cmd> ");
315 
316 			String cmd = in.readLine();
317 			cmd = cmd.trim();
318 			if (cmd.isEmpty()) {
319 				continue;
320 			}
321 			char cmdChar = cmd.charAt(0);
322 
323 			switch (cmdChar) {
324 				case 'q':
325 					cont = false;
326 					break;
327 				case 'h':
328 					System.out.println("(h) print this help");
329 					System.out.println("(q) quit");
330 					System.out.println("(m) set mode");
331 					System.out.println("(s) start/stop (in standalone mode)");
332 					System.out.println("(t) set tempo (for standalone and transport-aware mode)");
333 					break;
334 				case 'm':
335 					updateMode(in);
336 					break;
337 				case 's':
338 					startOrStop();
339 					break;
340 				case 't':
341 					updateTempo(in);
342 					break;
343 				default:
344 					System.err.println("Unknown command: " + cmd);
345 			}
346 		}
347 
348 		// close the client (disconnect from Jack)
349 		close();
350 	}
351 
352 	private void startOrStop() {
353 		if (currentMode != Mode.STANDALONE) {
354 			System.out.println("Available only in standalone mode");
355 			return;
356 		}
357 
358 		started = !started;
359 		reset = true;
360 		System.out.println(started ? "Started" : "Stopped");
361 	}
362 
363 	private void updateMode(BufferedReader in) throws IOException {
364 		System.out.print("Enter mode (s=standalone, t=transport aware, b=transport BBT aware): ");
365 		String line = in.readLine();
366 
367 		line = line.trim();
368 		if (line.isEmpty()) {
369 			return;
370 		}
371 		char modeChar = line.charAt(0);
372 
373 		Mode newMode;
374 		switch (modeChar) {
375 			case 's':
376 				newMode = Mode.STANDALONE;
377 				break;
378 			case 't':
379 				newMode = Mode.TRANSPORT_AWARE;
380 				break;
381 			case 'b':
382 				newMode = Mode.TRANSPORT_BBT_AWARE;
383 				break;
384 			default:
385 				System.err.println("Unknown mode");
386 				return;
387 		}
388 
389 		System.out.println("Setting mode to " + newMode);
390 		currentMode = newMode;
391 		reset = true;
392 	}
393 
394 	private void updateTempo(BufferedReader in) throws IOException {
395 		System.out.print("Enter tempo: ");
396 		try {
397 			bpmTempo = Double.parseDouble(in.readLine());
398 		} catch (NumberFormatException e) {
399 			System.err.println("Can't parse value:");
400 			e.printStackTrace();
401 		}
402 	}
403 
404 	public static void main(String[] args) throws Exception {
405 		if (args.length != 1) {
406 			throw new IllegalArgumentException("Expecting single parameter (client name)");
407 		}
408 		String clientName = args[0];
409 
410 		Metronome metronome = new Metronome(clientName, false, true, null);
411 
412 		metronome.run();
413 	}
414 }