Last active
September 3, 2023 11:12
-
-
Save ramast/c47bd5e57586e9c2deb74975e27089f0 to your computer and use it in GitHub Desktop.
Record a program's output with PulseAudio
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
anarcat
commented
May 28, 2020
via email
On 2020-05-28 03:09:46, ramast wrote:
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.
Yeah, the `--raw` path is not well tested. It might have written the raw
samples to recording.ogg, counter-intuitively. Obviously, the default
output file should be something like recording.raw if --raw is
specified.
I realized that it's because I didn't pass the `-i` parameter but what is happening in this case?
-i just prompts you for the sink to record instead of guessing
(ie. picking the first one).
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.
Yeah, I've refactored that bit of code. I personnally prefer your
interface, where you have the sinks ordered incrementally from 1, but
it complicated the code needlessly, especially for a feature (picking
the stream interactively) that I made optional.
I've ran it again with `--debug` option and I guess that was the issue
`WARNING:root:Couldn't find selected audio channel, retrying`
Ah-ah! Well there's something going on here... I have added debugging
around that piece of code, could you pull the latest and try again?
I frankly have no idea why that code is there, actually... It's not
clear to me that it is necessary or that we actually need it. But if it
worked before, I don't see why it should not still be working. It works
here, in any case...
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.
Yep, agreed. Seems I screwed up the logging... I added a patch to
default the log level to WARNING so those messages should show up
regardless of the debugging level.
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.
Let's see...
1. opening https://www.youtube.com/watch?v=7Nn7NZI_LN4
1.b) pressing play (i have an extension that disables autoplay)
2. press pause
3. run `pulse-record --debug --interactive`, pick "0 - AudioIPC Server"
4. hit play
5. go back to the script and hit enter
And lo and behold:
anarcat@angela:~(master)$ pulse-recorder.py --debug -i
DEBUG: running clients {21: 'AudioIPC Server'}, sinks {21: '0'}
running clients:
21 - AudioIPC Server
Please enter a number: 21
Press enter to record from AudioIPC Server...
Traceback (most recent call last):
File "/home/anarcat/bin/pulse-recorder.py", line 241, in <module>
main(args)
File "/home/anarcat/bin/pulse-recorder.py", line 136, in main
record_module_id = load_record_module(sinks[client_index])
KeyError: 21
So yeah, there's actually a problem when you pause the player in-between
confirming pulse-record... i'll look into that!
a.
…--
That's one of the remarkable things about life: it's never so bad that
it can't get worse.
- Calvin
On 2020-05-28 09:35:26, Antoine Beaupré wrote:
> I've ran it again with `--debug` option and I guess that was the issue
> `WARNING:root:Couldn't find selected audio channel, retrying`
Ah-ah! Well there's something going on here... I have added debugging
around that piece of code, could you pull the latest and try again?
I frankly have no idea why that code is there, actually... It's not
clear to me that it is necessary or that we actually need it. But if it
worked before, I don't see why it should not still be working. It works
here, in any case...
I fixed this by improving the redetection feature, probably modeling the
original you wrote, which I did not understand (so I removed it!). Sorry
about that... It should all be good now.
This is a special case that occurs only when you pause and restart a
stream (or at least specifically the firefox media player), since the
client index actually changes and needs to be found again.
I hope that helps!
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
Please ignore that, I don't think it was a mistake form the script.
Seems to be working fine now
I've updated my stackoverflow answer to give mention to your script.
Thanks for sharing!!
Hey @ramast, do you think this script can adapt to Win(10)+?
Hi @ati-ince, Windows doesn't use PulseAudio and this solution is based on PulseAudio so no it's impossible :(
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