Skip to content

Instantly share code, notes, and snippets.

@jj1bdx
Last active December 16, 2023 13:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jj1bdx/3257e0c9e36d03b7131a9eef36192463 to your computer and use it in GitHub Desktop.
Save jj1bdx/3257e0c9e36d03b7131a9eef36192463 to your computer and use it in GitHub Desktop.
PortAudio script for 32-bit float audio to output from stdin
#!/usr/bin/env python3
# Pyaudio output device for 32-bit floating audio input from stdin
import pyaudio
import signal
import sys
import time
argvs = sys.argv
argc = len(argvs)
if argc == 2:
channels = int(argvs[1])
devidx = None
elif argc == 3:
channels = int(argvs[1])
devidx = int(argvs[2])
else:
print('Usage: ', argvs[0], 'channels [device-index]\n')
quit()
channels = int(argvs[1])
sample_rate = 48000
sample_width = 4 # 32bit float
def terminate():
stream.stop_stream()
stream.close()
p.terminate()
def signal_handler(signal, frame):
print('Terminated by CTRL/C')
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
framesize = 480 # 10msec
readsize = framesize * sample_width * channels
p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paFloat32,
channels = channels,
rate = sample_rate,
output = True,
output_device_index = devidx,
frames_per_buffer = framesize
)
data = sys.stdin.buffer.read(readsize)
while data != '':
stream.write(data)
data = sys.stdin.buffer.read(readsize)
terminate()

ffmpeg streaming examples

Note

  • Audio latency measured via ethernet: ~700msec
  • Still unable to find an option to shorten the audio latency
  • Sender Linux: Raspberry Pi 4B with Raspberry Pi OS
  • Receiver macOS: macOS 10.15.7
  • ffplay does not recognize non-native RTP payload types. See Wikipedia RTP Payload Type list for the native format types.

48kHz 256kbps MP3 stereo RTP

# sender Linux
arecord --buffer-time=10000 -D plughw:CARD=CODEC,DEV=0 -f S16_LE -c2 -r48000 | ffmpeg -probesize 32 -fflags nobuffer -f s16le -sample_rate 48000 -channels 2 -vn -re -i - -codec libmp3lame -ar 48000 -ab 256k -ac 2 -f rtp rtp://receiver:34567
# receiver macOS
ffmpeg -probesize 32 -fflags nobuffer -fflags discardcorrupt -flags low_delay -avioflags direct -i rtp://receiver:34567 -f s16le - | r16auout.py 2

44.1kHz linear PCM mono RTP

# sender Linux
arecord --buffer-time=10000 -D plughw:CARD=CODEC,DEV=0 -f S16_LE -c1 -r44100 | ffmpeg -probesize 32 -fflags nobuffer -f s16le -sample_rate 44100 -channels 1 -vn -re -i - -acodec pcm_s16be -ar 44100 -ac 1 -f rtp rtp://receiver:34567
# receiver macOS
ffmpeg -probesize 32 -fflags nobuffer -fflags discardcorrupt -flags low_delay -avioflags direct -i rtp://receiver:34567 -f s16le - | r16-44100-auout.py 1

16kHz g.722 mono RTP

# sender Linux
arecord --buffer-time=10000 -D plughw:CARD=CODEC,DEV=0 -f S16_LE -c1 -r16000 | ffmpeg -probesize 32 -fflags nobuffer -f s16le -sample_rate 16000 -channels 1 -vn -re -i - -acodec g722 -ar 16000 -ac 1 -f rtp rtp://receiver:34567
# receiver macOS
ffplay -probesize 32 -fflags nobuffer -fflags discardcorrupt -flags low_delay -avioflags direct -i rtp://receiver:34567

16kHz g.722 mono with RTSP SDP TCP transport

# sender Linux
arecord --buffer-time=10000 -D plughw:CARD=CODEC,DEV=0 -f S16_LE -c1 -r16000 | ffmpeg -probesize 32 -fflags nobuffer -f s16le -sample_rate 16000 -channels 1 -vn -re -i - -acodec g722 -ar 16000 -ac 1 -f rtsp -rtsp_transport tcp rtsp://receiver:45678/live.sdp
# receiver macOS
ffplay -nodisp -probesize 32 -fflags nobuffer -fflags discardcorrupt -flags low_delay -avioflags direct -rtsp_flags listen "rtsp://receiver:45678/live.sdp"

44.1kHz linear PCM stereo with RTSP SDP TCP transport

# sender Linux
arecord --buffer-time=20000 -D plughw:CARD=CODEC,DEV=0 -f S16_LE -c2 -r44100 | ffmpeg -probesize 32 -fflags nobuffer -f s16le -sample_rate 44100 -channels 2 -vn -re -i - -acodec pcm_s16be -bufsize 100k -ar 44100 -ac 2 -f rtsp -rtsp_transport tcp rtsp://receiver:45678/live.sdp
# receiver macOS
ffplay -nodisp -probesize 32 -fflags nobuffer -fflags discardcorrupt -flags low_delay -avioflags direct -rtsp_flags listen rtsp://receiver:45678/live.sdp

48kHz 256kbps MPEG-1 Layer 2 (MP2) stereo with RTSP SDP TCP transport

  • For lower CPU load
# sender Linux
arecord --buffer-time=10000 -D plughw:CARD=CODEC,DEV=0 -f S16_LE -c2 -r48000 | ffmpeg -probesize 32 -fflags nobuffer -f s16le -sample_rate 48000 -channels 2 -vn -re -i - -codec mp2 -ar 48000 -ab 256k -ac 2 -f rtsp -rtsp_transport tcp rtsp://receiver:45678/live.sdp
# receiver macOS
ffmpeg -probesize 32 -fflags nobuffer -fflags discardcorrupt -flags low_delay -avioflags direct -rtsp_flags listen -i rtsp://receiver:45678/live.sdp -f s16le - | r16auout.py 2

gstreamer streaming examples

Note

  • Sender Linux: Raspberry Pi 4B with Raspberry Pi OS
  • Receiver macOS: macOS 10.15.7

Note for macOS

osxaudiosrc and osxaudiosink

  • Use osxaudiosrc for OSX device input
  • Use osxaudiosink for OSX device output
  • Use as osxaudiosink device=93
  • To obtain devices, install macos-audio-devices

48kHz Vorbis RTP stream over TCP

Measured delay: ~0.2sec

# server and sender Linux
gst-launch-1.0 alsasrc device=plughw:CARD=CODEC,DEV=0 provide-clock=true do-timestamp=true buffer-time=20000 ! "audio/x-raw,rate=48000" ! vorbisenc ! rtpvorbispay config-interval=1 ! rtpstreampay ! tcpserversink port=5678 host=sender
# client and receiver macOS
gst-launch-1.0 tcpclientsrc port=5678 host=sender do-timestamp=true ! "application/x-rtp-stream,media=audio,clock-rate=48000,encoding-name=VORBIS" ! rtpstreamdepay ! rtpvorbisdepay ! decodebin ! audioconvert ! audioresample ! autoaudiosink

Opus RTP stream

Measured delay: ~0.2sec

# sender Linux
gst-launch-1.0 alsasrc device=plughw:CARD=CODEC,DEV=0 provide-clock=true do-timestamp=true buffer-time=20000 ! audio/x-raw,channels=1 ! audiorate ! audioconvert ! opusenc bitrate=256000 frame-size=2.5 ! rtpopuspay ! udpsink host=receiver port=5008
# receiver macOS
gst-launch-1.0 udpsrc caps="application/x-rtp,channels=1" port=5008 ! rtpjitterbuffer latency=60 ! queue ! rtpopusdepay ! opusdec plc=true ! audioconvert ! audioresample ! autoaudiosink

8kHz ALAW RTP stream over TCP

Measured delay: ~0.1sec or lower

# server and sender Linux
gst-launch-1.0 alsasrc device=plughw:CARD=CODEC,DEV=0 provide-clock=true do-timestamp=true buffer-time=20000 ! audioconvert ! alawenc ! rtppcmapay ! rtpstreampay ! tcpserversink port=5678 host=sender
# client and receiver macOS
gst-launch-1.0 tcpclientsrc port=5678 host=172.29.189.82 do-timestamp=true ! "application/x-rtp-stream,media=(string)audio,clock-rate=(int)8000,encoding-name=(string)PCMA" ! rtpstreamdepay ! rtppcmadepay ! alawdec ! audioconvert ! audioresample ! autoaudiosink buffer_time=20000 latency_time=10000

44.1kHz linear PCM RTP stream over TCP

  • Measured delay: ~0.1sec or lower

Linux -> macOS

# server and sender Linux
gst-launch-1.0 alsasrc device=plughw:CARD=CODEC,DEV=0 provide-clock=true do-timestamp=true buffer-time=20000 ! audioconvert ! rtpL16pay ! rtpstreampay ! tcpserversink port=5678 host=sender
# client and receiver macOS
gst-launch-1.0 tcpclientsrc port=5678 host=172.29.189.82 do-timestamp=true ! "application/x-rtp-stream,media=(string)audio, clock-rate=(int)44100, encoding-name=(string)L16, encoding-params=(string)2, channels=(int)2, payload=(int)96" ! rtpstreamdepay ! rtpL16depay ! audioconvert ! audioresample ! autoaudiosink buffer_time=20000 latency_time=10000

macOS -> Linux (for the other direction)

Note: monitoring audio for this direction discovered that noticeable clicks (possibly phase disruption) were audible for every one second. (Timestamping issue?)

# server and sender macOS
gst-launch-1.0 -v osxaudiosrc device=130 provide-clock=true do-timestamp=true buffer-time=100000 ! audioconvert ! rtpL16pay ! rtpstreampay ! tcpserversink port=5678 host=sender
# client and receiver Linux
gst-launch-1.0 -v tcpclientsrc port=5678 host=sender do-timestamp=true ! "application/x-rtp-stream,media=(string)audio, clock-rate=(int)44100, encoding-name=(string)L16, encoding-params=(string)2, channels=(int)2, payload=(int)96" ! rtpstreamdepay ! rtpL16depay ! audioconvert ! audioresample ! alsasink device=plughw:CARD=CODEC,DEV=0

Usable macOS loopback devices

Expected usage

  • Live audio streaming between macOS and Linux
  • Minimum latency
  • Relaying macOS WSJT-X audio I/O to a Raspberry Pi Linux audio I/O

Installed drivers path on macOS

/Library/Audio/Plug-Ins/HAL

How to restart CoreAudio daemon

sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod

Paid products

  • Rogue Amoeba Loopback
    • Worth paying the price of USD109/license key
    • No problem without PortAudio input/output
    • No problem for gstreamer input/output
    • Able to instantly create/delete loopback devices with arbitrary names
    • Able to monitor loopback devices separately for each device
    • Able to control the loopback device sound volume for each device, useful for WSJT-X reception gain control

Free software

  • Blackhole
    • Single loopback device only (by the installer)
    • Unable to run multiple instance even with recompilation yet
    • No problem without PortAudio input/output
    • No problem for gstreamer input/output

Not usable for my purposes

Latency issues of PyAudio

  • PyAudio output latency is manageable
  • PyAudio input latency is not manageable, especially for lower sampling rates

Possible workarounds

  • Rewrite all PyAudio scripts by C/C++, i.e., rewriting them by PortAudio
#!/usr/bin/env python3
# Pyaudio output device for 32-bit floating audio input from stdin
import pyaudio
import signal
import sys
import time
argvs = sys.argv
argc = len(argvs)
if argc == 2:
channels = int(argvs[1])
devidx = None
elif argc == 3:
channels = int(argvs[1])
devidx = int(argvs[2])
else:
print('Usage: ', argvs[0], 'channels [device-index]\n')
quit()
channels = int(argvs[1])
sample_rate = 44100
sample_width = 2 # 16bit int
def terminate():
stream.stop_stream()
stream.close()
p.terminate()
def signal_handler(signal, frame):
print('Terminated by CTRL/C')
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
framesize = 1024 # 10msec
readsize = framesize * sample_width * channels
p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16,
channels = channels,
rate = sample_rate,
output = True,
output_device_index = devidx,
frames_per_buffer = framesize
)
data = sys.stdin.buffer.read(readsize)
while data != '':
stream.write(data)
data = sys.stdin.buffer.read(readsize)
terminate()
#!/usr/bin/env python3
# Pyaudio output device for 32-bit floating audio input from stdin
import pyaudio
import signal
import sys
import time
argvs = sys.argv
argc = len(argvs)
if argc == 2:
channels = int(argvs[1])
devidx = None
elif argc == 3:
channels = int(argvs[1])
devidx = int(argvs[2])
else:
print('Usage: ', argvs[0], 'channels [device-index]\n')
quit()
channels = int(argvs[1])
sample_rate = 48000
sample_width = 2 # 16bit int
def terminate():
stream.stop_stream()
stream.close()
p.terminate()
def signal_handler(signal, frame):
print('Terminated by CTRL/C')
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
framesize = 1024 # 10msec
readsize = framesize * sample_width * channels
p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16,
channels = channels,
rate = sample_rate,
output = True,
output_device_index = devidx,
frames_per_buffer = framesize
)
data = sys.stdin.buffer.read(readsize)
while data != '':
stream.write(data)
data = sys.stdin.buffer.read(readsize)
terminate()
#!/usr/bin/env python3
import pyaudio
import signal
import sys
import time
argvs = sys.argv
argc = len(argvs)
if argc == 2:
channels = int(argvs[1])
devidx = None
elif argc == 3:
channels = int(argvs[1])
devidx = int(argvs[2])
else:
print('Usage: ', argvs[0], 'channels [device-index]\n')
quit()
channels = int(argvs[1])
sample_rate = 11025
sample_width = 2 # 16bit integer
sample_format = pyaudio.paInt16
framesize = 110 # 10msec
if sys.platform == 'darwin':
CHANNELS = 1
def terminate():
stream.stop_stream()
stream.close()
p.terminate()
def signal_handler(signal, frame):
print('Terminated by CTRL/C')
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
def callback(in_data, frame_count, time_info, status):
sys.stdout.buffer.write(in_data)
return (None, pyaudio.paContinue)
p = pyaudio.PyAudio()
stream = p.open(format = sample_format,
channels = channels,
rate = sample_rate,
input = True,
input_device_index = devidx,
frames_per_buffer = framesize,
stream_callback = callback)
stream.start_stream()
while stream.is_active():
time.sleep(0.1)
terminate()
#!/usr/bin/env python3
# Pyaudio output device for 16-bit signed-integer audio input from stdin
import pyaudio
import signal
import sys
import time
argvs = sys.argv
argc = len(argvs)
if argc == 2:
channels = int(argvs[1])
devidx = None
elif argc == 3:
channels = int(argvs[1])
devidx = int(argvs[2])
else:
print('Usage: ', argvs[0], 'channels [device-index]\n')
quit()
channels = int(argvs[1])
sample_rate = 11025
sample_width = 2 # 16bit integer
def terminate():
stream.stop_stream()
stream.close()
p.terminate()
def signal_handler(signal, frame):
print('Terminated by CTRL/C')
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
def callback(in_data, frame_count, time_info, status):
data = sys.stdin.buffer.read(frame_count * sample_width * channels)
return (data, pyaudio.paContinue)
p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16,
channels = channels,
rate = sample_rate,
output = True,
output_device_index = devidx,
frames_per_buffer = 110, # 10msec
stream_callback = callback)
stream.start_stream()
while stream.is_active():
time.sleep(0.1)
terminate()
# Running JTDX or WSJT-X on macOS side
# Running flrig and audio I/O on Linux (Raspberry Pi) side
# Linux command 1
# Linux real device audio input -> network
arecord --buffer-time=10000 -D plughw:CARD=CODEC,DEV=0 -t raw -r 11025 -f S16_LE | nc macos.example.com 23456
# MacOS command 1
# network -> MacOS virtual audio cable input (at PyAudio device 4)
nc -l 23456 | rig16-auout.py 1 4
# MacOS command 2
# MacOS received audio from virtual audiio cable output -> network
rig16-auin.py 1 4 | nc linux.example.com 23456
# Linux command 2
# nexwork -> Linux output to real device audio output
nc -l 23456 | aplay -D plughw:CARD=CODEC,DEV=0 --buffer-time=20000 -t raw -f S16_LE -r 11025 -c 1 -q -
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment