1 /* 2 * Copyright (C) 2015 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.example.android.common.midi.synth; 18 19 import android.media.midi.MidiReceiver; 20 import android.util.Log; 21 22 import com.example.android.common.midi.MidiConstants; 23 import com.example.android.common.midi.MidiEventScheduler; 24 import com.example.android.common.midi.MidiEventScheduler.MidiEvent; 25 import com.example.android.common.midi.MidiFramer; 26 27 import java.io.IOException; 28 import java.util.ArrayList; 29 import java.util.Hashtable; 30 import java.util.Iterator; 31 32 /** 33 * Very simple polyphonic, single channel synthesizer. It runs a background 34 * thread that processes MIDI events and synthesizes audio. 35 */ 36 public class SynthEngine extends MidiReceiver { 37 38 private static final String TAG = "SynthEngine"; 39 40 public static final int FRAME_RATE = 48000; 41 private static final int FRAMES_PER_BUFFER = 240; 42 private static final int SAMPLES_PER_FRAME = 2; 43 44 private boolean go; 45 private Thread mThread; 46 private float[] mBuffer = new float[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME]; 47 private float mFrequencyScaler = 1.0f; 48 private float mBendRange = 2.0f; // semitones 49 private int mProgram; 50 51 private ArrayList<SynthVoice> mFreeVoices = new ArrayList<SynthVoice>(); 52 private Hashtable<Integer, SynthVoice> 53 mVoices = new Hashtable<Integer, SynthVoice>(); 54 private MidiEventScheduler mEventScheduler; 55 private MidiFramer mFramer; 56 private MidiReceiver mReceiver = new MyReceiver(); 57 private SimpleAudioOutput mAudioOutput; 58 59 public SynthEngine() { 60 this(new SimpleAudioOutput()); 61 } 62 63 public SynthEngine(SimpleAudioOutput audioOutput) { 64 mReceiver = new MyReceiver(); 65 mFramer = new MidiFramer(mReceiver); 66 mAudioOutput = audioOutput; 67 } 68 69 @Override 70 public void onSend(byte[] data, int offset, int count, long timestamp) 71 throws IOException { 72 if (mEventScheduler != null) { 73 if (!MidiConstants.isAllActiveSensing(data, offset, count)) { 74 mEventScheduler.getReceiver().send(data, offset, count, 75 timestamp); 76 } 77 } 78 } 79 80 private class MyReceiver extends MidiReceiver { 81 @Override 82 public void onSend(byte[] data, int offset, int count, long timestamp) 83 throws IOException { 84 byte command = (byte) (data[0] & MidiConstants.STATUS_COMMAND_MASK); 85 int channel = (byte) (data[0] & MidiConstants.STATUS_CHANNEL_MASK); 86 switch (command) { 87 case MidiConstants.STATUS_NOTE_OFF: 88 noteOff(channel, data[1], data[2]); 89 break; 90 case MidiConstants.STATUS_NOTE_ON: 91 noteOn(channel, data[1], data[2]); 92 break; 93 case MidiConstants.STATUS_PITCH_BEND: 94 int bend = (data[2] << 7) + data[1]; 95 pitchBend(channel, bend); 96 break; 97 case MidiConstants.STATUS_PROGRAM_CHANGE: 98 mProgram = data[1]; 99 mFreeVoices.clear(); 100 break; 101 default: 102 logMidiMessage(data, offset, count); 103 break; 104 } 105 } 106 } 107 108 class MyRunnable implements Runnable { 109 @Override 110 public void run() { 111 try { 112 mAudioOutput.start(FRAME_RATE); 113 onLoopStarted(); 114 while (go) { 115 processMidiEvents(); 116 generateBuffer(); 117 mAudioOutput.write(mBuffer, 0, mBuffer.length); 118 onBufferCompleted(FRAMES_PER_BUFFER); 119 } 120 } catch (Exception e) { 121 Log.e(TAG, "SynthEngine background thread exception.", e); 122 } finally { 123 onLoopEnded(); 124 mAudioOutput.stop(); 125 } 126 } 127 } 128 129 /** 130 * This is called form the synthesis thread before it starts looping. 131 */ 132 public void onLoopStarted() { 133 } 134 135 /** 136 * This is called once at the end of each synthesis loop. 137 * 138 * @param framesPerBuffer 139 */ 140 public void onBufferCompleted(int framesPerBuffer) { 141 } 142 143 /** 144 * This is called form the synthesis thread when it stop looping. 145 */ 146 public void onLoopEnded() { 147 } 148 149 /** 150 * Assume message has been aligned to the start of a MIDI message. 151 * 152 * @param data 153 * @param offset 154 * @param count 155 */ 156 public void logMidiMessage(byte[] data, int offset, int count) { 157 String text = "Received: "; 158 for (int i = 0; i < count; i++) { 159 text += String.format("0x%02X, ", data[offset + i]); 160 } 161 Log.i(TAG, text); 162 } 163 164 /** 165 * @throws IOException 166 * 167 */ 168 private void processMidiEvents() throws IOException { 169 long now = System.nanoTime(); // TODO use audio presentation time 170 MidiEvent event = (MidiEvent) mEventScheduler.getNextEvent(now); 171 while (event != null) { 172 mFramer.send(event.data, 0, event.count, event.getTimestamp()); 173 mEventScheduler.addEventToPool(event); 174 event = (MidiEvent) mEventScheduler.getNextEvent(now); 175 } 176 } 177 178 /** 179 * 180 */ 181 private void generateBuffer() { 182 for (int i = 0; i < mBuffer.length; i++) { 183 mBuffer[i] = 0.0f; 184 } 185 Iterator<SynthVoice> iterator = mVoices.values().iterator(); 186 while (iterator.hasNext()) { 187 SynthVoice voice = iterator.next(); 188 if (voice.isDone()) { 189 iterator.remove(); 190 // mFreeVoices.add(voice); 191 } else { 192 voice.mix(mBuffer, SAMPLES_PER_FRAME, 0.25f); 193 } 194 } 195 } 196 197 public void noteOff(int channel, int noteIndex, int velocity) { 198 SynthVoice voice = mVoices.get(noteIndex); 199 if (voice != null) { 200 voice.noteOff(); 201 } 202 } 203 204 public void allNotesOff() { 205 Iterator<SynthVoice> iterator = mVoices.values().iterator(); 206 while (iterator.hasNext()) { 207 SynthVoice voice = iterator.next(); 208 voice.noteOff(); 209 } 210 } 211 212 /** 213 * Create a SynthVoice. 214 */ 215 public SynthVoice createVoice(int program) { 216 // For every odd program number use a sine wave. 217 if ((program & 1) == 1) { 218 return new SineVoice(); 219 } else { 220 return new SawVoice(); 221 } 222 } 223 224 /** 225 * 226 * @param channel 227 * @param noteIndex 228 * @param velocity 229 */ 230 public void noteOn(int channel, int noteIndex, int velocity) { 231 if (velocity == 0) { 232 noteOff(channel, noteIndex, velocity); 233 } else { 234 mVoices.remove(noteIndex); 235 SynthVoice voice; 236 if (mFreeVoices.size() > 0) { 237 voice = mFreeVoices.remove(mFreeVoices.size() - 1); 238 } else { 239 voice = createVoice(mProgram); 240 } 241 voice.setFrequencyScaler(mFrequencyScaler); 242 voice.noteOn(noteIndex, velocity); 243 mVoices.put(noteIndex, voice); 244 } 245 } 246 247 public void pitchBend(int channel, int bend) { 248 double semitones = (mBendRange * (bend - 0x2000)) / 0x2000; 249 mFrequencyScaler = (float) Math.pow(2.0, semitones / 12.0); 250 Iterator<SynthVoice> iterator = mVoices.values().iterator(); 251 while (iterator.hasNext()) { 252 SynthVoice voice = iterator.next(); 253 voice.setFrequencyScaler(mFrequencyScaler); 254 } 255 } 256 257 /** 258 * Start the synthesizer. 259 */ 260 public void start() { 261 stop(); 262 go = true; 263 mThread = new Thread(new MyRunnable()); 264 mEventScheduler = new MidiEventScheduler(); 265 mThread.start(); 266 } 267 268 /** 269 * Stop the synthesizer. 270 */ 271 public void stop() { 272 go = false; 273 if (mThread != null) { 274 try { 275 mThread.interrupt(); 276 mThread.join(500); 277 } catch (InterruptedException e) { 278 // OK, just stopping safely. 279 } 280 mThread = null; 281 mEventScheduler = null; 282 } 283 } 284 } 285