1 package net.sf.jack4j.examples;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112 @Override
113 public int process(int bufferSize) throws Exception {
114
115 ByteBuffer outBuffer = audioOut.getByteBuffer(bufferSize);
116
117 outBuffer.clear();
118
119 TransportPosition transportPosition;
120
121
122 boolean produceClicks = false;
123 int samplesSinceLastClick = 0;
124 int clickPeriodInSamples = 0;
125 int beatsPerBar = 0;
126 int lastClickBeat = 0;
127
128
129 switch (currentMode) {
130 case STANDALONE:
131 if (started) {
132 produceClicks = true;
133
134
135
136 if (reset) {
137
138 standaloneSampleCounter = 0;
139 }
140
141 clickPeriodInSamples = (int) (getSampleRate() / (bpmTempo / 60.0));
142 samplesSinceLastClick = standaloneSampleCounter;
143
144
145 standaloneSampleCounter = (standaloneSampleCounter + bufferSize) % clickPeriodInSamples;
146 }
147 break;
148 case TRANSPORT_AWARE:
149 transportPosition = new TransportPosition();
150 if (queryTransport(transportPosition) == JackTransportState.ROLLING) {
151 produceClicks = true;
152 clickPeriodInSamples = (int) (getSampleRate() / (bpmTempo / 60.0));
153
154 samplesSinceLastClick = transportPosition.getFrame() % clickPeriodInSamples;
155 }
156 break;
157 case TRANSPORT_BBT_AWARE:
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
169 double tickLength = 60.0 / (transportBPM * transportPosition.getTicksPerBeat());
170
171 int bbtSamplesSinceBeatStart = (int) (transportPosition.getTick() * tickLength * getSampleRate());
172
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
185 for (int i = 0; i < bufferSize; i++) {
186
187 outBuffer.putFloat(i * (Float.SIZE / 8), 0.0f);
188 }
189
190
191
192 continueClickSound(outBuffer, bufferSize, 0);
193
194 if (produceClicks) {
195
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
216 return 0;
217 }
218
219
220
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
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
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
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
280 super(clientName, useExactName, canStartServer, serverName);
281
282
283
284
285 setDefaultThreadInitCallback();
286
287
288 setDefaultProcessCallback();
289
290
291 audioOut = addAudioPort("out", EnumSet.of(JackPortFlag.IS_OUTPUT));
292
293 currentMode = Mode.STANDALONE;
294 started = false;
295 reset = true;
296
297
298 }
299
300
301
302
303 public void run() throws JackException, IOException {
304
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
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 }