Skip to content

Commit 2a40b77

Browse files

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* #%L
3+
* SciJava Common shared library for SciJava software.
4+
* %%
5+
* Copyright (C) 2009 - 2014 Board of Regents of the University of
6+
* Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
7+
* Institute of Molecular Cell Biology and Genetics.
8+
* %%
9+
* Redistribution and use in source and binary forms, with or without
10+
* modification, are permitted provided that the following conditions are met:
11+
*
12+
* 1. Redistributions of source code must retain the above copyright notice,
13+
* this list of conditions and the following disclaimer.
14+
* 2. Redistributions in binary form must reproduce the above copyright notice,
15+
* this list of conditions and the following disclaimer in the documentation
16+
* and/or other materials provided with the distribution.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
* POSSIBILITY OF SUCH DAMAGE.
29+
* #L%
30+
*/
31+
32+
package org.scijava.util;
33+
34+
import java.util.Arrays;
35+
36+
import javax.sound.sampled.AudioFormat;
37+
import javax.sound.sampled.AudioSystem;
38+
import javax.sound.sampled.LineUnavailableException;
39+
import javax.sound.sampled.SourceDataLine;
40+
41+
/**
42+
* Any QBasic fans out there? ;-)
43+
*
44+
* @author Curtis Rueden
45+
*/
46+
public class TunePlayer {
47+
48+
private final int sampleRate;
49+
50+
private byte[] buf = new byte[0];
51+
52+
private int noteLength = 1;
53+
private int tempo = 60;
54+
private int octave = 4;
55+
56+
public TunePlayer() {
57+
this(16 * 1000); // 16KHz
58+
}
59+
60+
public TunePlayer(final int sampleRate) {
61+
this.sampleRate = sampleRate;
62+
}
63+
64+
// -- TunePlayer methods --
65+
66+
public int getSampleRate() {
67+
return sampleRate;
68+
}
69+
70+
public int getNoteLength() {
71+
return noteLength;
72+
}
73+
74+
public int getTempo() {
75+
return tempo;
76+
}
77+
78+
public int getOctave() {
79+
return octave;
80+
}
81+
82+
/** Gets the value of the given tone for the current octave. */
83+
public int getTone(final int step, final char mod) {
84+
int tone = 12 * (getOctave() - 4) + step;
85+
if (mod == '#' || mod == '+') tone++;
86+
if (mod == '-') tone--;
87+
return tone;
88+
}
89+
90+
/** Gets the current note length in milliseconds, by the current tempo. */
91+
public int getMillis() {
92+
return toMillis(getNoteLength());
93+
}
94+
95+
/** Converts the given note length to milliseconds, by the current tempo. */
96+
public int toMillis(final int noteLen) {
97+
// one "beat" is one quarter note; hence:
98+
// noteLen of 1 = 4 beats per note
99+
// noteLen of 4 = 1 beat per note
100+
// noteLen of 8 = 1/2 beat per note
101+
// generally: beatsPerNote = 4 / noteLen
102+
103+
// tempo of 60 = 1 second per beat = 1000 ms per beat
104+
// tempo of 120 = 1/2 second per beat = 500 ms per beat
105+
// generally: msPerBeat = 6000 / tempo
106+
107+
// msPerNote = beatsPerNote * msPerBeat = 4 / noteLen * 6000 / tempo
108+
109+
// TODO - Determine why timing is off by a factor of 10.
110+
return 10 * 24000 / (noteLen * getTempo());
111+
}
112+
113+
public void setNoteLength(final int noteLength) {
114+
this.noteLength = noteLength;
115+
}
116+
117+
public void setTempo(final int tempo) {
118+
this.tempo = tempo;
119+
}
120+
121+
public void setOctave(final int octave) {
122+
this.octave = octave;
123+
}
124+
125+
public void downOctave() {
126+
octave--;
127+
}
128+
129+
public void upOctave() {
130+
octave++;
131+
}
132+
133+
public SourceDataLine openLine() throws LineUnavailableException {
134+
final AudioFormat af = new AudioFormat(sampleRate, 8, 1, true, true);
135+
final SourceDataLine line = AudioSystem.getSourceDataLine(af);
136+
line.open(af, sampleRate);
137+
line.start();
138+
return line;
139+
}
140+
141+
public void closeLine(final SourceDataLine line) {
142+
line.drain();
143+
line.close();
144+
}
145+
146+
public boolean play(final String commandString) {
147+
final SourceDataLine line;
148+
try {
149+
line = openLine();
150+
}
151+
catch (final LineUnavailableException e) {
152+
return false;
153+
}
154+
155+
final String[] tokens = commandString.toUpperCase().split(" ");
156+
for (final String token : tokens) {
157+
final char command = token.charAt(0);
158+
final String arg = token.substring(1);
159+
final char mod = token.length() > 1 ? token.charAt(1) : '\0';
160+
switch (command) {
161+
case '<': // down one octave
162+
downOctave();
163+
break;
164+
case '>': // up one octave
165+
upOctave();
166+
break;
167+
case 'A':
168+
play(line, getTone(9, mod));
169+
break;
170+
case 'B':
171+
play(line, getTone(11, mod));
172+
break;
173+
case 'C':
174+
play(line, getTone(0, mod));
175+
break;
176+
case 'D':
177+
play(line, getTone(2, mod));
178+
break;
179+
case 'E':
180+
play(line, getTone(4, mod));
181+
break;
182+
case 'F':
183+
play(line, getTone(5, mod));
184+
break;
185+
case 'G':
186+
play(line, getTone(7, mod));
187+
break;
188+
case 'L': // change note length
189+
setNoteLength(Integer.parseInt(arg));
190+
break;
191+
case 'M': // change music mode
192+
// TODO
193+
break;
194+
case 'N': // note
195+
final int note = Integer.parseInt(arg);
196+
if (note == 0) play(line, null);
197+
else play(line, note - 48);
198+
break;
199+
case 'O': // change octave
200+
setOctave(Integer.parseInt(arg));
201+
break;
202+
case 'P': // pause
203+
int len;
204+
try {
205+
len = Integer.parseInt(arg);
206+
}
207+
catch (final NumberFormatException exc) {
208+
len = noteLength;
209+
}
210+
play(line, null, toMillis(len));
211+
break;
212+
case 'T': // change tempo
213+
setTempo(Integer.parseInt(arg));
214+
break;
215+
default:
216+
throw new RuntimeException("Unknown command: " + command);
217+
}
218+
}
219+
220+
closeLine(line);
221+
return true;
222+
}
223+
224+
// -- Helper methods --
225+
226+
private void play(final SourceDataLine line, final Integer tone) {
227+
play(line, tone, getMillis());
228+
}
229+
230+
private void
231+
play(final SourceDataLine line, final Integer tone, final int ms)
232+
{
233+
final int length = fill(tone, ms);
234+
int count = 0;
235+
while (count < length) {
236+
final int r = line.write(buf, count, length - count);
237+
if (r <= 0) throw new RuntimeException("Could not write to line");
238+
count += r;
239+
}
240+
}
241+
242+
/**
243+
* @param tone Use 1 for A4, +1 for half-step up, -1 for half-step down.
244+
* @param ms Milliseconds of data to fill.
245+
* @return Length of buffer filled, in bytes.
246+
*/
247+
private int fill(final Integer tone, final int ms) {
248+
final int length = sampleRate * ms / 1000;
249+
if (length > buf.length) {
250+
// ensure internal buffer is large enough
251+
buf = new byte[length];
252+
}
253+
if (tone == null) {
254+
// rest data
255+
Arrays.fill(buf, 0, length, (byte) 0);
256+
}
257+
else {
258+
// tone data
259+
final double exp = ((double) tone - 1) / 12d;
260+
final double f = 440d * Math.pow(2d, exp);
261+
for (int i = 0; i < length; i++) {
262+
final double period = sampleRate / f;
263+
final double angle = 2 * Math.PI * i / period;
264+
buf[i] = (byte) (127 * Math.sin(angle));
265+
}
266+
}
267+
return length;
268+
}
269+
270+
}

0 commit comments

Comments
 (0)