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 | |
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
57 files changed, 3408 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) { + } + +} diff --git a/media/MidiScope/Application/.gitignore b/media/MidiScope/Application/.gitignore new file mode 100644 index 00000000..6eb878d4 --- /dev/null +++ b/media/MidiScope/Application/.gitignore @@ -0,0 +1,16 @@ +# Copyright 2013 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. +src/template/ +src/common/ +build.gradle diff --git a/media/MidiScope/Application/proguard-project.txt b/media/MidiScope/Application/proguard-project.txt new file mode 100644 index 00000000..f2fe1559 --- /dev/null +++ b/media/MidiScope/Application/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/media/MidiScope/Application/src/androidTest/java/com/example/android/midiscope/test/SampleTests.java b/media/MidiScope/Application/src/androidTest/java/com/example/android/midiscope/test/SampleTests.java new file mode 100644 index 00000000..e132dae9 --- /dev/null +++ b/media/MidiScope/Application/src/androidTest/java/com/example/android/midiscope/test/SampleTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 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.midiscope.test; + +import com.example.android.midiscope.*; + +import android.test.ActivityInstrumentationTestCase2; + +/** + * Tests for MidiScope sample. + */ +public class SampleTests extends ActivityInstrumentationTestCase2<MainActivity> { + + private MainActivity mTestActivity; + + public SampleTests() { + super(MainActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // Starts the activity under test using the default Intent with: + // action = {@link Intent#ACTION_MAIN} + // flags = {@link Intent#FLAG_ACTIVITY_NEW_TASK} + // All other fields are null or empty. + mTestActivity = getActivity(); + } + + /** + * Test if the test fixture has been set up correctly. + */ + public void testPreconditions() { + //Try to add a message to add context to your assertions. These messages will be shown if + //a tests fails and make it easy to understand why a test failed + assertNotNull("mTestActivity is null", mTestActivity); + } + +} diff --git a/media/MidiScope/Application/src/main/AndroidManifest.xml b/media/MidiScope/Application/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c2b1c43f --- /dev/null +++ b/media/MidiScope/Application/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 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. +--> +<manifest + package="com.example.android.midiscope" + xmlns:android="http://schemas.android.com/apk/res/android" + android:versionCode="1" + android:versionName="1.0"> + + <uses-feature + android:name="android.software.midi" + android:required="true"/> + + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:theme="@style/MidiScopeTheme"> + + <activity + android:name=".MainActivity" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + + <service + android:name="MidiScope" + android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE"> + <intent-filter> + <action android:name="android.media.midi.MidiDeviceService"/> + </intent-filter> + <meta-data + android:name="android.media.midi.MidiDeviceService" + android:resource="@xml/scope_device_info"/> + </service> + + </application> + +</manifest> diff --git a/media/MidiScope/Application/src/main/java/com/example/android/midiscope/LoggingReceiver.java b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/LoggingReceiver.java new file mode 100644 index 00000000..23ce8f7c --- /dev/null +++ b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/LoggingReceiver.java @@ -0,0 +1,62 @@ +/* + * 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.midiscope; + +import android.media.midi.MidiReceiver; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * Convert incoming MIDI messages to a string and write them to a ScopeLogger. + * Assume that messages have been aligned using a MidiFramer. + */ +public class LoggingReceiver extends MidiReceiver { + public static final String TAG = "MidiScope"; + private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); + private long mStartTime; + private ScopeLogger mLogger; + + public LoggingReceiver(ScopeLogger logger) { + mStartTime = System.nanoTime(); + mLogger = logger; + } + + /* + * @see android.media.midi.MidiReceiver#onReceive(byte[], int, int, long) + */ + @Override + public void onSend(byte[] data, int offset, int count, long timestamp) + throws IOException { + StringBuilder sb = new StringBuilder(); + if (timestamp == 0) { + sb.append(String.format("-----0----: ")); + } else { + long monoTime = timestamp - mStartTime; + double seconds = (double) monoTime / NANOS_PER_SECOND; + sb.append(String.format("%10.3f: ", seconds)); + } + sb.append(MidiPrinter.formatBytes(data, offset, count)); + sb.append(": "); + sb.append(MidiPrinter.formatMessage(data, offset, count)); + String text = sb.toString(); + mLogger.log(text); + Log.i(TAG, text); + } + +}
\ No newline at end of file diff --git a/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MainActivity.java b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MainActivity.java new file mode 100644 index 00000000..41d74f03 --- /dev/null +++ b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MainActivity.java @@ -0,0 +1,162 @@ +/* + * 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.midiscope; + +import android.app.ActionBar; +import android.app.Activity; +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiManager; +import android.media.midi.MidiReceiver; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toolbar; + +import com.example.android.common.midi.MidiFramer; +import com.example.android.common.midi.MidiOutputPortSelector; +import com.example.android.common.midi.MidiPortWrapper; + +import java.util.LinkedList; + +/** + * App that provides a MIDI echo service. + */ +public class MainActivity extends Activity implements ScopeLogger { + + private static final int MAX_LINES = 100; + + private final LinkedList<String> mLogLines = new LinkedList<>(); + private TextView mLog; + private ScrollView mScroller; + private MidiOutputPortSelector mLogSenderSelector; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + setActionBar((Toolbar) findViewById(R.id.toolbar)); + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(false); + } + + mLog = (TextView) findViewById(R.id.log); + mScroller = (ScrollView) findViewById(R.id.scroll); + + // Setup MIDI + MidiManager midiManager = (MidiManager) getSystemService(MIDI_SERVICE); + + // Receiver that prints the messages. + MidiReceiver loggingReceiver = new LoggingReceiver(this); + + // Receiver that parses raw data into complete messages. + MidiFramer connectFramer = new MidiFramer(loggingReceiver); + + // Setup a menu to select an input source. + mLogSenderSelector = new MidiOutputPortSelector(midiManager, this, R.id.spinner_senders) { + @Override + public void onPortSelected(final MidiPortWrapper wrapper) { + super.onPortSelected(wrapper); + if (wrapper != null) { + mLogLines.clear(); + MidiDeviceInfo deviceInfo = wrapper.getDeviceInfo(); + if (deviceInfo == null) { + log(getString(R.string.header_text)); + } else { + log(MidiPrinter.formatDeviceInfo(deviceInfo)); + } + } + } + }; + mLogSenderSelector.getSender().connect(connectFramer); + + // Tell the virtual device to log its messages here.. + MidiScope.setScopeLogger(this); + } + + @Override + public void onDestroy() { + mLogSenderSelector.onClose(); + // The scope will live on as a service so we need to tell it to stop + // writing log messages to this Activity. + MidiScope.setScopeLogger(null); + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + setKeepScreenOn(menu.findItem(R.id.action_keep_screen_on).isChecked()); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_clear_all: + mLogLines.clear(); + logOnUiThread(""); + break; + case R.id.action_keep_screen_on: + boolean checked = !item.isChecked(); + setKeepScreenOn(checked); + item.setChecked(checked); + break; + } + return super.onOptionsItemSelected(item); + } + + private void setKeepScreenOn(boolean keepScreenOn) { + if (keepScreenOn) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + @Override + public void log(final String string) { + runOnUiThread(new Runnable() { + @Override + public void run() { + logOnUiThread(string); + } + }); + } + + /** + * Logs a message to our TextView. This needs to be called from the UI thread. + */ + private void logOnUiThread(String s) { + mLogLines.add(s); + if (mLogLines.size() > MAX_LINES) { + mLogLines.removeFirst(); + } + // Render line buffer to one String. + StringBuilder sb = new StringBuilder(); + for (String line : mLogLines) { + sb.append(line).append('\n'); + } + mLog.setText(sb.toString()); + mScroller.fullScroll(View.FOCUS_DOWN); + } +} diff --git a/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MidiPrinter.java b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MidiPrinter.java new file mode 100644 index 00000000..9e97c04c --- /dev/null +++ b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MidiPrinter.java @@ -0,0 +1,108 @@ +/* + * 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.midiscope; + +import android.media.midi.MidiDeviceInfo; +import android.media.midi.MidiDeviceInfo.PortInfo; +import android.os.Bundle; + +import com.example.android.common.midi.MidiConstants; + +/** + * Format a MIDI message for printing. + */ +public class MidiPrinter { + + public static final String[] CHANNEL_COMMAND_NAMES = { "NoteOff", "NoteOn", + "PolyTouch", "Control", "Program", "Pressure", "Bend" }; + public static final String[] SYSTEM_COMMAND_NAMES = { "SysEx", // F0 + "TimeCode", // F1 + "SongPos", // F2 + "SongSel", // F3 + "F4", // F4 + "F5", // F5 + "TuneReq", // F6 + "EndSysex", // F7 + "TimingClock", // F8 + "F9", // F9 + "Start", // FA + "Continue", // FB + "Stop", // FC + "FD", // FD + "ActiveSensing", // FE + "Reset" // FF + }; + + public static String getName(int status) { + if (status >= 0xF0) { + int index = status & 0x0F; + return SYSTEM_COMMAND_NAMES[index]; + } else if (status >= 0x80) { + int index = (status >> 4) & 0x07; + return CHANNEL_COMMAND_NAMES[index]; + } else { + return "data"; + } + } + + public static String formatBytes(byte[] data, int offset, int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(String.format(" %02X", data[offset + i])); + } + return sb.toString(); + } + + public static String formatMessage(byte[] data, int offset, int count) { + StringBuilder sb = new StringBuilder(); + byte statusByte = data[offset++]; + int status = statusByte & 0xFF; + sb.append(getName(status)).append("("); + int numData = MidiConstants.getBytesPerMessage(statusByte) - 1; + if ((status >= 0x80) && (status < 0xF0)) { // channel message + int channel = status & 0x0F; + // Add 1 for humans who think channels are numbered 1-16. + sb.append((channel + 1)).append(", "); + } + for (int i = 0; i < numData; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(data[offset++]); + } + sb.append(")"); + return sb.toString(); + } + + public static String formatDeviceInfo(MidiDeviceInfo info) { + StringBuilder sb = new StringBuilder(); + if (info != null) { + Bundle properties = info.getProperties(); + for (String key : properties.keySet()) { + Object value = properties.get(key); + sb.append(key).append(" = ").append(value).append('\n'); + } + for (PortInfo port : info.getPorts()) { + sb.append((port.getType() == PortInfo.TYPE_INPUT) ? "input" + : "output"); + sb.append("[").append(port.getPortNumber()).append("] = \"").append(port.getName() + + "\"\n"); + } + } + return sb.toString(); + } +} diff --git a/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MidiScope.java b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MidiScope.java new file mode 100644 index 00000000..3965d83c --- /dev/null +++ b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/MidiScope.java @@ -0,0 +1,83 @@ +/* + * 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.midiscope; + +import android.media.midi.MidiDeviceService; +import android.media.midi.MidiDeviceStatus; +import android.media.midi.MidiReceiver; + +import com.example.android.common.midi.MidiFramer; + +import java.io.IOException; + +/** + * Virtual MIDI Device that logs messages to a ScopeLogger. + */ + +public class MidiScope extends MidiDeviceService { + + private static ScopeLogger mScopeLogger; + private MidiReceiver mInputReceiver = new MyReceiver(); + private static MidiFramer mDeviceFramer; + + @Override + public MidiReceiver[] onGetInputPortReceivers() { + return new MidiReceiver[] { mInputReceiver }; + } + + public static ScopeLogger getScopeLogger() { + return mScopeLogger; + } + + public static void setScopeLogger(ScopeLogger logger) { + if (logger != null) { + // Receiver that prints the messages. + LoggingReceiver loggingReceiver = new LoggingReceiver(logger); + mDeviceFramer = new MidiFramer(loggingReceiver); + } + mScopeLogger = logger; + } + + private static class MyReceiver extends MidiReceiver { + @Override + public void onSend(byte[] data, int offset, int count, + long timestamp) throws IOException { + if (mScopeLogger != null) { + // Send raw data to be parsed into discrete messages. + mDeviceFramer.send(data, offset, count, timestamp); + } + } + } + + /** + * This will get called when clients connect or disconnect. + * Log device information. + */ + @Override + public void onDeviceStatusChanged(MidiDeviceStatus status) { + if (mScopeLogger != null) { + if (status.isInputPortOpen(0)) { + mScopeLogger.log("=== connected ==="); + String text = MidiPrinter.formatDeviceInfo( + status.getDeviceInfo()); + mScopeLogger.log(text); + } else { + mScopeLogger.log("--- disconnected ---"); + } + } + } +} diff --git a/media/MidiScope/Application/src/main/java/com/example/android/midiscope/ScopeLogger.java b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/ScopeLogger.java new file mode 100644 index 00000000..dc52efd6 --- /dev/null +++ b/media/MidiScope/Application/src/main/java/com/example/android/midiscope/ScopeLogger.java @@ -0,0 +1,25 @@ +/* + * 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.midiscope; + +public interface ScopeLogger { + /** + * Write the text string somewhere that the user can see it. + * @param text + */ + void log(String text); +} diff --git a/media/MidiScope/Application/src/main/res/drawable-hdpi/ic_clear_all.png b/media/MidiScope/Application/src/main/res/drawable-hdpi/ic_clear_all.png Binary files differnew file mode 100755 index 00000000..e23d886c --- /dev/null +++ b/media/MidiScope/Application/src/main/res/drawable-hdpi/ic_clear_all.png diff --git a/media/MidiScope/Application/src/main/res/drawable-mdpi/ic_clear_all.png b/media/MidiScope/Application/src/main/res/drawable-mdpi/ic_clear_all.png Binary files differnew file mode 100755 index 00000000..dca30487 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/drawable-mdpi/ic_clear_all.png diff --git a/media/MidiScope/Application/src/main/res/drawable-xhdpi/ic_clear_all.png b/media/MidiScope/Application/src/main/res/drawable-xhdpi/ic_clear_all.png Binary files differnew file mode 100755 index 00000000..fef5dcd2 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/drawable-xhdpi/ic_clear_all.png diff --git a/media/MidiScope/Application/src/main/res/drawable-xxhdpi/ic_clear_all.png b/media/MidiScope/Application/src/main/res/drawable-xxhdpi/ic_clear_all.png Binary files differnew file mode 100755 index 00000000..51d2d3da --- /dev/null +++ b/media/MidiScope/Application/src/main/res/drawable-xxhdpi/ic_clear_all.png diff --git a/media/MidiScope/Application/src/main/res/drawable-xxxhdpi/ic_clear_all.png b/media/MidiScope/Application/src/main/res/drawable-xxxhdpi/ic_clear_all.png Binary files differnew file mode 100755 index 00000000..9dbccf36 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/drawable-xxxhdpi/ic_clear_all.png diff --git a/media/MidiScope/Application/src/main/res/layout/main.xml b/media/MidiScope/Application/src/main/res/layout/main.xml new file mode 100644 index 00000000..71c52aa6 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/layout/main.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/colorPrimary" + android:elevation="4dp" + android:minHeight="?android:attr/actionBarSize" + android:popupTheme="@android:style/ThemeOverlay.Material.Light" + android:theme="@android:style/ThemeOverlay.Material.Dark.ActionBar"> + + <Spinner + android:id="@+id/spinner_senders" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:entries="@array/senders" + android:popupTheme="@android:style/ThemeOverlay.Material.Light"/> + + </Toolbar> + + <ScrollView + android:id="@+id/scroll" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1"> + + <TextView + android:id="@+id/log" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="8dp" + android:paddingEnd="16dp" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:textAppearance="?android:attr/textAppearanceMedium"/> + + </ScrollView> + +</LinearLayout> diff --git a/media/MidiScope/Application/src/main/res/menu/main.xml b/media/MidiScope/Application/src/main/res/menu/main.xml new file mode 100644 index 00000000..abb1842a --- /dev/null +++ b/media/MidiScope/Application/src/main/res/menu/main.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/action_clear_all" + android:icon="@drawable/ic_clear_all" + android:showAsAction="ifRoom" + android:title="@string/clear_log"/> + + <item + android:id="@+id/action_keep_screen_on" + android:checkable="true" + android:checked="true" + android:showAsAction="never" + android:title="@string/keep_screen_on"/> + +</menu> diff --git a/media/MidiScope/Application/src/main/res/mipmap-hdpi/ic_launcher.png b/media/MidiScope/Application/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100755 index 00000000..4a755241 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/media/MidiScope/Application/src/main/res/mipmap-mdpi/ic_launcher.png b/media/MidiScope/Application/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100755 index 00000000..09a42710 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/media/MidiScope/Application/src/main/res/mipmap-xhdpi/ic_launcher.png b/media/MidiScope/Application/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100755 index 00000000..e9c9a361 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/media/MidiScope/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/media/MidiScope/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100755 index 00000000..6e79c3bc --- /dev/null +++ b/media/MidiScope/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/media/MidiScope/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/media/MidiScope/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100755 index 00000000..638d4e13 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/media/MidiScope/Application/src/main/res/values/colors.xml b/media/MidiScope/Application/src/main/res/values/colors.xml new file mode 100644 index 00000000..eef48d88 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/values/colors.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. +--> +<resources> + <color name="primary">#009688</color> + <color name="primary_dark">#00796B</color> + <color name="primary_light">#B2DFDB</color> + <color name="accent">#FFC107</color> + <color name="primary_text">#212121</color> + <color name="secondary_text">#727272</color> + <color name="icons">#FFFFFF</color> + <color name="divider">#B6B6B6</color> +</resources> diff --git a/media/MidiScope/Application/src/main/res/values/strings.xml b/media/MidiScope/Application/src/main/res/values/strings.xml new file mode 100644 index 00000000..52beb4e1 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/values/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 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. +--> +<resources> + <string name="header_text">Select a MIDI source from the Spinner above or send messages to MidiScope.</string> + <string name="clear_log">Clear Log</string> + <string name="keep_screen_on">Keep Screen On</string> + <string-array name="senders"> + <item>"none"</item> + </string-array> +</resources> diff --git a/media/MidiScope/Application/src/main/res/values/styles.xml b/media/MidiScope/Application/src/main/res/values/styles.xml new file mode 100644 index 00000000..00360096 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/values/styles.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 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. +--> +<resources> + + <style name="MidiScopeTheme" parent="android:Theme.Material.Light.NoActionBar"> + <item name="android:colorPrimary">@color/primary</item> + <item name="android:colorPrimaryDark">@color/primary_dark</item> + <item name="android:colorAccent">@color/accent</item> + </style> + +</resources> diff --git a/media/MidiScope/Application/src/main/res/xml/scope_device_info.xml b/media/MidiScope/Application/src/main/res/xml/scope_device_info.xml new file mode 100644 index 00000000..f89f1109 --- /dev/null +++ b/media/MidiScope/Application/src/main/res/xml/scope_device_info.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. +--> +<devices> + <device + name="AndroidMidiScope" + manufacturer="AndroidTest" + product="Scope"> + <input-port name="input"/> + </device> +</devices> diff --git a/media/MidiScope/build.gradle b/media/MidiScope/build.gradle new file mode 100644 index 00000000..9b6a9ce4 --- /dev/null +++ b/media/MidiScope/build.gradle @@ -0,0 +1,12 @@ + + +// BEGIN_EXCLUDE +import com.example.android.samples.build.SampleGenPlugin +apply plugin: SampleGenPlugin + +samplegen { + pathToBuild "../../../../build" + pathToSamplesCommon "../../common" +} +apply from: "../../../../build/build.gradle" +// END_EXCLUDE diff --git a/media/MidiScope/buildSrc/build.gradle b/media/MidiScope/buildSrc/build.gradle new file mode 100644 index 00000000..d77115d0 --- /dev/null +++ b/media/MidiScope/buildSrc/build.gradle @@ -0,0 +1,16 @@ + +repositories { + jcenter() +} +dependencies { + compile 'org.freemarker:freemarker:2.3.20' +} + +sourceSets { + main { + groovy { + srcDir new File(rootDir, "../../../../../build/buildSrc/src/main/groovy") + } + } +} + diff --git a/media/MidiScope/gradle/wrapper/gradle-wrapper.jar b/media/MidiScope/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 00000000..8c0fb64a --- /dev/null +++ b/media/MidiScope/gradle/wrapper/gradle-wrapper.jar diff --git a/media/MidiScope/gradle/wrapper/gradle-wrapper.properties b/media/MidiScope/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..afb32963 --- /dev/null +++ b/media/MidiScope/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 10 15:27:10 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-bin.zip diff --git a/media/MidiScope/gradlew b/media/MidiScope/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/media/MidiScope/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/media/MidiScope/gradlew.bat b/media/MidiScope/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/media/MidiScope/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/media/MidiScope/screenshots/1-main.png b/media/MidiScope/screenshots/1-main.png Binary files differnew file mode 100644 index 00000000..0b4fb8e8 --- /dev/null +++ b/media/MidiScope/screenshots/1-main.png diff --git a/media/MidiScope/screenshots/2-signals.png b/media/MidiScope/screenshots/2-signals.png Binary files differnew file mode 100644 index 00000000..7e4b443e --- /dev/null +++ b/media/MidiScope/screenshots/2-signals.png diff --git a/media/MidiScope/screenshots/icon-web.png b/media/MidiScope/screenshots/icon-web.png Binary files differnew file mode 100644 index 00000000..6daec5cd --- /dev/null +++ b/media/MidiScope/screenshots/icon-web.png diff --git a/media/MidiScope/settings.gradle b/media/MidiScope/settings.gradle new file mode 100644 index 00000000..0a5c310b --- /dev/null +++ b/media/MidiScope/settings.gradle @@ -0,0 +1,2 @@ + +include 'Application' diff --git a/media/MidiScope/template-params.xml b/media/MidiScope/template-params.xml new file mode 100644 index 00000000..8caea80e --- /dev/null +++ b/media/MidiScope/template-params.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 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. +--> +<sample> + <name>MidiScope</name> + <group>Media</group> + <package>com.example.android.midiscope</package> + <minSdk>23</minSdk> + <compileSdkVersion>23</compileSdkVersion> + + <strings> + <intro> + <![CDATA[ +This sample demonstrates how to use the MIDI API to receive and process MIDI signals coming from an +attached input device. + ]]> + </intro> + </strings> + + <common src="midi"/> + <template src="base"/> + + <metadata> + <status>PUBLISHED</status> + <categories>Media</categories> + <technologies>Android</technologies> + <languages>Java</languages> + <solutions>Mobile</solutions> + <level>INTERMEDIATE</level> + <icon>screenshots/icon-web.png</icon> + <screenshots> + <img>screenshots/1-main.png</img> + <img>screenshots/2-settings.png</img> + </screenshots> + <api_refs> + <android>android.media.midi.MidiManager</android> + <android>android.media.midi.MidiReceiver</android> + </api_refs> + + <!-- 1-3 line description of the sample here. + + Avoid simply rearranging the sample's title. What does this sample actually + accomplish, and how does it do it? --> + <description> + <![CDATA[ +Sample demonstrating how to use the MIDI API to receive and process MIDI signals coming from an +attached device. + ]]> + </description> + + <!-- Multi-paragraph introduction to sample, from an educational point-of-view. + Makrdown formatting allowed. This will be used to generate a mini-article for the + sample on DAC. --> + <intro> + <![CDATA[ +The Android MIDI API ([android.media.midi][1]) allows developers to connect a MIDI device to Android +and process MIDI signals coming from it. This sample demonstrates some basic features of the MIDI +API, such as enumeration of currently available devices (Information includes name, vendor, +capabilities, etc), notification when MIDI devices are plugged in or unplugged, and receiving MIDI +signals. This sample simply shows all the received MIDI signals to the screen log and does not play +any sound for them. +[1]: https://developer.android.com/reference/android/media/midi/package-summary.html + ]]> + </intro> + </metadata> +</sample> |