1#!/usr/bin/python2.7
2"""
3Copyright (C) 2019 The Android Open Source Project
4
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9    http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16"""
17
18# Run OboeTester with progressive timing changes
19# to measure the DSP timing profile.
20# Print a CSV table of offsets and glitch counts.
21#
22# Run Automated Test using an Intent
23# https://github.com/google/oboe/blob/master/apps/OboeTester/docs/AutomatedTesting.md
24
25import array
26import collections
27import os
28import os.path
29import sys
30import subprocess
31import time
32
33from datetime import datetime
34
35kPropertyOutputOffset = "aaudio.out_mmap_offset_usec"
36kMinPeakAmplitude = 0.04
37kOffsetMin = 0000
38kOffsetMax = 4000
39kOffsetIncrement = 100
40kOutputFileBase = "/sdcard/dsp_timing_"
41gOutputFile = kOutputFileBase + "now.txt"
42
43def launchLatencyTest():
44    command = ["adb", "shell", "am", \
45               "start", "-n", "com.mobileer.oboetester/.MainActivity", \
46               "--es", "test", "latency", \
47               "--es", "file", gOutputFile, \
48               "--ei", "buffer_bursts", "1"]
49    return subprocess.check_output(command)
50
51def launchGlitchTest():
52    command = ["adb", "shell", "am", \
53               "start", "-n", "com.mobileer.oboetester/.MainActivity", \
54               "--es", "test", "glitch", \
55               "--es", "file", gOutputFile, \
56               "--es", "in_perf", "lowlat", \
57               "--es", "out_perf", "lowlat", \
58               "--es", "in_sharing", "exclusive", \
59               "--es", "out_sharing", "exclusive", \
60               "--ei", "buffer_bursts", "1"]
61    return subprocess.check_output(command)
62
63def setAndroidProperty(property, value):
64    return subprocess.check_output(["adb", "shell", "setprop", property, value])
65
66def getAndroidProperty(property):
67    return subprocess.check_output(["adb", "shell", "getprop", property])
68
69def setOutputOffset(offset):
70    setAndroidProperty(kPropertyOutputOffset, str(offset))
71
72def getOutputOffset():
73    offsetText = getAndroidProperty(kPropertyOutputOffset).strip()
74    if len(offsetText) == 0:
75        return 0;
76    return int(offsetText)
77
78def loadNameValuePairsFromFile(filename):
79    myvars = {}
80    with open(filename) as myfile:
81        for line in myfile:
82            name, var = line.partition("=")[::2]
83            myvars[name.strip()] = var
84    return myvars
85
86def loadNameValuePairsFromString(text):
87    myvars = {}
88    listOutput = text.splitlines()
89    for line in listOutput:
90            name, var = line.partition("=")[::2]
91            myvars[name.strip()] = var
92    return myvars
93
94def waitForTestResult():
95    testOutput = ""
96    for i in range(10):
97        if subprocess.call(["adb", "shell", "ls", gOutputFile, "2>/dev/null"]) == 0:
98            testOutput = subprocess.check_output(["adb", "shell", "cat", gOutputFile])
99            break
100        else:
101            print str(i) + ": waiting until test finishes..."
102            time.sleep(2)
103    # print testOutput
104    subprocess.call(["adb", "shell", "rm", gOutputFile])
105    return loadNameValuePairsFromString(testOutput)
106
107# volume too low?
108# return true if bad
109def checkPeakAmplitude(pairs):
110    if not pairs.has_key('peak.amplitude'):
111        print "ERROR no peak.amplitude"
112        return True
113    peakAmplitude = float(pairs['peak.amplitude'])
114    if peakAmplitude < kMinPeakAmplitude:
115        print "ERROR peakAmplitude = " + str(peakAmplitude) \
116            + " < " + str(kMinPeakAmplitude) \
117            + ", turn up volume"
118        return True
119    return False
120
121def startOneTest(offset):
122    print "=========================="
123    setOutputOffset(offset)
124    print "try offset = " + getAndroidProperty(kPropertyOutputOffset)
125    subprocess.call(["adb", "shell", "rm", gOutputFile, "2>/dev/null"])
126
127def runOneGlitchTest(offset):
128    startOneTest(offset)
129    print launchGlitchTest()
130    pairs = waitForTestResult()
131    if checkPeakAmplitude(pairs):
132        return -1
133    if not pairs.has_key('glitch.count'):
134        print "ERROR no glitch.count"
135        return -1
136    return int(pairs['glitch.count'])
137
138def runOneLatencyTest(offset):
139    startOneTest(offset)
140    print launchLatencyTest()
141    pairs = waitForTestResult()
142    if not pairs.has_key('latency.msec'):
143        print "ERROR no latency.msec"
144        return -1
145    return float(pairs['latency.msec'])
146
147def runGlitchSeries():
148    offsets = array.array('i')
149    glitches = array.array('i')
150    for offset in range(kOffsetMin, kOffsetMax, kOffsetIncrement):
151        offsets.append(offset)
152        result = runOneGlitchTest(offset)
153        glitches.append(result)
154        if result < 0:
155            break
156        print "offset = " + str(offset) + ", glitches = " + str(result)
157    print "offsetUs, glitches"
158    for i in range(len(offsets)):
159        print "  " + str(offsets[i]) + ", " + str(glitches[i])
160
161# return true if bad
162def checkLatencyValid():
163    startOneTest(0)
164    print launchLatencyTest()
165    pairs = waitForTestResult()
166    print "burst = " + pairs['out.burst.frames']
167    capacity = int(pairs['out.buffer.capacity.frames'])
168    if capacity < 0:
169        print "ERROR capacity = " + str(capacity)
170        return True
171    sampleRate = int(pairs['out.rate'])
172    capacityMillis = capacity * 1000.0 / sampleRate
173    print "capacityMillis = " + str(capacityMillis)
174    if pairs['in.mmap'].strip() != "yes":
175        print "ERROR Not using input MMAP"
176        return True
177    if pairs['out.mmap'].strip() != "yes":
178        print "ERROR Not using output MMAP"
179        return True
180    # Check whether we can change latency a moving the DSP pointer
181    # past the CPU pointer and wrapping the buffer.
182    latencyMin = runOneLatencyTest(kOffsetMin)
183    latencyMax = runOneLatencyTest(kOffsetMax + 1000)
184    print "latency = " + str(latencyMin) + " => " + str(latencyMax)
185    if latencyMax < (latencyMin + (capacityMillis / 2)):
186        print "ERROR Latency not affected by changing offset!"
187        return True
188    return False
189
190def isRunningAsRoot():
191    userName = subprocess.check_output(["adb", "shell", "whoami"]).strip()
192    if userName != "root":
193        print "WARNING: changing to 'adb root'"
194        subprocess.call(["adb", "root"])
195        userName = subprocess.check_output(["adb", "shell", "whoami"]).strip()
196        if userName != "root":
197            print "ERROR: cannot set 'adb root'"
198            return False
199    return True
200
201def isMMapSupported():
202    mmapPolicy = int(getAndroidProperty("aaudio.mmap_policy").strip())
203    if mmapPolicy < 2:
204        print "ERROR: AAudio MMAP not enabled, aaudio.mmap_policy = " + str(mmapPolicy)
205        return False
206    if checkLatencyValid():
207        return False;
208    return True
209
210def isTimingSeriesSupported():
211    if not isRunningAsRoot():
212        return False
213    if not isMMapSupported():
214        return False
215    return True
216
217def main():
218    global gOutputFile
219    print "gOutputFile = " + gOutputFile
220    now = datetime.now() # current date and time
221    gOutputFile = kOutputFileBase \
222            + now.strftime("%Y%m%d_%H%M%S") \
223            + ".txt"
224    print "gOutputFile = " + gOutputFile
225
226    initialOffset = getOutputOffset()
227    print "initial offset = " + str(initialOffset)
228
229    print "Android version = " + \
230            getAndroidProperty("ro.build.id").strip()
231    print "    release " + \
232            getAndroidProperty("ro.build.version.release").strip()
233    if (isTimingSeriesSupported()):
234        runGlitchSeries()
235        setOutputOffset(initialOffset)
236
237if __name__ == '__main__':
238    main()
239
240