Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@ramast
Last active September 3, 2023 11:12
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ramast/c47bd5e57586e9c2deb74975e27089f0 to your computer and use it in GitHub Desktop.
Save ramast/c47bd5e57586e9c2deb74975e27089f0 to your computer and use it in GitHub Desktop.
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
Copy link
Author

ramast commented Jul 23, 2019

@anarcat
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
Copy link
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
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
Copy link
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
Copy link

anarcat commented May 27, 2020

yeah i guess that's alright :)

@anarcat
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
Copy link
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
Copy link

anarcat commented May 28, 2020 via email

@anarcat
Copy link

anarcat commented May 28, 2020 via email

@ramast
Copy link
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
Copy link
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
Copy link
Author

ramast commented May 28, 2020

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

@ati-ince
Copy link

Hey @ramast, do you think this script can adapt to Win(10)+?

@ramast
Copy link
Author

ramast commented Jun 30, 2021

Hi @ati-ince, Windows doesn't use PulseAudio and this solution is based on PulseAudio so no it's impossible :(

@ati-ince
Copy link

Sorry to hear that @ramast :"-( I didn't find yet any operating system independent solution. So, thank you for solving the Linux side problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment