Recording kinda working but not quite
This commit is contained in:
parent
b347a8e22b
commit
efee6d3ef4
35
examples/record-app.php
Normal file
35
examples/record-app.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
use NoccyLabs\PulseAudio\PulseAudio;
|
||||
|
||||
$pulse = new PulseAudio();
|
||||
|
||||
// Find the client
|
||||
$inputs = $pulse->getSinkInputs();
|
||||
foreach ($inputs as $input) {
|
||||
if ($input->getProperty('application.name') == 'Audacious') {
|
||||
echo "Found audacious on input #".$input->getIndex()."\n";
|
||||
record($pulse, $input, "output.wav");
|
||||
}
|
||||
}
|
||||
|
||||
function record($pulse, $input, $filename)
|
||||
{
|
||||
echo "Creating null sink...\n";
|
||||
$recSink = $pulse->createNullSink();
|
||||
|
||||
echo "Creating recorder...\n";
|
||||
$recorder = $recSink->createRecorder();
|
||||
$recorder->setFilename($filename);
|
||||
|
||||
echo "Moving audacious to the null sink with index #".$recSink->getIndex()."...\n";
|
||||
$input->moveToSink($recSink);
|
||||
|
||||
echo "Recording 5 seconds...\n";
|
||||
$recorder->start();
|
||||
sleep(5);
|
||||
$recorder->stop();
|
||||
|
||||
}
|
@ -10,6 +10,8 @@ class Pacmd
|
||||
$cmdl = array_merge([ $command ], $args);
|
||||
$cmdl = join(" ", array_map("escapeshellarg", $cmdl));
|
||||
|
||||
echo "$ pacmd {$cmdl}\n";
|
||||
|
||||
// call pacmd
|
||||
exec("pacmd {$cmdl}", $output, $retval);
|
||||
|
||||
@ -29,105 +31,10 @@ class Pacmd
|
||||
$status = array_shift($output);
|
||||
// printf("[pacmd] %s\n", $status);
|
||||
|
||||
$parser = new ListParser();
|
||||
$parser = new PacmdListParser();
|
||||
$output = $parser->parse($output);
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
class ListParser
|
||||
{
|
||||
public function parse(array $lines)
|
||||
{
|
||||
$lines = $this->prepare($lines);
|
||||
return $this->parseRecursive($lines, 0);
|
||||
}
|
||||
|
||||
private function prepare(array $lines)
|
||||
{
|
||||
$data = [];
|
||||
foreach ($lines as $line) {
|
||||
if ($line=="") continue;
|
||||
if ($line[0] == " ") {
|
||||
$data[] = [ 0, trim($line) ];
|
||||
} else {
|
||||
$depth = 0;
|
||||
while ($line[0]=="\t") {
|
||||
$depth++;
|
||||
$line = substr($line,1);
|
||||
}
|
||||
$data[] = [ $depth, $line ];
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function parseRecursive(array $lines, $current=0)
|
||||
{
|
||||
$ret = [];
|
||||
while (count($lines)>0) {
|
||||
$line = array_shift($lines);
|
||||
$children = [];
|
||||
while ((count($lines)>0) && ($lines[0][0]>$current)) {
|
||||
$children[] = array_shift($lines);
|
||||
}
|
||||
if (count($children)>0) {
|
||||
$key = trim(str_replace("index: ","", $line[1]),":* ");
|
||||
$ret[$key] = $this->parseRecursive($children, $line[0]+1);
|
||||
} else {
|
||||
if (strpos($line[1]," = ")!==false) {
|
||||
list ($k,$v) = array_map("trim", explode("=",$line[1],2));
|
||||
$ret[$k] = $this->parseVal($v);
|
||||
} elseif (strpos($line[1],": ")!==false) {
|
||||
list ($k,$v) = array_map("trim", explode(":",$line[1],2));
|
||||
$ret[$k] = $this->parseVal($v);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
return $ret;
|
||||
|
||||
}
|
||||
|
||||
private function parseVal($value)
|
||||
{
|
||||
if ($value=="") {
|
||||
return null;
|
||||
} elseif ($value=="yes") {
|
||||
return true;
|
||||
} elseif ($value=="no") {
|
||||
return false;
|
||||
} elseif ($value[0]=="<") {
|
||||
$kvs = trim($value,"<>");
|
||||
if (strpos($kvs,"=")===false) {
|
||||
return $kvs;
|
||||
}
|
||||
$kvs = explode(" ",$kvs);
|
||||
$val = [];
|
||||
foreach ($kvs as $kv) {
|
||||
list ($k,$v) = explode("=",$kv,2);
|
||||
$val[$k] = $v;
|
||||
}
|
||||
return $val;
|
||||
} elseif ($value[0]=="\"") {
|
||||
return trim($value,"\"");
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseKeyVal($kvs)
|
||||
{
|
||||
$kvs = explode(" ",$kvs,2 );
|
||||
$val = [];
|
||||
foreach ($kvs as $kv) {
|
||||
list ($k,$v) = explode("=",$kv,2);
|
||||
$val[$k] = $this->parseKeyVal($v);
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
}
|
||||
|
100
src/Helper/PacmdListParser.php
Normal file
100
src/Helper/PacmdListParser.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Helper;
|
||||
|
||||
class PacmdListParser
|
||||
{
|
||||
public function parse(array $lines)
|
||||
{
|
||||
$lines = $this->prepare($lines);
|
||||
return $this->parseRecursive($lines, 0);
|
||||
}
|
||||
|
||||
private function prepare(array $lines)
|
||||
{
|
||||
$data = [];
|
||||
foreach ($lines as $line) {
|
||||
if ($line=="") continue;
|
||||
if ($line[0] == " ") {
|
||||
$data[] = [ 0, trim($line) ];
|
||||
} else {
|
||||
$depth = 0;
|
||||
while ($line[0]=="\t") {
|
||||
$depth++;
|
||||
$line = substr($line,1);
|
||||
}
|
||||
$data[] = [ $depth, $line ];
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function parseRecursive(array $lines, $current=0)
|
||||
{
|
||||
$ret = [];
|
||||
while (count($lines)>0) {
|
||||
$line = array_shift($lines);
|
||||
$children = [];
|
||||
while ((count($lines)>0) && ($lines[0][0]>$current)) {
|
||||
$children[] = array_shift($lines);
|
||||
}
|
||||
if (count($children)>0) {
|
||||
$key = trim(str_replace("index: ","", $line[1]),":* ");
|
||||
$ret[$key] = $this->parseRecursive($children, $line[0]+1);
|
||||
} else {
|
||||
if (strpos($line[1]," = ")!==false) {
|
||||
list ($k,$v) = array_map("trim", explode("=",$line[1],2));
|
||||
$ret[$k] = $this->parseVal($v);
|
||||
} elseif (strpos($line[1],": ")!==false) {
|
||||
list ($k,$v) = array_map("trim", explode(":",$line[1],2));
|
||||
$ret[$k] = $this->parseVal($v);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
return $ret;
|
||||
|
||||
}
|
||||
|
||||
private function parseVal($value)
|
||||
{
|
||||
if ($value=="") {
|
||||
return null;
|
||||
} elseif ($value=="yes") {
|
||||
return true;
|
||||
} elseif ($value=="no") {
|
||||
return false;
|
||||
} elseif ($value[0]=="<") {
|
||||
$kvs = trim($value,"<>");
|
||||
if (strpos($kvs,"=")===false) {
|
||||
return $kvs;
|
||||
}
|
||||
$kvs = explode(" ",$kvs);
|
||||
$val = [];
|
||||
foreach ($kvs as $kv) {
|
||||
list ($k,$v) = explode("=",$kv,2);
|
||||
$val[$k] = $v;
|
||||
}
|
||||
return $val;
|
||||
} elseif ($value[0]=="\"") {
|
||||
return trim($value,"\"");
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseKeyVal($kvs)
|
||||
{
|
||||
$kvs = explode(" ",$kvs,2 );
|
||||
$val = [];
|
||||
foreach ($kvs as $kv) {
|
||||
list ($k,$v) = explode("=",$kv,2);
|
||||
$val[$k] = $this->parseKeyVal($v);
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
}
|
||||
|
29
src/Helper/Pactl.php
Normal file
29
src/Helper/Pactl.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Helper;
|
||||
|
||||
class Pactl
|
||||
{
|
||||
public static function call($command, array $args=[])
|
||||
{
|
||||
// assemble command lin
|
||||
$cmdl = array_merge([ $command ], $args);
|
||||
$cmdl = join(" ", array_map("escapeshellarg", $cmdl));
|
||||
|
||||
echo "$ pactl {$cmdl}\n";
|
||||
|
||||
// call pacmd
|
||||
exec("pactl {$cmdl}", $output, $retval);
|
||||
|
||||
// handle errors
|
||||
if ($retval != 0) {
|
||||
throw new \RuntimeException("Failed to call pacmd. exitcode={$retval}");
|
||||
}
|
||||
|
||||
// return output
|
||||
if (count($output)>0) {
|
||||
return $output[0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -26,8 +26,9 @@ class PropertyList implements ArrayAccess, IteratorAggregate, Countable
|
||||
|
||||
public function offsetGet($key)
|
||||
{
|
||||
if (!array_key_exist($key, $this->properties)) {
|
||||
throw new InvalidArgumentException("No such property: {$key}");
|
||||
if (!array_key_exists($key, $this->properties)) {
|
||||
return null;
|
||||
// throw new InvalidArgumentException("No such property: {$key}");
|
||||
}
|
||||
return $this->properties[$key];
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace NoccyLabs\PulseAudio;
|
||||
use NoccyLabs\PulseAudio\Module\ModuleList;
|
||||
use NoccyLabs\PulseAudio\Sink\SinkList;
|
||||
use NoccyLabs\PulseAudio\Sink\SinkInputList;
|
||||
use NoccyLabs\PulseAudio\Sink\NullSink;
|
||||
use NoccyLabs\PulseAudio\Source\SourceList;
|
||||
use NoccyLabs\PulseAudio\Source\SourceOutputList;
|
||||
use NoccyLabs\PulseAudio\Client\ClientList;
|
||||
@ -19,37 +20,54 @@ class PulseAudio
|
||||
*/
|
||||
public function getModules()
|
||||
{
|
||||
return new ModuleList();
|
||||
return new ModuleList($this);
|
||||
}
|
||||
|
||||
public function getSinks()
|
||||
{
|
||||
return new SinkList();
|
||||
return new SinkList($this);
|
||||
}
|
||||
|
||||
public function createNullSink($name=null)
|
||||
{
|
||||
return new NullSink($this, $name);
|
||||
}
|
||||
|
||||
public function getSinkByIndex($index)
|
||||
{
|
||||
$sinks = $this->getSinks();
|
||||
return $sinks[$index];
|
||||
}
|
||||
|
||||
public function getSinkInputs()
|
||||
{
|
||||
return new SinkInputList();
|
||||
return new SinkInputList($this);
|
||||
}
|
||||
|
||||
public function getSources()
|
||||
{
|
||||
return new SourceList();
|
||||
return new SourceList($this);
|
||||
}
|
||||
|
||||
public function getSourceOutputs()
|
||||
{
|
||||
return new SourceOutputList();
|
||||
return new SourceOutputList($this);
|
||||
}
|
||||
|
||||
public function getClients()
|
||||
{
|
||||
return new ClientList();
|
||||
return new ClientList($this);
|
||||
}
|
||||
|
||||
public function getClientByIndex($index)
|
||||
{
|
||||
$clients = $this->getClients();
|
||||
return $clients[$index];
|
||||
}
|
||||
|
||||
public function getCards()
|
||||
{
|
||||
return new CardList();
|
||||
return new CardList($this);
|
||||
}
|
||||
|
||||
}
|
||||
|
53
src/Sink/NullSink.php
Normal file
53
src/Sink/NullSink.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Sink;
|
||||
|
||||
use NoccyLabs\PulseAudio\Helper\Pactl;
|
||||
use NoccyLabs\PulseAudio\Helper\Pacmd;
|
||||
use NoccyLabs\PulseAudio\PulseAudio;
|
||||
use NoccyLabs\PulseAudio\PropertyList;
|
||||
|
||||
class NullSink extends Sink
|
||||
{
|
||||
|
||||
protected $destroyed = false;
|
||||
|
||||
protected $moduleIndex;
|
||||
|
||||
public function __construct(PulseAudio $pulse, $name=null)
|
||||
{
|
||||
$name = $name?:uniqid("null");
|
||||
|
||||
$this->moduleIndex = Pactl::call('load-module', [ 'module-null-sink', 'sink_name='.$name ]);
|
||||
|
||||
$sinks = Pacmd::query('list-sinks');
|
||||
foreach ($sinks as $index=>$sink) {
|
||||
if ($sink['name'] == $name) {
|
||||
parent::__construct($pulse, $index, $sink);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Unable to create null sink!");
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (!$this->isDestroyed()) {
|
||||
$this->destroySink();
|
||||
}
|
||||
}
|
||||
|
||||
public function destroySink()
|
||||
{
|
||||
Pactl::call('unload-module', [ $this->moduleIndex ]);
|
||||
$this->destroyed = true;
|
||||
}
|
||||
|
||||
public function isDestroyed()
|
||||
{
|
||||
return $this->destroyed;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Sink;
|
||||
|
||||
/**
|
||||
* Record from a PulseAudio sink
|
||||
*
|
||||
*/
|
||||
class Recorder
|
||||
{
|
||||
/** @const The number of seconds to wait for parec to exit */
|
||||
const STOP_TIMEOUT = 5;
|
||||
/** @var Sink The sink to record from */
|
||||
protected $sink;
|
||||
/** @var resource The process handle of parec */
|
||||
protected $proc;
|
||||
/** @var array|null The pipes of the sink */
|
||||
protected $pipes;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Sink $sink The sink to record from
|
||||
*/
|
||||
public function __construct(Sink $sink)
|
||||
{
|
||||
$this->sink = $sink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filename to write to
|
||||
*
|
||||
* @param string $filename
|
||||
*/
|
||||
public function setFilename($filename)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start recording.
|
||||
*
|
||||
* @return bool True if started, false if a capture is already running
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
// Don't do anything if we are already recording'
|
||||
if ($this->proc) {
|
||||
$status = proc_get_status($this->proc);
|
||||
if ($status['running']==true) {
|
||||
return false;
|
||||
}
|
||||
proc_close($this->proc);
|
||||
}
|
||||
// Otherwise start recording
|
||||
$cmdl = sprintf(
|
||||
"parecord --format=s16le --file-format=wav --monitor-stream %d",
|
||||
$this->sink->getMonitorIndex()
|
||||
);
|
||||
|
||||
$filename = $this->filename?:"output.wav";
|
||||
|
||||
echo "$ {$cmdl}\n";
|
||||
|
||||
$ds = [
|
||||
0 => STDIN,
|
||||
1 => fopen($filename,'wb'),
|
||||
2 => STDERR
|
||||
];
|
||||
$this->proc = proc_open($cmdl, $ds, $pipes);
|
||||
$this->pipes = $pipes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop recording.
|
||||
*
|
||||
* @return bool True if not capturing or capture stopped ok
|
||||
*/
|
||||
public function stop()
|
||||
{
|
||||
if (!$this->proc) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Send a SIGINT to parec
|
||||
proc_terminate($this->proc, 2);
|
||||
|
||||
// Wait for parec to close
|
||||
$expire = microtime(true)+self::STOP_TIMEOUT;
|
||||
$this->pipes = null;
|
||||
do {
|
||||
$status = proc_get_status($this->proc);
|
||||
usleep(10000);
|
||||
} while ($status['running'] && (microtime(true)<$expire));
|
||||
|
||||
// Return status; if running return false
|
||||
return !$status['running'];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Sink;
|
||||
|
||||
use NoccyLabs\PulseAudio\PulseAudio;
|
||||
use NoccyLabs\PulseAudio\PropertyList;
|
||||
|
||||
class Sink
|
||||
@ -17,12 +18,18 @@ class Sink
|
||||
|
||||
protected $properties;
|
||||
|
||||
public function __construct($index, array $sink)
|
||||
protected $pulse;
|
||||
|
||||
protected $monitor;
|
||||
|
||||
public function __construct(PulseAudio $pulse, $index, array $sink)
|
||||
{
|
||||
$this->pulse = $pulse;
|
||||
$this->index = $index;
|
||||
$this->name = $sink['name'];
|
||||
$this->card = $sink['card'];
|
||||
//$this->card = $sink['card'];
|
||||
$this->driver = $sink['driver'];
|
||||
$this->monitor = $sink['monitor source'];
|
||||
$this->properties = new PropertyList($sink['properties'], true);
|
||||
}
|
||||
|
||||
@ -51,10 +58,20 @@ class Sink
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
public function getMonitorIndex()
|
||||
{
|
||||
return $this->monitor;
|
||||
}
|
||||
|
||||
public function moveToSink(Sink $sink)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function createRecorder()
|
||||
{
|
||||
return new Recorder($this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Sink;
|
||||
|
||||
use NoccyLabs\PulseAudio\Helper\Pactl;
|
||||
use NoccyLabs\PulseAudio\PulseAudio;
|
||||
use NoccyLabs\PulseAudio\PropertyList;
|
||||
|
||||
class SinkInput
|
||||
@ -15,7 +17,9 @@ class SinkInput
|
||||
|
||||
protected $properties;
|
||||
|
||||
public function __construct($index, array $input=[])
|
||||
protected $pulse;
|
||||
|
||||
public function __construct(PulseAudio $pulse, $index, array $input=[])
|
||||
{
|
||||
$this->index = $index;
|
||||
$this->sink = $input['sink'];
|
||||
@ -38,9 +42,14 @@ class SinkInput
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function getProperty($key)
|
||||
{
|
||||
return $this->properties[$key];
|
||||
}
|
||||
|
||||
public function moveToSink(Sink $sink)
|
||||
{
|
||||
|
||||
Pactl::call('move-sink-input', [ $this->index, $sink->getName() ]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Sink;
|
||||
|
||||
use NoccyLabs\PulseAudio\PulseAudio;
|
||||
use NoccyLabs\PulseAudio\Helper\Pacmd;
|
||||
use ArrayAccess;
|
||||
use ArrayIterator;
|
||||
@ -12,11 +13,14 @@ class SinkInputList implements ArrayAccess, IteratorAggregate, Countable
|
||||
{
|
||||
protected $inputs = [];
|
||||
|
||||
public function __construct()
|
||||
protected $pulse;
|
||||
|
||||
public function __construct(PulseAudio $pulse)
|
||||
{
|
||||
$this->pulse = $pulse;
|
||||
$inputs = Pacmd::query("list-sink-inputs");
|
||||
foreach ($inputs as $index=>$input) {
|
||||
$this->inputs[] = new SinkInput($index, $input);
|
||||
$this->inputs[] = new SinkInput($pulse, $index, $input);
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +40,7 @@ class SinkInputList implements ArrayAccess, IteratorAggregate, Countable
|
||||
public function offsetGet($key)
|
||||
{
|
||||
foreach ($this->inputs as $input) {
|
||||
if ($input->getIndex() == $key) {
|
||||
if (($input->getIndex() == $key) || ($input->getName()==$key)) {
|
||||
return $input;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace NoccyLabs\PulseAudio\Sink;
|
||||
|
||||
use NoccyLabs\PulseAudio\PulseAudio;
|
||||
use NoccyLabs\PulseAudio\Helper\Pacmd;
|
||||
use ArrayAccess;
|
||||
use IteratorAggregate;
|
||||
@ -12,15 +13,23 @@ class SinkList implements ArrayAccess, IteratorAggregate, Countable
|
||||
{
|
||||
protected $sinks = [];
|
||||
|
||||
public function __construct()
|
||||
protected $pulse;
|
||||
|
||||
public function __construct(PulseAudio $pulse)
|
||||
{
|
||||
$this->pulse = $pulse;
|
||||
$sinks = Pacmd::query("list-sinks");
|
||||
foreach ($sinks as $index=>$sink) {
|
||||
$this->sinks[] = new Sink($index, $sink);
|
||||
$this->sinks[] = new Sink($pulse, $index, $sink);
|
||||
}
|
||||
}
|
||||
|
||||
public function createSink($name)
|
||||
public function createNullSink($name)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function destroyNullSink(Sink $sink)
|
||||
{
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user