Skip to content

Most visited

Recently visited

navigation
MidiScope / src / com.example.android.common.midi / synth /

SynthEngine.java

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
}