Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Record a program's output with PulseAudio
#!/usr/bin/env python3
# Based on code from these stackoverflow answers:
# https://askubuntu.com/questions/60837/record-a-programs-output-with-pulseaudio/910879#910879
import re
import subprocess
import sys
import os
import signal
from time import sleep
INDEX_RE = re.compile(r'[0-9]+$')
APP_NAME_RE = re.compile(r'"([^"]+)"')
SINK_RE=re.compile("\s*sink: ([0-9]+) <.*>")
DEFAULT_OUTPUT_RE = re.compile(r'^\s*name: <([^ >]+)>')
record_module_id = None
def get_default_output():
#pacmd list-sinks | grep -A1 "* index" | grep -oP "<\K[^ >]+"
output = subprocess.run(["pacmd", "list-sinks"], stdout=subprocess.PIPE, check=True).stdout
for line in output.decode('utf-8').split('\n'):
match = DEFAULT_OUTPUT_RE.match(line)
if match:
return match[1]
print("Can't seem to find proper input sink, are you using pulseaudio?")
sys.exit(3)
def load_record_module():
default_output = get_default_output()
output = subprocess.run(
["pactl", "load-module", "module-combine-sink", "sink_name=record-n-play", f"slaves={default_output}",
"sink_properties=device.description=Record-and-Play"],
stdout=subprocess.PIPE, check=True).stdout
return int(output.strip())
def load_apps():
output = subprocess.run(["pacmd", "list-sink-inputs"], stdout=subprocess.PIPE, check=True).stdout
output = output.decode('utf-8').split('\n')
indexes = []
app_names = []
sinks = []
for line in output:
if "index" in line:
index = INDEX_RE.findall(line)[0]
indexes.append(index)
elif "application.name" in line:
app_name = APP_NAME_RE.findall(line)[0]
app_names.append(app_name)
elif len(sinks) < len(indexes) and "sink: " in line:
sink = SINK_RE.match(line)[1]
sinks.append(sink)
if len(indexes) == 0:
print("Sorry, couldn't find any input audio channels")
sys.exit(1)
return indexes, app_names, sinks
def cleanup(*args, **kwargs):
if record_module_id is None:
sys.exit(0)
return
os.system(f"pactl move-sink-input {indexes[user_selection]} {sinks[user_selection]}")
os.system(f"pactl unload-module {record_module_id}")
print("Terminated")
sys.exit(0)
signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup)
if os.path.exists("temp.mp3"):
print("temp.mp3 already exist, aborting")
sys.exit(2)
_, app_names, _ = load_apps()
print("")
for idx, app_name in enumerate(app_names):
print(f"{idx + 1} - {app_name}")
print("")
while True:
try:
user_selection = int(input("Please enter a number: "))
except ValueError:
print("Only numbers are allowed")
continue
if user_selection > len(app_names) or user_selection <= 0:
print("Number out of range")
continue
user_selection = int(user_selection) - 1
break
app_name = app_names[user_selection]
print(f"Your selection was: {app_name}")
input("Please press enter when you are ready to start")
while True:
indexes, app_names, sinks = load_apps()
if app_name not in app_names:
print("Couldn't find selected audio channel, retrying")
sleep(0.2)
continue
user_selection = app_names.index(app_name)
record_module_id=load_record_module()
os.system(f"pactl move-sink-input {indexes[user_selection]} record-n-play")
os.system(f"parec --format=s16le -d record-n-play.monitor | lame -r -q 3 --lowpass 17 --abr 192 - 'temp.mp3'")
cleanup()
@ramast

This comment has been minimized.

Copy link
Owner Author

ramast commented Jul 23, 2019

@anarcat

This comment has been minimized.

Copy link

anarcat commented May 27, 2020

cleaned up this code (with the black formatter), switched to subprocess for calls and made the encoder configurable in:

https://gitlab.com/anarcat/scripts/-/blob/master/pulse-recorder.py

let me know what the license of this is so i can give proper credit! :)

@ramast

This comment has been minimized.

Copy link
Owner Author

ramast commented May 27, 2020

Thanks Anarcat, This work is all based on code written on stackoverflow
https://askubuntu.com/questions/60837/record-a-programs-output-with-pulseaudio/910879#910879
By users Waschtl and KrisWebDev

If you want to give credit it, I guess can link to that stackoverflow link ?
As far as I know there are no license, you can do whatever you want with it.

@anarcat

This comment has been minimized.

Copy link

anarcat commented May 27, 2020

If you want to give credit it, I guess can link to that stackoverflow link ?

I had that already in the commitlogs, but made it explicit in the comments at the top of file.

As far as I know there are no license, you can do whatever you want with it.

Actually, contents on Stackoverflow is covered by the CC-BY-SA-4.0 license, so that kind of matters (for example, I have to give attribution, and so do you!) :)

@ramast

This comment has been minimized.

Copy link
Owner Author

ramast commented May 27, 2020

Actually, contents on Stackoverflow is covered by the CC-BY-SA-4.0

Thanks, This is really good to know. I've already gave attributions first comment after the code.

Hopefully that'd be enough

@anarcat

This comment has been minimized.

Copy link

anarcat commented May 27, 2020

yeah i guess that's alright :)

@anarcat

This comment has been minimized.

Copy link

anarcat commented May 27, 2020

oh, and by the way, the script has evolved quite a bit. it now properly handles multiple outputs and has an "automatic" mode that doesn't prompt the user. i hope you like it!

@ramast

This comment has been minimized.

Copy link
Owner Author

ramast commented May 28, 2020

Your scripts looks a lot more sophisticated and judging by the code I think it has also more features.

I've tried to run it but ran into a problem

First I ran the script like this python3 ~/pulse-recorder.py --raw and it just hangs with no output.
I realized that it's because I didn't pass the -i parameter but what is happening in this case?

I've tried again with -i and I liked how I could identify the process by it's unique ID. Really helpful but after choosing the process I wanted to record it didn't record anything.

I've ran it again with --debug option and I guess that was the issue
WARNING:root:Couldn't find selected audio channel, retrying

I think this output should be visible without the need for --debug. I am not sure why it couldn't find "selected audio channel` though? I've tried same experiment with my old script and seemed to record fine.

Steps to reproduce:

  1. Open youtube video (i've used firefox if that make any difference)
  2. pause the video
  3. run the script and choose Firefox process id
  4. run the video
  5. go back to the script and press enter to record.
@anarcat

This comment has been minimized.

Copy link

anarcat commented May 28, 2020

@anarcat

This comment has been minimized.

Copy link

anarcat commented May 28, 2020

@ramast

This comment has been minimized.

Copy link
Owner Author

ramast commented May 28, 2020

Thanks but still doesn't seem to be working :(

running clients:
6966 - Firefox
Please enter a number: 6966
Press enter to record from Firefox...
INFO: Recording from client 6966 (Firefox)

Traceback (most recent call last):
  File "/home/ramast/pulse-recorder.py", line 250, in <module>
    main(args)
  File "/home/ramast/pulse-recorder.py", line 145, in main
    record_module_id = load_record_module(sinks[client_index])
KeyError: 6966
@ramast

This comment has been minimized.

Copy link
Owner Author

ramast commented May 28, 2020

Please ignore that, I don't think it was a mistake form the script.
Seems to be working fine now

@ramast

This comment has been minimized.

Copy link
Owner Author

ramast commented May 28, 2020

I've updated my stackoverflow answer to give mention to your script.
Thanks for sharing!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.