Skip to content

Instantly share code, notes, and snippets.

@ramast
Last active November 18, 2024 16:11
Show Gist options
  • Select an option

  • Save ramast/c47bd5e57586e9c2deb74975e27089f0 to your computer and use it in GitHub Desktop.

Select an option

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

ramast commented Jul 23, 2019

Copy link
Copy Markdown
Author

@anarcat

anarcat commented May 27, 2020

Copy link
Copy Markdown

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

ramast commented May 27, 2020

Copy link
Copy Markdown
Author

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

anarcat commented May 27, 2020

Copy link
Copy Markdown

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

ramast commented May 27, 2020

Copy link
Copy Markdown
Author

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

anarcat commented May 27, 2020

Copy link
Copy Markdown

yeah i guess that's alright :)

@anarcat

anarcat commented May 27, 2020

Copy link
Copy Markdown

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

ramast commented May 28, 2020

Copy link
Copy Markdown
Author

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

anarcat commented May 28, 2020 via email

Copy link
Copy Markdown

@anarcat

anarcat commented May 28, 2020 via email

Copy link
Copy Markdown

@ramast

ramast commented May 28, 2020

Copy link
Copy Markdown
Author

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

ramast commented May 28, 2020

Copy link
Copy Markdown
Author

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

@ramast

ramast commented May 28, 2020

Copy link
Copy Markdown
Author

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

@ati-ince

Copy link
Copy Markdown

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

@ramast

ramast commented Jun 30, 2021

Copy link
Copy Markdown
Author

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

@ati-ince

Copy link
Copy Markdown

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

@p5B2EA84B3

p5B2EA84B3 commented Nov 18, 2024

Copy link
Copy Markdown

Code review:

signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup)

Please review these lines as they are not indented.

@ramast

ramast commented Nov 18, 2024

Copy link
Copy Markdown
Author

Thank you for the correction though pulseaudio is now obsolete in favor of pipewire.
With pipewire its much simpler to record. Following line does the recording as well as converting the audio to mp3

pw-record -P '{ stream.capture.sink=true }' -q 1 --channels 2 --format=s16 -| lame -r -q 3 --lowpass 17 --abr 192 - "temp.mp3"

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