|
@@ -36,6 +36,8 @@ import qubes.tests
|
|
|
import qubes.vm.appvm
|
|
|
import qubes.vm.templatevm
|
|
|
|
|
|
+import numpy as np
|
|
|
+
|
|
|
|
|
|
class TC_00_AppVMMixin(object):
|
|
|
def setUp(self):
|
|
@@ -421,17 +423,14 @@ class TC_00_AppVMMixin(object):
|
|
|
# then wait for the stream to appear in dom0
|
|
|
local_user = grp.getgrnam('qubes').gr_mem[0]
|
|
|
p = self.loop.run_until_complete(asyncio.create_subprocess_shell(
|
|
|
- "sudo -E -u {} timeout 30s sh -c '"
|
|
|
- "while ! pactl list sink-inputs | grep -q :{}; do sleep 1; done'".format(
|
|
|
- local_user, vm.name)))
|
|
|
+ "sudo -E -u {} timeout 60s sh -c '"
|
|
|
+ "while ! pactl list sink-inputs | grep -q :{}; do sleep 1; done'"
|
|
|
+ .format(local_user, vm.name)))
|
|
|
self.loop.run_until_complete(p.wait())
|
|
|
# and some more...
|
|
|
self.loop.run_until_complete(asyncio.sleep(1))
|
|
|
|
|
|
-
|
|
|
- @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
- "pulseaudio-utils not installed in dom0")
|
|
|
- def test_220_audio_playback(self):
|
|
|
+ def prepare_audio_vm(self):
|
|
|
if 'whonix-gw' in self.template:
|
|
|
self.skipTest('whonix-gw have no audio')
|
|
|
self.loop.run_until_complete(self.testvm1.start())
|
|
@@ -440,22 +439,46 @@ class TC_00_AppVMMixin(object):
|
|
|
self.testvm1.run_for_stdio('which parecord'))
|
|
|
except subprocess.CalledProcessError:
|
|
|
self.skipTest('pulseaudio-utils not installed in VM')
|
|
|
-
|
|
|
self.wait_for_pulseaudio_startup(self.testvm1)
|
|
|
- # generate some "audio" data
|
|
|
- audio_in = b'\x20' * 44100
|
|
|
+
|
|
|
+ def check_audio_sample(self, sample, sfreq):
|
|
|
+ rec = np.fromstring(sample, dtype=np.float32)
|
|
|
+ # determine sample size using silence threshold
|
|
|
+ threshold = 10**-3
|
|
|
+ rec_size = np.count_nonzero((rec > threshold) | (rec < -threshold))
|
|
|
+ if not rec_size:
|
|
|
+ self.fail('only silence detected, no useful audio data')
|
|
|
+ # find zero crossings
|
|
|
+ crossings = np.nonzero((rec[1:] > threshold) &
|
|
|
+ (rec[:-1] < -threshold))[0]
|
|
|
+ np.seterr('raise')
|
|
|
+ # compare against sine wave frequency
|
|
|
+ rec_freq = rec_size/np.mean(np.diff(crossings))
|
|
|
+ if not sfreq*0.8 < rec_freq < sfreq*1.2:
|
|
|
+ self.fail('frequency {} not in specified range'
|
|
|
+ .format(rec_freq))
|
|
|
+
|
|
|
+ def common_audio_playback(self):
|
|
|
+ # sine frequency
|
|
|
+ sfreq = 4400
|
|
|
+ # generate signal
|
|
|
+ audio_in = np.sin(2*np.pi*np.arange(44100)*sfreq/44100)
|
|
|
self.loop.run_until_complete(
|
|
|
- self.testvm1.run_for_stdio('cat > audio_in.raw', input=audio_in))
|
|
|
+ self.testvm1.run_for_stdio('cat > audio_in.raw',
|
|
|
+ input=audio_in.astype(np.float32).tobytes()))
|
|
|
local_user = grp.getgrnam('qubes').gr_mem[0]
|
|
|
with tempfile.NamedTemporaryFile() as recorded_audio:
|
|
|
os.chmod(recorded_audio.name, 0o666)
|
|
|
# FIXME: -d 0 assumes only one audio device
|
|
|
p = subprocess.Popen(['sudo', '-E', '-u', local_user,
|
|
|
- 'parecord', '-d', '0', '--raw', recorded_audio.name],
|
|
|
- stdout=subprocess.PIPE)
|
|
|
+ 'parecord', '-d', '0', '--raw',
|
|
|
+ '--format=float32le', '--rate=44100', '--channels=1',
|
|
|
+ recorded_audio.name], stdout=subprocess.PIPE)
|
|
|
try:
|
|
|
self.loop.run_until_complete(
|
|
|
- self.testvm1.run_for_stdio('paplay --raw audio_in.raw'))
|
|
|
+ self.testvm1.run_for_stdio(
|
|
|
+ 'paplay --format=float32le --rate=44100 \
|
|
|
+ --channels=1 --raw audio_in.raw'))
|
|
|
except subprocess.CalledProcessError as err:
|
|
|
self.fail('{} stderr: {}'.format(str(err), err.stderr))
|
|
|
# wait for possible parecord buffering
|
|
@@ -464,15 +487,7 @@ class TC_00_AppVMMixin(object):
|
|
|
# for some reason sudo do not relay SIGTERM sent above
|
|
|
subprocess.check_call(['pkill', 'parecord'])
|
|
|
p.wait()
|
|
|
- # allow up to 20ms missing, don't use assertIn, to avoid printing
|
|
|
- # the whole data in error message
|
|
|
- recorded_audio = recorded_audio.file.read()
|
|
|
- if audio_in[:-3528] not in recorded_audio:
|
|
|
- found_bytes = recorded_audio.count(audio_in[0])
|
|
|
- all_bytes = len(audio_in)
|
|
|
- self.fail('played sound not found in dom0, '
|
|
|
- 'missing {} bytes out of {}'.format(
|
|
|
- all_bytes-found_bytes, all_bytes))
|
|
|
+ self.check_audio_sample(recorded_audio.file.read(), sfreq)
|
|
|
|
|
|
def _configure_audio_recording(self, vm):
|
|
|
'''Connect VM's output-source to sink monitor instead of mic'''
|
|
@@ -497,19 +512,7 @@ class TC_00_AppVMMixin(object):
|
|
|
subprocess.check_call(sudo +
|
|
|
['pacmd', 'move-source-output', last_index, '0'])
|
|
|
|
|
|
- @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
- "pulseaudio-utils not installed in dom0")
|
|
|
- def test_221_audio_record_muted(self):
|
|
|
- if 'whonix-gw' in self.template:
|
|
|
- self.skipTest('whonix-gw have no audio')
|
|
|
- self.loop.run_until_complete(self.testvm1.start())
|
|
|
- try:
|
|
|
- self.loop.run_until_complete(
|
|
|
- self.testvm1.run_for_stdio('which parecord'))
|
|
|
- except subprocess.CalledProcessError:
|
|
|
- self.skipTest('pulseaudio-utils not installed in VM')
|
|
|
-
|
|
|
- self.wait_for_pulseaudio_startup(self.testvm1)
|
|
|
+ def common_audio_record_muted(self):
|
|
|
# connect VM's recording source output monitor (instead of mic)
|
|
|
self._configure_audio_recording(self.testvm1)
|
|
|
|
|
@@ -535,36 +538,25 @@ class TC_00_AppVMMixin(object):
|
|
|
if audio_in[:32] in recorded_audio:
|
|
|
self.fail('VM recorded something, even though mic disabled')
|
|
|
|
|
|
- @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
- "pulseaudio-utils not installed in dom0")
|
|
|
- def test_222_audio_record_unmuted(self):
|
|
|
- if 'whonix-gw' in self.template:
|
|
|
- self.skipTest('whonix-gw have no audio')
|
|
|
- self.loop.run_until_complete(self.testvm1.start())
|
|
|
- try:
|
|
|
- self.loop.run_until_complete(
|
|
|
- self.testvm1.run_for_stdio('which parecord'))
|
|
|
- except subprocess.CalledProcessError:
|
|
|
- self.skipTest('pulseaudio-utils not installed in VM')
|
|
|
-
|
|
|
- self.wait_for_pulseaudio_startup(self.testvm1)
|
|
|
- da = qubes.devices.DeviceAssignment(self.app.domains[0], 'mic')
|
|
|
+ def common_audio_record_unmuted(self):
|
|
|
+ deva = qubes.devices.DeviceAssignment(self.app.domains[0], 'mic')
|
|
|
self.loop.run_until_complete(
|
|
|
- self.testvm1.devices['mic'].attach(da))
|
|
|
+ self.testvm1.devices['mic'].attach(deva))
|
|
|
# connect VM's recording source output monitor (instead of mic)
|
|
|
self._configure_audio_recording(self.testvm1)
|
|
|
-
|
|
|
- # generate some "audio" data
|
|
|
- audio_in = b'\x20' * 44100
|
|
|
+ sfreq = 4400
|
|
|
+ audio_in = np.sin(2*np.pi*np.arange(44100)*sfreq/44100)
|
|
|
local_user = grp.getgrnam('qubes').gr_mem[0]
|
|
|
- record = self.loop.run_until_complete(
|
|
|
- self.testvm1.run('parecord --raw audio_rec.raw'))
|
|
|
+ record = self.loop.run_until_complete(self.testvm1.run(
|
|
|
+ 'parecord --raw --format=float32le --rate=44100 \
|
|
|
+ --channels=1 audio_rec.raw'))
|
|
|
# give it time to start recording
|
|
|
self.loop.run_until_complete(asyncio.sleep(0.5))
|
|
|
p = subprocess.Popen(['sudo', '-E', '-u', local_user,
|
|
|
- 'paplay', '--raw'],
|
|
|
+ 'paplay', '--raw', '--format=float32le',
|
|
|
+ '--rate=44100', '--channels=1'],
|
|
|
stdin=subprocess.PIPE)
|
|
|
- p.communicate(audio_in)
|
|
|
+ p.communicate(audio_in.astype(np.float32).tobytes())
|
|
|
# wait for possible parecord buffering
|
|
|
self.loop.run_until_complete(asyncio.sleep(1))
|
|
|
self.loop.run_until_complete(
|
|
@@ -573,15 +565,60 @@ class TC_00_AppVMMixin(object):
|
|
|
if record_stderr:
|
|
|
self.fail('parecord printed something on stderr: {}'.format(
|
|
|
record_stderr))
|
|
|
+
|
|
|
recorded_audio, _ = self.loop.run_until_complete(
|
|
|
self.testvm1.run_for_stdio('cat audio_rec.raw'))
|
|
|
- # allow up to 20ms to be missing
|
|
|
- if audio_in[:-3528] not in recorded_audio:
|
|
|
- found_bytes = recorded_audio.count(audio_in[0])
|
|
|
- all_bytes = len(audio_in)
|
|
|
- self.fail('VM not recorded expected data, '
|
|
|
- 'missing {} bytes out of {}'.format(
|
|
|
- all_bytes-found_bytes, all_bytes))
|
|
|
+ self.check_audio_sample(recorded_audio, sfreq)
|
|
|
+
|
|
|
+ @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
+ "pulseaudio-utils not installed in dom0")
|
|
|
+ def test_220_audio_play(self):
|
|
|
+ self.prepare_audio_vm()
|
|
|
+ self.common_audio_playback()
|
|
|
+
|
|
|
+ @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
+ "pulseaudio-utils not installed in dom0")
|
|
|
+ def test_221_audio_rec_muted(self):
|
|
|
+ self.prepare_audio_vm()
|
|
|
+ self.common_audio_record_muted()
|
|
|
+
|
|
|
+ @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
+ "pulseaudio-utils not installed in dom0")
|
|
|
+ def test_222_audio_rec_unmuted(self):
|
|
|
+ self.prepare_audio_vm()
|
|
|
+ self.common_audio_record_unmuted()
|
|
|
+
|
|
|
+ @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
+ "pulseaudio-utils not installed in dom0")
|
|
|
+ def test_223_audio_play_hvm(self):
|
|
|
+ self.testvm1.virt_mode = 'hvm'
|
|
|
+ self.testvm1.features['audio-model'] = 'ich6'
|
|
|
+ self.prepare_audio_vm()
|
|
|
+ self.loop.run_until_complete(
|
|
|
+ self.testvm1.run_for_stdio('pacmd unload-module module-vchan-sink'))
|
|
|
+ self.common_audio_playback()
|
|
|
+
|
|
|
+ @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
+ "pulseaudio-utils not installed in dom0")
|
|
|
+ def test_224_audio_rec_muted_hvm(self):
|
|
|
+ self.testvm1.virt_mode = 'hvm'
|
|
|
+ self.testvm1.features['audio-model'] = 'ich6'
|
|
|
+ self.prepare_audio_vm()
|
|
|
+ self.loop.run_until_complete(
|
|
|
+ self.testvm1.run_for_stdio('pacmd unload-module module-vchan-sink'))
|
|
|
+ self.common_audio_record_muted()
|
|
|
+
|
|
|
+ @unittest.skipUnless(spawn.find_executable('parecord'),
|
|
|
+ "pulseaudio-utils not installed in dom0")
|
|
|
+ def test_225_audio_rec_unmuted_hvm(self):
|
|
|
+ self.testvm1.virt_mode = 'hvm'
|
|
|
+ self.testvm1.features['audio-model'] = 'ich6'
|
|
|
+ self.prepare_audio_vm()
|
|
|
+ self.loop.run_until_complete(
|
|
|
+ self.testvm1.run_for_stdio('pacmd set-sink-volume 1 0x10000'))
|
|
|
+ self.loop.run_until_complete(
|
|
|
+ self.testvm1.run_for_stdio('pacmd unload-module module-vchan-sink'))
|
|
|
+ self.common_audio_record_unmuted()
|
|
|
|
|
|
def test_250_resize_private_img(self):
|
|
|
"""
|