Created
September 9, 2020 16:32
-
-
Save pdc4444/5b5d99cce9e850c7d2e908a88f468db0 to your computer and use it in GitHub Desktop.
Easily create an audio pipe for use with OBS
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
<?php | |
/** | |
* This script was created for use in Linux with PulseAudio. It will dynamically create a null sink via pactl and a combined sink to listen in on that newly created null sink on the output of your choice. | |
* My personal use case is to easily create a new combined audio pipe that includes game audio and the virtual audio sink so I can stream only my game audio via OBS but also be able to hear it! | |
* | |
* Dependencies: | |
* pulseaudio | |
* pavucontrol | |
* php-cli | |
* | |
* Known Issues: | |
* You can't create a new audio sink off a pre-existing sink that you already made due to how I'm using the sink descriptions. This is out of scope for my use case so I don't need this to work. | |
* | |
* TO DO (Maybe): | |
* Add the option to just create a combined stream (w/o the null sink) | |
* Resolve the known issue so users can combine streams from pre existing user created sinks | |
* | |
* Author: Peter Cooper | |
*/ | |
userInteraction(); | |
/** | |
* Find all combined and null sinks and return their module numbers in array form | |
* | |
* @return array $sink_numbers - an array of found null or combined sink module numbers | |
*/ | |
function findOtherSinks() | |
{ | |
$sink_numbers = []; | |
$targets = ['module-null-sink', 'module-combine-sink']; | |
foreach ($targets as $target) { | |
$cmd = "pactl list modules short | grep '" . $target . "'"; | |
$res = my_shell_exec($cmd); | |
if (!empty($res['stdout'])) { | |
$potential_lines = explode("\n", $res['stdout']); | |
foreach ($potential_lines as $line) { | |
if (!empty($line)) { | |
$sink = preg_replace('/\s+/', ' ', $line); | |
$exploded_result = explode(' ', $sink); | |
$sink_numbers[] = current($exploded_result); | |
} | |
} | |
} | |
} | |
return $sink_numbers; | |
} | |
/** | |
* Take an array of sink module numbers and unload them, exit if an error is encountered and report it to the user | |
* | |
* @param array $sink_numbers an array of found null or combined sink module numbers | |
*/ | |
function removeSinks($sink_numbers) | |
{ | |
foreach ($sink_numbers as $sink) { | |
$cmd = 'pactl unload-module ' . $sink; | |
$res = my_shell_exec($cmd); | |
if (!empty($res['stderr'])) { | |
exit("Error removing previous sink: " . $res['stderr'] . "\nCommand Used: " . $cmd); | |
} | |
} | |
} | |
/** | |
* Interact with the user and accept user input to proceed with audio sink creation. | |
*/ | |
function userInteraction() | |
{ | |
$previous_sinks = findOtherSinks(); | |
if (!empty($previous_sinks)) { | |
echo "Previously created sinks have been found, would you like to remove them?\n"; | |
$res = readline('(y/n) > '); | |
if (strlen($res) == 1 && strtolower($res) == 'y') { | |
removeSinks($previous_sinks); | |
echo "Previous Sinks removed\n"; | |
} | |
} | |
$available_sinks = getAvailableSinks(); | |
echo "Please decide which audio output you want to use:\n\n"; | |
foreach ($available_sinks as $key => $sink) { | |
echo " [" . $key . "] " . $sink . "\n"; | |
} | |
echo "\n"; | |
$choice = readline("Number: "); | |
if (isset($available_sinks[$choice])) { | |
echo "\n'" . $available_sinks[$choice] . "' has been selected.\n"; | |
$sink_desc = getSinkDescription($available_sinks[$choice]); | |
if ($sink_desc !== FALSE) { | |
echo "\nPlease name the new virtual audio sink you want to create\n"; | |
$user_sink_name = readline("> "); | |
$user_sink_name = createNullSink($user_sink_name); | |
if ($user_sink_name !== FALSE) { | |
createCombinedSink($user_sink_name, $sink_desc, $available_sinks[$choice]); | |
echo "\nYou have created a new combined sink!\n\nUse the command 'pavucontrol' to open your volume controls.\n" . | |
"You can set applications to pipe audio directly to your newly created sink or you can set them to your newly created combined sink so the audio is piped to the virtual output as well as the real audio output so you can listen along!\n"; | |
} | |
} else { | |
exit("No sinks have been detected as running. Please open pavucontrol and select the 'Output Devices' tab and try again\n"); | |
} | |
} else { | |
exit($choice . " is not a valid option.\n"); | |
} | |
} | |
/** | |
* Create the combined sink which is a combination of two audio sources. | |
* | |
* @param string $null_sink - The name of the first sink to combine with | |
* @param string $chosen_output_desc - The description of the output we are combining with (expected to be a result of getSinkDescription) | |
* @param string $chosen_output - The name of the user selected audio output to combine $null_sink with | |
*/ | |
function createCombinedSink($null_sink, $chosen_output_desc, $chosen_output) | |
{ | |
$combined_name = "combined_" . $null_sink . "_with_" . $chosen_output_desc; | |
$cmd = "pactl load-module module-combine-sink sink_properties=device.descrpition='" . $combined_name . "' sink_name='" . $combined_name . "' slaves=" . $null_sink . "," . $chosen_output . ""; | |
$res = my_shell_exec($cmd); | |
if (!empty($res['stderr'])) { | |
exit("Unable to create a combined sink!\nError: " . $res['stderr'] . "\nCommand used: " . $cmd . "\n"); | |
} | |
} | |
/** | |
* Creates the null sink with the description and name set to the users selection. | |
* @param string $user_chosen_name - The passed name extracted from the users input. Any spaces are replaced with underscore | |
* @return mixed False if failure, or the modified $user_chosen_name | |
*/ | |
function createNullSink($user_chosen_name) | |
{ | |
$user_chosen_name = str_replace(' ', '_', $user_chosen_name); | |
$cmd = "pactl load-module module-null-sink sink_properties=device.description='" . $user_chosen_name . "' sink_name='" . $user_chosen_name . "'"; | |
$res = my_shell_exec($cmd); | |
if (strpos($res['stderr'], 'fail') === FALSE) { | |
return $user_chosen_name; | |
} | |
return FALSE; | |
} | |
/** | |
* Gets all available sinks and returns the names in array format | |
* | |
* @return mixed False if failure, or an array of sink names | |
*/ | |
function getAvailableSinks() | |
{ | |
$cmd = 'pactl list sinks short'; | |
$res = my_shell_exec($cmd); | |
if (!empty($res['stdout'])) { | |
$exploded_sinks = explode("\n", $res['stdout']); | |
$sink_names = []; | |
foreach ($exploded_sinks as $sink) { | |
if (!empty($sink)) { | |
$sink = preg_replace('/\s+/', ' ', $sink); | |
$exploded_sink = explode(' ', $sink); | |
if (isset($exploded_sink[1])) { | |
$sink_names[] = $exploded_sink[1]; | |
} | |
} | |
} | |
return $sink_names; | |
} | |
return FALSE; | |
} | |
/** | |
* Gets the description of the sink based upon the sink name | |
* | |
* @param string $sink_name - The name of the sink we are looking up the description of. It's expected that this name is the result of getAvailableSinks() | |
* @return mixed False if failure, else the description of the sink with the extra spaces trimmed, 'Description: Monitor of' removed, and spaces converted to underscores | |
*/ | |
function getSinkDescription($sink_name) | |
{ | |
$cmd = "pactl list | grep -a2 -i running | grep -a1 '" . $sink_name . "' | grep -i 'description'"; | |
$res = my_shell_exec($cmd); | |
if (!empty($res['stdout'])) { | |
$name = explode("\n", $res['stdout'])[0]; // Only the first line result | |
$name = preg_replace('/\s+/', ' ', $name); // Remove excessive spaces | |
$remove_these = ['Description: Monitor of', 'Description:']; // Get rid of these strings | |
$name = str_replace($remove_these, '', $name); | |
$name = trim($name); // Trim any spaces before and after the string | |
$name = str_replace(' ', '_', $name); // Convert any spaces in the string to underscore | |
return $name; | |
} | |
return FALSE; | |
} | |
/** | |
* Allows a shell command via a new process and captures the stdout and stderr for examination | |
* | |
* @param string $cmd - The command to be run | |
* @return array The result of the command run for both stdout and stderr | |
*/ | |
function my_shell_exec($cmd) | |
{ | |
$proc = proc_open($cmd,[ | |
1 => ['pipe','w'], | |
2 => ['pipe','w'], | |
],$pipes); | |
$stdout = stream_get_contents($pipes[1]); | |
fclose($pipes[1]); | |
$stderr = stream_get_contents($pipes[2]); | |
fclose($pipes[2]); | |
proc_close($proc); | |
return ['stdout' => $stdout, 'stderr' => $stderr]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment