From efee6d3ef4ce333db1885cc05f17112874cdb213 Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Mon, 13 Feb 2017 22:50:11 +0100 Subject: [PATCH] Recording kinda working but not quite --- examples/record-app.php | 35 +++++++++++ src/Helper/Pacmd.php | 99 +------------------------------- src/Helper/PacmdListParser.php | 100 ++++++++++++++++++++++++++++++++ src/Helper/Pactl.php | 29 ++++++++++ src/PropertyList.php | 5 +- src/PulseAudio.php | 32 ++++++++--- src/Sink/NullSink.php | 53 +++++++++++++++++ src/Sink/Recorder.php | 102 +++++++++++++++++++++++++++++++++ src/Sink/Sink.php | 21 ++++++- src/Sink/SinkInput.php | 13 ++++- src/Sink/SinkInputList.php | 10 +++- src/Sink/SinkList.php | 15 ++++- 12 files changed, 399 insertions(+), 115 deletions(-) create mode 100644 examples/record-app.php create mode 100644 src/Helper/PacmdListParser.php create mode 100644 src/Helper/Pactl.php create mode 100644 src/Sink/NullSink.php diff --git a/examples/record-app.php b/examples/record-app.php new file mode 100644 index 0000000..b19831b --- /dev/null +++ b/examples/record-app.php @@ -0,0 +1,35 @@ +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(); + +} diff --git a/src/Helper/Pacmd.php b/src/Helper/Pacmd.php index b6d7293..46a0cc2 100644 --- a/src/Helper/Pacmd.php +++ b/src/Helper/Pacmd.php @@ -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; - } - -} diff --git a/src/Helper/PacmdListParser.php b/src/Helper/PacmdListParser.php new file mode 100644 index 0000000..7f733a3 --- /dev/null +++ b/src/Helper/PacmdListParser.php @@ -0,0 +1,100 @@ +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; + } + +} + diff --git a/src/Helper/Pactl.php b/src/Helper/Pactl.php new file mode 100644 index 0000000..946fee1 --- /dev/null +++ b/src/Helper/Pactl.php @@ -0,0 +1,29 @@ +0) { + return $output[0]; + } + } + +} \ No newline at end of file diff --git a/src/PropertyList.php b/src/PropertyList.php index ca357f4..a2dde8e 100644 --- a/src/PropertyList.php +++ b/src/PropertyList.php @@ -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]; } diff --git a/src/PulseAudio.php b/src/PulseAudio.php index e64cf7e..764ec44 100644 --- a/src/PulseAudio.php +++ b/src/PulseAudio.php @@ -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); } } diff --git a/src/Sink/NullSink.php b/src/Sink/NullSink.php new file mode 100644 index 0000000..97ee922 --- /dev/null +++ b/src/Sink/NullSink.php @@ -0,0 +1,53 @@ +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; + } + +} + diff --git a/src/Sink/Recorder.php b/src/Sink/Recorder.php index e69de29..2df4a22 100644 --- a/src/Sink/Recorder.php +++ b/src/Sink/Recorder.php @@ -0,0 +1,102 @@ +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']; + } + + +} + diff --git a/src/Sink/Sink.php b/src/Sink/Sink.php index eca4dd5..b458fbb 100644 --- a/src/Sink/Sink.php +++ b/src/Sink/Sink.php @@ -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); + } + } diff --git a/src/Sink/SinkInput.php b/src/Sink/SinkInput.php index e7cd475..772209e 100644 --- a/src/Sink/SinkInput.php +++ b/src/Sink/SinkInput.php @@ -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() ]); } } diff --git a/src/Sink/SinkInputList.php b/src/Sink/SinkInputList.php index 5ca6e19..ff040a1 100644 --- a/src/Sink/SinkInputList.php +++ b/src/Sink/SinkInputList.php @@ -2,6 +2,7 @@ namespace NoccyLabs\PulseAudio\Sink; +use NoccyLabs\PulseAudio\PulseAudio; use NoccyLabs\PulseAudio\Helper\Pacmd; use ArrayAccess; use ArrayIterator; @@ -11,12 +12,15 @@ use Countable; class SinkInputList implements ArrayAccess, IteratorAggregate, Countable { protected $inputs = []; + + protected $pulse; - public function __construct() + 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; } } diff --git a/src/Sink/SinkList.php b/src/Sink/SinkList.php index 96b2b3d..eb3a14d 100644 --- a/src/Sink/SinkList.php +++ b/src/Sink/SinkList.php @@ -2,6 +2,7 @@ namespace NoccyLabs\PulseAudio\Sink; +use NoccyLabs\PulseAudio\PulseAudio; use NoccyLabs\PulseAudio\Helper\Pacmd; use ArrayAccess; use IteratorAggregate; @@ -12,19 +13,27 @@ 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) + { + + } + public function createRecorder($name) {