freebsd-dev/sys/dev/sound/pcm/sound.c
Alexander Leidinger b611c801f0 MFp4 the sound Google Summer of Code project:
The goal was to sync with the OSSv4 API 4Front Technologies uses in their
proprietary OSS driver. This was successful as far as possible. The part
of the API which is stable is implemented, for the rest there are some
stubs already.

New system ioctls:
 - SNDCTL_SYSINFO - obtain audio system info (version, # of audio/midi/
   mixer devices, etc.)
 - SNDCTL_AUDIOINFO - fetch details about a specific audio device
 - SNDCTL_MIXERINFO - fetch details about a specific mixer device

New audio ioctls:
 - Sync groups (SNDCTL_DSP_SYNCGROUP/SNDCTL_DSP_SYNCSTART) which allow
   triggered playback/recording on multiple devices (even across processes
   simultaneously).
 - Peak meters (SNDCTL_DSP_GETIPEAKS/SNDCTL_DSP_GETOPEAKS) - can query
   audio drivers for peak levels (needs driver support, disabled for now).
 - Per channel playback/recording levels -
   SNDCTL_DSP_{GET,SET}{PLAY,REC}VOL.  Note that these are still in name
   only, just wrapping around the AC97-style mixer at the moment. The next
   step is to push them down to the drivers.

Audio ioctls still under development by 4Front (for which stubs may exist
in this commit):
 - SNDCTL_GETNAME, SNDCTL_{GET,SET}{SONG,LABEL}
 - SNDCTL_DSP_{GET,SET}_CHNORDER
 - SNDCTL_MIX_ENUMINFO, SNDCTL_MIX_EXTINFO - (might be documented enough in
   the OSS releases to work on this.  These ioctls cover the cool "twiddle
   any knob on your card" features.)

Missing:
 - SNDCTL_DSP_COOKEDMODE -- this ioctl is used to give applications direct
   access to a card's buffers, bypassing the feeder architecture.  It's
   a toughy -- "someone" needs to decide :
   (a) if this is desireable, and (b) if it's reasonably feasible.

Updates for driver writers:
 So far, only two routines to the channel class (in channel_if.m) are added.
 One is for fetching a list of discrete supported playback/recording rates
 of a channel, and the other is for fetching peak level info (useful for
 drawing peak meters).  Interested parties may want to help pushing down
 SNDCTL_DSP_{GET,SET}{PLAY,REC}VOL into the drivers.

To use the new stuff you need to rebuild the sound drivers or your kernel
(depending on if you use modules or not) and to install soundcard.h (a
buildworld/installworld handles this).

Sponsored by:	Google SoC 2006
Submitted by:	ryanb
Many thanks to:	4Front Technologies for their cooperation, explanations
		and the nice license of their soundcard.h.
2006-09-23 20:45:47 +00:00

1277 lines
29 KiB
C

/*-
* Copyright (c) 1999 Cameron Grant <cg@freebsd.org>
* (C) 1997 Luigi Rizzo (luigi@iet.unipi.it)
* 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/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);
struct sysctl_ctx_list *
snd_sysctl_tree(device_t dev)
{
struct snddev_info *d = device_get_softc(dev);
return &d->sysctl_tree;
}
struct sysctl_oid *
snd_sysctl_tree_top(device_t dev)
{
struct snddev_info *d = device_get_softc(dev);
return d->sysctl_tree_top;
}
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);
if (m == NULL)
return NULL;
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, 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->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->dspr_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", "");
#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", "");
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);
if (!ch)
return NULL;
ch->methods = kobj_create(cls, M_DEVBUF, M_WAITOK);
if (!ch->methods) {
free(ch, M_DEVBUF);
return NULL;
}
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;
/*
* 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);
if (!sce) {
return ENOMEM;
}
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
namelen = strlen(ch->name);
if ((CHN_NAMELEN - namelen) > 10) { /* ":dspXX.YYY" */
snprintf(ch->name + namelen,
CHN_NAMELEN - namelen, ":dsp%d.%d",
device, sce->chan_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);
if (ch->direction == PCMDIR_REC)
sce->dspr_devt = make_dev(&dsp_cdevsw,
PCMMKMINOR(device, SND_DEV_DSPREC,
sce->chan_num), UID_ROOT, GID_WHEEL,
0666, "dspr%d.%d", device, sce->chan_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);
strncpy(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
sysctl_ctx_init(&d->sysctl_tree);
d->sysctl_tree_top = SYSCTL_ADD_NODE(&d->sysctl_tree,
SYSCTL_STATIC_CHILDREN(_hw_snd), OID_AUTO,
device_get_nameunit(dev), CTLFLAG_RD, 0, "");
if (d->sysctl_tree_top == NULL) {
sysctl_ctx_free(&d->sysctl_tree);
goto no;
}
/* 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(snd_sysctl_tree(dev), SYSCTL_CHILDREN(snd_sysctl_tree_top(dev)),
OID_AUTO, "_buffersize", CTLFLAG_RD, &d->bufsz, 0, "");
#endif
if (numplay > 0) {
d->flags |= SD_F_AUTOVCHAN;
vchan_initsys(dev);
}
sndstat_register(dev, d->status, sndstat_prepare_pcm);
return 0;
no:
snd_mtxfree(d->lock);
return ENXIO;
}
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->dspr_devt) {
destroy_dev(sce->dspr_devt);
sce->dspr_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
d->sysctl_tree_top = NULL;
sysctl_ctx_free(&d->sysctl_tree);
#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, hfree %d, sfree %d [b:%d/%d/%d|bs:%d/%d/%d]",
c->xruns, 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, ready %d [b:%d/%d/%d|bs:%d/%d/%d]",
c->xruns, 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);