content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.example.android.common.midi.synth;
  
  import android.media.midi.MidiReceiver;
  import android.util.Log;
  
  import com.example.android.common.midi.MidiConstants;
  import com.example.android.common.midi.MidiEventScheduler;
  import com.example.android.common.midi.MidiEventScheduler.MidiEvent;
  import com.example.android.common.midi.MidiFramer;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Hashtable;
  import java.util.Iterator;
  
  /**
   * Very simple polyphonic, single channel synthesizer. It runs a background
   * thread that processes MIDI events and synthesizes audio.
   */
  public class SynthEngine extends MidiReceiver {
  
      private static final String TAG = "SynthEngine";
  
      public static final int FRAME_RATE = 48000;
      private static final int FRAMES_PER_BUFFER = 240;
      private static final int SAMPLES_PER_FRAME = 2;
  
      private boolean go;
      private Thread mThread;
      private float[] mBuffer = new float[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
      private float mFrequencyScaler = 1.0f;
      private float mBendRange = 2.0f; // semitones
      private int mProgram;
  
      private ArrayList<SynthVoice> mFreeVoices = new ArrayList<SynthVoice>();
      private Hashtable<Integer, SynthVoice>
              mVoices = new Hashtable<Integer, SynthVoice>();
      private MidiEventScheduler mEventScheduler;
      private MidiFramer mFramer;
      private MidiReceiver mReceiver = new MyReceiver();
      private SimpleAudioOutput mAudioOutput;
  
      public SynthEngine() {
          this(new SimpleAudioOutput());
      }
  
      public SynthEngine(SimpleAudioOutput audioOutput) {
          mReceiver = new MyReceiver();
          mFramer = new MidiFramer(mReceiver);
          mAudioOutput = audioOutput;
      }
  
      @Override
      public void onSend(byte[] data, int offset, int count, long timestamp)
              throws IOException {
          if (mEventScheduler != null) {
              if (!MidiConstants.isAllActiveSensing(data, offset, count)) {
                  mEventScheduler.getReceiver().send(data, offset, count,
                          timestamp);
              }
          }
      }
  
      private class MyReceiver extends MidiReceiver {
          @Override
          public void onSend(byte[] data, int offset, int count, long timestamp)
                  throws IOException {
              byte command = (byte) (data[0] & MidiConstants.STATUS_COMMAND_MASK);
              int channel = (byte) (data[0] & MidiConstants.STATUS_CHANNEL_MASK);
              switch (command) {
              case MidiConstants.STATUS_NOTE_OFF:
                  noteOff(channel, data[1], data[2]);
                  break;
              case MidiConstants.STATUS_NOTE_ON:
                  noteOn(channel, data[1], data[2]);
                  break;
              case MidiConstants.STATUS_PITCH_BEND:
                  int bend = (data[2] << 7) + data[1];
                  pitchBend(channel, bend);
                  break;
              case MidiConstants.STATUS_PROGRAM_CHANGE:
                  mProgram = data[1];
                  mFreeVoices.clear();
                  break;
              default:
                  logMidiMessage(data, offset, count);
                  break;
              }
          }
      }
  
      class MyRunnable implements Runnable {
          @Override
          public void run() {
              try {
                  mAudioOutput.start(FRAME_RATE);
                  onLoopStarted();
                  while (go) {
                      processMidiEvents();
                      generateBuffer();
                      mAudioOutput.write(mBuffer, 0, mBuffer.length);
                      onBufferCompleted(FRAMES_PER_BUFFER);
                  }
              } catch (Exception e) {
                  Log.e(TAG, "SynthEngine background thread exception.", e);
              } finally {
                  onLoopEnded();
                  mAudioOutput.stop();
              }
          }
      }
  
      /**
       * This is called form the synthesis thread before it starts looping.
       */
      public void onLoopStarted() {
      }
  
      /**
       * This is called once at the end of each synthesis loop.
       *
       * @param framesPerBuffer
       */
      public void onBufferCompleted(int framesPerBuffer) {
      }
  
      /**
       * This is called form the synthesis thread when it stop looping.
       */
      public void onLoopEnded() {
      }
  
      /**
       * Assume message has been aligned to the start of a MIDI message.
       *
       * @param data
       * @param offset
       * @param count
       */
      public void logMidiMessage(byte[] data, int offset, int count) {
          String text = "Received: ";
          for (int i = 0; i < count; i++) {
              text += String.format("0x%02X, ", data[offset + i]);
          }
          Log.i(TAG, text);
      }
  
      /**
       * @throws IOException
       *
       */
      private void processMidiEvents() throws IOException {
          long now = System.nanoTime(); // TODO use audio presentation time
          MidiEvent event = (MidiEvent) mEventScheduler.getNextEvent(now);
          while (event != null) {
              mFramer.send(event.data, 0, event.count, event.getTimestamp());
              mEventScheduler.addEventToPool(event);
              event = (MidiEvent) mEventScheduler.getNextEvent(now);
          }
      }
  
      /**
       *
       */
      private void generateBuffer() {
          for (int i = 0; i < mBuffer.length; i++) {
              mBuffer[i] = 0.0f;
          }
          Iterator<SynthVoice> iterator = mVoices.values().iterator();
          while (iterator.hasNext()) {
              SynthVoice voice = iterator.next();
              if (voice.isDone()) {
                  iterator.remove();
                  // mFreeVoices.add(voice);
              } else {
                  voice.mix(mBuffer, SAMPLES_PER_FRAME, 0.25f);
              }
          }
      }
  
      public void noteOff(int channel, int noteIndex, int velocity) {
          SynthVoice voice = mVoices.get(noteIndex);
          if (voice != null) {
              voice.noteOff();
          }
      }
  
      public void allNotesOff() {
          Iterator<SynthVoice> iterator = mVoices.values().iterator();
          while (iterator.hasNext()) {
              SynthVoice voice = iterator.next();
              voice.noteOff();
          }
      }
  
      /**
       * Create a SynthVoice.
       */
      public SynthVoice createVoice(int program) {
          // For every odd program number use a sine wave.
          if ((program & 1) == 1) {
              return new SineVoice();
          } else {
              return new SawVoice();
          }
      }
  
      /**
       *
       * @param channel
       * @param noteIndex
       * @param velocity
       */
      public void noteOn(int channel, int noteIndex, int velocity) {
          if (velocity == 0) {
              noteOff(channel, noteIndex, velocity);
          } else {
              mVoices.remove(noteIndex);
              SynthVoice voice;
              if (mFreeVoices.size() > 0) {
                  voice = mFreeVoices.remove(mFreeVoices.size() - 1);
              } else {
                  voice = createVoice(mProgram);
              }
              voice.setFrequencyScaler(mFrequencyScaler);
              voice.noteOn(noteIndex, velocity);
              mVoices.put(noteIndex, voice);
          }
      }
  
      public void pitchBend(int channel, int bend) {
          double semitones = (mBendRange * (bend - 0x2000)) / 0x2000;
          mFrequencyScaler = (float) Math.pow(2.0, semitones / 12.0);
          Iterator<SynthVoice> iterator = mVoices.values().iterator();
          while (iterator.hasNext()) {
              SynthVoice voice = iterator.next();
              voice.setFrequencyScaler(mFrequencyScaler);
          }
      }
  
      /**
       * Start the synthesizer.
       */
      public void start() {
          stop();
          go = true;
          mThread = new Thread(new MyRunnable());
          mEventScheduler = new MidiEventScheduler();
          mThread.start();
      }
  
      /**
       * Stop the synthesizer.
       */
      public void stop() {
          go = false;
          if (mThread != null) {
              try {
                  mThread.interrupt();
                  mThread.join(500);
              } catch (InterruptedException e) {
                  // OK, just stopping safely.
              }
              mThread = null;
              mEventScheduler = null;
          }
      }
  }
  