source: trunk/src/org/apollo/audio/Metronome.java@ 1102

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

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