Home | History | Annotate | Download | only in synth
      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