xref: /aosp_15_r20/external/autotest/client/cros/bluetooth/bluetooth_audio_test_data.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""bluetooth audio test dat for A2DP, AVRCP, and HFP."""
7
8import logging
9import os
10import subprocess
11
12import common
13from autotest_lib.client.common_lib import error
14from autotest_lib.client.bin import utils
15
16
17# Chameleon device's data storing path.
18DEVICE_AUDIO_RECORD_DIR = '/tmp/audio'
19# Refer to TEST_DATA_DIR in the chameleon/deploy/deploy file.
20DEVICE_AUDIO_DATA_DIR = '/usr/share/autotest/audio-test-data'
21
22
23DIST_FILES = 'gs://chromeos-localmirror/distfiles'
24DOWNLOAD_TIMEOUT = 90 # timeout for gsutil downloads
25DATA_DIR = '/tmp'
26
27
28VISQOL_TARBALL = os.path.join(DIST_FILES, 'visqol-binary.tar.gz')
29# Path to ViSQOL tarball in autotest server
30VISQOL_TARBALL_LOCAL_PATH = os.path.join(DATA_DIR,
31                                         os.path.split(VISQOL_TARBALL)[1])
32VISQOL_FOLDER = os.path.join(DATA_DIR, 'visqol')
33VISQOL_PATH = os.path.join(VISQOL_FOLDER, 'visqol')
34# There are several available models for VISQOL, since these VISQOL based tests
35# are primarily for voice quality, this model is more tuned for voice quality.
36# experimentally, the scores have been fairly similar to the default model
37# 'libsvm_nu_svr_model.txt'. Details:
38# github.com/google/visqol/tree/61cdced26b7a03098f0c78f7ab71c25dc2e461f5/model
39VISQOL_SIMILARITY_MODEL = os.path.join(
40        VISQOL_FOLDER, 'visqol.runfiles', '__main__', 'model',
41        'tcdvoip_nu.568_c5.31474325639_g3.17773760038_model.txt')
42VISQOL_TEST_DIR = os.path.join(VISQOL_FOLDER, 'bt-test-output')
43
44
45AUDIO_TARBALL = os.path.join(DIST_FILES, 'chameleon-bundle',
46                             'audio-test-data.tar.gz')
47AUDIO_TEST_DIR = '/usr/local/autotest/cros/audio/test_data'
48AUDIO_RECORD_DIR = os.path.join(DATA_DIR, 'audio')
49
50# AUDIO_TARBALL_NAME is the name of the tarball, i.e. audio-test-data.tar.gz
51AUDIO_TARBALL_NAME = os.path.split(AUDIO_TARBALL)[1]
52# AUDIO_TEST_DATA_DIR is the path of the audio-test-data directory,
53# i.e. /tmp/audio-test-data/
54AUDIO_TEST_DATA_DIR = os.path.join(DATA_DIR,
55                                   AUDIO_TARBALL_NAME.split('.', 1)[0])
56AUDIO_DATA_TARBALL_PATH = os.path.join(DATA_DIR, AUDIO_TARBALL_NAME)
57
58
59A2DP = 'a2dp'
60A2DP_MEDIUM = 'a2dp_medium'
61A2DP_LONG = 'a2dp_long'
62AVRCP = 'avrcp'
63HFP_NBS = 'hfp_nbs'
64HFP_NBS_MEDIUM = 'hfp_nbs_medium'
65HFP_WBS = 'hfp_wbs'
66HFP_WBS_MEDIUM = 'hfp_wbs_medium'
67VISQOL_BUFFER_LENGTH = 10.0
68
69
70def download_file_from_bucket(dir, file_address, verify_download):
71    """Extract tarball specified by tar_path to directory dir.
72
73    @param dir: Path to directory to download file to.
74    @param file_address: URL of the file to download.
75    @param verify_download: A function that accepts stdout, stderr, and the
76            process as args and verifies if the download succeeded.
77
78    @retuns: The result of a call to verify_download.
79    """
80    download_cmd = 'gsutil cp -r {0} {1}'.format(file_address, dir)
81    download_proc = subprocess.Popen(download_cmd.split(),
82                                     stdout=subprocess.PIPE,
83                                     stderr=subprocess.PIPE)
84
85    try:
86        stdout, stderr = utils.poll_for_condition(
87                download_proc.communicate,
88                error.TestError('Failed to download'), timeout=DOWNLOAD_TIMEOUT,
89                desc='Downloading {}'.format(file_address))
90    except Exception as e:
91        download_proc.terminate()
92        return False
93    else:
94        return verify_download(stdout, stderr, download_proc)
95
96
97def extract_tarball(dir, tar_path, verify_extraction):
98    """Extract tarball specified by tar_path to directory dir.
99
100    @param dir: Path to directory to extract to.
101    @param tar_path: Path to the tarball to extract.
102    @param verify_extraction: A function that accepts stdout, stderr, and the
103            process as args and verifies if the extraction succeeded.
104
105    @retuns: The result of a call to verify_extraction.
106    """
107    extract_cmd = 'tar -xf {0} -C {1}'.format(tar_path, dir)
108    extract_proc = subprocess.Popen(extract_cmd.split(), stdout=subprocess.PIPE,
109                                    stderr=subprocess.PIPE)
110
111    try:
112        stdout, stderr = utils.poll_for_condition(
113                extract_proc.communicate, error.TestError('Failed to extract'),
114                timeout=DOWNLOAD_TIMEOUT, desc='Extracting {}'.format(tar_path))
115    except Exception as e:
116        extract_proc.terminate()
117        return False
118    else:
119        return verify_extraction(stdout, stderr, extract_proc)
120
121
122def verify_visqol_extraction(stdout, stderr, process):
123    """Verify all important components of VISQOL are present in expected
124    locations.
125
126    @param stdout: Output of the extract process.
127    @param stderr: Error output of the extract process.
128    @param process: The Popen object of the extract process.
129
130    @returns: True if all required components are present and extraction process
131            suceeded.
132    """
133    return (not stderr and
134            os.path.isdir(VISQOL_FOLDER) and
135            os.path.isdir(VISQOL_TEST_DIR) and
136            os.path.exists(VISQOL_PATH) and
137            os.path.exists(VISQOL_SIMILARITY_MODEL))
138
139
140def get_visqol_binary():
141    """Download visqol binary.
142
143    If visqol binary not already available, download from DIST_FILES, otherwise
144    skip this step.
145    """
146    logging.debug('Downloading ViSQOL binary on autotest server')
147    if verify_visqol_extraction(None, None, None):
148        logging.debug('VISQOL binary already exists, skipping')
149        return
150
151    # download from VISQOL_TARBALL
152    if not download_file_from_bucket(DATA_DIR, VISQOL_TARBALL,
153                                     lambda _, __, p: p.returncode == 0):
154        raise error.TestError('Failed to download ViSQOL binary')
155    # Extract tarball tp DATA_DIR
156    if not extract_tarball(DATA_DIR, VISQOL_TARBALL_LOCAL_PATH,
157                           verify_visqol_extraction):
158        raise error.TestError('Failed to extract ViSQOL binary')
159
160
161def get_audio_test_data():
162    """Download audio test data files
163
164    Download and unzip audio files for audio tests from DIST_FILES.
165    """
166    logging.debug('Downloading audio test data on autotest server')
167
168    # download from AUDIO_TARBALL
169    if not download_file_from_bucket(DATA_DIR, AUDIO_TARBALL,
170                                     lambda _, __, p: p.returncode == 0):
171        raise error.TestError('Failed to download audio test data')
172    # Extract tarball to DATA_DIR
173    if not extract_tarball(
174            DATA_DIR, AUDIO_DATA_TARBALL_PATH,
175            lambda _, __, ___: os.path.isdir(AUDIO_TEST_DATA_DIR)):
176        raise error.TestError('Failed to extract audio test data')
177
178
179# Audio test data for hfp narrow band speech
180hfp_nbs_test_data = {
181    'rate': 8000,
182    'channels': 1,
183    'frequencies': (3500,),
184    'file': os.path.join(AUDIO_TEST_DIR,
185                         'sine_3500hz_rate8000_ch1_5secs.raw'),
186    'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
187                                     'hfp_nbs_recorded_by_peer.wav'),
188    'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
189                                    'hfp_nbs_recorded_by_dut.raw'),
190    'chunk_in_secs': 1,
191    'bit_width': 16,
192    'format': 'S16_LE',
193    'duration': 5,
194    'chunk_checking_duration': 5,
195
196    # Device side data used by StartPlayingAudioSubprocess function in
197    # bluetooth_audio.py.
198    'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
199                                'sine_3500hz_rate8000_ch1_5secs.wav'),
200
201    # Device side data used by HandleOneChunk function in bluetooth_audio.py.
202    'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR,
203                               'hfp_nbs_recorded_by_peer_%d.raw'),
204
205    'visqol_test_files': [
206        {
207            'file': os.path.join(AUDIO_TEST_DATA_DIR,
208                                 'voice_8k.wav'),
209            'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
210                                             'voice_8k_deg_peer.wav'),
211            'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
212                                            'voice_8k_deg_dut.raw'),
213            'channels': 1,
214            'rate': 8000,
215            'duration': 26.112 + VISQOL_BUFFER_LENGTH,
216            'chunk_checking_duration': 26.112 + VISQOL_BUFFER_LENGTH,
217            'bit_width': 16,
218            'format': 'S16_LE',
219            # convenient way to differentiate ViSQOL tests from regular tests
220            'visqol_test': True,
221            'encoding': 'signed-integer',
222            'speech_mode': True,
223            # Passing scored are determined mostly experimentally.
224            # TODO(b/179501232) - NBS is currently not uniformly >= 4.0 on all
225            # devices so reduce the passing score.
226            'sink_passing_score': 3.5,
227            'source_passing_score': 3.5,
228            'reporting_type': 'voice-8k',
229
230            # Device side data used by StartPlayingAudioSubprocess function in
231            # bluetooth_audio.py.
232            'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
233                                        'voice_8k.wav'),
234        },
235        {
236            'file': os.path.join(AUDIO_TEST_DATA_DIR,
237                                 'sine_3500hz_rate8000_ch1_5secs.wav'),
238            'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
239                                             'sine_3k_deg_peer.wav'),
240            'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
241                                            'sine_3k_deg_dut.raw'),
242            'channels': 1,
243            'rate': 8000,
244            'duration': 5.0 + VISQOL_BUFFER_LENGTH,
245            'chunk_checking_duration': 5.0 + VISQOL_BUFFER_LENGTH,
246            'bit_width': 16,
247            'format': 'S16_LE',
248            # convenient way to differentiate ViSQOL tests from regular tests
249            'visqol_test': True,
250            'encoding': 'signed-integer',
251            'speech_mode': True,
252            # Sine tones don't work very well with ViSQOL on the NBS tests, both
253            # directions score fairly low, however I've kept it in as a test
254            # file because its a good for reference, makes it easy to see
255            # degradation and verify that this is transmitting the frequency
256            # range we would expect
257            # TODO(b/179501232) - NBS is currently not uniformly >= 2.0 on all
258            # devices so reduce the passing score.
259            'sink_passing_score': 1.0,
260            'source_passing_score': 1.0,
261            'reporting_type': 'sine-3.5k',
262
263            # Device side data used by StartPlayingAudioSubprocess function in
264            # bluetooth_audio.py.
265            'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
266                                        'sine_3500hz_rate8000_ch1_5secs.wav'),
267        }
268    ]
269}
270
271
272# Audio test data for hfp wide band speech
273hfp_wbs_test_data = {
274    'rate': 16000,
275    'channels': 1,
276
277    'frequencies': (7000,),
278    'file': os.path.join(AUDIO_TEST_DIR,
279                         'sine_7000hz_rate16000_ch1_5secs.raw'),
280    'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
281                                     'hfp_wbs_recorded_by_peer.wav'),
282    'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
283                                    'hfp_wbs_recorded_by_dut.raw'),
284    'chunk_in_secs': 1,
285    'bit_width': 16,
286    'format': 'S16_LE',
287    'duration': 5,
288    'chunk_checking_duration': 5,
289
290    # Device side data used by StartPlayingAudioSubprocess function in
291    # bluetooth_audio.py.
292    'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
293                                'sine_7000hz_rate16000_ch1_5secs.wav'),
294
295    # Device side data used by HandleOneChunk function in bluetooth_audio.py.
296    'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR,
297                               'hfp_wbs_recorded_by_peer_%d.raw'),
298
299    'visqol_test_files': [
300        {
301            'file': os.path.join(AUDIO_TEST_DATA_DIR,
302                                 'voice.wav'),
303            'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
304                                             'voice_deg_peer.wav'),
305            'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
306                                            'voice_deg_dut.raw'),
307            'channels': 1,
308            'rate': 16000,
309            'duration': 26.112 + VISQOL_BUFFER_LENGTH,
310            'chunk_checking_duration': 26.112 + VISQOL_BUFFER_LENGTH,
311            'bit_width': 16,
312            'format': 'S16_LE',
313            # convenient way to differentiate ViSQOL tests from regular tests
314            'visqol_test': True,
315            'encoding': 'signed-integer',
316            'speech_mode': True,
317            # Passing scored are determined mostly experimentally.
318            'sink_passing_score': 4.0,
319            'source_passing_score': 4.0,
320            'reporting_type': 'voice-16k',
321
322            # Device side data used by StartPlayingAudioSubprocess function in
323            # bluetooth_audio.py.
324            'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
325                                        'voice.wav'),
326        },
327        {
328            'file': os.path.join(AUDIO_TEST_DATA_DIR,
329                                 'sine_7000hz_rate16000_ch1_5secs.wav'),
330            'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
331                                             'sine_7k_deg_peer.wav'),
332            'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
333                                            'sine_7k_deg_dut.raw'),
334            'channels': 1,
335            'rate': 16000,
336            'duration': 5.0 + VISQOL_BUFFER_LENGTH,
337            'chunk_checking_duration': 5.0 + VISQOL_BUFFER_LENGTH,
338            'bit_width': 16,
339            'format': 'S16_LE',
340            # convenient way to differentiate ViSQOL tests from regular tests
341            'visqol_test': True,
342            'encoding': 'signed-integer',
343            'speech_mode': True,
344            # Passing scored are determined mostly experimentally.
345            'sink_passing_score': 4.0,
346            'source_passing_score': 4.0,
347            'reporting_type': 'sine-7k',
348
349            # Device side data used by StartPlayingAudioSubprocess function in
350            # bluetooth_audio.py.
351            'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
352                                        'sine_7000hz_rate16000_ch1_5secs.wav'),
353        }
354    ]
355}
356
357# Audio test data for hfp nbs medium test.
358hfp_nbs_medium_test_data = {
359    'rate': 8000,
360    'channels': 1,
361    'frequencies': (3500,),
362    'file': os.path.join(AUDIO_TEST_DIR,
363                         'sine_3500hz_rate8000_ch1_60secs.raw'),
364    'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
365                                     'hfp_nbs_medium_recorded_by_peer.raw'),
366    'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
367                                    'hfp_nbs_medium_recorded_by_dut.raw'),
368    'chunk_in_secs': 1,
369    'bit_width': 16,
370    'format': 'S16_LE',
371    'duration': 60,
372    'chunk_checking_duration': 5,
373
374    # Device side data used by StartPlayingAudioSubprocess function in
375    # bluetooth_audio.py.
376    'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
377                                'sine_3500hz_rate8000_ch1_60secs.wav'),
378    # Device side data used by HandleOneChunk function in bluetooth_audio.py.
379    'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR,
380                               'hfp_nbs_medium_recorded_by_peer_%d.raw'),
381}
382
383
384# Audio test data for hfp wbs medium test.
385hfp_wbs_medium_test_data = {
386    'rate': 16000,
387    'channels': 1,
388    'frequencies': (7000,),
389    'file': os.path.join(AUDIO_TEST_DIR,
390                         'sine_7000hz_rate16000_ch1_60secs.raw'),
391    'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
392                                     'hfp_wbs_medium_recorded_by_peer.raw'),
393    'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR,
394                                    'hfp_wbs_medium_recorded_by_dut.raw'),
395    'chunk_in_secs': 1,
396    'bit_width': 16,
397    'format': 'S16_LE',
398    'duration': 60,
399    'chunk_checking_duration': 5,
400
401    # Device side data used by StartPlayingAudioSubprocess function in
402    # bluetooth_audio.py.
403    'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR,
404                                'sine_7000hz_rate16000_ch1_60secs.wav'),
405    # Device side data used by HandleOneChunk function in bluetooth_audio.py.
406    'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR,
407                               'hfp_wbs_medium_recorded_by_peer_%d.raw'),
408}
409
410
411# Audio test data for a2dp
412a2dp_test_data = {
413    'rate': 48000,
414    'channels': 2,
415    'frequencies': (440, 20000),
416    'file': os.path.join(AUDIO_TEST_DIR,
417                         'binaural_sine_440hz_20000hz_rate48000_%dsecs.raw'),
418    'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
419                                     'a2dp_recorded_by_peer.raw'),
420    'chunk_in_secs': 5,
421    'bit_width': 16,
422    'format': 'S16_LE',
423    'duration': 5,
424
425    # Device side data used by HandleOneChunk function in bluetooth_audio.py.
426    'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR,
427                               'a2dp_recorded_by_peer_%d.raw'),
428}
429
430
431# Audio test data for a2dp long test. The file and duration attributes
432# are dynamic and will be determined during run time.
433a2dp_long_test_data = a2dp_test_data.copy()
434a2dp_long_test_data.update({
435    'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
436                                     'a2dp_long_recorded_by_peer.raw'),
437    'duration': 0,       # determined at run time
438    'chunk_in_secs': 1,
439    # Device side data used by HandleOneChunk function in bluetooth_audio.py.
440    'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR,
441                               'a2dp_long_recorded_by_peer_%d.raw'),
442})
443
444
445# Audio test data for a2dp medium test.
446a2dp_medium_test_data = a2dp_test_data.copy()
447a2dp_medium_test_data.update({
448    'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR,
449                                     'a2dp_medium_recorded_by_peer.raw'),
450    'duration': 60,
451    'chunk_in_secs': 1,
452    'chunk_checking_duration': 5,
453    # Device side data used by HandleOneChunk function in bluetooth_audio.py.
454    'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR,
455                               'a2dp_medium_recorded_by_peer_%d.raw'),
456})
457
458
459audio_test_data = {
460    A2DP: a2dp_test_data,
461    A2DP_MEDIUM: a2dp_medium_test_data,
462    A2DP_LONG: a2dp_long_test_data,
463    HFP_WBS: hfp_wbs_test_data,
464    HFP_WBS_MEDIUM: hfp_wbs_medium_test_data,
465    HFP_NBS: hfp_nbs_test_data,
466    HFP_NBS_MEDIUM: hfp_nbs_medium_test_data,
467}
468