1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.mobileer.miditools.synth; 18 19 import android.media.midi.MidiReceiver; 20 import android.util.Log; 21 22 import com.mobileer.miditools.MidiConstants; 23 import com.mobileer.miditools.MidiEventScheduler; 24 import com.mobileer.miditools.MidiFramer; 25 26 import java.io.IOException; 27 import java.util.ArrayList; 28 import java.util.Hashtable; 29 import java.util.Iterator; 30 31 /** 32 * Very simple polyphonic, single channel synthesizer. It runs a background 33 * thread that processes MIDI events and synthesizes audio. 34 */ 35 public class SynthEngine extends MidiReceiver { 36 37 private static final String TAG = "SynthEngine"; 38 // 64 is the greatest common divisor of 192 and 128 39 private static final int DEFAULT_FRAMES_PER_BLOCK = 64; 40 private static final int SAMPLES_PER_FRAME = 2; 41 42 private volatile boolean mThreadEnabled; 43 private Thread mThread; 44 private float[] mBuffer = null; 45 private float mFrequencyScaler = 1.0f; 46 private float mBendRange = 2.0f; // semitones 47 private int mProgram; 48 49 private ArrayList<SynthVoice> mFreeVoices = new ArrayList<SynthVoice>(); 50 private Hashtable<Integer, SynthVoice> 51 mVoices = new Hashtable<Integer, SynthVoice>(); 52 private MidiEventScheduler mEventScheduler; 53 private MidiFramer mFramer; 54 private MidiReceiver mReceiver = new MyReceiver(); 55 private SimpleAudioOutput mAudioOutput; 56 private int mSampleRate; 57 private int mFramesPerBlock = DEFAULT_FRAMES_PER_BLOCK; 58 private int mMidiByteCount; 59 SynthEngine()60 public SynthEngine() { 61 this(new SimpleAudioOutput()); 62 } 63 SynthEngine(SimpleAudioOutput audioOutput)64 public SynthEngine(SimpleAudioOutput audioOutput) { 65 mAudioOutput = audioOutput; 66 mReceiver = new MyReceiver(); 67 mFramer = new MidiFramer(mReceiver); 68 } 69 getAudioOutput()70 public SimpleAudioOutput getAudioOutput() { 71 return mAudioOutput; 72 } 73 74 /* This will be called when MIDI data arrives. */ 75 @Override onSend(byte[] data, int offset, int count, long timestamp)76 public void onSend(byte[] data, int offset, int count, long timestamp) 77 throws IOException { 78 if (mEventScheduler != null) { 79 if (!MidiConstants.isAllActiveSensing(data, offset, count)) { 80 mEventScheduler.getReceiver().send(data, offset, count, 81 timestamp); 82 } 83 } 84 mMidiByteCount += count; 85 } 86 87 /** 88 * Call this before the engine is started. 89 * @param framesPerBlock 90 */ setFramesPerBlock(int framesPerBlock)91 public void setFramesPerBlock(int framesPerBlock) { 92 mFramesPerBlock = framesPerBlock; 93 } 94 95 96 private class MyReceiver extends MidiReceiver { 97 @Override onSend(byte[] data, int offset, int count, long timestamp)98 public void onSend(byte[] data, int offset, int count, long timestamp) 99 throws IOException { 100 byte command = (byte) (data[0] & MidiConstants.STATUS_COMMAND_MASK); 101 int channel = (byte) (data[0] & MidiConstants.STATUS_CHANNEL_MASK); 102 switch (command) { 103 case MidiConstants.STATUS_NOTE_OFF: 104 noteOff(channel, data[1], data[2]); 105 break; 106 case MidiConstants.STATUS_NOTE_ON: 107 noteOn(channel, data[1], data[2]); 108 break; 109 case MidiConstants.STATUS_PITCH_BEND: 110 int bend = (data[2] << 7) + data[1]; 111 pitchBend(channel, bend); 112 break; 113 case MidiConstants.STATUS_PROGRAM_CHANGE: 114 mProgram = data[1]; 115 mFreeVoices.clear(); 116 break; 117 default: 118 logMidiMessage(data, offset, count); 119 break; 120 } 121 } 122 } 123 124 class MyRunnable implements Runnable { 125 @Override run()126 public void run() { 127 try { 128 mAudioOutput.start(mFramesPerBlock); 129 mSampleRate = mAudioOutput.getFrameRate(); // rate is now valid 130 if (mBuffer == null) { 131 mBuffer = new float[mFramesPerBlock * SAMPLES_PER_FRAME]; 132 } 133 onLoopStarted(); 134 // The safest way to exit from a thread is to check a variable. 135 while (mThreadEnabled) { 136 processMidiEvents(); 137 generateBuffer(); 138 float[] buffer = mBuffer; 139 mAudioOutput.write(buffer, 0, buffer.length); 140 onBufferCompleted(mFramesPerBlock); 141 } 142 } catch (Exception e) { 143 Log.e(TAG, "SynthEngine background thread exception.", e); 144 } finally { 145 onLoopEnded(); 146 mAudioOutput.stop(); 147 } 148 } 149 } 150 151 /** 152 * This is called from the synthesis thread before it starts looping. 153 */ onLoopStarted()154 public void onLoopStarted() { 155 } 156 157 /** 158 * This is called once at the end of each synthesis loop. 159 * 160 * @param framesPerBuffer 161 */ onBufferCompleted(int framesPerBuffer)162 public void onBufferCompleted(int framesPerBuffer) { 163 } 164 165 /** 166 * This is called from the synthesis thread when it stops looping. 167 */ onLoopEnded()168 public void onLoopEnded() { 169 } 170 171 /** 172 * Assume message has been aligned to the start of a MIDI message. 173 * 174 * @param data 175 * @param offset 176 * @param count 177 */ logMidiMessage(byte[] data, int offset, int count)178 public void logMidiMessage(byte[] data, int offset, int count) { 179 String text = "Received: "; 180 for (int i = 0; i < count; i++) { 181 text += String.format("0x%02X, ", data[offset + i]); 182 } 183 Log.i(TAG, text); 184 } 185 186 /** 187 * @throws IOException 188 * 189 */ processMidiEvents()190 private void processMidiEvents() throws IOException { 191 long now = System.nanoTime(); // TODO use audio presentation time 192 MidiEventScheduler.MidiEvent event = (MidiEventScheduler.MidiEvent) mEventScheduler.getNextEvent(now); 193 while (event != null) { 194 mFramer.send(event.data, 0, event.count, event.getTimestamp()); 195 mEventScheduler.addEventToPool(event); 196 event = (MidiEventScheduler.MidiEvent) mEventScheduler.getNextEvent(now); 197 } 198 } 199 200 /** 201 * Mix the output of each active voice into a buffer. 202 */ generateBuffer()203 private void generateBuffer() { 204 float[] buffer = mBuffer; 205 for (int i = 0; i < buffer.length; i++) { 206 buffer[i] = 0.0f; 207 } 208 Iterator<SynthVoice> iterator = mVoices.values().iterator(); 209 while (iterator.hasNext()) { 210 SynthVoice voice = iterator.next(); 211 if (voice.isDone()) { 212 iterator.remove(); 213 // mFreeVoices.add(voice); 214 } else { 215 voice.mix(buffer, SAMPLES_PER_FRAME, 0.25f); 216 } 217 } 218 } 219 noteOff(int channel, int noteIndex, int velocity)220 public void noteOff(int channel, int noteIndex, int velocity) { 221 SynthVoice voice = mVoices.get(noteIndex); 222 if (voice != null) { 223 voice.noteOff(); 224 } 225 } 226 allNotesOff()227 public void allNotesOff() { 228 Iterator<SynthVoice> iterator = mVoices.values().iterator(); 229 while (iterator.hasNext()) { 230 SynthVoice voice = iterator.next(); 231 voice.noteOff(); 232 } 233 } 234 235 /** 236 * Create a SynthVoice. 237 */ createVoice(int program)238 public SynthVoice createVoice(int program) { 239 // For every odd program number use a sine wave. 240 if ((program & 1) == 1) { 241 return new SineVoice(mSampleRate); 242 } else { 243 return new SawVoice(mSampleRate); 244 } 245 } 246 247 /** 248 * 249 * @param channel 250 * @param noteIndex 251 * @param velocity 252 */ noteOn(int channel, int noteIndex, int velocity)253 public void noteOn(int channel, int noteIndex, int velocity) { 254 if (velocity == 0) { 255 noteOff(channel, noteIndex, velocity); 256 } else { 257 mVoices.remove(noteIndex); 258 SynthVoice voice; 259 if (mFreeVoices.size() > 0) { 260 voice = mFreeVoices.remove(mFreeVoices.size() - 1); 261 } else { 262 voice = createVoice(mProgram); 263 } 264 voice.setFrequencyScaler(mFrequencyScaler); 265 voice.noteOn(noteIndex, velocity); 266 mVoices.put(noteIndex, voice); 267 } 268 } 269 pitchBend(int channel, int bend)270 public void pitchBend(int channel, int bend) { 271 double semitones = (mBendRange * (bend - 0x2000)) / 0x2000; 272 mFrequencyScaler = (float) Math.pow(2.0, semitones / 12.0); 273 Iterator<SynthVoice> iterator = mVoices.values().iterator(); 274 while (iterator.hasNext()) { 275 SynthVoice voice = iterator.next(); 276 voice.setFrequencyScaler(mFrequencyScaler); 277 } 278 } 279 280 /** 281 * Start the synthesizer. 282 */ start()283 public void start() { 284 stop(); 285 mThreadEnabled = true; 286 mThread = new Thread(new MyRunnable()); 287 mEventScheduler = new MidiEventScheduler(); 288 mThread.start(); 289 } 290 291 /** 292 * Stop the synthesizer. 293 */ stop()294 public void stop() { 295 mThreadEnabled = false; 296 if (mThread != null) { 297 try { 298 mThread.interrupt(); 299 mThread.join(500); 300 } catch (InterruptedException e) { 301 // OK, just stopping safely. 302 } 303 mThread = null; 304 mEventScheduler = null; 305 } 306 } 307 getLatencyController()308 public LatencyController getLatencyController() { 309 return mAudioOutput.getLatencyController(); 310 } 311 getMidiByteCount()312 public int getMidiByteCount() { 313 return mMidiByteCount; 314 } 315 } 316