Skip to content

Commit

Permalink
Add SoundCard Support (#433)
Browse files Browse the repository at this point in the history
* add test for sound device lib

* add trace for missing portaudio

* optimize imports

* add test for pyaudio

* initial soundcard implementation with health check

* enable rx for soundcard

* enable tx for soundcard

* make sample rate configurable

* python 3.4 compat
  • Loading branch information
jopohl authored May 1, 2018
1 parent 98a7771 commit a0469f1
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/urh/controller/dialogs/OptionsDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ def on_btn_rebuild_native_clicked(self):
@pyqtSlot()
def on_btn_health_check_clicked(self):
info = ExtensionHelper.perform_health_check()
info += "\n" + BackendHandler.perform_soundcard_health_check()

if util.get_windows_lib_path():
info += "\n\n[INFO] Used DLLs from " + util.get_windows_lib_path()
Expand Down
2 changes: 2 additions & 0 deletions src/urh/controller/widgets/DeviceSettingsWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ def set_device_ui_items_visibility(self, device_name: str, adjust_gains=True):
spinbox.setMaximum(max(conf[key]))
spinbox.setSingleStep(conf[key][1] - conf[key][0])
spinbox.auto_update_step_size = False
if "default_" + key in conf:
spinbox.setValue(conf["default_"+key])
else:
spinbox.setMinimum(conf[key].start)
spinbox.setMaximum(conf[key].stop)
Expand Down
24 changes: 23 additions & 1 deletion src/urh/dev/BackendHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ class BackendHandler(object):
3) Manage the selection of devices backend
"""
DEVICE_NAMES = ("AirSpy R2", "AirSpy Mini", "Bladerf", "FUNcube", "HackRF", "LimeSDR", "RTL-SDR", "RTL-TCP", "SDRPlay", "USRP")
DEVICE_NAMES = ("AirSpy R2", "AirSpy Mini", "Bladerf", "FUNcube", "HackRF",
"LimeSDR", "RTL-SDR", "RTL-TCP", "SDRPlay", "SoundCard", "USRP")

def __init__(self):

Expand Down Expand Up @@ -129,6 +130,14 @@ def __usrp_native_enabled(self) -> bool:
except ImportError:
return False

@property
def __soundcard_enabled(self) -> bool:
try:
import pyaudio
return True
except ImportError:
return False

@property
def __airspy_native_enabled(self) -> bool:
try:
Expand Down Expand Up @@ -234,6 +243,10 @@ def __avail_backends_for_device(self, devname: str):
supports_rx, supports_tx = True, False
backends.add(Backends.native)

if devname.lower() == "soundcard" and self.__soundcard_enabled:
supports_rx, supports_tx = True, True
backends.add(Backends.native)

return backends, supports_rx, supports_tx

def get_backends(self):
Expand All @@ -243,6 +256,15 @@ def get_backends(self):
container = BackendContainer(device_name.lower(), ab, rx_suprt, tx_suprt)
self.device_backends[device_name.lower()] = container

@staticmethod
def perform_soundcard_health_check():
result = "SoundCard -- "
try:
import pyaudio
return result + "OK"
except Exception as e:
return result + str(e)

def __get_python2_interpreter(self):
paths = os.get_exec_path()

Expand Down
3 changes: 3 additions & 0 deletions src/urh/dev/VirtualDevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ def __init__(self, backend_handler, name: str, mode: Mode, freq=None, sample_rat
from urh.dev.native.SDRPlay import SDRPlay
self.__dev = SDRPlay(freq, gain, bandwidth, gain, if_gain=if_gain,
resume_on_full_receive_buffer=resume_on_full_receive_buffer)
elif name == "soundcard":
from urh.dev.native.SoundCard import SoundCard
self.__dev = SoundCard(sample_rate, resume_on_full_receive_buffer=resume_on_full_receive_buffer)
else:
raise NotImplementedError("Native Backend for {0} not yet implemented".format(name))

Expand Down
5 changes: 5 additions & 0 deletions src/urh/dev/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
"rx_antenna_default_index": 0,
}

DEVICE_CONFIG["SoundCard"] = {
"sample_rate": [16e3, 22.05e3, 24e3, 32e3, 44.1e3, 48e3, 96e3, 192e3],
"default_sample_rate": 48e3,
}

DEVICE_CONFIG["Fallback"] = {
"center_freq": dev_range(start=1*M, stop=6 * G, step=1),
"sample_rate": dev_range(start=2 * M, stop=20 * M, step=1),
Expand Down
115 changes: 115 additions & 0 deletions src/urh/dev/native/SoundCard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from collections import OrderedDict
from multiprocessing import Array
from multiprocessing.connection import Connection

import numpy as np
import pyaudio

from urh.dev.native.Device import Device
from urh.util.Logger import logger


class SoundCard(Device):
DEVICE_LIB = pyaudio
ASYNCHRONOUS = False
DEVICE_METHODS = dict()

CHUNK_SIZE = 1024
SYNC_TX_CHUNK_SIZE = 2 * CHUNK_SIZE

SAMPLE_RATE = 48000

pyaudio_handle = None
pyaudio_stream = None

@classmethod
def init_device(cls, ctrl_connection: Connection, is_tx: bool, parameters: OrderedDict) -> bool:
try:
cls.SAMPLE_RATE = int(parameters[cls.Command.SET_SAMPLE_RATE.name])
except (KeyError, ValueError):
pass
return super().init_device(ctrl_connection, is_tx, parameters)

@classmethod
def setup_device(cls, ctrl_connection: Connection, device_identifier):
ctrl_connection.send("Initializing pyaudio...")
try:
cls.pyaudio_handle = pyaudio.PyAudio()
ctrl_connection.send("Initialized pyaudio")
return True
except Exception as e:
logger.exception(e)
ctrl_connection.send("Failed to initialize pyaudio")

@classmethod
def prepare_sync_receive(cls, ctrl_connection: Connection):
try:
cls.pyaudio_stream = cls.pyaudio_handle.open(format=pyaudio.paFloat32,
channels=2,
rate=cls.SAMPLE_RATE,
input=True,
frames_per_buffer=cls.CHUNK_SIZE)
ctrl_connection.send("Successfully started pyaudio stream")
return 0
except Exception as e:
logger.exception(e)
ctrl_connection.send("Failed to start pyaudio stream")

@classmethod
def prepare_sync_send(cls, ctrl_connection: Connection):
try:
cls.pyaudio_stream = cls.pyaudio_handle.open(format=pyaudio.paFloat32,
channels=2,
rate=cls.SAMPLE_RATE,
output=True)
ctrl_connection.send("Successfully started pyaudio stream")
return 0
except Exception as e:
logger.exception(e)
ctrl_connection.send("Failed to start pyaudio stream")

@classmethod
def receive_sync(cls, data_conn: Connection):
if cls.pyaudio_stream:
data_conn.send_bytes(cls.pyaudio_stream.read(cls.CHUNK_SIZE))

@classmethod
def send_sync(cls, data):
if cls.pyaudio_stream:
cls.pyaudio_stream.write(data.tostring())

@classmethod
def shutdown_device(cls, ctrl_connection, is_tx: bool):
logger.debug("shutting down pyaudio...")
try:
if cls.pyaudio_stream:
cls.pyaudio_stream.stop_stream()
cls.pyaudio_stream.close()
if cls.pyaudio_handle:
cls.pyaudio_handle.terminate()
ctrl_connection.send("CLOSE:0")
except Exception as e:
logger.exception(e)
ctrl_connection.send("Failed to shut down pyaudio")

def __init__(self, sample_rate, resume_on_full_receive_buffer=False):
super().__init__(center_freq=0, sample_rate=sample_rate, bandwidth=0,
gain=1, if_gain=1, baseband_gain=1,
resume_on_full_receive_buffer=resume_on_full_receive_buffer)

self.success = 0

@property
def device_parameters(self) -> OrderedDict:
return OrderedDict([(self.Command.SET_SAMPLE_RATE.name, self.sample_rate)])

@staticmethod
def unpack_complex(buffer):
return np.frombuffer(buffer, dtype=np.complex64)

@staticmethod
def pack_complex(complex_samples: np.ndarray):
arr = Array("f", 2*len(complex_samples), lock=False)
numpy_view = np.frombuffer(arr, dtype=np.float32)
numpy_view[:] = complex_samples.view(np.float32)
return arr
105 changes: 105 additions & 0 deletions tests/device/TestSoundCard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import numpy as np


def test_sounddevice_lib():
import time

import numpy as np
from sounddevice import InputStream, OutputStream, sleep as sd_sleep
"""
if no portaudio installed:
Traceback (most recent call last):
File "TestSoundCard.py", line 42, in <module>
test_sounddevice_lib()
File "TestSoundCard.py", line 5, in test_sounddevice_lib
import sounddevice as sd
File "/usr/lib/python3.6/site-packages/sounddevice.py", line 64, in <module>
raise OSError('PortAudio library not found')
OSError: PortAudio library not found
"""

duration = 2.5 # seconds

rx_buffer = np.ones((10 ** 6, 2), dtype=np.float32)
global current_rx, current_tx
current_rx = 0
current_tx = 0

def rx_callback(indata: np.ndarray, frames: int, time, status):
global current_rx
if status:
print(status)

rx_buffer[current_rx:current_rx + frames] = indata
current_rx += frames

def tx_callback(outdata: np.ndarray, frames: int, time, status):
global current_tx
if status:
print(status)

outdata[:] = rx_buffer[current_tx:current_tx + frames]
current_tx += frames

with InputStream(channels=2, callback=rx_callback):
sd_sleep(int(duration * 1000))

print("Current rx", current_rx)

with OutputStream(channels=2, callback=tx_callback):
sd_sleep(int(duration * 1000))

print("Current tx", current_tx)


def test_pyaudio():
import pyaudio

CHUNK = 1024
p = pyaudio.PyAudio()

stream = p.open(format=pyaudio.paFloat32,
channels=2,
rate=48000,
input=True,
frames_per_buffer=CHUNK)

print("* recording")

frames = []

for i in range(0, 100):
data = stream.read(CHUNK)
frames.append(data)

print("* done recording")

stream.stop_stream()
stream.close()
p.terminate()
data = b''.join(frames)

print("* playing")

p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paFloat32,
channels=2,
rate=48000,
output=True,
)

for i in range(0, len(data), CHUNK):
stream.write(data[i:i+CHUNK])

stream.stop_stream()
stream.close()

p.terminate()

print("* done playing")


if __name__ == '__main__':
# test_sounddevice_lib()
test_pyaudio()

0 comments on commit a0469f1

Please sign in to comment.