[331] | 1 | package org.apollo.audio;
|
---|
| 2 |
|
---|
| 3 | import java.util.Timer;
|
---|
| 4 | import java.util.TimerTask;
|
---|
| 5 |
|
---|
| 6 | import javax.sound.midi.InvalidMidiDataException;
|
---|
| 7 | import javax.sound.midi.MidiSystem;
|
---|
| 8 | import javax.sound.midi.MidiUnavailableException;
|
---|
| 9 | import javax.sound.midi.Receiver;
|
---|
| 10 | import javax.sound.midi.ShortMessage;
|
---|
| 11 |
|
---|
| 12 | import org.apollo.mvc.AbstractSubject;
|
---|
[332] | 13 | import org.apollo.mvc.Observer;
|
---|
| 14 | import org.apollo.mvc.Subject;
|
---|
[331] | 15 | import org.apollo.mvc.SubjectChangedEvent;
|
---|
| 16 |
|
---|
| 17 | /**
|
---|
| 18 | * A midi-based metronome for Apollo
|
---|
| 19 | * @author Brook Novak
|
---|
| 20 | *
|
---|
| 21 | */
|
---|
| 22 | public 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
|
---|
[332] | 35 | private boolean enabled = false; // purely used by outside
|
---|
[331] | 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() {
|
---|
[332] | 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 | });
|
---|
[331] | 98 | }
|
---|
| 99 |
|
---|
| 100 | /**
|
---|
| 101 | * @return
|
---|
| 102 | * The singleton instance
|
---|
| 103 | */
|
---|
| 104 | public static Metronome getInstance() {
|
---|
| 105 | return instance;
|
---|
| 106 | }
|
---|
| 107 |
|
---|
[332] | 108 | private void intialize() throws MidiUnavailableException {
|
---|
[331] | 109 | if (receiver == null) {
|
---|
| 110 | receiver = MidiSystem.getReceiver();
|
---|
| 111 | }
|
---|
| 112 | }
|
---|
[332] | 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 | }
|
---|
[331] | 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 | */
|
---|
[332] | 150 | public void start() throws MidiUnavailableException {
|
---|
[331] | 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 | */
|
---|
[332] | 159 | private void start(boolean isAdjusting) throws MidiUnavailableException {
|
---|
| 160 |
|
---|
[331] | 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 | */
|
---|
[332] | 262 | public void setTempo(int bpm) throws MidiUnavailableException {
|
---|
[331] | 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 |
|
---|
[332] | 289 | if (receiver == null) return;
|
---|
| 290 |
|
---|
[331] | 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 | }
|
---|
[332] | 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 | }
|
---|
[331] | 323 |
|
---|
[332] | 324 |
|
---|
| 325 |
|
---|
[331] | 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();
|
---|
[332] | 340 | }
|
---|
[331] | 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 | }
|
---|