source: trunk/src_apollo/org/apollo/audio/Metronome.java@ 332

Last change on this file since 332 was 332, checked in by bjn8, 16 years ago

metronome improvements

File size: 8.5 KB
Line 
1package org.apollo.audio;
2
3import java.util.Timer;
4import java.util.TimerTask;
5
6import javax.sound.midi.InvalidMidiDataException;
7import javax.sound.midi.MidiSystem;
8import javax.sound.midi.MidiUnavailableException;
9import javax.sound.midi.Receiver;
10import javax.sound.midi.ShortMessage;
11
12import org.apollo.mvc.AbstractSubject;
13import org.apollo.mvc.Observer;
14import org.apollo.mvc.Subject;
15import org.apollo.mvc.SubjectChangedEvent;
16
17/**
18 * A midi-based metronome for Apollo
19 * @author Brook Novak
20 *
21 */
22public class Metronome extends AbstractSubject {
23
24 private Receiver receiver = null;
25
26 private ShortMessage accentBeatOn; // Accents on new meause
27 private ShortMessage accentBeatOff;
28
29 private ShortMessage beatOn;
30 private ShortMessage beatOff;
31
32 // Model data
33 private int tempoBPM = 100;
34 private int beatsPerMeasure = 4; // i.e. upper time sig
35 private boolean enabled = false; // purely used by outside
36
37 // Internel data
38 private int currentBeat; // wraps per measure
39 private int usecDelayBetweenBeats;
40
41 private Timer timer;
42
43 private static final int PERCUSSION_CHANNEL = 9;
44 private static final int ACOUSTIC_BASS = 35;
45 //private static final int ACOUSTIC_SNARE = 38;
46 //private static final int HAND_CLAP = 39;
47 //private static final int PEDAL_HIHAT = 44;
48 //private static final int LO_TOM = 45;
49 private static final int CLOSED_HIHAT = 42;
50 //private static final int CRASH_CYMBAL1 = 49;
51 //private static final int HI_TOM = 50;
52 //private static final int RIDE_BELL = 53;
53
54 private static Metronome instance = new Metronome();
55 private Metronome() {
56
57 try {
58 beatOn = createNoteOnMsg(CLOSED_HIHAT, 90);
59 beatOff = createNoteOffMsg(CLOSED_HIHAT);
60 accentBeatOn = createNoteOnMsg(ACOUSTIC_BASS,127);
61 accentBeatOff = createNoteOffMsg(ACOUSTIC_BASS);
62
63 } catch (InvalidMidiDataException e1) {
64 e1.printStackTrace();
65 }
66
67 RecordManager.getInstance().addObserver(new Observer() {
68
69 public Subject getObservedSubject() {
70 return null;
71 }
72
73 public void modelChanged(Subject source, SubjectChangedEvent event) {
74
75 if (!isEnabled()) return;
76
77 if (event.getID() == ApolloSubjectChangedEvent.CAPTURE_STARTED) {
78
79 if (isPlaying())
80 stop();
81
82 try {
83 start();
84 } catch (MidiUnavailableException e) {
85 e.printStackTrace();
86 }
87
88 } else if (event.getID() == ApolloSubjectChangedEvent.CAPTURE_STOPPED) {
89 stop();
90 }
91
92 }
93
94 public void setObservedSubject(Subject parent) {
95 }
96
97 });
98 }
99
100 /**
101 * @return
102 * The singleton instance
103 */
104 public static Metronome getInstance() {
105 return instance;
106 }
107
108 private void intialize() throws MidiUnavailableException {
109 if (receiver == null) {
110 receiver = MidiSystem.getReceiver();
111 }
112 }
113
114 /**
115 * In order to play sampled audio the midi receiver must be closed via this method.
116 */
117 public void release() {
118 stop();
119 if (receiver != null) {
120 receiver.close();
121 receiver = null;
122 }
123 }
124
125 private ShortMessage createNoteOnMsg(int note, int velocity) throws InvalidMidiDataException {
126 ShortMessage msg = new ShortMessage();
127 msg.setMessage(ShortMessage.NOTE_ON, PERCUSSION_CHANNEL, note, velocity);
128 return msg;
129 }
130
131 private ShortMessage createNoteOffMsg(int note) throws InvalidMidiDataException {
132 ShortMessage msg = new ShortMessage();
133 msg.setMessage(ShortMessage.NOTE_OFF, PERCUSSION_CHANNEL, note, 0);
134 return msg;
135 }
136
137 /**
138 * Starts the metronome. If the metronome is already playing then nothing will result on the
139 * invocation of this method.
140 *
141 * Fires a ApolloSubjectChangedEvent.METRONOME_STARTED event if started.
142 *
143 * @throws MidiUnavailableException
144 * If failed to initialize MIDI receiver.
145 *
146 * @throws InvalidMidiDataException
147 * If failed to initialize MIDI messages.
148 *
149 */
150 public void start() throws MidiUnavailableException {
151 start(false);
152 }
153
154 /**
155 * @param isAdjusting
156 * True to suppress event and avoid reseting beat counter and delay the playback
157 * to give seemless-like playback while adjust tempo in realtime
158 */
159 private void start(boolean isAdjusting) throws MidiUnavailableException {
160
161 intialize();
162
163 if (timer == null) {
164
165 long preDelay;
166
167 if (isAdjusting) {
168 assert(usecDelayBetweenBeats > 0);
169 preDelay = 100;
170 } else {
171 currentBeat = 0;
172 preDelay = 0;
173 }
174
175 usecDelayBetweenBeats = 60000000 / tempoBPM;
176
177
178
179 timer = new Timer("Metronome timer", false);
180 timer.scheduleAtFixedRate(new MetronomeTask(), preDelay, usecDelayBetweenBeats / 1000);
181
182 if(!isAdjusting) {
183 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.METRONOME_STARTED));
184 }
185
186 }
187 }
188
189 /**
190 * Stops the metronome from playing. If not playing then nothing will result in this call.
191 *
192 * Fires a ApolloSubjectChangedEvent.METRONOME_STOPPED event if stopped.
193 *
194 */
195 public void stop() {
196 stop(false);
197 }
198
199 private void stop(boolean supressEvent) {
200 if (timer != null) {
201 timer.cancel();
202 timer = null;
203 if (!supressEvent) {
204 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.METRONOME_STOPPED));
205 }
206 }
207 }
208
209 /**
210 * @return
211 * True if and only if the metronome is playing.
212 */
213 public boolean isPlaying() {
214 return timer != null;
215 }
216
217 /**
218 * Sets the beats-per-measure.
219 *
220 * Fires a ApolloSubjectChangedEvent.METRONOME_BEATS_PER_MEASURE_CHANGED event.
221 *
222 * @param bpm
223 * The amount of beats per measure. Must be larger than zero.
224 *
225 * @throws IllegalArgumentException
226 * If bpm is smaller or equal to zero
227 */
228 public void setBeatsPerMeasure(int bpm) {
229 if (bpm <= 0) throw new IllegalArgumentException("bpm <= 0");
230 if (this.beatsPerMeasure == bpm) return;
231 this.beatsPerMeasure = bpm;
232
233 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.METRONOME_BEATS_PER_MEASURE_CHANGED));
234 }
235
236 /**
237 * @return
238 * The current beats-per-measure setting. Never smaller or equal to zero.
239 */
240 public int getBeatsPerMeasure() {
241 return beatsPerMeasure;
242 }
243
244
245 /**
246 * Sets the tempo. Adjusts the time in realtime if it is currently playing.
247 *
248 * Fires a ApolloSubjectChangedEvent.METRONOME_TEMPO_CHANGED event.
249 *
250 * @param bpm
251 * In beats per minute. Must be larger than zero.
252 *
253 * @throws IllegalArgumentException
254 * If bpm is smaller or equal to zero
255 *
256 * @throws MidiUnavailableException
257 * If failed to initialize MIDI receiver if it had to be re-initialized.
258 *
259 * @throws InvalidMidiDataException
260 * If failed to initialize MIDI messages if they had to be re-initialized.
261 */
262 public void setTempo(int bpm) throws MidiUnavailableException {
263 if (bpm <= 0) throw new IllegalArgumentException("bpm <= 0");
264 if (this.tempoBPM == bpm) return;
265 this.tempoBPM = bpm;
266
267 // When the tempo changes while the metronome is playing, must reset the
268 // timer
269 if (timer != null) {
270 stop(true);
271 start(true);
272 }
273
274 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.METRONOME_TEMPO_CHANGED));
275 }
276 /**
277 *
278 * @return
279 * The current tempo setting in Beats Per Minute. Never smaller or equal to zero.
280 */
281 public int getTempo() {
282 return tempoBPM;
283 }
284
285 private class MetronomeTask extends TimerTask {
286
287 public void run() {
288
289 if (receiver == null) return;
290
291 // Select beat tone
292 ShortMessage selectedBeatOn;
293 ShortMessage selectedBeatOff;
294
295 if (currentBeat == 0) {
296 selectedBeatOn = accentBeatOn;
297 selectedBeatOff = accentBeatOff;
298 } else {
299 selectedBeatOn = beatOn;
300 selectedBeatOff = beatOff;
301 }
302
303 currentBeat ++;
304 if (currentBeat >= beatsPerMeasure) currentBeat = 0; // wrap per measure
305
306 // Play selected beat
307 receiver.send(selectedBeatOn, -1);
308 receiver.send(selectedBeatOff, 1);
309
310 }
311 }
312
313 public boolean isEnabled() {
314 return enabled;
315 }
316
317 public void setEnabled(boolean enabled) {
318 if (this.enabled != enabled) {
319 this.enabled = enabled;
320 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.METRONOME_ENABLED_CHANGED));
321 }
322 }
323
324
325
326 /**
327 * For debuf purposes
328 * @param args
329 */
330 public static void main(String[] args) {
331
332 try {
333
334 getInstance().start();
335
336
337 } catch (MidiUnavailableException e) {
338 // TODO Auto-generated catch block
339 e.printStackTrace();
340 }
341
342
343 try {
344 Thread.sleep(3000);
345 getInstance().setBeatsPerMeasure(2);
346 Thread.sleep(3000);
347 getInstance().setBeatsPerMeasure(3);
348 getInstance().setTempo(140);
349 Thread.sleep(4000);
350 getInstance().stop();
351 } catch (InterruptedException e) {
352 // TODO Auto-generated catch block
353 e.printStackTrace();
354 } catch (MidiUnavailableException e) {
355 // TODO Auto-generated catch block
356 e.printStackTrace();
357 }
358
359 }
360}
Note: See TracBrowser for help on using the repository browser.