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 
SynthEngine()59     public SynthEngine() {
60         this(new SimpleAudioOutput());
61     }
62 
SynthEngine(SimpleAudioOutput audioOutput)63     public SynthEngine(SimpleAudioOutput audioOutput) {
64         mReceiver = new MyReceiver();
65         mFramer = new MidiFramer(mReceiver);
66         mAudioOutput = audioOutput;
67     }
68 
69     @Override
onSend(byte[] data, int offset, int count, long timestamp)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
onSend(byte[] data, int offset, int count, long timestamp)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
run()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      */
onLoopStarted()132     public void onLoopStarted() {
133     }
134 
135     /**
136      * This is called once at the end of each synthesis loop.
137      *
138      * @param framesPerBuffer
139      */
onBufferCompleted(int framesPerBuffer)140     public void onBufferCompleted(int framesPerBuffer) {
141     }
142 
143     /**
144      * This is called form the synthesis thread when it stop looping.
145      */
onLoopEnded()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      */
logMidiMessage(byte[] data, int offset, int count)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      */
processMidiEvents()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      */
generateBuffer()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 
noteOff(int channel, int noteIndex, int velocity)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 
allNotesOff()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      */
createVoice(int program)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      */
noteOn(int channel, int noteIndex, int velocity)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 
pitchBend(int channel, int bend)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      */
start()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      */
stop()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