• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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