diff options
author | Yuichi Araki <yaraki@google.com> | 2015-08-17 15:22:28 +0900 |
---|---|---|
committer | Yuichi Araki <yaraki@google.com> | 2015-08-17 15:22:28 +0900 |
commit | 6ab42c36e877ca920212b0a1586268a2e855c2e3 (patch) | |
tree | 378b3b83e6502169d99e8e0aa67e22fbb3300bf1 /common | |
parent | 80b550a664dce8f9577d064efa44d814b81fe0d8 (diff) | |
download | android-6ab42c36e877ca920212b0a1586268a2e855c2e3.tar.gz |
MidiScope: Add a sample
This brings back MidiScope that was deleted in order to resolve a merge
conflict.
Change-Id: I027d53ad57eea89827d486918c0000af5521d463
Diffstat (limited to 'common')
21 files changed, 2263 insertions, 0 deletions
diff --git a/common/src/java/com/example/android/common/midi/EventScheduler.java b/common/src/java/com/example/android/common/midi/EventScheduler.java new file mode 100644 index 00000000..37c0140d --- /dev/null +++ b/common/src/java/com/example/android/common/midi/EventScheduler.java @@ -0,0 +1,243 @@ +/* + * 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; + +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Store SchedulableEvents in a timestamped buffer. + * Events may be written in any order. + * Events will be read in sorted order. + * Events with the same timestamp will be read in the order they were added. + * + * Only one Thread can write into the buffer. + * And only one Thread can read from the buffer. + */ +public class EventScheduler { + private static final long NANOS_PER_MILLI = 1000000; + + private final Object lock = new Object(); + private SortedMap<Long, FastEventQueue> mEventBuffer; + // This does not have to be guarded. It is only set by the writing thread. + // If the reader sees a null right before being set then that is OK. + private FastEventQueue mEventPool = null; + private static final int MAX_POOL_SIZE = 200; + + public EventScheduler() { + mEventBuffer = new TreeMap<Long, FastEventQueue>(); + } + + // If we keep at least one node in the list then it can be atomic + // and non-blocking. + private class FastEventQueue { + // One thread takes from the beginning of the list. + volatile SchedulableEvent mFirst; + // A second thread returns events to the end of the list. + volatile SchedulableEvent mLast; + volatile long mEventsAdded; + volatile long mEventsRemoved; + + FastEventQueue(SchedulableEvent event) { + mFirst = event; + mLast = mFirst; + mEventsAdded = 1; // Always created with one event added. Never empty. + mEventsRemoved = 0; // None removed yet. + } + + int size() { + return (int)(mEventsAdded - mEventsRemoved); + } + + /** + * Do not call this unless there is more than one event + * in the list. + * @return first event in the list + */ + public SchedulableEvent remove() { + // Take first event. + mEventsRemoved++; + SchedulableEvent event = mFirst; + mFirst = event.mNext; + return event; + } + + /** + * @param event + */ + public void add(SchedulableEvent event) { + event.mNext = null; + mLast.mNext = event; + mLast = event; + mEventsAdded++; + } + } + + /** + * Base class for events that can be stored in the EventScheduler. + */ + public static class SchedulableEvent { + private long mTimestamp; + private SchedulableEvent mNext = null; + + /** + * @param timestamp + */ + public SchedulableEvent(long timestamp) { + mTimestamp = timestamp; + } + + /** + * @return timestamp + */ + public long getTimestamp() { + return mTimestamp; + } + + /** + * The timestamp should not be modified when the event is in the + * scheduling buffer. + */ + public void setTimestamp(long timestamp) { + mTimestamp = timestamp; + } + } + + /** + * Get an event from the pool. + * Always leave at least one event in the pool. + * @return event or null + */ + public SchedulableEvent removeEventfromPool() { + SchedulableEvent event = null; + if (mEventPool != null && (mEventPool.size() > 1)) { + event = mEventPool.remove(); + } + return event; + } + + /** + * Return events to a pool so they can be reused. + * + * @param event + */ + public void addEventToPool(SchedulableEvent event) { + if (mEventPool == null) { + mEventPool = new FastEventQueue(event); + // If we already have enough items in the pool then just + // drop the event. This prevents unbounded memory leaks. + } else if (mEventPool.size() < MAX_POOL_SIZE) { + mEventPool.add(event); + } + } + + /** + * Add an event to the scheduler. Events with the same time will be + * processed in order. + * + * @param event + */ + public void add(SchedulableEvent event) { + synchronized (lock) { + FastEventQueue list = mEventBuffer.get(event.getTimestamp()); + if (list == null) { + long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE + : mEventBuffer.firstKey(); + list = new FastEventQueue(event); + mEventBuffer.put(event.getTimestamp(), list); + // If the event we added is earlier than the previous earliest + // event then notify any threads waiting for the next event. + if (event.getTimestamp() < lowestTime) { + lock.notify(); + } + } else { + list.add(event); + } + } + } + + // Caller must synchronize on lock before calling. + private SchedulableEvent removeNextEventLocked(long lowestTime) { + SchedulableEvent event; + FastEventQueue list = mEventBuffer.get(lowestTime); + // Remove list from tree if this is the last node. + if ((list.size() == 1)) { + mEventBuffer.remove(lowestTime); + } + event = list.remove(); + return event; + } + + /** + * Check to see if any scheduled events are ready to be processed. + * + * @param timestamp + * @return next event or null if none ready + */ + public SchedulableEvent getNextEvent(long time) { + SchedulableEvent event = null; + synchronized (lock) { + if (!mEventBuffer.isEmpty()) { + long lowestTime = mEventBuffer.firstKey(); + // Is it time for this list to be processed? + if (lowestTime <= time) { + event = removeNextEventLocked(lowestTime); + } + } + } + // Log.i(TAG, "getNextEvent: event = " + event); + return event; + } + + /** + * Return the next available event or wait until there is an event ready to + * be processed. This method assumes that the timestamps are in nanoseconds + * and that the current time is System.nanoTime(). + * + * @return event + * @throws InterruptedException + */ + public SchedulableEvent waitNextEvent() throws InterruptedException { + SchedulableEvent event = null; + while (true) { + long millisToWait = Integer.MAX_VALUE; + synchronized (lock) { + if (!mEventBuffer.isEmpty()) { + long now = System.nanoTime(); + long lowestTime = mEventBuffer.firstKey(); + // Is it time for the earliest list to be processed? + if (lowestTime <= now) { + event = removeNextEventLocked(lowestTime); + break; + } else { + // Figure out how long to sleep until next event. + long nanosToWait = lowestTime - now; + // Add 1 millisecond so we don't wake up before it is + // ready. + millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI); + // Clip 64-bit value to 32-bit max. + if (millisToWait > Integer.MAX_VALUE) { + millisToWait = Integer.MAX_VALUE; + } + } + } + lock.wait((int) millisToWait); + } + } + return event; + } +} diff --git a/common/src/java/com/example/android/common/midi/MidiConstants.java b/common/src/java/com/example/android/common/midi/MidiConstants.java new file mode 100644 index 00000000..38c25d50 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiConstants.java @@ -0,0 +1,102 @@ +/* + * 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; + +/** + * MIDI related constants and static methods. + * These values are defined in the MIDI Standard 1.0 + * available from the MIDI Manufacturers Association. + */ +public class MidiConstants { + protected final static String TAG = "MidiTools"; + public static final byte STATUS_COMMAND_MASK = (byte) 0xF0; + public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F; + + // Channel voice messages. + public static final byte STATUS_NOTE_OFF = (byte) 0x80; + public static final byte STATUS_NOTE_ON = (byte) 0x90; + public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0; + public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0; + public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0; + public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0; + public static final byte STATUS_PITCH_BEND = (byte) 0xE0; + + // System Common Messages. + public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0; + public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1; + public static final byte STATUS_SONG_POSITION = (byte) 0xF2; + public static final byte STATUS_SONG_SELECT = (byte) 0xF3; + public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6; + public static final byte STATUS_END_SYSEX = (byte) 0xF7; + + // System Real-Time Messages + public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8; + public static final byte STATUS_START = (byte) 0xFA; + public static final byte STATUS_CONTINUE = (byte) 0xFB; + public static final byte STATUS_STOP = (byte) 0xFC; + public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE; + public static final byte STATUS_RESET = (byte) 0xFF; + + /** Number of bytes in a message nc from 8c to Ec */ + public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 }; + + /** Number of bytes in a message Fn from F0 to FF */ + public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1 }; + + /** + * MIDI messages, except for SysEx, are 1,2 or 3 bytes long. + * You can tell how long a MIDI message is from the first status byte. + * Do not call this for SysEx, which has variable length. + * @param statusByte + * @return number of bytes in a complete message, zero if data byte passed + */ + public static int getBytesPerMessage(byte statusByte) { + // Java bytes are signed so we need to mask off the high bits + // to get a value between 0 and 255. + int statusInt = statusByte & 0xFF; + if (statusInt >= 0xF0) { + // System messages use low nibble for size. + return SYSTEM_BYTE_LENGTHS[statusInt & 0x0F]; + } else if(statusInt >= 0x80) { + // Channel voice messages use high nibble for size. + return CHANNEL_BYTE_LENGTHS[(statusInt >> 4) - 8]; + } else { + return 0; // data byte + } + } + + /** + * @param msg + * @param offset + * @param count + * @return true if the entire message is ActiveSensing commands + */ + public static boolean isAllActiveSensing(byte[] msg, int offset, + int count) { + // Count bytes that are not active sensing. + int goodBytes = 0; + for (int i = 0; i < count; i++) { + byte b = msg[offset + i]; + if (b != MidiConstants.STATUS_ACTIVE_SENSING) { + goodBytes++; + } + } + return (goodBytes == 0); + } + +} diff --git a/common/src/java/com/example/android/common/midi/MidiDispatcher.java b/common/src/java/com/example/android/common/midi/MidiDispatcher.java new file mode 100644 index 00000000..b7f1fe1e --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiDispatcher.java @@ -0,0 +1,95 @@ +/* + * 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; + +import android.media.midi.MidiReceiver; +import android.media.midi.MidiSender; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s. + * This class subclasses {@link MidiReceiver} and dispatches any data it receives + * to its receiver list. Any receivers that throw an exception upon receiving data will + * be automatically removed from the receiver list, but no IOException will be returned + * from the dispatcher's {@link MidiReceiver#onReceive} in that case. + */ +public final class MidiDispatcher extends MidiReceiver { + + private final CopyOnWriteArrayList<MidiReceiver> mReceivers + = new CopyOnWriteArrayList<MidiReceiver>(); + + private final MidiSender mSender = new MidiSender() { + /** + * Called to connect a {@link MidiReceiver} to the sender + * + * @param receiver the receiver to connect + */ + @Override + public void onConnect(MidiReceiver receiver) { + mReceivers.add(receiver); + } + + /** + * Called to disconnect a {@link MidiReceiver} from the sender + * + * @param receiver the receiver to disconnect + */ + @Override + public void onDisconnect(MidiReceiver receiver) { + mReceivers.remove(receiver); + } + }; + + /** + * Returns the number of {@link MidiReceiver}s this dispatcher contains. + * @return the number of receivers + */ + public int getReceiverCount() { + return mReceivers.size(); + } + + /** + * Returns a {@link MidiSender} which is used to add and remove + * {@link MidiReceiver}s + * to the dispatcher's receiver list. + * @return the dispatcher's MidiSender + */ + public MidiSender getSender() { + return mSender; + } + + @Override + public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException { + for (MidiReceiver receiver : mReceivers) { + try { + receiver.send(msg, offset, count, timestamp); + } catch (IOException e) { + // if the receiver fails we remove the receiver but do not propagate the exception + mReceivers.remove(receiver); + } + } + } + + @Override + public void flush() throws IOException { + for (MidiReceiver receiver : mReceivers) { + receiver.flush(); + } + } +} diff --git a/common/src/java/com/example/android/common/midi/MidiEventScheduler.java b/common/src/java/com/example/android/common/midi/MidiEventScheduler.java new file mode 100644 index 00000000..513d3939 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiEventScheduler.java @@ -0,0 +1,119 @@ +/* + * 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; + +import android.media.midi.MidiReceiver; + +import java.io.IOException; + +/** + * Add MIDI Events to an EventScheduler + */ +public class MidiEventScheduler extends EventScheduler { + private static final String TAG = "MidiEventScheduler"; + // Maintain a pool of scheduled events to reduce memory allocation. + // This pool increases performance by about 14%. + private final static int POOL_EVENT_SIZE = 16; + private MidiReceiver mReceiver = new SchedulingReceiver(); + + private class SchedulingReceiver extends MidiReceiver + { + /** + * Store these bytes in the EventScheduler to be delivered at the specified + * time. + */ + @Override + public void onSend(byte[] msg, int offset, int count, long timestamp) + throws IOException { + MidiEvent event = createScheduledEvent(msg, offset, count, timestamp); + if (event != null) { + add(event); + } + } + } + + public static class MidiEvent extends SchedulableEvent { + public int count = 0; + public byte[] data; + + private MidiEvent(int count) { + super(0); + data = new byte[count]; + } + + private MidiEvent(byte[] msg, int offset, int count, long timestamp) { + super(timestamp); + data = new byte[count]; + System.arraycopy(msg, offset, data, 0, count); + this.count = count; + } + + @Override + public String toString() { + String text = "Event: "; + for (int i = 0; i < count; i++) { + text += data[i] + ", "; + } + return text; + } + } + + /** + * Create an event that contains the message. + */ + private MidiEvent createScheduledEvent(byte[] msg, int offset, int count, + long timestamp) { + MidiEvent event; + if (count > POOL_EVENT_SIZE) { + event = new MidiEvent(msg, offset, count, timestamp); + } else { + event = (MidiEvent) removeEventfromPool(); + if (event == null) { + event = new MidiEvent(POOL_EVENT_SIZE); + } + System.arraycopy(msg, offset, event.data, 0, count); + event.count = count; + event.setTimestamp(timestamp); + } + return event; + } + + /** + * Return events to a pool so they can be reused. + * + * @param event + */ + @Override + public void addEventToPool(SchedulableEvent event) { + // Make sure the event is suitable for the pool. + if (event instanceof MidiEvent) { + MidiEvent midiEvent = (MidiEvent) event; + if (midiEvent.data.length == POOL_EVENT_SIZE) { + super.addEventToPool(event); + } + } + } + + /** + * This MidiReceiver will write date to the scheduling buffer. + * @return the MidiReceiver + */ + public MidiReceiver getReceiver() { + return mReceiver; + } + +} diff --git a/common/src/java/com/example/android/common/midi/MidiEventThread.java b/common/src/java/com/example/android/common/midi/MidiEventThread.java new file mode 100644 index 00000000..626e83cf --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiEventThread.java @@ -0,0 +1,90 @@ +/* + * 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; + +import android.media.midi.MidiSender; +import android.util.Log; + +import java.io.IOException; + +public class MidiEventThread extends MidiEventScheduler { + protected static final String TAG = "MidiEventThread"; + + private EventThread mEventThread; + MidiDispatcher mDispatcher = new MidiDispatcher(); + + class EventThread extends Thread { + private boolean go = true; + + @Override + public void run() { + while (go) { + try { + MidiEvent event = (MidiEvent) waitNextEvent(); + try { + Log.i(TAG, "Fire event " + event.data[0] + " at " + + event.getTimestamp()); + mDispatcher.send(event.data, 0, + event.count, event.getTimestamp()); + } catch (IOException e) { + e.printStackTrace(); + } + // Put event back in the pool for future use. + addEventToPool(event); + } catch (InterruptedException e) { + // OK, this is how we stop the thread. + } + } + } + + /** + * Asynchronously tell the thread to stop. + */ + public void requestStop() { + go = false; + interrupt(); + } + } + + public void start() { + stop(); + mEventThread = new EventThread(); + mEventThread.start(); + } + + /** + * Asks the thread to stop then waits for it to stop. + */ + public void stop() { + if (mEventThread != null) { + mEventThread.requestStop(); + try { + mEventThread.join(500); + } catch (InterruptedException e) { + Log.e(TAG, + "Interrupted while waiting for MIDI EventScheduler thread to stop."); + } finally { + mEventThread = null; + } + } + } + + public MidiSender getSender() { + return mDispatcher.getSender(); + } + +} diff --git a/common/src/java/com/example/android/common/midi/MidiFramer.java b/common/src/java/com/example/android/common/midi/MidiFramer.java new file mode 100644 index 00000000..c274925a --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiFramer.java @@ -0,0 +1,112 @@ +/* + * 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; + +import android.media.midi.MidiReceiver; +import android.util.Log; + +import java.io.IOException; + +/** + * Convert stream of arbitrary MIDI bytes into discrete messages. + * + * Parses the incoming bytes and then posts individual messages to the receiver + * specified in the constructor. Short messages of 1-3 bytes will be complete. + * System Exclusive messages may be posted in pieces. + * + * Resolves Running Status and interleaved System Real-Time messages. + */ +public class MidiFramer extends MidiReceiver { + private MidiReceiver mReceiver; + private byte[] mBuffer = new byte[3]; + private int mCount; + private byte mRunningStatus; + private int mNeeded; + private boolean mInSysEx; + + public MidiFramer(MidiReceiver receiver) { + mReceiver = receiver; + } + + /* + * @see android.midi.MidiReceiver#onSend(byte[], int, int, long) + */ + @Override + public void onSend(byte[] data, int offset, int count, long timestamp) + throws IOException { + int sysExStartOffset = (mInSysEx ? offset : -1); + + for (int i = 0; i < count; i++) { + final byte currentByte = data[offset]; + final int currentInt = currentByte & 0xFF; + if (currentInt >= 0x80) { // status byte? + if (currentInt < 0xF0) { // channel message? + mRunningStatus = currentByte; + mCount = 1; + mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1; + } else if (currentInt < 0xF8) { // system common? + if (currentInt == 0xF0 /* SysEx Start */) { + // Log.i(TAG, "SysEx Start"); + mInSysEx = true; + sysExStartOffset = offset; + } else if (currentInt == 0xF7 /* SysEx End */) { + // Log.i(TAG, "SysEx End"); + if (mInSysEx) { + mReceiver.send(data, sysExStartOffset, + offset - sysExStartOffset + 1, timestamp); + mInSysEx = false; + sysExStartOffset = -1; + } + } else { + mBuffer[0] = currentByte; + mRunningStatus = 0; + mCount = 1; + mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1; + } + } else { // real-time? + // Single byte message interleaved with other data. + if (mInSysEx) { + mReceiver.send(data, sysExStartOffset, + offset - sysExStartOffset, timestamp); + sysExStartOffset = offset + 1; + } + mReceiver.send(data, offset, 1, timestamp); + } + } else { // data byte + if (!mInSysEx) { + mBuffer[mCount++] = currentByte; + if (--mNeeded == 0) { + if (mRunningStatus != 0) { + mBuffer[0] = mRunningStatus; + } + mReceiver.send(mBuffer, 0, mCount, timestamp); + mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1; + mCount = 1; + } + } + } + ++offset; + } + + // send any accumulatedSysEx data + if (sysExStartOffset >= 0 && sysExStartOffset < offset) { + mReceiver.send(data, sysExStartOffset, + offset - sysExStartOffset, timestamp); + } + } + +} diff --git a/common/src/java/com/example/android/common/midi/MidiInputPortSelector.java b/common/src/java/com/example/android/common/midi/MidiInputPortSelector.java new file mode 100644 index 00000000..7ca22722 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiInputPortSelector.java @@ -0,0 +1,90 @@ +/* + * 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; + +import android.app.Activity; +import android.media.midi.MidiDevice; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiInputPort; +import android.media.midi.MidiManager; +import android.media.midi.MidiReceiver; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.io.IOException; + +/** + * Manages a Spinner for selecting a MidiInputPort. + */ +public class MidiInputPortSelector extends MidiPortSelector { + private static final String TAG = "MidiInputPortSelector"; + + private MidiInputPort mInputPort; + private MidiDevice mOpenDevice; + + /** + * @param midiManager + * @param activity + * @param spinnerId ID from the layout resource + */ + public MidiInputPortSelector(MidiManager midiManager, Activity activity, + int spinnerId) { + super(midiManager, activity, spinnerId, TYPE_INPUT); + } + + @Override + public void onPortSelected(final MidiPortWrapper wrapper) { + onClose(); + + final MidiDeviceInfo info = wrapper.getDeviceInfo(); + if (info != null) { + mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() { + @Override + public void onDeviceOpened(MidiDevice device) { + if (device == null) { + Log.e(TAG, "could not open " + info); + } else { + mOpenDevice = device; + mInputPort = mOpenDevice.openInputPort( + wrapper.getPortIndex()); + } + } + }, new Handler(Looper.getMainLooper())); + } + } + + public MidiReceiver getReceiver() { + return mInputPort; + } + + @Override + public void onClose() { + try { + if (mInputPort != null) { + mInputPort.close(); + } + mInputPort = null; + if (mOpenDevice != null) { + mOpenDevice.close(); + } + mOpenDevice = null; + } catch (IOException e) { + Log.e(TAG, "cleanup failed", e); + } + } +} diff --git a/common/src/java/com/example/android/common/midi/MidiOutputPortSelector.java b/common/src/java/com/example/android/common/midi/MidiOutputPortSelector.java new file mode 100644 index 00000000..d01d3041 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiOutputPortSelector.java @@ -0,0 +1,105 @@ +/* + * 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; + +import android.app.Activity; +import android.media.midi.MidiDevice; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; +import android.media.midi.MidiOutputPort; +import android.media.midi.MidiSender; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.io.IOException; + +/** + * Manages a Spinner for selecting a MidiOutputPort. + */ +public class MidiOutputPortSelector extends MidiPortSelector { + + private static final String TAG = "MidiOutputPortSelector"; + + private MidiOutputPort mSender; + private MidiDispatcher mDispatcher = new MidiDispatcher(); + private MidiDevice mOpenDevice; + + /** + * @param midiManager + * @param activity + * @param spinnerId ID from the layout resource + */ + public MidiOutputPortSelector(MidiManager midiManager, Activity activity, + int spinnerId) { + super(midiManager, activity, spinnerId, TYPE_OUTPUT); + } + + @Override + public void onPortSelected(final MidiPortWrapper wrapper) { + Log.i(TAG, "onPortSelected: " + wrapper); + onClose(); + + final MidiDeviceInfo info = wrapper.getDeviceInfo(); + if (info != null) { + mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() { + + @Override + public void onDeviceOpened(MidiDevice device) { + if (device == null) { + Log.e(TAG, "could not open " + info); + } else { + mOpenDevice = device; + mSender = device.openOutputPort(wrapper.getPortIndex()); + if (mSender == null) { + Log.e(TAG, + "could not get sender for " + info); + return; + } + mSender.connect(mDispatcher); + } + } + }, new Handler(Looper.getMainLooper())); + } + } + + @Override + public void onClose() { + try { + if (mSender != null) { + mSender.disconnect(mDispatcher); + } + mSender = null; + if (mOpenDevice != null) { + mOpenDevice.close(); + } + mOpenDevice = null; + } catch (IOException e) { + Log.e(TAG, "cleanup failed", e); + } + } + + /** + * You can connect your MidiReceivers to this sender. The user will then select which output + * port will send messages through this MidiSender. + * @return a MidiSender that will send the messages from the selected port. + */ + public MidiSender getSender() { + return mDispatcher.getSender(); + } + +} diff --git a/common/src/java/com/example/android/common/midi/MidiPortConnector.java b/common/src/java/com/example/android/common/midi/MidiPortConnector.java new file mode 100644 index 00000000..92517be0 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiPortConnector.java @@ -0,0 +1,217 @@ +/* + * 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; + +import android.media.midi.MidiDevice; +import android.media.midi.MidiDevice.MidiConnection; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiInputPort; +import android.media.midi.MidiManager; +import android.os.Handler; +import android.util.Log; + +import java.io.IOException; + +/** + * Simple wrapper for connecting MIDI ports. + */ +public class MidiPortConnector { + private final MidiManager mMidiManager; + private MidiDevice mSourceDevice; + private MidiDevice mDestinationDevice; + private MidiConnection mConnection; + + /** + * @param mMidiManager + */ + public MidiPortConnector(MidiManager midiManager) { + mMidiManager = midiManager; + } + + public void close() throws IOException { + if (mConnection != null) { + mConnection.close(); + mConnection = null; + } + if (mSourceDevice != null) { + mSourceDevice.close(); + mSourceDevice = null; + } + if (mDestinationDevice != null) { + mDestinationDevice.close(); + mDestinationDevice = null; + } + } + + /** + * @return a device that matches the manufacturer and product or null + */ + public MidiDeviceInfo findDevice(String manufacturer, String product) { + for (MidiDeviceInfo info : mMidiManager.getDevices()) { + String deviceManufacturer = info.getProperties() + .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER); + if ((manufacturer != null) + && manufacturer.equals(deviceManufacturer)) { + String deviceProduct = info.getProperties() + .getString(MidiDeviceInfo.PROPERTY_PRODUCT); + if ((product != null) && product.equals(deviceProduct)) { + return info; + } + } + } + return null; + } + + /** + * Listener class used for receiving the results of + * {@link #connectToDevicePort} + */ + public interface OnPortsConnectedListener { + /** + * Called to respond to a {@link #connectToDevicePort} request + * + * @param connection + * a {@link MidiConnection} that represents the connected + * ports, or null if connection failed + */ + abstract public void onPortsConnected(MidiConnection connection); + } + + /** + * Open two devices and connect their ports. + * + * @param sourceDeviceInfo + * @param sourcePortIndex + * @param destinationDeviceInfo + * @param destinationPortIndex + */ + public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo, + final int sourcePortIndex, + final MidiDeviceInfo destinationDeviceInfo, + final int destinationPortIndex) { + connectToDevicePort(sourceDeviceInfo, sourcePortIndex, + destinationDeviceInfo, destinationPortIndex, null, null); + } + + /** + * Open two devices and connect their ports. + * + * @param sourceDeviceInfo + * @param sourcePortIndex + * @param destinationDeviceInfo + * @param destinationPortIndex + */ + public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo, + final int sourcePortIndex, + final MidiDeviceInfo destinationDeviceInfo, + final int destinationPortIndex, + final OnPortsConnectedListener listener, final Handler handler) { + mMidiManager.openDevice(destinationDeviceInfo, + new MidiManager.OnDeviceOpenedListener() { + @Override + public void onDeviceOpened(MidiDevice device) { + if (device == null) { + Log.e(MidiConstants.TAG, + "could not open " + destinationDeviceInfo); + if (listener != null) { + listener.onPortsConnected(null); + } + } else { + Log.i(MidiConstants.TAG, + "connectToDevicePort opened " + + destinationDeviceInfo); + // Destination device was opened so go to next step. + mDestinationDevice = device; + MidiInputPort destinationInputPort = device + .openInputPort(destinationPortIndex); + if (destinationInputPort != null) { + Log.i(MidiConstants.TAG, + "connectToDevicePort opened port on " + + destinationDeviceInfo); + connectToDevicePort(sourceDeviceInfo, + sourcePortIndex, destinationInputPort, + listener, handler); + } else { + Log.e(MidiConstants.TAG, + "could not open port on " + + destinationDeviceInfo); + if (listener != null) { + listener.onPortsConnected(null); + } + } + } + } + }, handler); + } + + /** + * Open a source device and connect its output port to the + * destinationInputPort. + * + * @param sourceDeviceInfo + * @param sourcePortIndex + * @param destinationInputPort + */ + public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo, + final int sourcePortIndex, + final MidiInputPort destinationInputPort) { + connectToDevicePort(sourceDeviceInfo, sourcePortIndex, + destinationInputPort, null, null); + } + + /** + * Open a source device and connect its output port to the + * destinationInputPort. + * + * @param sourceDeviceInfo + * @param sourcePortIndex + * @param destinationInputPort + */ + public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo, + final int sourcePortIndex, final MidiInputPort destinationInputPort, + final OnPortsConnectedListener listener, final Handler handler) { + mMidiManager.openDevice(sourceDeviceInfo, + new MidiManager.OnDeviceOpenedListener() { + @Override + public void onDeviceOpened(MidiDevice device) { + if (device == null) { + Log.e(MidiConstants.TAG, + "could not open " + sourceDeviceInfo); + if (listener != null) { + listener.onPortsConnected(null); + } + } else { + Log.i(MidiConstants.TAG, + "connectToDevicePort opened " + + sourceDeviceInfo); + // Device was opened so connect the ports. + mSourceDevice = device; + mConnection = device.connectPorts( + destinationInputPort, sourcePortIndex); + if (mConnection == null) { + Log.e(MidiConstants.TAG, "could not connect to " + + sourceDeviceInfo); + } + if (listener != null) { + listener.onPortsConnected(mConnection); + } + } + } + }, handler); + } + +} diff --git a/common/src/java/com/example/android/common/midi/MidiPortSelector.java b/common/src/java/com/example/android/common/midi/MidiPortSelector.java new file mode 100644 index 00000000..d491c037 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiPortSelector.java @@ -0,0 +1,127 @@ +/* + * 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; + +import android.app.Activity; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; +import android.media.midi.MidiManager.DeviceCallback; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; + +/** + * Base class that uses a Spinner to select available MIDI ports. + */ +public abstract class MidiPortSelector extends DeviceCallback { + + public static final int TYPE_INPUT = 0; + public static final int TYPE_OUTPUT = 1; + private int mType = TYPE_INPUT; + protected ArrayAdapter<MidiPortWrapper> mAdapter; + private Spinner mSpinner; + protected MidiManager mMidiManager; + protected Activity mActivity; + private MidiPortWrapper mCurrentWrapper; + + /** + * + * @param midiManager + * @param activity + * @param spinnerId ID from the layout resource + * @param type TYPE_INPUT or TYPE_OUTPUT + */ + public MidiPortSelector(MidiManager midiManager, Activity activity, + int spinnerId, int type) { + mMidiManager = midiManager; + mActivity = activity; + mType = type; + mAdapter = new ArrayAdapter<MidiPortWrapper>(activity, + android.R.layout.simple_spinner_item); + mAdapter.setDropDownViewResource( + android.R.layout.simple_spinner_dropdown_item); + mAdapter.add(new MidiPortWrapper(null, 0)); + + mSpinner = (Spinner) activity.findViewById(spinnerId); + mSpinner.setOnItemSelectedListener( + new AdapterView.OnItemSelectedListener() { + + public void onItemSelected(AdapterView<?> parent, View view, + int pos, long id) { + mCurrentWrapper = mAdapter.getItem(pos); + onPortSelected(mCurrentWrapper); + } + + public void onNothingSelected(AdapterView<?> parent) { + onPortSelected(null); + mCurrentWrapper = null; + } + }); + mSpinner.setAdapter(mAdapter); + + mMidiManager.registerDeviceCallback(this, + new Handler(Looper.getMainLooper())); + + MidiDeviceInfo[] infos = mMidiManager.getDevices(); + for (MidiDeviceInfo info : infos) { + onDeviceAdded(info); + } + } + + private int getInfoPortCount(final MidiDeviceInfo info) { + int portCount = (mType == TYPE_INPUT) ? info.getInputPortCount() + : info.getOutputPortCount(); + return portCount; + } + + @Override + public void onDeviceAdded(final MidiDeviceInfo info) { + int portCount = getInfoPortCount(info); + for (int i = 0; i < portCount; ++i) { + mAdapter.add(new MidiPortWrapper(info, i)); + } + } + + @Override + public void onDeviceRemoved(final MidiDeviceInfo info) { + int portCount = getInfoPortCount(info); + for (int i = 0; i < portCount; ++i) { + MidiPortWrapper wrapper = new MidiPortWrapper(info, i); + MidiPortWrapper currentWrapper = mCurrentWrapper; + mAdapter.remove(wrapper); + // If the currently selected port was removed then select no port. + if (wrapper.equals(currentWrapper)) { + mSpinner.setSelection(0); + } + } + } + + /** + * Implement this method to handle the user selecting a port on a device. + * + * @param wrapper + */ + public abstract void onPortSelected(MidiPortWrapper wrapper); + + /** + * Implement this method to clean up any open resources. + */ + public abstract void onClose(); +} diff --git a/common/src/java/com/example/android/common/midi/MidiPortWrapper.java b/common/src/java/com/example/android/common/midi/MidiPortWrapper.java new file mode 100644 index 00000000..9f826947 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/MidiPortWrapper.java @@ -0,0 +1,82 @@ +/* + * 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; + +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiDeviceInfo.PortInfo; + +// Wrapper for a MIDI device and port description. +public class MidiPortWrapper { + private MidiDeviceInfo mInfo; + private int mPortIndex; + private String mString; + + public MidiPortWrapper(MidiDeviceInfo info, int portIndex) { + mInfo = info; + mPortIndex = portIndex; + if (mInfo == null) { + mString = "- - - - - -"; + } else { + StringBuilder sb = new StringBuilder(); + String name = mInfo.getProperties() + .getString(MidiDeviceInfo.PROPERTY_NAME); + if (name == null) { + name = mInfo.getProperties() + .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER) + + ", " + mInfo.getProperties() + .getString(MidiDeviceInfo.PROPERTY_PRODUCT); + } + sb.append("#" + mInfo.getId()).append(", ").append(name); + PortInfo portInfo = mInfo.getPorts()[portIndex]; + sb.append(", ").append(portInfo.getName()); + mString = sb.toString(); + } + } + + public int getPortIndex() { + return mPortIndex; + } + + public MidiDeviceInfo getDeviceInfo() { + return mInfo; + } + + @Override + public String toString() { + return mString; + } + + @Override + public boolean equals(Object other) { + if (other == null) + return false; + if (!(other instanceof MidiPortWrapper)) + return false; + MidiPortWrapper otherWrapper = (MidiPortWrapper) other; + if (mPortIndex != otherWrapper.mPortIndex) + return false; + if (mInfo == null) + return (otherWrapper.mInfo == null); + return mInfo.equals(otherWrapper.mInfo); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + +} diff --git a/common/src/java/com/example/android/common/midi/synth/EnvelopeADSR.java b/common/src/java/com/example/android/common/midi/synth/EnvelopeADSR.java new file mode 100644 index 00000000..a29a1933 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/EnvelopeADSR.java @@ -0,0 +1,111 @@ +/* + * 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; + +/** + * Very simple Attack, Decay, Sustain, Release envelope with linear ramps. + * + * Times are in seconds. + */ +public class EnvelopeADSR extends SynthUnit { + private static final int IDLE = 0; + private static final int ATTACK = 1; + private static final int DECAY = 2; + private static final int SUSTAIN = 3; + private static final int RELEASE = 4; + private static final int FINISHED = 5; + private static final float MIN_TIME = 0.001f; + + private float mAttackRate; + private float mRreleaseRate; + private float mSustainLevel; + private float mDecayRate; + private float mCurrent; + private int mSstate = IDLE; + + public EnvelopeADSR() { + setAttackTime(0.003f); + setDecayTime(0.08f); + setSustainLevel(0.3f); + setReleaseTime(1.0f); + } + + public void setAttackTime(float time) { + if (time < MIN_TIME) + time = MIN_TIME; + mAttackRate = 1.0f / (SynthEngine.FRAME_RATE * time); + } + + public void setDecayTime(float time) { + if (time < MIN_TIME) + time = MIN_TIME; + mDecayRate = 1.0f / (SynthEngine.FRAME_RATE * time); + } + + public void setSustainLevel(float level) { + if (level < 0.0f) + level = 0.0f; + mSustainLevel = level; + } + + public void setReleaseTime(float time) { + if (time < MIN_TIME) + time = MIN_TIME; + mRreleaseRate = 1.0f / (SynthEngine.FRAME_RATE * time); + } + + public void on() { + mSstate = ATTACK; + } + + public void off() { + mSstate = RELEASE; + } + + @Override + public float render() { + switch (mSstate) { + case ATTACK: + mCurrent += mAttackRate; + if (mCurrent > 1.0f) { + mCurrent = 1.0f; + mSstate = DECAY; + } + break; + case DECAY: + mCurrent -= mDecayRate; + if (mCurrent < mSustainLevel) { + mCurrent = mSustainLevel; + mSstate = SUSTAIN; + } + break; + case RELEASE: + mCurrent -= mRreleaseRate; + if (mCurrent < 0.0f) { + mCurrent = 0.0f; + mSstate = FINISHED; + } + break; + } + return mCurrent; + } + + public boolean isDone() { + return mSstate == FINISHED; + } + +} diff --git a/common/src/java/com/example/android/common/midi/synth/SawOscillator.java b/common/src/java/com/example/android/common/midi/synth/SawOscillator.java new file mode 100644 index 00000000..c02a6a1a --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SawOscillator.java @@ -0,0 +1,73 @@ +/* + * 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; + +public class SawOscillator extends SynthUnit { + private float mPhase = 0.0f; + private float mPhaseIncrement = 0.01f; + private float mFrequency = 0.0f; + private float mFrequencyScaler = 1.0f; + private float mAmplitude = 1.0f; + + public void setPitch(float pitch) { + float freq = (float) pitchToFrequency(pitch); + setFrequency(freq); + } + + public void setFrequency(float frequency) { + mFrequency = frequency; + updatePhaseIncrement(); + } + + private void updatePhaseIncrement() { + mPhaseIncrement = 2.0f * mFrequency * mFrequencyScaler / 48000.0f; + } + + public void setAmplitude(float amplitude) { + mAmplitude = amplitude; + } + + public float getAmplitude() { + return mAmplitude; + } + + public float getFrequencyScaler() { + return mFrequencyScaler; + } + + public void setFrequencyScaler(float frequencyScaler) { + mFrequencyScaler = frequencyScaler; + updatePhaseIncrement(); + } + + float incrementWrapPhase() { + mPhase += mPhaseIncrement; + while (mPhase > 1.0) { + mPhase -= 2.0; + } + while (mPhase < -1.0) { + mPhase += 2.0; + } + return mPhase; + } + + @Override + public float render() { + return incrementWrapPhase() * mAmplitude; + } + +} diff --git a/common/src/java/com/example/android/common/midi/synth/SawOscillatorDPW.java b/common/src/java/com/example/android/common/midi/synth/SawOscillatorDPW.java new file mode 100644 index 00000000..e5d661d5 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SawOscillatorDPW.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * Band limited sawtooth oscillator. + * This will have very little aliasing at high frequencies. + */ +public class SawOscillatorDPW extends SawOscillator { + private float mZ1 = 0.0f; // delayed values + private float mZ2 = 0.0f; + private float mScaler; // frequency dependent scaler + private final static float VERY_LOW_FREQ = 0.0000001f; + + @Override + public void setFrequency(float freq) { + /* Calculate scaling based on frequency. */ + freq = Math.abs(freq); + super.setFrequency(freq); + if (freq < VERY_LOW_FREQ) { + mScaler = (float) (0.125 * 44100 / VERY_LOW_FREQ); + } else { + mScaler = (float) (0.125 * 44100 / freq); + } + } + + @Override + public float render() { + float phase = incrementWrapPhase(); + /* Square the raw sawtooth. */ + float squared = phase * phase; + float diffed = squared - mZ2; + mZ2 = mZ1; + mZ1 = squared; + return diffed * mScaler * getAmplitude(); + } + +} diff --git a/common/src/java/com/example/android/common/midi/synth/SawVoice.java b/common/src/java/com/example/android/common/midi/synth/SawVoice.java new file mode 100644 index 00000000..3b3e543e --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SawVoice.java @@ -0,0 +1,65 @@ +/* + * 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; + +/** + * Sawtooth oscillator with an ADSR. + */ +public class SawVoice extends SynthVoice { + private SawOscillator mOscillator; + private EnvelopeADSR mEnvelope; + + public SawVoice() { + mOscillator = createOscillator(); + mEnvelope = new EnvelopeADSR(); + } + + protected SawOscillator createOscillator() { + return new SawOscillator(); + } + + @Override + public void noteOn(int noteIndex, int velocity) { + super.noteOn(noteIndex, velocity); + mOscillator.setPitch(noteIndex); + mOscillator.setAmplitude(getAmplitude()); + mEnvelope.on(); + } + + @Override + public void noteOff() { + super.noteOff(); + mEnvelope.off(); + } + + @Override + public void setFrequencyScaler(float scaler) { + mOscillator.setFrequencyScaler(scaler); + } + + @Override + public float render() { + float output = mOscillator.render() * mEnvelope.render(); + return output; + } + + @Override + public boolean isDone() { + return mEnvelope.isDone(); + } + +} diff --git a/common/src/java/com/example/android/common/midi/synth/SimpleAudioOutput.java b/common/src/java/com/example/android/common/midi/synth/SimpleAudioOutput.java new file mode 100644 index 00000000..04aa19c0 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SimpleAudioOutput.java @@ -0,0 +1,95 @@ +/* + * 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.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; +import android.util.Log; + +/** + * Simple base class for implementing audio output for examples. + * This can be sub-classed for experimentation or to redirect audio output. + */ +public class SimpleAudioOutput { + + private static final String TAG = "AudioOutputTrack"; + public static final int SAMPLES_PER_FRAME = 2; + public static final int BYTES_PER_SAMPLE = 4; // float + public static final int BYTES_PER_FRAME = SAMPLES_PER_FRAME * BYTES_PER_SAMPLE; + private AudioTrack mAudioTrack; + private int mFrameRate; + + /** + * + */ + public SimpleAudioOutput() { + super(); + } + + /** + * Create an audio track then call play(). + * + * @param frameRate + */ + public void start(int frameRate) { + stop(); + mFrameRate = frameRate; + mAudioTrack = createAudioTrack(frameRate); + // AudioTrack will wait until it has enough data before starting. + mAudioTrack.play(); + } + + public AudioTrack createAudioTrack(int frameRate) { + int minBufferSizeBytes = AudioTrack.getMinBufferSize(frameRate, + AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_FLOAT); + Log.i(TAG, "AudioTrack.minBufferSize = " + minBufferSizeBytes + + " bytes = " + (minBufferSizeBytes / BYTES_PER_FRAME) + + " frames"); + int bufferSize = 8 * minBufferSizeBytes / 8; + int outputBufferSizeFrames = bufferSize / BYTES_PER_FRAME; + Log.i(TAG, "actual bufferSize = " + bufferSize + " bytes = " + + outputBufferSizeFrames + " frames"); + + AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC, + mFrameRate, AudioFormat.CHANNEL_OUT_STEREO, + AudioFormat.ENCODING_PCM_FLOAT, bufferSize, + AudioTrack.MODE_STREAM); + Log.i(TAG, "created AudioTrack"); + return player; + } + + public int write(float[] buffer, int offset, int length) { + return mAudioTrack.write(buffer, offset, length, + AudioTrack.WRITE_BLOCKING); + } + + public void stop() { + if (mAudioTrack != null) { + mAudioTrack.stop(); + mAudioTrack = null; + } + } + + public int getFrameRate() { + return mFrameRate; + } + + public AudioTrack getAudioTrack() { + return mAudioTrack; + } +} diff --git a/common/src/java/com/example/android/common/midi/synth/SineOscillator.java b/common/src/java/com/example/android/common/midi/synth/SineOscillator.java new file mode 100644 index 00000000..c638c344 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SineOscillator.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * Sinewave oscillator. + */ +public class SineOscillator extends SawOscillator { + // Factorial constants. + private static final float IF3 = 1.0f / (2 * 3); + private static final float IF5 = IF3 / (4 * 5); + private static final float IF7 = IF5 / (6 * 7); + private static final float IF9 = IF7 / (8 * 9); + private static final float IF11 = IF9 / (10 * 11); + + /** + * Calculate sine using Taylor expansion. Do not use values outside the range. + * + * @param currentPhase in the range of -1.0 to +1.0 for one cycle + */ + public static float fastSin(float currentPhase) { + + /* Wrap phase back into region where results are more accurate. */ + float yp = (currentPhase > 0.5f) ? 1.0f - currentPhase + : ((currentPhase < (-0.5f)) ? (-1.0f) - currentPhase : currentPhase); + + float x = (float) (yp * Math.PI); + float x2 = (x * x); + /* Taylor expansion out to x**11/11! factored into multiply-adds */ + return x * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1); + } + + @Override + public float render() { + // Convert raw sawtooth to sine. + float phase = incrementWrapPhase(); + return fastSin(phase) * getAmplitude(); + } + +} diff --git a/common/src/java/com/example/android/common/midi/synth/SineVoice.java b/common/src/java/com/example/android/common/midi/synth/SineVoice.java new file mode 100644 index 00000000..e80d2c7e --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SineVoice.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * Replace sawtooth with a sine wave. + */ +public class SineVoice extends SawVoice { + @Override + protected SawOscillator createOscillator() { + return new SineOscillator(); + } +} diff --git a/common/src/java/com/example/android/common/midi/synth/SynthEngine.java b/common/src/java/com/example/android/common/midi/synth/SynthEngine.java new file mode 100644 index 00000000..6cd02a60 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SynthEngine.java @@ -0,0 +1,284 @@ +/* + * 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; + } + } +} diff --git a/common/src/java/com/example/android/common/midi/synth/SynthUnit.java b/common/src/java/com/example/android/common/midi/synth/SynthUnit.java new file mode 100644 index 00000000..90599e28 --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SynthUnit.java @@ -0,0 +1,35 @@ +/* + * 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; + +public abstract class SynthUnit { + + private static final double CONCERT_A_PITCH = 69.0; + private static final double CONCERT_A_FREQUENCY = 440.0; + + /** + * @param pitch + * MIDI pitch in semitones + * @return frequency + */ + public static double pitchToFrequency(double pitch) { + double semitones = pitch - CONCERT_A_PITCH; + return CONCERT_A_FREQUENCY * Math.pow(2.0, semitones / 12.0); + } + + public abstract float render(); +} diff --git a/common/src/java/com/example/android/common/midi/synth/SynthVoice.java b/common/src/java/com/example/android/common/midi/synth/SynthVoice.java new file mode 100644 index 00000000..78ba09ac --- /dev/null +++ b/common/src/java/com/example/android/common/midi/synth/SynthVoice.java @@ -0,0 +1,85 @@ +/* + * 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; + +/** + * Base class for a polyphonic synthesizer voice. + */ +public abstract class SynthVoice { + private int mNoteIndex; + private float mAmplitude; + public static final int STATE_OFF = 0; + public static final int STATE_ON = 1; + private int mState = STATE_OFF; + + public SynthVoice() { + mNoteIndex = -1; + } + + public void noteOn(int noteIndex, int velocity) { + mState = STATE_ON; + this.mNoteIndex = noteIndex; + setAmplitude(velocity / 128.0f); + } + + public void noteOff() { + mState = STATE_OFF; + } + + /** + * Add the output of this voice to an output buffer. + * + * @param outputBuffer + * @param samplesPerFrame + * @param level + */ + public void mix(float[] outputBuffer, int samplesPerFrame, float level) { + int numFrames = outputBuffer.length / samplesPerFrame; + for (int i = 0; i < numFrames; i++) { + float output = render(); + int offset = i * samplesPerFrame; + for (int jf = 0; jf < samplesPerFrame; jf++) { + outputBuffer[offset + jf] += output * level; + } + } + } + + public abstract float render(); + + public boolean isDone() { + return mState == STATE_OFF; + } + + public int getNoteIndex() { + return mNoteIndex; + } + + public float getAmplitude() { + return mAmplitude; + } + + public void setAmplitude(float amplitude) { + this.mAmplitude = amplitude; + } + + /** + * @param scaler + */ + public void setFrequencyScaler(float scaler) { + } + +} |