1260 lines
28 KiB
C
1260 lines
28 KiB
C
/*-
|
|
* Copyright (c) 1999 Cameron Grant <cg@freebsd.org>
|
|
* (C) 1997 Luigi Rizzo
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
#include <dev/sound/pcm/sound.h>
|
|
#include <dev/sound/pcm/ac97.h>
|
|
#include <dev/sound/pcm/vchan.h>
|
|
#include <dev/sound/pcm/dsp.h>
|
|
#include <sys/limits.h>
|
|
#include <sys/sysctl.h>
|
|
|
|
#include "feeder_if.h"
|
|
|
|
SND_DECLARE_FILE("$FreeBSD$");
|
|
|
|
devclass_t pcm_devclass;
|
|
|
|
int pcm_veto_load = 1;
|
|
|
|
#ifdef USING_DEVFS
|
|
int snd_unit = 0;
|
|
TUNABLE_INT("hw.snd.default_unit", &snd_unit);
|
|
#endif
|
|
|
|
int snd_maxautovchans = 4;
|
|
/* XXX: a tunable implies that we may need more than one sound channel before
|
|
the system can change a sysctl (/etc/sysctl.conf), do we really need
|
|
this? */
|
|
TUNABLE_INT("hw.snd.maxautovchans", &snd_maxautovchans);
|
|
|
|
SYSCTL_NODE(_hw, OID_AUTO, snd, CTLFLAG_RD, 0, "Sound driver");
|
|
|
|
/**
|
|
* @brief Unit number allocator for syncgroup IDs
|
|
*/
|
|
struct unrhdr *pcmsg_unrhdr = NULL;
|
|
|
|
static int sndstat_prepare_pcm(struct sbuf *s, device_t dev, int verbose);
|
|
|
|
void *
|
|
snd_mtxcreate(const char *desc, const char *type)
|
|
{
|
|
#ifdef USING_MUTEX
|
|
struct mtx *m;
|
|
|
|
m = malloc(sizeof(*m), M_DEVBUF, M_WAITOK | M_ZERO);
|
|
mtx_init(m, desc, type, MTX_DEF);
|
|
return m;
|
|
#else
|
|
return (void *)0xcafebabe;
|
|
#endif
|
|
}
|
|
|
|
void
|
|
snd_mtxfree(void *m)
|
|
{
|
|
#ifdef USING_MUTEX
|
|
struct mtx *mtx = m;
|
|
|
|
/* mtx_assert(mtx, MA_OWNED); */
|
|
mtx_destroy(mtx);
|
|
free(mtx, M_DEVBUF);
|
|
#endif
|
|
}
|
|
|
|
void
|
|
snd_mtxassert(void *m)
|
|
{
|
|
#ifdef USING_MUTEX
|
|
#ifdef INVARIANTS
|
|
struct mtx *mtx = m;
|
|
|
|
mtx_assert(mtx, MA_OWNED);
|
|
#endif
|
|
#endif
|
|
}
|
|
/*
|
|
void
|
|
snd_mtxlock(void *m)
|
|
{
|
|
#ifdef USING_MUTEX
|
|
struct mtx *mtx = m;
|
|
|
|
mtx_lock(mtx);
|
|
#endif
|
|
}
|
|
|
|
void
|
|
snd_mtxunlock(void *m)
|
|
{
|
|
#ifdef USING_MUTEX
|
|
struct mtx *mtx = m;
|
|
|
|
mtx_unlock(mtx);
|
|
#endif
|
|
}
|
|
*/
|
|
int
|
|
snd_setup_intr(device_t dev, struct resource *res, int flags, driver_intr_t hand, void *param, void **cookiep)
|
|
{
|
|
#ifdef USING_MUTEX
|
|
flags &= INTR_MPSAFE;
|
|
flags |= INTR_TYPE_AV;
|
|
#else
|
|
flags = INTR_TYPE_AV;
|
|
#endif
|
|
return bus_setup_intr(dev, res, flags,
|
|
#if __FreeBSD_version >= 700031
|
|
NULL,
|
|
#endif
|
|
hand, param, cookiep);
|
|
}
|
|
|
|
#ifndef PCM_DEBUG_MTX
|
|
void
|
|
pcm_lock(struct snddev_info *d)
|
|
{
|
|
snd_mtxlock(d->lock);
|
|
}
|
|
|
|
void
|
|
pcm_unlock(struct snddev_info *d)
|
|
{
|
|
snd_mtxunlock(d->lock);
|
|
}
|
|
#endif
|
|
|
|
struct pcm_channel *
|
|
pcm_getfakechan(struct snddev_info *d)
|
|
{
|
|
return d->fakechan;
|
|
}
|
|
|
|
static int
|
|
pcm_setvchans(struct snddev_info *d, int newcnt)
|
|
{
|
|
struct snddev_channel *sce = NULL;
|
|
struct pcm_channel *c = NULL;
|
|
int err = 0, vcnt, dcnt, i;
|
|
|
|
pcm_inprog(d, 1);
|
|
|
|
if (d->playcount < 1) {
|
|
err = ENODEV;
|
|
goto setvchans_out;
|
|
}
|
|
|
|
if (!(d->flags & SD_F_AUTOVCHAN)) {
|
|
err = EINVAL;
|
|
goto setvchans_out;
|
|
}
|
|
|
|
vcnt = d->vchancount;
|
|
dcnt = d->playcount + d->reccount;
|
|
|
|
if (newcnt < 0 || (dcnt + newcnt) > (PCMMAXCHAN + 1)) {
|
|
err = E2BIG;
|
|
goto setvchans_out;
|
|
}
|
|
|
|
dcnt += vcnt;
|
|
|
|
if (newcnt > vcnt) {
|
|
/* add new vchans - find a parent channel first */
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
CHN_LOCK(c);
|
|
if (c->direction == PCMDIR_PLAY &&
|
|
((c->flags & CHN_F_HAS_VCHAN) ||
|
|
(vcnt == 0 &&
|
|
!(c->flags & (CHN_F_BUSY | CHN_F_VIRTUAL)))))
|
|
goto addok;
|
|
CHN_UNLOCK(c);
|
|
}
|
|
err = EBUSY;
|
|
goto setvchans_out;
|
|
addok:
|
|
c->flags |= CHN_F_BUSY;
|
|
while (err == 0 && newcnt > vcnt) {
|
|
if (dcnt > PCMMAXCHAN) {
|
|
device_printf(d->dev, "%s: Maximum channel reached.\n", __func__);
|
|
break;
|
|
}
|
|
err = vchan_create(c);
|
|
if (err == 0) {
|
|
vcnt++;
|
|
dcnt++;
|
|
} else if (err == E2BIG && newcnt > vcnt)
|
|
device_printf(d->dev, "%s: err=%d Maximum channel reached.\n", __func__, err);
|
|
}
|
|
if (vcnt == 0)
|
|
c->flags &= ~CHN_F_BUSY;
|
|
CHN_UNLOCK(c);
|
|
} else if (newcnt < vcnt) {
|
|
#define ORPHAN_CDEVT(cdevt) \
|
|
((cdevt) == NULL || ((cdevt)->si_drv1 == NULL && \
|
|
(cdevt)->si_drv2 == NULL))
|
|
while (err == 0 && newcnt < vcnt) {
|
|
i = 0;
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
CHN_LOCK(c);
|
|
if (c->direction == PCMDIR_PLAY &&
|
|
(c->flags & CHN_F_VIRTUAL) &&
|
|
(i++ == newcnt)) {
|
|
if (!(c->flags & CHN_F_BUSY) &&
|
|
ORPHAN_CDEVT(sce->dsp_devt) &&
|
|
ORPHAN_CDEVT(sce->dspW_devt) &&
|
|
ORPHAN_CDEVT(sce->audio_devt) &&
|
|
ORPHAN_CDEVT(sce->dspHW_devt))
|
|
goto remok;
|
|
/*
|
|
* Either we're busy, or our cdev
|
|
* has been stolen by dsp_clone().
|
|
* Skip, and increase newcnt.
|
|
*/
|
|
if (!(c->flags & CHN_F_BUSY))
|
|
device_printf(d->dev,
|
|
"%s: <%s> somebody steal my cdev!\n",
|
|
__func__, c->name);
|
|
newcnt++;
|
|
}
|
|
CHN_UNLOCK(c);
|
|
}
|
|
if (vcnt != newcnt)
|
|
err = EBUSY;
|
|
break;
|
|
remok:
|
|
CHN_UNLOCK(c);
|
|
err = vchan_destroy(c);
|
|
if (err == 0)
|
|
vcnt--;
|
|
else
|
|
device_printf(d->dev,
|
|
"%s: WARNING: vchan_destroy() failed!",
|
|
__func__);
|
|
}
|
|
}
|
|
|
|
setvchans_out:
|
|
pcm_inprog(d, -1);
|
|
return err;
|
|
}
|
|
|
|
/* return error status and a locked channel */
|
|
int
|
|
pcm_chnalloc(struct snddev_info *d, struct pcm_channel **ch, int direction,
|
|
pid_t pid, int chnum)
|
|
{
|
|
struct pcm_channel *c;
|
|
struct snddev_channel *sce;
|
|
int err;
|
|
|
|
retry_chnalloc:
|
|
err = ENODEV;
|
|
/* scan for a free channel */
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
CHN_LOCK(c);
|
|
if (c->direction == direction && !(c->flags & CHN_F_BUSY)) {
|
|
if (chnum < 0 || sce->chan_num == chnum) {
|
|
c->flags |= CHN_F_BUSY;
|
|
c->pid = pid;
|
|
*ch = c;
|
|
return 0;
|
|
}
|
|
}
|
|
if (sce->chan_num == chnum) {
|
|
if (c->direction != direction)
|
|
err = EOPNOTSUPP;
|
|
else if (c->flags & CHN_F_BUSY)
|
|
err = EBUSY;
|
|
else
|
|
err = EINVAL;
|
|
CHN_UNLOCK(c);
|
|
return err;
|
|
} else if (c->direction == direction && (c->flags & CHN_F_BUSY))
|
|
err = EBUSY;
|
|
CHN_UNLOCK(c);
|
|
}
|
|
|
|
/* no channel available */
|
|
if (chnum == -1 && direction == PCMDIR_PLAY && d->vchancount > 0 &&
|
|
d->vchancount < snd_maxautovchans &&
|
|
d->devcount <= PCMMAXCHAN) {
|
|
err = pcm_setvchans(d, d->vchancount + 1);
|
|
if (err == 0) {
|
|
chnum = -2;
|
|
goto retry_chnalloc;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/* release a locked channel and unlock it */
|
|
int
|
|
pcm_chnrelease(struct pcm_channel *c)
|
|
{
|
|
CHN_LOCKASSERT(c);
|
|
c->flags &= ~CHN_F_BUSY;
|
|
c->pid = -1;
|
|
CHN_UNLOCK(c);
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
pcm_chnref(struct pcm_channel *c, int ref)
|
|
{
|
|
int r;
|
|
|
|
CHN_LOCKASSERT(c);
|
|
c->refcount += ref;
|
|
r = c->refcount;
|
|
return r;
|
|
}
|
|
|
|
int
|
|
pcm_inprog(struct snddev_info *d, int delta)
|
|
{
|
|
int r;
|
|
|
|
if (delta == 0)
|
|
return d->inprog;
|
|
|
|
/* backtrace(); */
|
|
pcm_lock(d);
|
|
d->inprog += delta;
|
|
r = d->inprog;
|
|
pcm_unlock(d);
|
|
return r;
|
|
}
|
|
|
|
static void
|
|
pcm_setmaxautovchans(struct snddev_info *d, int num)
|
|
{
|
|
if (num > 0 && d->vchancount == 0)
|
|
pcm_setvchans(d, 1);
|
|
else if (num == 0 && d->vchancount > 0)
|
|
pcm_setvchans(d, 0);
|
|
}
|
|
|
|
#ifdef USING_DEVFS
|
|
static int
|
|
sysctl_hw_snd_default_unit(SYSCTL_HANDLER_ARGS)
|
|
{
|
|
struct snddev_info *d;
|
|
int error, unit;
|
|
|
|
unit = snd_unit;
|
|
error = sysctl_handle_int(oidp, &unit, sizeof(unit), req);
|
|
if (error == 0 && req->newptr != NULL) {
|
|
if (unit < 0 || unit >= devclass_get_maxunit(pcm_devclass))
|
|
return EINVAL;
|
|
d = devclass_get_softc(pcm_devclass, unit);
|
|
if (d == NULL || SLIST_EMPTY(&d->channels))
|
|
return EINVAL;
|
|
snd_unit = unit;
|
|
}
|
|
return (error);
|
|
}
|
|
/* XXX: do we need a way to let the user change the default unit? */
|
|
SYSCTL_PROC(_hw_snd, OID_AUTO, default_unit, CTLTYPE_INT | CTLFLAG_RW,
|
|
0, sizeof(int), sysctl_hw_snd_default_unit, "I", "default sound device");
|
|
#endif
|
|
|
|
static int
|
|
sysctl_hw_snd_maxautovchans(SYSCTL_HANDLER_ARGS)
|
|
{
|
|
struct snddev_info *d;
|
|
int i, v, error;
|
|
|
|
v = snd_maxautovchans;
|
|
error = sysctl_handle_int(oidp, &v, sizeof(v), req);
|
|
if (error == 0 && req->newptr != NULL) {
|
|
if (v < 0 || v > PCMMAXCHAN)
|
|
return E2BIG;
|
|
if (pcm_devclass != NULL && v != snd_maxautovchans) {
|
|
for (i = 0; i < devclass_get_maxunit(pcm_devclass); i++) {
|
|
d = devclass_get_softc(pcm_devclass, i);
|
|
if (!d)
|
|
continue;
|
|
pcm_setmaxautovchans(d, v);
|
|
}
|
|
}
|
|
snd_maxautovchans = v;
|
|
}
|
|
return (error);
|
|
}
|
|
SYSCTL_PROC(_hw_snd, OID_AUTO, maxautovchans, CTLTYPE_INT | CTLFLAG_RW,
|
|
0, sizeof(int), sysctl_hw_snd_maxautovchans, "I", "maximum virtual channel");
|
|
|
|
struct pcm_channel *
|
|
pcm_chn_create(struct snddev_info *d, struct pcm_channel *parent, kobj_class_t cls, int dir, void *devinfo)
|
|
{
|
|
struct snddev_channel *sce;
|
|
struct pcm_channel *ch, *c;
|
|
char *dirs;
|
|
uint32_t flsearch = 0;
|
|
int direction, err, rpnum, *pnum;
|
|
|
|
switch(dir) {
|
|
case PCMDIR_PLAY:
|
|
dirs = "play";
|
|
direction = PCMDIR_PLAY;
|
|
pnum = &d->playcount;
|
|
break;
|
|
|
|
case PCMDIR_REC:
|
|
dirs = "record";
|
|
direction = PCMDIR_REC;
|
|
pnum = &d->reccount;
|
|
break;
|
|
|
|
case PCMDIR_VIRTUAL:
|
|
dirs = "virtual";
|
|
direction = PCMDIR_PLAY;
|
|
pnum = &d->vchancount;
|
|
flsearch = CHN_F_VIRTUAL;
|
|
break;
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
|
|
ch = malloc(sizeof(*ch), M_DEVBUF, M_WAITOK | M_ZERO);
|
|
ch->methods = kobj_create(cls, M_DEVBUF, M_WAITOK);
|
|
|
|
snd_mtxlock(d->lock);
|
|
ch->num = 0;
|
|
rpnum = 0;
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
if (direction != c->direction ||
|
|
(c->flags & CHN_F_VIRTUAL) != flsearch)
|
|
continue;
|
|
if (ch->num == c->num)
|
|
ch->num++;
|
|
else {
|
|
#if 0
|
|
device_printf(d->dev,
|
|
"%s: %s channel numbering screwed (Expect: %d, Got: %d)\n",
|
|
__func__, dirs, ch->num, c->num);
|
|
#endif
|
|
goto retry_num_search;
|
|
}
|
|
rpnum++;
|
|
}
|
|
goto retry_num_search_out;
|
|
retry_num_search:
|
|
rpnum = 0;
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
if (direction != c->direction ||
|
|
(c->flags & CHN_F_VIRTUAL) != flsearch)
|
|
continue;
|
|
if (ch->num == c->num) {
|
|
ch->num++;
|
|
goto retry_num_search;
|
|
}
|
|
rpnum++;
|
|
}
|
|
retry_num_search_out:
|
|
if (*pnum != rpnum) {
|
|
device_printf(d->dev,
|
|
"%s: WARNING: pnum screwed : dirs=%s, pnum=%d, rpnum=%d\n",
|
|
__func__, dirs, *pnum, rpnum);
|
|
*pnum = rpnum;
|
|
}
|
|
(*pnum)++;
|
|
snd_mtxunlock(d->lock);
|
|
|
|
ch->pid = -1;
|
|
ch->parentsnddev = d;
|
|
ch->parentchannel = parent;
|
|
ch->dev = d->dev;
|
|
snprintf(ch->name, CHN_NAMELEN, "%s:%s:%d", device_get_nameunit(ch->dev), dirs, ch->num);
|
|
|
|
err = chn_init(ch, devinfo, dir, direction);
|
|
if (err) {
|
|
device_printf(d->dev, "chn_init(%s) failed: err = %d\n", ch->name, err);
|
|
kobj_delete(ch->methods, M_DEVBUF);
|
|
free(ch, M_DEVBUF);
|
|
snd_mtxlock(d->lock);
|
|
(*pnum)--;
|
|
snd_mtxunlock(d->lock);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
return ch;
|
|
}
|
|
|
|
int
|
|
pcm_chn_destroy(struct pcm_channel *ch)
|
|
{
|
|
struct snddev_info *d;
|
|
int err;
|
|
|
|
d = ch->parentsnddev;
|
|
err = chn_kill(ch);
|
|
if (err) {
|
|
device_printf(d->dev, "chn_kill(%s) failed, err = %d\n", ch->name, err);
|
|
return err;
|
|
}
|
|
|
|
kobj_delete(ch->methods, M_DEVBUF);
|
|
free(ch, M_DEVBUF);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
pcm_chn_add(struct snddev_info *d, struct pcm_channel *ch)
|
|
{
|
|
struct snddev_channel *sce, *tmp, *after;
|
|
unsigned rdevcount;
|
|
int device = device_get_unit(d->dev);
|
|
size_t namelen;
|
|
char dtype;
|
|
|
|
/*
|
|
* Note it's confusing nomenclature.
|
|
* dev_t
|
|
* device -> pcm_device
|
|
* unit -> pcm_channel
|
|
* channel -> snddev_channel
|
|
* device_t
|
|
* unit -> pcm_device
|
|
*/
|
|
|
|
sce = malloc(sizeof(*sce), M_DEVBUF, M_WAITOK | M_ZERO);
|
|
|
|
snd_mtxlock(d->lock);
|
|
sce->channel = ch;
|
|
sce->chan_num = 0;
|
|
rdevcount = 0;
|
|
after = NULL;
|
|
SLIST_FOREACH(tmp, &d->channels, link) {
|
|
if (sce->chan_num == tmp->chan_num)
|
|
sce->chan_num++;
|
|
else {
|
|
#if 0
|
|
device_printf(d->dev,
|
|
"%s: cdev numbering screwed (Expect: %d, Got: %d)\n",
|
|
__func__, sce->chan_num, tmp->chan_num);
|
|
#endif
|
|
goto retry_chan_num_search;
|
|
}
|
|
after = tmp;
|
|
rdevcount++;
|
|
}
|
|
goto retry_chan_num_search_out;
|
|
retry_chan_num_search:
|
|
/*
|
|
* Look for possible channel numbering collision. This may not
|
|
* be optimized, but it will ensure that no collision occured.
|
|
* Can be considered cheap since none of the locking/unlocking
|
|
* operations involved.
|
|
*/
|
|
rdevcount = 0;
|
|
after = NULL;
|
|
SLIST_FOREACH(tmp, &d->channels, link) {
|
|
if (sce->chan_num == tmp->chan_num) {
|
|
sce->chan_num++;
|
|
goto retry_chan_num_search;
|
|
}
|
|
if (sce->chan_num > tmp->chan_num)
|
|
after = tmp;
|
|
rdevcount++;
|
|
}
|
|
retry_chan_num_search_out:
|
|
/*
|
|
* Don't overflow PCMMKMINOR / PCMMAXCHAN.
|
|
*/
|
|
if (sce->chan_num > PCMMAXCHAN) {
|
|
snd_mtxunlock(d->lock);
|
|
device_printf(d->dev,
|
|
"%s: WARNING: sce->chan_num overflow! (%d)\n",
|
|
__func__, sce->chan_num);
|
|
free(sce, M_DEVBUF);
|
|
return E2BIG;
|
|
}
|
|
if (d->devcount != rdevcount) {
|
|
device_printf(d->dev,
|
|
"%s: WARNING: devcount screwed! d->devcount=%u, rdevcount=%u\n",
|
|
__func__, d->devcount, rdevcount);
|
|
d->devcount = rdevcount;
|
|
}
|
|
d->devcount++;
|
|
if (after == NULL) {
|
|
SLIST_INSERT_HEAD(&d->channels, sce, link);
|
|
} else {
|
|
SLIST_INSERT_AFTER(after, sce, link);
|
|
}
|
|
#if 0
|
|
if (1) {
|
|
int cnum = 0;
|
|
SLIST_FOREACH(tmp, &d->channels, link) {
|
|
if (cnum != tmp->chan_num)
|
|
device_printf(d->dev,
|
|
"%s: WARNING: inconsistent cdev numbering! (Expect: %d, Got: %d)\n",
|
|
__func__, cnum, tmp->chan_num);
|
|
cnum++;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (ch->flags & CHN_F_VIRTUAL)
|
|
dtype = 'v';
|
|
else if (ch->direction == PCMDIR_PLAY)
|
|
dtype = 'p';
|
|
else if (ch->direction == PCMDIR_REC)
|
|
dtype = 'r';
|
|
else
|
|
dtype = 'u'; /* we're screwed */
|
|
|
|
namelen = strlen(ch->name);
|
|
if ((CHN_NAMELEN - namelen) > 11) { /* ":dspXX.TYYY" */
|
|
snprintf(ch->name + namelen,
|
|
CHN_NAMELEN - namelen, ":dsp%d.%c%d",
|
|
device, dtype, ch->num);
|
|
}
|
|
snd_mtxunlock(d->lock);
|
|
|
|
/*
|
|
* I will revisit these someday, and nuke it mercilessly..
|
|
*/
|
|
sce->dsp_devt = make_dev(&dsp_cdevsw,
|
|
PCMMKMINOR(device, SND_DEV_DSP, sce->chan_num),
|
|
UID_ROOT, GID_WHEEL, 0666, "dsp%d.%d",
|
|
device, sce->chan_num);
|
|
|
|
sce->dspW_devt = make_dev(&dsp_cdevsw,
|
|
PCMMKMINOR(device, SND_DEV_DSP16, sce->chan_num),
|
|
UID_ROOT, GID_WHEEL, 0666, "dspW%d.%d",
|
|
device, sce->chan_num);
|
|
|
|
sce->audio_devt = make_dev(&dsp_cdevsw,
|
|
PCMMKMINOR(device, SND_DEV_AUDIO, sce->chan_num),
|
|
UID_ROOT, GID_WHEEL, 0666, "audio%d.%d",
|
|
device, sce->chan_num);
|
|
|
|
/* Except this. */
|
|
sce->dspHW_devt = make_dev(&dsp_cdevsw,
|
|
PCMMKMINOR(device, SND_DEV_DSPHW, sce->chan_num),
|
|
UID_ROOT, GID_WHEEL, 0666, "dsp%d.%c%d",
|
|
device, dtype, ch->num);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
pcm_chn_remove(struct snddev_info *d, struct pcm_channel *ch)
|
|
{
|
|
struct snddev_channel *sce;
|
|
#if 0
|
|
int ourlock;
|
|
|
|
ourlock = 0;
|
|
if (!mtx_owned(d->lock)) {
|
|
snd_mtxlock(d->lock);
|
|
ourlock = 1;
|
|
}
|
|
#endif
|
|
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
if (sce->channel == ch)
|
|
goto gotit;
|
|
}
|
|
#if 0
|
|
if (ourlock)
|
|
snd_mtxunlock(d->lock);
|
|
#endif
|
|
return EINVAL;
|
|
gotit:
|
|
SLIST_REMOVE(&d->channels, sce, snddev_channel, link);
|
|
|
|
if (ch->flags & CHN_F_VIRTUAL)
|
|
d->vchancount--;
|
|
else if (ch->direction == PCMDIR_REC)
|
|
d->reccount--;
|
|
else
|
|
d->playcount--;
|
|
|
|
#if 0
|
|
if (ourlock)
|
|
snd_mtxunlock(d->lock);
|
|
#endif
|
|
free(sce, M_DEVBUF);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
pcm_addchan(device_t dev, int dir, kobj_class_t cls, void *devinfo)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
struct pcm_channel *ch;
|
|
int err;
|
|
|
|
ch = pcm_chn_create(d, NULL, cls, dir, devinfo);
|
|
if (!ch) {
|
|
device_printf(d->dev, "pcm_chn_create(%s, %d, %p) failed\n", cls->name, dir, devinfo);
|
|
return ENODEV;
|
|
}
|
|
|
|
err = pcm_chn_add(d, ch);
|
|
if (err) {
|
|
device_printf(d->dev, "pcm_chn_add(%s) failed, err=%d\n", ch->name, err);
|
|
pcm_chn_destroy(ch);
|
|
return err;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int
|
|
pcm_killchan(device_t dev)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
struct snddev_channel *sce;
|
|
struct pcm_channel *ch;
|
|
int error = 0;
|
|
|
|
sce = SLIST_FIRST(&d->channels);
|
|
ch = sce->channel;
|
|
|
|
error = pcm_chn_remove(d, sce->channel);
|
|
if (error)
|
|
return (error);
|
|
return (pcm_chn_destroy(ch));
|
|
}
|
|
|
|
int
|
|
pcm_setstatus(device_t dev, char *str)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
|
|
snd_mtxlock(d->lock);
|
|
strlcpy(d->status, str, SND_STATUSLEN);
|
|
snd_mtxunlock(d->lock);
|
|
if (snd_maxautovchans > 0)
|
|
pcm_setvchans(d, 1);
|
|
return 0;
|
|
}
|
|
|
|
uint32_t
|
|
pcm_getflags(device_t dev)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
|
|
return d->flags;
|
|
}
|
|
|
|
void
|
|
pcm_setflags(device_t dev, uint32_t val)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
|
|
d->flags = val;
|
|
}
|
|
|
|
void *
|
|
pcm_getdevinfo(device_t dev)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
|
|
return d->devinfo;
|
|
}
|
|
|
|
unsigned int
|
|
pcm_getbuffersize(device_t dev, unsigned int minbufsz, unsigned int deflt, unsigned int maxbufsz)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
int sz, x;
|
|
|
|
sz = 0;
|
|
if (resource_int_value(device_get_name(dev), device_get_unit(dev), "buffersize", &sz) == 0) {
|
|
x = sz;
|
|
RANGE(sz, minbufsz, maxbufsz);
|
|
if (x != sz)
|
|
device_printf(dev, "'buffersize=%d' hint is out of range (%d-%d), using %d\n", x, minbufsz, maxbufsz, sz);
|
|
x = minbufsz;
|
|
while (x < sz)
|
|
x <<= 1;
|
|
if (x > sz)
|
|
x >>= 1;
|
|
if (x != sz) {
|
|
device_printf(dev, "'buffersize=%d' hint is not a power of 2, using %d\n", sz, x);
|
|
sz = x;
|
|
}
|
|
} else {
|
|
sz = deflt;
|
|
}
|
|
|
|
d->bufsz = sz;
|
|
|
|
return sz;
|
|
}
|
|
|
|
int
|
|
pcm_register(device_t dev, void *devinfo, int numplay, int numrec)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
|
|
if (pcm_veto_load) {
|
|
device_printf(dev, "disabled due to an error while initialising: %d\n", pcm_veto_load);
|
|
|
|
return EINVAL;
|
|
}
|
|
|
|
d->lock = snd_mtxcreate(device_get_nameunit(dev), "sound cdev");
|
|
|
|
#if 0
|
|
/*
|
|
* d->flags should be cleared by the allocator of the softc.
|
|
* We cannot clear this field here because several devices set
|
|
* this flag before calling pcm_register().
|
|
*/
|
|
d->flags = 0;
|
|
#endif
|
|
d->dev = dev;
|
|
d->devinfo = devinfo;
|
|
d->devcount = 0;
|
|
d->reccount = 0;
|
|
d->playcount = 0;
|
|
d->vchancount = 0;
|
|
d->inprog = 0;
|
|
|
|
SLIST_INIT(&d->channels);
|
|
|
|
if ((numplay == 0 || numrec == 0) && numplay != numrec)
|
|
d->flags |= SD_F_SIMPLEX;
|
|
|
|
d->fakechan = fkchan_setup(dev);
|
|
chn_init(d->fakechan, NULL, 0, 0);
|
|
|
|
#ifdef SND_DYNSYSCTL
|
|
/* XXX: an user should be able to set this with a control tool, the
|
|
sysadmin then needs min+max sysctls for this */
|
|
SYSCTL_ADD_INT(device_get_sysctl_ctx(dev),
|
|
SYSCTL_CHILDREN(device_get_sysctl_tree(dev)),
|
|
OID_AUTO, "buffersize", CTLFLAG_RD, &d->bufsz, 0, "allocated buffer size");
|
|
#endif
|
|
if (numplay > 0) {
|
|
d->flags |= SD_F_AUTOVCHAN;
|
|
vchan_initsys(dev);
|
|
}
|
|
|
|
sndstat_register(dev, d->status, sndstat_prepare_pcm);
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
pcm_unregister(device_t dev)
|
|
{
|
|
struct snddev_info *d = device_get_softc(dev);
|
|
struct snddev_channel *sce;
|
|
struct pcmchan_children *pce;
|
|
struct pcm_channel *ch;
|
|
|
|
if (sndstat_acquire() != 0) {
|
|
device_printf(dev, "unregister: sndstat busy\n");
|
|
return EBUSY;
|
|
}
|
|
|
|
snd_mtxlock(d->lock);
|
|
if (d->inprog) {
|
|
device_printf(dev, "unregister: operation in progress\n");
|
|
snd_mtxunlock(d->lock);
|
|
sndstat_release();
|
|
return EBUSY;
|
|
}
|
|
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
ch = sce->channel;
|
|
if (ch->refcount > 0) {
|
|
device_printf(dev, "unregister: channel %s busy (pid %d)\n", ch->name, ch->pid);
|
|
snd_mtxunlock(d->lock);
|
|
sndstat_release();
|
|
return EBUSY;
|
|
}
|
|
}
|
|
|
|
if (mixer_uninit(dev) == EBUSY) {
|
|
device_printf(dev, "unregister: mixer busy\n");
|
|
snd_mtxunlock(d->lock);
|
|
sndstat_release();
|
|
return EBUSY;
|
|
}
|
|
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
if (sce->dsp_devt) {
|
|
destroy_dev(sce->dsp_devt);
|
|
sce->dsp_devt = NULL;
|
|
}
|
|
if (sce->dspW_devt) {
|
|
destroy_dev(sce->dspW_devt);
|
|
sce->dspW_devt = NULL;
|
|
}
|
|
if (sce->audio_devt) {
|
|
destroy_dev(sce->audio_devt);
|
|
sce->audio_devt = NULL;
|
|
}
|
|
if (sce->dspHW_devt) {
|
|
destroy_dev(sce->dspHW_devt);
|
|
sce->dspHW_devt = NULL;
|
|
}
|
|
d->devcount--;
|
|
ch = sce->channel;
|
|
if (ch == NULL)
|
|
continue;
|
|
pce = SLIST_FIRST(&ch->children);
|
|
while (pce != NULL) {
|
|
#if 0
|
|
device_printf(d->dev, "<%s> removing <%s>\n",
|
|
ch->name, (pce->channel != NULL) ?
|
|
pce->channel->name : "unknown");
|
|
#endif
|
|
SLIST_REMOVE(&ch->children, pce, pcmchan_children, link);
|
|
free(pce, M_DEVBUF);
|
|
pce = SLIST_FIRST(&ch->children);
|
|
}
|
|
}
|
|
|
|
#ifdef SND_DYNSYSCTL
|
|
#if 0
|
|
d->sysctl_tree_top = NULL;
|
|
sysctl_ctx_free(&d->sysctl_tree);
|
|
#endif
|
|
#endif
|
|
|
|
#if 0
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
ch = sce->channel;
|
|
if (ch == NULL)
|
|
continue;
|
|
if (!SLIST_EMPTY(&ch->children))
|
|
device_printf(d->dev, "%s: WARNING: <%s> dangling child!\n",
|
|
__func__, ch->name);
|
|
}
|
|
#endif
|
|
while (!SLIST_EMPTY(&d->channels))
|
|
pcm_killchan(dev);
|
|
|
|
chn_kill(d->fakechan);
|
|
fkchan_kill(d->fakechan);
|
|
|
|
#if 0
|
|
device_printf(d->dev, "%s: devcount=%u, playcount=%u, "
|
|
"reccount=%u, vchancount=%u\n",
|
|
__func__, d->devcount, d->playcount, d->reccount,
|
|
d->vchancount);
|
|
#endif
|
|
snd_mtxunlock(d->lock);
|
|
snd_mtxfree(d->lock);
|
|
sndstat_unregister(dev);
|
|
sndstat_release();
|
|
return 0;
|
|
}
|
|
|
|
/************************************************************************/
|
|
|
|
static int
|
|
sndstat_prepare_pcm(struct sbuf *s, device_t dev, int verbose)
|
|
{
|
|
struct snddev_info *d;
|
|
struct snddev_channel *sce;
|
|
struct pcm_channel *c;
|
|
struct pcm_feeder *f;
|
|
int pc, rc, vc;
|
|
|
|
if (verbose < 1)
|
|
return 0;
|
|
|
|
d = device_get_softc(dev);
|
|
if (!d)
|
|
return ENXIO;
|
|
|
|
snd_mtxlock(d->lock);
|
|
if (!SLIST_EMPTY(&d->channels)) {
|
|
pc = rc = vc = 0;
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
if (c->direction == PCMDIR_PLAY) {
|
|
if (c->flags & CHN_F_VIRTUAL)
|
|
vc++;
|
|
else
|
|
pc++;
|
|
} else
|
|
rc++;
|
|
}
|
|
sbuf_printf(s, " (%dp/%dr/%dv channels%s%s)", d->playcount, d->reccount, d->vchancount,
|
|
(d->flags & SD_F_SIMPLEX)? "" : " duplex",
|
|
#ifdef USING_DEVFS
|
|
(device_get_unit(dev) == snd_unit)? " default" : ""
|
|
#else
|
|
""
|
|
#endif
|
|
);
|
|
|
|
if (verbose <= 1) {
|
|
snd_mtxunlock(d->lock);
|
|
return 0;
|
|
}
|
|
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
|
|
KASSERT(c->bufhard != NULL && c->bufsoft != NULL,
|
|
("hosed pcm channel setup"));
|
|
|
|
sbuf_printf(s, "\n\t");
|
|
|
|
/* it would be better to indent child channels */
|
|
sbuf_printf(s, "%s[%s]: ", c->parentchannel? c->parentchannel->name : "", c->name);
|
|
sbuf_printf(s, "spd %d", c->speed);
|
|
if (c->speed != sndbuf_getspd(c->bufhard))
|
|
sbuf_printf(s, "/%d", sndbuf_getspd(c->bufhard));
|
|
sbuf_printf(s, ", fmt 0x%08x", c->format);
|
|
if (c->format != sndbuf_getfmt(c->bufhard))
|
|
sbuf_printf(s, "/0x%08x", sndbuf_getfmt(c->bufhard));
|
|
sbuf_printf(s, ", flags 0x%08x, 0x%08x", c->flags, c->feederflags);
|
|
if (c->pid != -1)
|
|
sbuf_printf(s, ", pid %d", c->pid);
|
|
sbuf_printf(s, "\n\t");
|
|
|
|
sbuf_printf(s, "interrupts %d, ", c->interrupts);
|
|
if (c->direction == PCMDIR_REC)
|
|
sbuf_printf(s, "overruns %d, feed %u, hfree %d, sfree %d [b:%d/%d/%d|bs:%d/%d/%d]",
|
|
c->xruns, c->feedcount, sndbuf_getfree(c->bufhard), sndbuf_getfree(c->bufsoft),
|
|
sndbuf_getsize(c->bufhard), sndbuf_getblksz(c->bufhard),
|
|
sndbuf_getblkcnt(c->bufhard),
|
|
sndbuf_getsize(c->bufsoft), sndbuf_getblksz(c->bufsoft),
|
|
sndbuf_getblkcnt(c->bufsoft));
|
|
else
|
|
sbuf_printf(s, "underruns %d, feed %u, ready %d [b:%d/%d/%d|bs:%d/%d/%d]",
|
|
c->xruns, c->feedcount, sndbuf_getready(c->bufsoft),
|
|
sndbuf_getsize(c->bufhard), sndbuf_getblksz(c->bufhard),
|
|
sndbuf_getblkcnt(c->bufhard),
|
|
sndbuf_getsize(c->bufsoft), sndbuf_getblksz(c->bufsoft),
|
|
sndbuf_getblkcnt(c->bufsoft));
|
|
sbuf_printf(s, "\n\t");
|
|
|
|
sbuf_printf(s, "{%s}", (c->direction == PCMDIR_REC)? "hardware" : "userland");
|
|
sbuf_printf(s, " -> ");
|
|
f = c->feeder;
|
|
while (f->source != NULL)
|
|
f = f->source;
|
|
while (f != NULL) {
|
|
sbuf_printf(s, "%s", f->class->name);
|
|
if (f->desc->type == FEEDER_FMT)
|
|
sbuf_printf(s, "(0x%08x -> 0x%08x)", f->desc->in, f->desc->out);
|
|
if (f->desc->type == FEEDER_RATE)
|
|
sbuf_printf(s, "(%d -> %d)", FEEDER_GET(f, FEEDRATE_SRC), FEEDER_GET(f, FEEDRATE_DST));
|
|
if (f->desc->type == FEEDER_ROOT || f->desc->type == FEEDER_MIXER ||
|
|
f->desc->type == FEEDER_VOLUME)
|
|
sbuf_printf(s, "(0x%08x)", f->desc->out);
|
|
sbuf_printf(s, " -> ");
|
|
f = f->parent;
|
|
}
|
|
sbuf_printf(s, "{%s}", (c->direction == PCMDIR_REC)? "userland" : "hardware");
|
|
}
|
|
} else
|
|
sbuf_printf(s, " (mixer only)");
|
|
snd_mtxunlock(d->lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/************************************************************************/
|
|
|
|
#ifdef SND_DYNSYSCTL
|
|
int
|
|
sysctl_hw_snd_vchans(SYSCTL_HANDLER_ARGS)
|
|
{
|
|
struct snddev_info *d;
|
|
int err, newcnt;
|
|
|
|
d = oidp->oid_arg1;
|
|
|
|
newcnt = d->vchancount;
|
|
err = sysctl_handle_int(oidp, &newcnt, sizeof(newcnt), req);
|
|
|
|
if (err == 0 && req->newptr != NULL && d->vchancount != newcnt)
|
|
err = pcm_setvchans(d, newcnt);
|
|
|
|
return err;
|
|
}
|
|
#endif
|
|
|
|
/************************************************************************/
|
|
|
|
/**
|
|
* @brief Handle OSSv4 SNDCTL_SYSINFO ioctl.
|
|
*
|
|
* @param si Pointer to oss_sysinfo struct where information about the
|
|
* sound subsystem will be written/copied.
|
|
*
|
|
* This routine returns information about the sound system, such as the
|
|
* current OSS version, number of audio, MIDI, and mixer drivers, etc.
|
|
* Also includes a bitmask showing which of the above types of devices
|
|
* are open (busy).
|
|
*
|
|
* @note
|
|
* Calling threads must not hold any snddev_info or pcm_channel locks.
|
|
*
|
|
* @author Ryan Beasley <ryanb@FreeBSD.org>
|
|
*/
|
|
void
|
|
sound_oss_sysinfo(oss_sysinfo *si)
|
|
{
|
|
static char si_product[] = "FreeBSD native OSS ABI";
|
|
static char si_version[] = __XSTRING(__FreeBSD_version);
|
|
static int intnbits = sizeof(int) * 8; /* Better suited as macro?
|
|
Must pester a C guru. */
|
|
|
|
struct snddev_channel *sce;
|
|
struct snddev_info *d;
|
|
struct pcm_channel *c;
|
|
int i, j, ncards;
|
|
|
|
ncards = 0;
|
|
|
|
strlcpy(si->product, si_product, sizeof(si->product));
|
|
strlcpy(si->version, si_version, sizeof(si->version));
|
|
si->versionnum = SOUND_VERSION;
|
|
|
|
/*
|
|
* Iterate over PCM devices and their channels, gathering up data
|
|
* for the numaudios, ncards, and openedaudio fields.
|
|
*/
|
|
si->numaudios = 0;
|
|
bzero((void *)&si->openedaudio, sizeof(si->openedaudio));
|
|
|
|
if (pcm_devclass != NULL) {
|
|
j = 0;
|
|
|
|
for (i = 0; i < devclass_get_maxunit(pcm_devclass); i++) {
|
|
d = devclass_get_softc(pcm_devclass, i);
|
|
if (!d)
|
|
continue;
|
|
|
|
/* See note in function's docblock */
|
|
mtx_assert(d->lock, MA_NOTOWNED);
|
|
/* Increment device's "operations in progress" */
|
|
pcm_inprog(d, 1);
|
|
pcm_lock(d);
|
|
|
|
si->numaudios += d->devcount;
|
|
++ncards;
|
|
|
|
SLIST_FOREACH(sce, &d->channels, link) {
|
|
c = sce->channel;
|
|
mtx_assert(c->lock, MA_NOTOWNED);
|
|
CHN_LOCK(c);
|
|
if (c->flags & CHN_F_BUSY)
|
|
si->openedaudio[j / intnbits] |=
|
|
(1 << (j % intnbits));
|
|
CHN_UNLOCK(c);
|
|
j++;
|
|
}
|
|
|
|
pcm_unlock(d);
|
|
pcm_inprog(d, -1);
|
|
}
|
|
}
|
|
|
|
si->numsynths = 0; /* OSSv4 docs: this field is obsolete */
|
|
/**
|
|
* @todo Collect num{midis,timers}.
|
|
*
|
|
* Need access to sound/midi/midi.c::midistat_lock in order
|
|
* to safely touch midi_devices and get a head count of, well,
|
|
* MIDI devices. midistat_lock is a global static (i.e., local to
|
|
* midi.c), but midi_devices is a regular global; should the mutex
|
|
* be publicized, or is there another way to get this information?
|
|
*
|
|
* NB: MIDI/sequencer stuff is currently on hold.
|
|
*/
|
|
si->nummidis = 0;
|
|
si->numtimers = 0;
|
|
si->nummixers = mixer_count;
|
|
si->numcards = ncards;
|
|
/* OSSv4 docs: Intended only for test apps; API doesn't
|
|
really have much of a concept of cards. Shouldn't be
|
|
used by applications. */
|
|
|
|
/**
|
|
* @todo Fill in "busy devices" fields.
|
|
*
|
|
* si->openedmidi = " MIDI devices
|
|
*/
|
|
bzero((void *)&si->openedmidi, sizeof(si->openedmidi));
|
|
|
|
/*
|
|
* Si->filler is a reserved array, but according to docs each
|
|
* element should be set to -1.
|
|
*/
|
|
for (i = 0; i < sizeof(si->filler)/sizeof(si->filler[0]); i++)
|
|
si->filler[i] = -1;
|
|
}
|
|
|
|
/************************************************************************/
|
|
|
|
static int
|
|
sound_modevent(module_t mod, int type, void *data)
|
|
{
|
|
int ret;
|
|
#if 0
|
|
return (midi_modevent(mod, type, data));
|
|
#else
|
|
ret = 0;
|
|
|
|
switch(type) {
|
|
case MOD_LOAD:
|
|
pcmsg_unrhdr = new_unrhdr(1, INT_MAX, NULL);
|
|
break;
|
|
case MOD_UNLOAD:
|
|
case MOD_SHUTDOWN:
|
|
if (pcmsg_unrhdr != NULL) {
|
|
delete_unrhdr(pcmsg_unrhdr);
|
|
pcmsg_unrhdr = NULL;
|
|
}
|
|
break;
|
|
default:
|
|
ret = EOPNOTSUPP;
|
|
}
|
|
|
|
return ret;
|
|
#endif
|
|
}
|
|
|
|
DEV_MODULE(sound, sound_modevent, NULL);
|
|
MODULE_VERSION(sound, SOUND_MODVER);
|