upload android base code part7

This commit is contained in:
August 2018-08-08 18:09:17 +08:00
parent 4e516ec6ed
commit 841ae54672
25229 changed files with 1709508 additions and 0 deletions

View file

@ -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.midisynth"
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/MidiSynthTheme">
<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=".MidiSynthDeviceService"
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/synth_device_info"/>
</service>
</application>
</manifest>

View file

@ -0,0 +1,11 @@
page.tags="MidiSynth"
sample.group=Media
@jd:body
<p>
This sample demonstrates how to use the MIDI API to receive and play MIDI messages coming from an
attached input device.
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

View file

@ -0,0 +1,53 @@
<?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.
-->
<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_synth_sender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:entries="@array/senders"
android:popupTheme="@android:style/ThemeOverlay.Material.Light" />
</Toolbar>
<TextView
android:id="@+id/message"
style="@android:style/TextAppearance.Medium"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingBottom="8dp"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:text="@string/synth_sender_text" />
</LinearLayout>

View file

@ -0,0 +1,25 @@
<!--
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_keep_screen_on"
android:checkable="true"
android:checked="true"
android:showAsAction="never"
android:title="@string/keep_screen_on"/>
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,24 @@
<!--
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.
-->
<resources>
<!-- Semantic definitions -->
<dimen name="horizontal_page_margin">@dimen/margin_huge</dimen>
<dimen name="vertical_page_margin">@dimen/margin_medium</dimen>
</resources>

View file

@ -0,0 +1,25 @@
<!--
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.
-->
<resources>
<style name="Widget.SampleMessage">
<item name="android:textAppearance">?android:textAppearanceLarge</item>
<item name="android:lineSpacingMultiplier">1.2</item>
<item name="android:shadowDy">-6.5</item>
</style>
</resources>

View file

@ -0,0 +1,22 @@
<!--
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.
-->
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<resources>
</resources>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<resources>
<string name="app_name">MidiSynth</string>
<string name="intro_message">
<![CDATA[
This sample demonstrates how to use the MIDI API to receive and play MIDI messages coming from an
attached input device.
]]>
</string>
</resources>

View file

@ -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">#4CAF50</color>
<color name="primary_dark">#388E3C</color>
<color name="primary_light">#C8E6C9</color>
<color name="accent">#FFEB3B</color>
<color name="primary_text">#212121</color>
<color name="secondary_text">#727272</color>
<color name="icons">#FFFFFF</color>
<color name="divider">#B6B6B6</color>
</resources>

View file

@ -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>
<string name="synth_sender_text">Select Sender for Synth</string>
<string name="error_port_busy">Selected port is in use or unavailable.</string>
<string name="port_open_ok">Port opened OK.</string>
<string-array name="senders">
<item>"none"</item>
</string-array>
<string name="keep_screen_on">Keep screen on</string>
</resources>

View file

@ -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="MidiSynthTheme" 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>

View file

@ -0,0 +1,32 @@
<!--
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.
-->
<resources>
<!-- Define standard dimensions to comply with Holo-style grids and rhythm. -->
<dimen name="margin_tiny">4dp</dimen>
<dimen name="margin_small">8dp</dimen>
<dimen name="margin_medium">16dp</dimen>
<dimen name="margin_large">32dp</dimen>
<dimen name="margin_huge">64dp</dimen>
<!-- Semantic definitions -->
<dimen name="horizontal_page_margin">@dimen/margin_medium</dimen>
<dimen name="vertical_page_margin">@dimen/margin_medium</dimen>
</resources>

View file

@ -0,0 +1,42 @@
<!--
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.
-->
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />
<style name="AppTheme" parent="Theme.Sample" />
<!-- Widget styling -->
<style name="Widget" />
<style name="Widget.SampleMessage">
<item name="android:textAppearance">?android:textAppearanceMedium</item>
<item name="android:lineSpacingMultiplier">1.1</item>
</style>
<style name="Widget.SampleMessageTile">
<item name="android:background">@drawable/tile</item>
<item name="android:shadowColor">#7F000000</item>
<item name="android:shadowDy">-3.5</item>
<item name="android:shadowRadius">2</item>
</style>
</resources>

View file

@ -0,0 +1,21 @@
<?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 manufacturer="AndroidTest" product="SynthExample">
<input-port name="input" />
</device>
</devices>

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,93 @@
/*
* 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 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, MidiDeviceInfo.PortInfo.TYPE_INPUT);
}
@Override
public void onPortSelected(final MidiPortWrapper wrapper) {
close();
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(MidiConstants.TAG, "could not open " + info);
} else {
mOpenDevice = device;
mInputPort = mOpenDevice.openInputPort(
wrapper.getPortIndex());
if (mInputPort == null) {
Log.e(MidiConstants.TAG, "could not open input port on " + info);
}
}
}
}, null);
// Don't run the callback on the UI thread because openInputPort might take a while.
}
}
public MidiReceiver getReceiver() {
return mInputPort;
}
@Override
public void onClose() {
try {
if (mInputPort != null) {
Log.i(MidiConstants.TAG, "MidiInputPortSelector.onClose() - close port");
mInputPort.close();
}
mInputPort = null;
if (mOpenDevice != null) {
mOpenDevice.close();
}
mOpenDevice = null;
} catch (IOException e) {
Log.e(MidiConstants.TAG, "cleanup failed", e);
}
}
}

View file

@ -0,0 +1,84 @@
/*
* 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.util.Log;
import java.io.IOException;
/**
* Select an output port and connect it to a destination input port.
*/
public class MidiOutputPortConnectionSelector extends MidiPortSelector {
private MidiPortConnector mSynthConnector;
private MidiDeviceInfo mDestinationDeviceInfo;
private int mDestinationPortIndex;
private MidiPortConnector.OnPortsConnectedListener mConnectedListener;
/**
* @param midiManager
* @param activity
* @param spinnerId
* @param type
*/
public MidiOutputPortConnectionSelector(MidiManager midiManager,
Activity activity, int spinnerId,
MidiDeviceInfo destinationDeviceInfo, int destinationPortIndex) {
super(midiManager, activity, spinnerId,
MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
mDestinationDeviceInfo = destinationDeviceInfo;
mDestinationPortIndex = destinationPortIndex;
}
@Override
public void onPortSelected(final MidiPortWrapper wrapper) {
Log.i(MidiConstants.TAG, "connectPortToSynth: " + wrapper);
onClose();
if (wrapper.getDeviceInfo() != null) {
mSynthConnector = new MidiPortConnector(mMidiManager);
mSynthConnector.connectToDevicePort(wrapper.getDeviceInfo(),
wrapper.getPortIndex(), mDestinationDeviceInfo,
mDestinationPortIndex,
// not safe on UI thread
mConnectedListener, null);
}
}
@Override
public void onClose() {
try {
if (mSynthConnector != null) {
mSynthConnector.close();
mSynthConnector = null;
}
} catch (IOException e) {
Log.e(MidiConstants.TAG, "Exception in closeSynthResources()", e);
}
}
/**
* @param myPortsConnectedListener
*/
public void setConnectedListener(
MidiPortConnector.OnPortsConnectedListener connectedListener) {
mConnectedListener = connectedListener;
}
}

View file

@ -0,0 +1,103 @@
/*
* 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 MidiOutputPort mOutputPort;
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, MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
}
@Override
public void onPortSelected(final MidiPortWrapper wrapper) {
Log.i(MidiConstants.TAG, "onPortSelected: " + wrapper);
close();
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(MidiConstants.TAG, "could not open " + info);
} else {
mOpenDevice = device;
mOutputPort = device.openOutputPort(wrapper.getPortIndex());
if (mOutputPort == null) {
Log.e(MidiConstants.TAG,
"could not open output port for " + info);
return;
}
mOutputPort.connect(mDispatcher);
}
}
}, null);
// Don't run the callback on the UI thread because openOutputPort might take a while.
}
}
@Override
public void onClose() {
try {
if (mOutputPort != null) {
mOutputPort.disconnect(mDispatcher);
}
mOutputPort = null;
if (mOpenDevice != null) {
mOpenDevice.close();
}
mOpenDevice = null;
} catch (IOException e) {
Log.e(MidiConstants.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();
}
}

View file

@ -0,0 +1,200 @@
/*
* 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;
/**
* Tool for connecting MIDI ports on two remote devices.
*/
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) {
Log.i(MidiConstants.TAG,
"MidiPortConnector closing connection " + mConnection);
mConnection.close();
mConnection = null;
}
if (mSourceDevice != null) {
mSourceDevice.close();
mSourceDevice = null;
}
if (mDestinationDevice != null) {
mDestinationDevice.close();
mDestinationDevice = null;
}
}
private void safeClose() {
try {
close();
} catch (IOException e) {
Log.e(MidiConstants.TAG, "could not close resources", e);
}
}
/**
* 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) {
safeClose();
mMidiManager.openDevice(destinationDeviceInfo,
new MidiManager.OnDeviceOpenedListener() {
@Override
public void onDeviceOpened(MidiDevice destinationDevice) {
if (destinationDevice == null) {
Log.e(MidiConstants.TAG,
"could not open " + destinationDeviceInfo);
if (listener != null) {
listener.onPortsConnected(null);
}
} else {
mDestinationDevice = destinationDevice;
Log.i(MidiConstants.TAG,
"connectToDevicePort opened "
+ destinationDeviceInfo);
// Destination device was opened so go to next step.
MidiInputPort destinationInputPort = destinationDevice
.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);
safeClose();
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
*/
private 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);
safeClose();
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);
safeClose();
}
if (listener != null) {
listener.onPortsConnected(mConnection);
}
}
}
}, handler);
}
}

View file

@ -0,0 +1,183 @@
/*
* 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.MidiDeviceStatus;
import android.media.midi.MidiManager;
import android.media.midi.MidiManager.DeviceCallback;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.HashSet;
/**
* Base class that uses a Spinner to select available MIDI ports.
*/
public abstract class MidiPortSelector extends DeviceCallback {
private int mType = MidiDeviceInfo.PortInfo.TYPE_INPUT;
protected ArrayAdapter<MidiPortWrapper> mAdapter;
protected HashSet<MidiPortWrapper> mBusyPorts = new HashSet<MidiPortWrapper>();
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, 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);
}
}
/**
* Set to no port selected.
*/
public void clearSelection() {
mSpinner.setSelection(0);
}
private int getInfoPortCount(final MidiDeviceInfo info) {
int portCount = (mType == MidiDeviceInfo.PortInfo.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) {
MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
mAdapter.add(wrapper);
Log.i(MidiConstants.TAG, wrapper + " was added");
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onDeviceRemoved(final MidiDeviceInfo info) {
int portCount = getInfoPortCount(info);
for (int i = 0; i < portCount; ++i) {
MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
MidiPortWrapper currentWrapper = mCurrentWrapper;
mAdapter.remove(wrapper);
// If the currently selected port was removed then select no port.
if (wrapper.equals(currentWrapper)) {
clearSelection();
}
mAdapter.notifyDataSetChanged();
Log.i(MidiConstants.TAG, wrapper + " was removed");
}
}
@Override
public void onDeviceStatusChanged(final MidiDeviceStatus status) {
// If an input port becomes busy then remove it from the menu.
// If it becomes free then add it back to the menu.
if (mType == MidiDeviceInfo.PortInfo.TYPE_INPUT) {
MidiDeviceInfo info = status.getDeviceInfo();
Log.i(MidiConstants.TAG, "MidiPortSelector.onDeviceStatusChanged status = " + status
+ ", mType = " + mType
+ ", activity = " + mActivity.getPackageName()
+ ", info = " + info);
// Look for transitions from free to busy.
int portCount = info.getInputPortCount();
for (int i = 0; i < portCount; ++i) {
MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
if (!wrapper.equals(mCurrentWrapper)) {
if (status.isInputPortOpen(i)) { // busy?
if (!mBusyPorts.contains(wrapper)) {
// was free, now busy
mBusyPorts.add(wrapper);
mAdapter.remove(wrapper);
mAdapter.notifyDataSetChanged();
}
} else {
if (mBusyPorts.remove(wrapper)) {
// was busy, now free
mAdapter.add(wrapper);
mAdapter.notifyDataSetChanged();
}
}
}
}
}
}
/**
* 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();
/**
*
*/
public void close() {
onClose();
}
}

View file

@ -0,0 +1,125 @@
/*
* 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;
import android.util.Log;
// Wrapper for a MIDI device and port description.
public class MidiPortWrapper {
private MidiDeviceInfo mInfo;
private int mPortIndex;
private int mType;
private String mString;
/**
* Wrapper for a MIDI device and port description.
* @param info
* @param portType
* @param portIndex
*/
public MidiPortWrapper(MidiDeviceInfo info, int portType, int portIndex) {
mInfo = info;
mType = portType;
mPortIndex = portIndex;
}
private void updateString() {
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());
sb.append(", ").append(name);
PortInfo portInfo = findPortInfo();
sb.append("[" + mPortIndex + "]");
if (portInfo != null) {
sb.append(", ").append(portInfo.getName());
} else {
sb.append(", null");
}
mString = sb.toString();
}
}
/**
* @param info
* @param portIndex
* @return
*/
private PortInfo findPortInfo() {
PortInfo[] ports = mInfo.getPorts();
for (PortInfo portInfo : ports) {
if (portInfo.getPortNumber() == mPortIndex
&& portInfo.getType() == mType) {
return portInfo;
}
}
return null;
}
public int getPortIndex() {
return mPortIndex;
}
public MidiDeviceInfo getDeviceInfo() {
return mInfo;
}
@Override
public String toString() {
if (mString == null) {
updateString();
}
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 (mType != otherWrapper.mType)
return false;
if (mInfo == null)
return (otherWrapper.mInfo == null);
return mInfo.equals(otherWrapper.mInfo);
}
@Override
public int hashCode() {
int hashCode = 1;
hashCode = 31 * hashCode + mPortIndex;
hashCode = 31 * hashCode + mType;
hashCode = 31 * hashCode + mInfo.hashCode();
return hashCode;
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.MidiManager;
/**
* Miscellaneous tools for Android MIDI.
*/
public class MidiTools {
/**
* @return a device that matches the manufacturer and product or null
*/
public static MidiDeviceInfo findDevice(MidiManager midiManager,
String manufacturer, String product) {
for (MidiDeviceInfo info : midiManager.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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}

View file

@ -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) {
}
}

View file

@ -0,0 +1,135 @@
/*
* 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.midisynth;
import android.app.ActionBar;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.media.midi.MidiDevice.MidiConnection;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.WindowManager;
import android.widget.Toast;
import android.widget.Toolbar;
import com.example.android.common.midi.MidiOutputPortConnectionSelector;
import com.example.android.common.midi.MidiPortConnector;
import com.example.android.common.midi.MidiTools;
/**
* Simple synthesizer as a MIDI Device.
*/
public class MainActivity extends Activity {
static final String TAG = "MidiSynthExample";
private MidiManager mMidiManager;
private MidiOutputPortConnectionSelector mPortSelector;
@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);
}
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI)) {
setupMidi();
}
}
@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_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);
}
}
private void setupMidi() {
// Setup MIDI
mMidiManager = (MidiManager) getSystemService(MIDI_SERVICE);
MidiDeviceInfo synthInfo = MidiTools.findDevice(mMidiManager, "AndroidTest",
"SynthExample");
int portIndex = 0;
mPortSelector = new MidiOutputPortConnectionSelector(mMidiManager, this,
R.id.spinner_synth_sender, synthInfo, portIndex);
mPortSelector.setConnectedListener(new MyPortsConnectedListener());
}
private void closeSynthResources() {
if (mPortSelector != null) {
mPortSelector.close();
}
}
// TODO A better way would be to listen to the synth server
// for open/close events and then disable/enable the spinner.
private class MyPortsConnectedListener
implements MidiPortConnector.OnPortsConnectedListener {
@Override
public void onPortsConnected(final MidiConnection connection) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (connection == null) {
Toast.makeText(MainActivity.this,
R.string.error_port_busy, Toast.LENGTH_SHORT)
.show();
mPortSelector.clearSelection();
} else {
Toast.makeText(MainActivity.this,
R.string.port_open_ok, Toast.LENGTH_SHORT)
.show();
}
}
});
}
}
@Override
public void onDestroy() {
closeSynthResources();
super.onDestroy();
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.midisynth;
import android.media.midi.MidiDeviceService;
import android.media.midi.MidiDeviceStatus;
import android.media.midi.MidiReceiver;
import com.example.android.common.midi.synth.SynthEngine;
public class MidiSynthDeviceService extends MidiDeviceService {
private static final String TAG = MainActivity.TAG;
private SynthEngine mSynthEngine = new SynthEngine();
private boolean mSynthStarted = false;
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onDestroy() {
mSynthEngine.stop();
super.onDestroy();
}
@Override
public MidiReceiver[] onGetInputPortReceivers() {
return new MidiReceiver[]{mSynthEngine};
}
/**
* This will get called when clients connect or disconnect.
*/
@Override
public void onDeviceStatusChanged(MidiDeviceStatus status) {
if (status.isInputPortOpen(0) && !mSynthStarted) {
mSynthEngine.start();
mSynthStarted = true;
} else if (!status.isInputPortOpen(0) && mSynthStarted) {
mSynthEngine.stop();
mSynthStarted = false;
}
}
}