Recording kinda working but not quite

This commit is contained in:
Chris 2017-02-13 22:50:11 +01:00
parent b347a8e22b
commit efee6d3ef4
12 changed files with 399 additions and 115 deletions

35
examples/record-app.php Normal file
View 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();
}

View File

@ -10,6 +10,8 @@ class Pacmd
$cmdl = array_merge([ $command ], $args); $cmdl = array_merge([ $command ], $args);
$cmdl = join(" ", array_map("escapeshellarg", $cmdl)); $cmdl = join(" ", array_map("escapeshellarg", $cmdl));
echo "$ pacmd {$cmdl}\n";
// call pacmd // call pacmd
exec("pacmd {$cmdl}", $output, $retval); exec("pacmd {$cmdl}", $output, $retval);
@ -29,105 +31,10 @@ class Pacmd
$status = array_shift($output); $status = array_shift($output);
// printf("[pacmd] %s\n", $status); // printf("[pacmd] %s\n", $status);
$parser = new ListParser(); $parser = new PacmdListParser();
$output = $parser->parse($output); $output = $parser->parse($output);
return $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;
}
}

View 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
View 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];
}
}
}

View File

@ -26,8 +26,9 @@ class PropertyList implements ArrayAccess, IteratorAggregate, Countable
public function offsetGet($key) public function offsetGet($key)
{ {
if (!array_key_exist($key, $this->properties)) { if (!array_key_exists($key, $this->properties)) {
throw new InvalidArgumentException("No such property: {$key}"); return null;
// throw new InvalidArgumentException("No such property: {$key}");
} }
return $this->properties[$key]; return $this->properties[$key];
} }

View File

@ -5,6 +5,7 @@ namespace NoccyLabs\PulseAudio;
use NoccyLabs\PulseAudio\Module\ModuleList; use NoccyLabs\PulseAudio\Module\ModuleList;
use NoccyLabs\PulseAudio\Sink\SinkList; use NoccyLabs\PulseAudio\Sink\SinkList;
use NoccyLabs\PulseAudio\Sink\SinkInputList; use NoccyLabs\PulseAudio\Sink\SinkInputList;
use NoccyLabs\PulseAudio\Sink\NullSink;
use NoccyLabs\PulseAudio\Source\SourceList; use NoccyLabs\PulseAudio\Source\SourceList;
use NoccyLabs\PulseAudio\Source\SourceOutputList; use NoccyLabs\PulseAudio\Source\SourceOutputList;
use NoccyLabs\PulseAudio\Client\ClientList; use NoccyLabs\PulseAudio\Client\ClientList;
@ -19,37 +20,54 @@ class PulseAudio
*/ */
public function getModules() public function getModules()
{ {
return new ModuleList(); return new ModuleList($this);
} }
public function getSinks() 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() public function getSinkInputs()
{ {
return new SinkInputList(); return new SinkInputList($this);
} }
public function getSources() public function getSources()
{ {
return new SourceList(); return new SourceList($this);
} }
public function getSourceOutputs() public function getSourceOutputs()
{ {
return new SourceOutputList(); return new SourceOutputList($this);
} }
public function getClients() public function getClients()
{ {
return new ClientList(); return new ClientList($this);
}
public function getClientByIndex($index)
{
$clients = $this->getClients();
return $clients[$index];
} }
public function getCards() public function getCards()
{ {
return new CardList(); return new CardList($this);
} }
} }

53
src/Sink/NullSink.php Normal file
View 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;
}
}

View File

@ -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'];
}
}

View File

@ -2,6 +2,7 @@
namespace NoccyLabs\PulseAudio\Sink; namespace NoccyLabs\PulseAudio\Sink;
use NoccyLabs\PulseAudio\PulseAudio;
use NoccyLabs\PulseAudio\PropertyList; use NoccyLabs\PulseAudio\PropertyList;
class Sink class Sink
@ -17,12 +18,18 @@ class Sink
protected $properties; 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->index = $index;
$this->name = $sink['name']; $this->name = $sink['name'];
$this->card = $sink['card']; //$this->card = $sink['card'];
$this->driver = $sink['driver']; $this->driver = $sink['driver'];
$this->monitor = $sink['monitor source'];
$this->properties = new PropertyList($sink['properties'], true); $this->properties = new PropertyList($sink['properties'], true);
} }
@ -51,10 +58,20 @@ class Sink
return $this->driver; return $this->driver;
} }
public function getMonitorIndex()
{
return $this->monitor;
}
public function moveToSink(Sink $sink) public function moveToSink(Sink $sink)
{ {
} }
public function createRecorder()
{
return new Recorder($this);
}
} }

View File

@ -2,6 +2,8 @@
namespace NoccyLabs\PulseAudio\Sink; namespace NoccyLabs\PulseAudio\Sink;
use NoccyLabs\PulseAudio\Helper\Pactl;
use NoccyLabs\PulseAudio\PulseAudio;
use NoccyLabs\PulseAudio\PropertyList; use NoccyLabs\PulseAudio\PropertyList;
class SinkInput class SinkInput
@ -15,7 +17,9 @@ class SinkInput
protected $properties; protected $properties;
public function __construct($index, array $input=[]) protected $pulse;
public function __construct(PulseAudio $pulse, $index, array $input=[])
{ {
$this->index = $index; $this->index = $index;
$this->sink = $input['sink']; $this->sink = $input['sink'];
@ -38,9 +42,14 @@ class SinkInput
return $this->client; return $this->client;
} }
public function getProperty($key)
{
return $this->properties[$key];
}
public function moveToSink(Sink $sink) public function moveToSink(Sink $sink)
{ {
Pactl::call('move-sink-input', [ $this->index, $sink->getName() ]);
} }
} }

View File

@ -2,6 +2,7 @@
namespace NoccyLabs\PulseAudio\Sink; namespace NoccyLabs\PulseAudio\Sink;
use NoccyLabs\PulseAudio\PulseAudio;
use NoccyLabs\PulseAudio\Helper\Pacmd; use NoccyLabs\PulseAudio\Helper\Pacmd;
use ArrayAccess; use ArrayAccess;
use ArrayIterator; use ArrayIterator;
@ -11,12 +12,15 @@ use Countable;
class SinkInputList implements ArrayAccess, IteratorAggregate, Countable class SinkInputList implements ArrayAccess, IteratorAggregate, Countable
{ {
protected $inputs = []; protected $inputs = [];
protected $pulse;
public function __construct() public function __construct(PulseAudio $pulse)
{ {
$this->pulse = $pulse;
$inputs = Pacmd::query("list-sink-inputs"); $inputs = Pacmd::query("list-sink-inputs");
foreach ($inputs as $index=>$input) { 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) public function offsetGet($key)
{ {
foreach ($this->inputs as $input) { foreach ($this->inputs as $input) {
if ($input->getIndex() == $key) { if (($input->getIndex() == $key) || ($input->getName()==$key)) {
return $input; return $input;
} }
} }

View File

@ -2,6 +2,7 @@
namespace NoccyLabs\PulseAudio\Sink; namespace NoccyLabs\PulseAudio\Sink;
use NoccyLabs\PulseAudio\PulseAudio;
use NoccyLabs\PulseAudio\Helper\Pacmd; use NoccyLabs\PulseAudio\Helper\Pacmd;
use ArrayAccess; use ArrayAccess;
use IteratorAggregate; use IteratorAggregate;
@ -12,19 +13,27 @@ class SinkList implements ArrayAccess, IteratorAggregate, Countable
{ {
protected $sinks = []; protected $sinks = [];
public function __construct() protected $pulse;
public function __construct(PulseAudio $pulse)
{ {
$this->pulse = $pulse;
$sinks = Pacmd::query("list-sinks"); $sinks = Pacmd::query("list-sinks");
foreach ($sinks as $index=>$sink) { 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) public function createRecorder($name)
{ {