[PATCH 10/10] ALSA: aloop: Use timer of linked card if chosen

twischer
2019-03-26T07:53:15Z

From: Timo Wischer <twischer@de.adit-jv.com>

If there is a hardware sound card linked to the loopback device
the sound timer of the hardware sound card will be used for this
loopback device. Such a link will be created when snd_pcm_link() was
called.
Linked dummy and loopback devices will be ignored.

This feature can be enabled when loading the module with the following
command:
$ modprobe snd_aloop enable=1 timer_source=-1

Signed-off-by: Timo Wischer <twischer@de.adit-jv.com>
---
sound/drivers/aloop.c | 187 ++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 172 insertions(+), 15 deletions(-)

diff --git a/sound/drivers/aloop.c b/sound/drivers/aloop.c
index a411aeb..e2f1d64 100644
--- a/sound/drivers/aloop.c
+++ b/sound/drivers/aloop.c
@@ -51,7 +51,8 @@ MODULE_LICENSE("GPL");
MODULE_SUPPORTED_DEVICE("{{ALSA,Loopback soundcard}}");

#define MAX_PCM_SUBSTREAMS 8
-#define TIMER_SRC_JIFFIES -1
+#define TIMER_SRC_JIFFIES -2
+#define TIMER_SRC_AUTODETECT -1

static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX; /* Index 0-MAX */
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR; /* ID for this card */
@@ -76,7 +77,7 @@ MODULE_PARM_DESC(pcm_notify, "Break capture when PCM format/rate/channels change
* sound timer is not supported.
*/
module_param_array(timer_source, charp, NULL, 0444);
-MODULE_PARM_DESC(timer_source, "Sound card number of timer to be used (>=0). -2 jiffies timer [default].");
+MODULE_PARM_DESC(timer_source, "Sound card number of timer to be used (>=0). -2 jiffies timer [default]. -1 auto detect sound timer.");

#define NO_PITCH 100000

@@ -252,6 +253,14 @@ static int loopback_snd_timer_start(struct loopback_pcm *dpcm)
{
int err;

+ /* only in auto detect mode the sound timer will not be opened when the
+ * device was opened. It will be opened on the first snd_pcm_link() call
+ */
+ if (!dpcm->cable->snd_timer.instance) {
+ pcm_err(dpcm->substream->pcm, "Sound timer not auto detected. At least one application of the same loopback cable has to call snd_pcm_link() to detect the sound timer.");
+ return -EINVAL;
+ }
+
/* Loopback device has to use same period as timer card. Therefore
* wake up for each snd_pcm_period_elapsed() call of timer card.
*/
@@ -286,6 +295,10 @@ static int loopback_snd_timer_stop(struct loopback_pcm *dpcm)
{
int err;

+ /* no need to stop the timer if it was not yet opened */
+ if (!dpcm->cable->snd_timer.instance)
+ return 0;
+
/* only stop if both devices (playback and capture) are not running */
if (dpcm->cable->running)
return 0;
@@ -307,10 +320,15 @@ static inline int loopback_jiffies_timer_stop_sync(struct loopback_pcm *dpcm)
}

/* call in loopback->cable_lock */
-static int loopback_snd_timer_close_cable(struct loopback_pcm *dpcm)
+static int loopback_snd_timer_close(struct loopback_pcm *dpcm,
+ struct snd_timer_instance * const timer)
{
int err;

+ /* no need to close the timer if it was not yet opened */
+ if (!timer)
+ return 0;
+
/* wait till drain tasklet has finished if requested */
tasklet_kill(&dpcm->cable->snd_timer.event_tasklet);

@@ -319,7 +337,7 @@ static int loopback_snd_timer_close_cable(struct loopback_pcm *dpcm)
* loopback->cable_lock is locked. Therefore no need to lock
* cable->lock;
*/
- err = snd_timer_close(dpcm->cable->snd_timer.instance);
+ err = snd_timer_close(timer);
if (err < 0) {
pcm_err(dpcm->substream->pcm, "snd_timer_close(card %d) = %d",
dpcm->cable->snd_timer.id.card, err);
@@ -329,6 +347,12 @@ static int loopback_snd_timer_close_cable(struct loopback_pcm *dpcm)
return err;
}

+/* call in loopback->cable_lock */
+static int loopback_snd_timer_close_cable(struct loopback_pcm *dpcm)
+{
+ return loopback_snd_timer_close(dpcm, dpcm->cable->snd_timer.instance);
+}
+
static int loopback_check_format(struct loopback_cable *cable, int stream)
{
struct snd_pcm_runtime *runtime, *cruntime;
@@ -1065,11 +1089,11 @@ static int loopback_snd_card_by_name(const char * const name)
return -EINVAL;
}

-/* call in loopback->cable_lock */
-static int loopback_snd_timer_open(struct loopback_pcm *dpcm)
+/* call in cable->lock */
+static int loopback_snd_timer_source_update(struct loopback_pcm *dpcm)
{
- int err = 0;
- unsigned long flags;
+ int changed = 0;
+ struct snd_pcm_substream *s;
struct snd_timer_id tid = {
.dev_class = SNDRV_TIMER_CLASS_PCM,
.dev_sclass = SNDRV_TIMER_SCLASS_APPLICATION,
@@ -1078,18 +1102,109 @@ static int loopback_snd_timer_open(struct loopback_pcm *dpcm)
.device = 0,
.subdevice = 0,
};
- struct snd_timer_instance *timer = NULL;

- spin_lock_irqsave(&dpcm->cable->lock, flags);
+ /* Enforce kernel module parameter if not auto detect */
+ if (tid.card != TIMER_SRC_AUTODETECT) {
+ dpcm->cable->snd_timer.owner = dpcm->substream->stream;
+ dpcm->cable->snd_timer.id = tid;
+ return 0;
+ }
+
+ /* find the first HW device which is linked to this loop device */
+ snd_pcm_group_for_each_entry(s, dpcm->substream) {
+ /* ignore all linked devices also using jiffies timer */
+ if (strcmp(s->pcm->card->driver, "Loopback") == 0)
+ continue;
+ if (strcmp(s->pcm->card->driver, "Dummy") == 0)
+ continue;
+
+ tid.card = s->pcm->card->number;
+ tid.device = s->pcm->device;
+ tid.subdevice = s->number;
+ break;
+ }
+
+ /* check if a sound timer could already be detected */
+ if (tid.card < 0)
+ return -ENODEV;
+
+ /* check if timer source has changed */
+ changed = (dpcm->cable->snd_timer.id.card != tid.card ||
+ dpcm->cable->snd_timer.id.device != tid.device ||
+ dpcm->cable->snd_timer.id.subdevice != tid.subdevice);
+ /* Do not change anything if we are the second device
+ * which calls snd_pcm_link()
+ */
+ if (changed) {
+ if (dpcm->cable->snd_timer.owner > 0 &&
+ dpcm->cable->snd_timer.owner != dpcm->substream->stream) {
+ pcm_warn(dpcm->substream->pcm, "Both devices of the same loopback cable are requesting different sound timers. May be the wrong one is used. (used hw:%d,%d,%d reqested hw:%d,%d,%d)",
+ dpcm->cable->snd_timer.id.card,
+ dpcm->cable->snd_timer.id.device,
+ dpcm->cable->snd_timer.id.subdevice, tid.card,
+ tid.device, tid.subdevice);
+ return 0;
+ } else if (dpcm->cable->running) {
+ pcm_warn(dpcm->substream->pcm, "Another sound timer was requested but at least one device is already running. May be the wrong one is used. (used hw:%d,%d,%d reqested hw:%d,%d,%d)",
+ dpcm->cable->snd_timer.id.card,
+ dpcm->cable->snd_timer.id.device,
+ dpcm->cable->snd_timer.id.subdevice, tid.card,
+ tid.device, tid.subdevice);
+ return 0;
+ }
+ }
+
+ /* always return a valid timer id with defined classes. Also in case
+ * when it looks like card has not changed because hw:0,0,0 should be
+ * used
+ */
dpcm->cable->snd_timer.owner = dpcm->substream->stream;
dpcm->cable->snd_timer.id = tid;

- /* check if timer was already opened. It is only opened once
- * per playback and capture subdevice (aka cable).
+ return changed;
+}
+
+/* call in loopback->cable_lock */
+static int loopback_snd_timer_open(struct loopback_pcm *dpcm)
+{
+ int err = 0;
+ unsigned long flags;
+ struct snd_timer_instance *timer = NULL;
+
+ spin_lock_irqsave(&dpcm->cable->lock, flags);
+ err = loopback_snd_timer_source_update(dpcm);
+ if (err < 0) {
+ /* Could not yet detect the right sound timer because no valid
+ * snd_pcm_link() exists. This should not be handled as an error
+ * because the right timer will be opened with the call to
+ * snd_pcm_link().
+ */
+ if (err == -ENODEV)
+ err = 0;
+ goto unlock;
+ }
+ /* do not reopen the timer if it is already opened and nothing has
+ * changed
*/
- if (dpcm->cable->snd_timer.instance)
+ if (dpcm->cable->snd_timer.instance && err < 1)
goto unlock;

+ /* Avoids that any function accesses an invalid timer instance when
+ * reopening the sound timer. Reopening the sound timer is only
+ * supported in TIMER_SRC_AUTODETECT mode. snd_pcm_start() will fail on
+ * the second device when the first device currently reopens the_timer:
+ * [proc1] Calls snd_pcm_link() -> loopback_timer_open() ->
+ * Unlock cable->lock for snd_timer_close/open() call
+ * [proc2] Calls snd_pcm_start() when timer reopening is in progress
+ * But this is fine because snd_pcm_start() would also fail if
+ * snd_pcm_link() was not called from any device of the same cable in
+ * TIMER_SRC_AUTODETECT mode. Therefore the user has to guarantee in
+ * TIMER_SRC_AUTODETECT mode that snd_pcm_link() is called before anyone
+ * calls snd_pcm_start() of the same cable.
+ */
+ timer = dpcm->cable->snd_timer.instance;
+ dpcm->cable->snd_timer.instance = NULL;
+
/* snd_timer_close() and snd_timer_open() should not be called with
* locked spinlock because both functions can block on a mutex. The
* mutex loopback->cable_lock is kept locked. Therefore snd_timer_open()
@@ -1103,6 +1218,10 @@ static int loopback_snd_timer_open(struct loopback_pcm *dpcm)
* instance
*/
spin_unlock_irqrestore(&dpcm->cable->lock, flags);
+ /* close timer if there is already something open */
+ err = loopback_snd_timer_close(dpcm, timer);
+ if (err < 0)
+ return err;
err = snd_timer_open(&timer, dpcm->loopback->card->id,
&dpcm->cable->snd_timer.id,
current->pid);
@@ -1137,6 +1256,19 @@ static int loopback_snd_timer_open(struct loopback_pcm *dpcm)
return err;
}

+static int loopback_snd_timer_link_changed(struct snd_pcm_substream *substream)
+{
+ int err;
+ struct loopback_pcm *dpcm = substream->runtime->private_data;
+
+ mutex_lock(&dpcm->loopback->cable_lock);
+ /* Try to reopen the timer here if the link_list has changed. */
+ err = loopback_snd_timer_open(dpcm);
+ mutex_unlock(&dpcm->loopback->cable_lock);
+
+ return err;
+}
+
/* stop_sync() is not required for sound timer because it does not need to be
* restarted in loopback_prepare() on Xrun recovery
*/
@@ -1148,6 +1280,13 @@ static struct loopback_ops loopback_snd_timer_ops = {
.dpcm_info = loopback_snd_timer_dpcm_info,
};

+static struct loopback_ops loopback_snd_timer_auto_ops = {
+ .start = loopback_snd_timer_start,
+ .stop = loopback_snd_timer_stop,
+ .close_cable = loopback_snd_timer_close_cable,
+ .dpcm_info = loopback_snd_timer_dpcm_info,
+};
+
static int loopback_open(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
@@ -1178,6 +1317,8 @@ static int loopback_open(struct snd_pcm_substream *substream)
cable->hw = loopback_pcm_hardware;
if (loopback->timer_source <= TIMER_SRC_JIFFIES)
cable->ops = &loopback_jiffies_timer_ops;
+ else if (loopback->timer_source == TIMER_SRC_AUTODETECT)
+ cable->ops = &loopback_snd_timer_auto_ops;
else
cable->ops = &loopback_snd_timer_ops;
loopback->cables[substream->number][dev] = cable;
@@ -1276,18 +1417,34 @@ static const struct snd_pcm_ops loopback_pcm_ops = {
.page = snd_pcm_lib_get_vmalloc_page,
};

+static const struct snd_pcm_ops loopback_pcm_auto_ops = {
+ .open = loopback_open,
+ .close = loopback_close,
+ .ioctl = snd_pcm_lib_ioctl,
+ .hw_params = loopback_hw_params,
+ .hw_free = loopback_hw_free,
+ .prepare = loopback_prepare,
+ .trigger = loopback_trigger,
+ .pointer = loopback_pointer,
+ .page = snd_pcm_lib_get_vmalloc_page,
+ .link_changed = loopback_snd_timer_link_changed,
+};
+
static int loopback_pcm_new(struct loopback *loopback,
int device, int substreams)
{
struct snd_pcm *pcm;
int err;
+ const struct snd_pcm_ops * const ops =
+ (loopback->timer_source == TIMER_SRC_AUTODETECT) ?
+ &loopback_pcm_auto_ops : &loopback_pcm_ops;

err = snd_pcm_new(loopback->card, "Loopback PCM", device,
substreams, substreams, &pcm);
if (err < 0)
return err;
- snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &loopback_pcm_ops);
- snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &loopback_pcm_ops);
+ snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, ops);
+ snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, ops);

pcm->private_data = loopback;
pcm->info_flags = 0;
--
2.7.4