• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.sample.oboe.manualtest;
17 
18 
19 import android.media.midi.MidiDeviceService;
20 import android.media.midi.MidiReceiver;
21 import android.util.Log;
22 
23 import com.mobileer.miditools.MidiConstants;
24 import com.mobileer.miditools.MidiFramer;
25 
26 import java.io.IOException;
27 import java.util.ArrayList;
28 
29 /**
30  * Measure the latency of various output paths by playing a blip.
31  * Report the results back to the TestListeners.
32  */
33 public class AudioMidiTester extends MidiDeviceService {
34 
35     // Sometimes the service can be run without the MainActivity being run!
36     static {
37         // Must match name in CMakeLists.txt
38         System.loadLibrary("oboetester");
39     }
40 
41     private static final float MAX_TOUCH_LATENCY = 0.200f;
42     private static final float MAX_OUTPUT_LATENCY = 0.600f;
43     private static final float ANALYSIS_TIME_MARGIN = 0.250f;
44 
45     private static final float ANALYSIS_TIME_DELAY = MAX_OUTPUT_LATENCY;
46     private static final float ANALYSIS_TIME_TOTAL = MAX_TOUCH_LATENCY + MAX_OUTPUT_LATENCY;
47     private static final float ANALYSIS_TIME_MAX = ANALYSIS_TIME_TOTAL + ANALYSIS_TIME_MARGIN;
48     private static final int ANALYSIS_SAMPLE_RATE = 48000; // need not match output rate
49 
50     private ArrayList<TestListener> mListeners = new ArrayList<TestListener>();
51     private MyMidiReceiver mReceiver = new MyMidiReceiver();
52     private MidiFramer mMidiFramer = new MidiFramer(mReceiver);
53     private boolean mRecordEnabled = true;
54 
55     private static AudioMidiTester mInstance;
56     private AudioRecordThread mRecorder;
57     private TapLatencyAnalyser mTapLatencyAnalyser;
58 
59     private AudioOutputTester mAudioOutputTester;
60 
61     public static class TestResult {
62         public float[] samples;
63         public float[] filtered;
64         public int frameRate;
65         public TapLatencyAnalyser.TapLatencyEvent[] events;
66     }
67 
68     public static interface TestListener {
onTestFinished(TestResult result)69         public void onTestFinished(TestResult result);
70 
onNoteOn(int pitch)71         public void onNoteOn(int pitch);
72     }
73 
74     /**
75      * This is a Service so it is only created when a client requests the service.
76      */
AudioMidiTester()77     public AudioMidiTester() {
78         mInstance = this;
79     }
80 
addTestListener(TestListener listener)81     public void addTestListener(TestListener listener) {
82         mListeners.add(listener);
83     }
84 
removeTestListener(TestListener listener)85     public void removeTestListener(TestListener listener) {
86         mListeners.remove(listener);
87     }
88 
89     @Override
onCreate()90     public void onCreate() {
91         super.onCreate();
92         if (mRecordEnabled) {
93             mRecorder = new AudioRecordThread(ANALYSIS_SAMPLE_RATE,
94                     1,
95                     (int) (ANALYSIS_TIME_MAX * ANALYSIS_SAMPLE_RATE));
96         }
97 
98         mAudioOutputTester = AudioOutputTester.getInstance();
99 
100         mTapLatencyAnalyser = new TapLatencyAnalyser();
101     }
102 
103     @Override
onDestroy()104     public void onDestroy() {
105         // do stuff here
106         super.onDestroy();
107     }
108 
getInstance()109     public static AudioMidiTester getInstance() {
110         return mInstance;
111     }
112 
113     class MyMidiReceiver extends MidiReceiver {
onSend(byte[] data, int offset, int count, long timestamp)114         public void onSend(byte[] data, int offset,
115                            int count, long timestamp) throws IOException {
116             // parse MIDI
117             byte command = (byte) (data[0] & 0x0F0);
118             if (command == MidiConstants.STATUS_NOTE_ON) {
119                 if (data[2] == 0) {
120                     noteOff(data[1]);
121                 } else {
122                     noteOn(data[1]);
123                 }
124             } else if (command == MidiConstants.STATUS_NOTE_OFF) {
125                 noteOff(data[1]);
126             }
127             Log.i(TapToToneActivity.TAG, "MIDI command = " + command);
128         }
129     }
130 
noteOn(byte b)131     private void noteOn(byte b) {
132         trigger();
133         fireNoteOn(b);
134     }
135 
fireNoteOn(byte pitch)136     private void fireNoteOn(byte pitch) {
137         for (TestListener listener : mListeners) {
138             listener.onNoteOn(pitch);
139         }
140     }
141 
noteOff(byte b)142     private void noteOff(byte b) {}
143 
144     @Override
onGetInputPortReceivers()145     public MidiReceiver[] onGetInputPortReceivers() {
146         return new MidiReceiver[]{mMidiFramer};
147     }
148 
149 
start()150     public void start() throws IOException {
151         if (mRecordEnabled) {
152             mRecorder.startAudio();
153         }
154     }
155 
trigger()156     public void trigger() {
157         mAudioOutputTester.trigger();
158         if (mRecordEnabled) {
159             // schedule an analysis to start in the near future
160             int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_DELAY);
161             Runnable task = new Runnable() {
162                 public void run() {
163                     new Thread() {
164                         public void run() {
165                             analyzeCapturedAudio();
166                         }
167                     }.start();
168                 }
169             };
170 
171             mRecorder.scheduleTask(numSamples, task);
172         }
173     }
174 
analyzeCapturedAudio()175     private void analyzeCapturedAudio() {
176         if (!mRecordEnabled) return;
177         int numSamples = (int) (mRecorder.getSampleRate() * ANALYSIS_TIME_TOTAL);
178         float[] buffer = new float[numSamples];
179         mRecorder.setCaptureEnabled(false); // TODO wait for it to settle
180         int numRead = mRecorder.readMostRecent(buffer);
181 
182         TestResult result = new TestResult();
183         result.samples = buffer;
184         result.frameRate = mRecorder.getSampleRate();
185         result.events = mTapLatencyAnalyser.analyze(buffer, 0, numRead);
186         result.filtered = mTapLatencyAnalyser.getFilteredBuffer();
187         mRecorder.setCaptureEnabled(true);
188         // notify listeners
189         for (TestListener listener : mListeners) {
190             listener.onTestFinished(result);
191         }
192     }
193 
194 
stop()195     public void stop() {
196         if (mRecordEnabled) {
197             mRecorder.stopAudio();
198         }
199     }
200 
201 }
202