/* * Copyright (c) 1999 Cameron Grant <cg@freebsd.org> * Portions Copyright by Luigi Rizzo - 1997-99 * 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 "feeder_if.h" SND_DECLARE_FILE("$FreeBSD: src/sys/dev/sound/pcm/channel.c,v 1.97 2004/02/28 19:47:02 truckman Exp $"); #define MIN_CHUNK_SIZE 256 /* for uiomove etc. */ #define DMA_ALIGN_THRESHOLD 4 #define DMA_ALIGN_MASK (~(DMA_ALIGN_THRESHOLD - 1)) #define CANCHANGE(c) (!(c->flags & CHN_F_TRIGGERED)) /* #define DEB(x) x */ static int chn_targetirqrate = 32; TUNABLE_INT("hw.snd.targetirqrate", &chn_targetirqrate); static int sysctl_hw_snd_targetirqrate(SYSCTL_HANDLER_ARGS) { int err, val; val = chn_targetirqrate; err = sysctl_handle_int(oidp, &val, sizeof(val), req); if (val < 16 || val > 512) err = EINVAL; else chn_targetirqrate = val; return err; } SYSCTL_PROC(_hw_snd, OID_AUTO, targetirqrate, CTLTYPE_INT | CTLFLAG_RW, 0, sizeof(int), sysctl_hw_snd_targetirqrate, "I", ""); static int report_soft_formats = 1; SYSCTL_INT(_hw_snd, OID_AUTO, report_soft_formats, CTLFLAG_RW, &report_soft_formats, 1, "report software-emulated formats"); static int chn_buildfeeder(struct pcm_channel *c); static void chn_lockinit(struct pcm_channel *c, int dir) { switch(dir) { case PCMDIR_PLAY: c->lock = snd_mtxcreate(c->name, "pcm play channel"); break; case PCMDIR_REC: c->lock = snd_mtxcreate(c->name, "pcm record channel"); break; case PCMDIR_VIRTUAL: c->lock = snd_mtxcreate(c->name, "pcm virtual play channel"); break; case 0: c->lock = snd_mtxcreate(c->name, "pcm fake channel"); break; } } static void chn_lockdestroy(struct pcm_channel *c) { snd_mtxfree(c->lock); } static int chn_polltrigger(struct pcm_channel *c) { struct snd_dbuf *bs = c->bufsoft; unsigned amt, lim; CHN_LOCKASSERT(c); if (c->flags & CHN_F_MAPPED) { if (sndbuf_getprevblocks(bs) == 0) return 1; else return (sndbuf_getblocks(bs) > sndbuf_getprevblocks(bs))? 1 : 0; } else { amt = (c->direction == PCMDIR_PLAY)? sndbuf_getfree(bs) : sndbuf_getready(bs); lim = (c->flags & CHN_F_HAS_SIZE)? sndbuf_getblksz(bs) : 1; lim = 1; return (amt >= lim)? 1 : 0; } return 0; } static int chn_pollreset(struct pcm_channel *c) { struct snd_dbuf *bs = c->bufsoft; CHN_LOCKASSERT(c); sndbuf_updateprevtotal(bs); return 1; } static void chn_wakeup(struct pcm_channel *c) { struct snd_dbuf *bs = c->bufsoft; struct pcmchan_children *pce; CHN_LOCKASSERT(c); if (SLIST_EMPTY(&c->children)) { if (SEL_WAITING(sndbuf_getsel(bs)) && chn_polltrigger(c)) selwakeuppri(sndbuf_getsel(bs), PRIBIO); } else { SLIST_FOREACH(pce, &c->children, link) { CHN_LOCK(pce->channel); chn_wakeup(pce->channel); CHN_UNLOCK(pce->channel); } } wakeup(bs); } static int chn_sleep(struct pcm_channel *c, char *str, int timeout) { struct snd_dbuf *bs = c->bufsoft; int ret; CHN_LOCKASSERT(c); #ifdef USING_MUTEX ret = msleep(bs, c->lock, PRIBIO | PCATCH, str, timeout); #else ret = tsleep(bs, PRIBIO | PCATCH, str, timeout); #endif return ret; } /* * chn_dmaupdate() tracks the status of a dma transfer, * updating pointers. It must be called at spltty(). */ static unsigned int chn_dmaupdate(struct pcm_channel *c) { struct snd_dbuf *b = c->bufhard; unsigned int delta, old, hwptr, amt; KASSERT(sndbuf_getsize(b) > 0, ("bufsize == 0")); CHN_LOCKASSERT(c); old = sndbuf_gethwptr(b); hwptr = chn_getptr(c); delta = (sndbuf_getsize(b) + hwptr - old) % sndbuf_getsize(b); sndbuf_sethwptr(b, hwptr); DEB( if (delta >= ((sndbuf_getsize(b) * 15) / 16)) { if (!(c->flags & (CHN_F_CLOSING | CHN_F_ABORTING))) device_printf(c->dev, "hwptr went backwards %d -> %d\n", old, hwptr); } ); if (c->direction == PCMDIR_PLAY) { amt = MIN(delta, sndbuf_getready(b)); if (amt > 0) sndbuf_dispose(b, NULL, amt); } else { amt = MIN(delta, sndbuf_getfree(b)); if (amt > 0) sndbuf_acquire(b, NULL, amt); } return delta; } void chn_wrupdate(struct pcm_channel *c) { int ret; CHN_LOCKASSERT(c); KASSERT(c->direction == PCMDIR_PLAY, ("chn_wrupdate on bad channel")); if ((c->flags & (CHN_F_MAPPED | CHN_F_VIRTUAL)) || !(c->flags & CHN_F_TRIGGERED)) return; chn_dmaupdate(c); ret = chn_wrfeed(c); /* tell the driver we've updated the primary buffer */ chn_trigger(c, PCMTRIG_EMLDMAWR); DEB(if (ret) printf("chn_wrupdate: chn_wrfeed returned %d\n", ret);) } int chn_wrfeed(struct pcm_channel *c) { struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; unsigned int ret, amt; CHN_LOCKASSERT(c); /* DEB( if (c->flags & CHN_F_CLOSING) { sndbuf_dump(b, "b", 0x02); sndbuf_dump(bs, "bs", 0x02); }) */ if (c->flags & CHN_F_MAPPED) sndbuf_acquire(bs, NULL, sndbuf_getfree(bs)); amt = sndbuf_getfree(b); KASSERT(amt <= sndbuf_getsize(bs), ("%s(%s): amt %d > source size %d, flags 0x%x", __func__, c->name, amt, sndbuf_getsize(bs), c->flags)); if (sndbuf_getready(bs) < amt) c->xruns++; ret = (amt > 0)? sndbuf_feed(bs, b, c, c->feeder, amt) : ENOSPC; if (ret == 0 && sndbuf_getfree(b) < amt) chn_wakeup(c); return ret; } static void chn_wrintr(struct pcm_channel *c) { int ret; CHN_LOCKASSERT(c); /* update pointers in primary buffer */ chn_dmaupdate(c); /* ...and feed from secondary to primary */ ret = chn_wrfeed(c); /* tell the driver we've updated the primary buffer */ chn_trigger(c, PCMTRIG_EMLDMAWR); DEB(if (ret) printf("chn_wrintr: chn_wrfeed returned %d\n", ret);) } /* * user write routine - uiomove data into secondary buffer, trigger if necessary * if blocking, sleep, rinse and repeat. * * called externally, so must handle locking */ int chn_write(struct pcm_channel *c, struct uio *buf) { int ret, timeout, newsize, count, sz; struct snd_dbuf *bs = c->bufsoft; void *off; int t, x,togo,p; CHN_LOCKASSERT(c); /* * XXX Certain applications attempt to write larger size * of pcm data than c->blocksize2nd without blocking, * resulting partial write. Expand the block size so that * the write operation avoids blocking. */ if ((c->flags & CHN_F_NBIO) && buf->uio_resid > sndbuf_getblksz(bs)) { DEB(device_printf(c->dev, "broken app, nbio and tried to write %d bytes with fragsz %d\n", buf->uio_resid, sndbuf_getblksz(bs))); newsize = 16; while (newsize < min(buf->uio_resid, CHN_2NDBUFMAXSIZE / 2)) newsize <<= 1; chn_setblocksize(c, sndbuf_getblkcnt(bs), newsize); DEB(device_printf(c->dev, "frags reset to %d x %d\n", sndbuf_getblkcnt(bs), sndbuf_getblksz(bs))); } ret = 0; count = hz; while (!ret && (buf->uio_resid > 0) && (count > 0)) { sz = sndbuf_getfree(bs); if (sz == 0) { if (c->flags & CHN_F_NBIO) ret = EWOULDBLOCK; else { timeout = (hz * sndbuf_getblksz(bs)) / (sndbuf_getspd(bs) * sndbuf_getbps(bs)); if (timeout < 1) timeout = 1; timeout = 1; ret = chn_sleep(c, "pcmwr", timeout); if (ret == EWOULDBLOCK) { count -= timeout; ret = 0; } else if (ret == 0) count = hz; } } else { sz = MIN(sz, buf->uio_resid); KASSERT(sz > 0, ("confusion in chn_write")); /* printf("sz: %d\n", sz); */ /* * The following assumes that the free space in * the buffer can never be less around the * unlock-uiomove-lock sequence. */ togo = sz; while (ret == 0 && togo> 0) { p = sndbuf_getfreeptr(bs); t = MIN(togo, sndbuf_getsize(bs) - p); off = sndbuf_getbufofs(bs, p); CHN_UNLOCK(c); ret = uiomove(off, t, buf); CHN_LOCK(c); togo -= t; x = sndbuf_acquire(bs, NULL, t); } ret = 0; if (ret == 0 && !(c->flags & CHN_F_TRIGGERED)) chn_start(c, 0); } } /* printf("ret: %d left: %d\n", ret, buf->uio_resid); */ if (count <= 0) { c->flags |= CHN_F_DEAD; printf("%s: play interrupt timeout, channel dead\n", c->name); } return ret; } static int chn_rddump(struct pcm_channel *c, unsigned int cnt) { struct snd_dbuf *b = c->bufhard; CHN_LOCKASSERT(c); sndbuf_setxrun(b, sndbuf_getxrun(b) + cnt); return sndbuf_dispose(b, NULL, cnt); } /* * Feed new data from the read buffer. Can be called in the bottom half. * Hence must be called at spltty. */ int chn_rdfeed(struct pcm_channel *c) { struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; unsigned int ret, amt; CHN_LOCKASSERT(c); DEB( if (c->flags & CHN_F_CLOSING) { sndbuf_dump(b, "b", 0x02); sndbuf_dump(bs, "bs", 0x02); }) amt = sndbuf_getready(b); if (sndbuf_getfree(bs) < amt) { c->xruns++; amt = sndbuf_getfree(bs); } ret = (amt > 0)? sndbuf_feed(b, bs, c, c->feeder, amt) : 0; amt = sndbuf_getready(b); if (amt > 0) chn_rddump(c, amt); chn_wakeup(c); return ret; } void chn_rdupdate(struct pcm_channel *c) { int ret; CHN_LOCKASSERT(c); KASSERT(c->direction == PCMDIR_REC, ("chn_rdupdate on bad channel")); if ((c->flags & CHN_F_MAPPED) || !(c->flags & CHN_F_TRIGGERED)) return; chn_trigger(c, PCMTRIG_EMLDMARD); chn_dmaupdate(c); ret = chn_rdfeed(c); if (ret) printf("chn_rdfeed: %d\n", ret); } /* read interrupt routine. Must be called with interrupts blocked. */ static void chn_rdintr(struct pcm_channel *c) { int ret; CHN_LOCKASSERT(c); /* tell the driver to update the primary buffer if non-dma */ chn_trigger(c, PCMTRIG_EMLDMARD); /* update pointers in primary buffer */ chn_dmaupdate(c); /* ...and feed from primary to secondary */ ret = chn_rdfeed(c); } /* * user read routine - trigger if necessary, uiomove data from secondary buffer * if blocking, sleep, rinse and repeat. * * called externally, so must handle locking */ int chn_read(struct pcm_channel *c, struct uio *buf) { int ret, timeout, sz, count; struct snd_dbuf *bs = c->bufsoft; void *off; int t, x,togo,p; CHN_LOCKASSERT(c); if (!(c->flags & CHN_F_TRIGGERED)) chn_start(c, 0); ret = 0; count = hz; while (!ret && (buf->uio_resid > 0) && (count > 0)) { sz = MIN(buf->uio_resid, sndbuf_getready(bs)); if (sz > 0) { /* * The following assumes that the free space in * the buffer can never be less around the * unlock-uiomove-lock sequence. */ togo = sz; while (ret == 0 && togo> 0) { p = sndbuf_getreadyptr(bs); t = MIN(togo, sndbuf_getsize(bs) - p); off = sndbuf_getbufofs(bs, p); CHN_UNLOCK(c); ret = uiomove(off, t, buf); CHN_LOCK(c); togo -= t; x = sndbuf_dispose(bs, NULL, t); } ret = 0; } else { if (c->flags & CHN_F_NBIO) { ret = EWOULDBLOCK; } else { timeout = (hz * sndbuf_getblksz(bs)) / (sndbuf_getspd(bs) * sndbuf_getbps(bs)); if (timeout < 1) timeout = 1; ret = chn_sleep(c, "pcmrd", timeout); if (ret == EWOULDBLOCK) { count -= timeout; ret = 0; } else { count = hz; } } } } if (count <= 0) { c->flags |= CHN_F_DEAD; printf("%s: record interrupt timeout, channel dead\n", c->name); } return ret; } void chn_intr(struct pcm_channel *c) { CHN_LOCK(c); c->interrupts++; if (c->direction == PCMDIR_PLAY) chn_wrintr(c); else chn_rdintr(c); CHN_UNLOCK(c); } u_int32_t chn_start(struct pcm_channel *c, int force) { u_int32_t i, j; struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; CHN_LOCKASSERT(c); /* if we're running, or if we're prevented from triggering, bail */ if ((c->flags & CHN_F_TRIGGERED) || ((c->flags & CHN_F_NOTRIGGER) && !force)) return EINVAL; i = (c->direction == PCMDIR_PLAY)? sndbuf_getready(bs) : sndbuf_getfree(bs); j = (c->direction == PCMDIR_PLAY)? sndbuf_getfree(b) : sndbuf_getready(b); if (force || (i >= j)) { c->flags |= CHN_F_TRIGGERED; /* * if we're starting because a vchan started, don't feed any data * or it becomes impossible to start vchans synchronised with the * first one. the hardbuf should be empty so we top it up with * silence to give it something to chew. the real data will be * fed at the first irq. */ if (c->direction == PCMDIR_PLAY) { if (SLIST_EMPTY(&c->children)) chn_wrfeed(c); else sndbuf_fillsilence(b); } sndbuf_setrun(b, 1); c->xruns = 0; chn_trigger(c, PCMTRIG_START); return 0; } return 0; } void chn_resetbuf(struct pcm_channel *c) { struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; c->blocks = 0; sndbuf_reset(b); sndbuf_reset(bs); } /* * chn_sync waits until the space in the given channel goes above * a threshold. The threshold is checked against fl or rl respectively. * Assume that the condition can become true, do not check here... */ int chn_sync(struct pcm_channel *c, int threshold) { u_long rdy; int ret; struct snd_dbuf *bs = c->bufsoft; CHN_LOCKASSERT(c); /* if we haven't yet started and nothing is buffered, else start*/ if (!(c->flags & CHN_F_TRIGGERED)) { if (sndbuf_getready(bs) > 0) { ret = chn_start(c, 1); if (ret) return ret; } else { return 0; } } for (;;) { rdy = (c->direction == PCMDIR_PLAY)? sndbuf_getfree(bs) : sndbuf_getready(bs); if (rdy <= threshold) { ret = chn_sleep(c, "pcmsyn", 1); if (ret == ERESTART || ret == EINTR) { DEB(printf("chn_sync: tsleep returns %d\n", ret)); return -1; } } else break; } return 0; } /* called externally, handle locking */ int chn_poll(struct pcm_channel *c, int ev, struct thread *td) { struct snd_dbuf *bs = c->bufsoft; int ret; CHN_LOCKASSERT(c); if (!(c->flags & CHN_F_MAPPED) && !(c->flags & CHN_F_TRIGGERED)) chn_start(c, 1); ret = 0; if (chn_polltrigger(c) && chn_pollreset(c)) ret = ev; else selrecord(td, sndbuf_getsel(bs)); return ret; } /* * chn_abort terminates a running dma transfer. it may sleep up to 200ms. * it returns the number of bytes that have not been transferred. * * called from: dsp_close, dsp_ioctl, with channel locked */ int chn_abort(struct pcm_channel *c) { int missing = 0; struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; CHN_LOCKASSERT(c); if (!(c->flags & CHN_F_TRIGGERED)) return 0; c->flags |= CHN_F_ABORTING; c->flags &= ~CHN_F_TRIGGERED; /* kill the channel */ chn_trigger(c, PCMTRIG_ABORT); sndbuf_setrun(b, 0); if (!(c->flags & CHN_F_VIRTUAL)) chn_dmaupdate(c); missing = sndbuf_getready(bs) + sndbuf_getready(b); c->flags &= ~CHN_F_ABORTING; return missing; } /* * this routine tries to flush the dma transfer. It is called * on a close of a playback channel. * first, if there is data in the buffer, but the dma has not yet * begun, we need to start it. * next, we wait for the play buffer to drain * finally, we stop the dma. * * called from: dsp_close, not valid for record channels. */ int chn_flush(struct pcm_channel *c) { int ret, count, resid, resid_p; struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; CHN_LOCKASSERT(c); KASSERT(c->direction == PCMDIR_PLAY, ("chn_flush on bad channel")); DEB(printf("chn_flush: c->flags 0x%08x\n", c->flags)); /* if we haven't yet started and nothing is buffered, else start*/ if (!(c->flags & CHN_F_TRIGGERED)) { if (sndbuf_getready(bs) > 0) { ret = chn_start(c, 1); if (ret) return ret; } else { return 0; } } c->flags |= CHN_F_CLOSING; resid = sndbuf_getready(bs) + sndbuf_getready(b); resid_p = resid; count = 10; ret = 0; while ((count > 0) && (resid > sndbuf_getsize(b)) && (ret == 0)) { /* still pending output data. */ ret = chn_sleep(c, "pcmflu", hz / 10); if (ret == EWOULDBLOCK) ret = 0; if (ret == 0) { resid = sndbuf_getready(bs) + sndbuf_getready(b); if (resid == resid_p) count--; if (resid > resid_p) DEB(printf("chn_flush: buffer length increasind %d -> %d\n", resid_p, resid)); resid_p = resid; } } if (count == 0) DEB(printf("chn_flush: timeout, hw %d, sw %d\n", sndbuf_getready(b), sndbuf_getready(bs))); c->flags &= ~CHN_F_TRIGGERED; /* kill the channel */ chn_trigger(c, PCMTRIG_ABORT); sndbuf_setrun(b, 0); c->flags &= ~CHN_F_CLOSING; return 0; } int fmtvalid(u_int32_t fmt, u_int32_t *fmtlist) { int i; for (i = 0; fmtlist[i]; i++) if (fmt == fmtlist[i]) return 1; return 0; } int chn_reset(struct pcm_channel *c, u_int32_t fmt) { int hwspd, r; CHN_LOCKASSERT(c); c->flags &= CHN_F_RESET; c->interrupts = 0; c->xruns = 0; r = CHANNEL_RESET(c->methods, c->devinfo); if (fmt != 0) { hwspd = DSP_DEFAULT_SPEED; /* only do this on a record channel until feederbuilder works */ if (c->direction == PCMDIR_REC) RANGE(hwspd, chn_getcaps(c)->minspeed, chn_getcaps(c)->maxspeed); c->speed = hwspd; if (r == 0) r = chn_setformat(c, fmt); if (r == 0) r = chn_setspeed(c, hwspd); if (r == 0) r = chn_setvolume(c, 100, 100); } if (r == 0) r = chn_setblocksize(c, 0, 0); if (r == 0) { chn_resetbuf(c); r = CHANNEL_RESETDONE(c->methods, c->devinfo); } return r; } int chn_init(struct pcm_channel *c, void *devinfo, int dir, int direction) { struct feeder_class *fc; struct snd_dbuf *b, *bs; int ret; chn_lockinit(c, dir); b = NULL; bs = NULL; c->devinfo = NULL; c->feeder = NULL; ret = ENOMEM; b = sndbuf_create(c->dev, c->name, "primary", c); if (b == NULL) goto out; bs = sndbuf_create(c->dev, c->name, "secondary", c); if (bs == NULL) goto out; CHN_LOCK(c); ret = EINVAL; fc = feeder_getclass(NULL); if (fc == NULL) goto out; if (chn_addfeeder(c, fc, NULL)) goto out; /* * XXX - sndbuf_setup() & sndbuf_resize() expect to be called * with the channel unlocked because they are also called * from driver methods that don't know about locking */ CHN_UNLOCK(c); sndbuf_setup(bs, NULL, 0); CHN_LOCK(c); c->bufhard = b; c->bufsoft = bs; c->flags = 0; c->feederflags = 0; ret = ENODEV; CHN_UNLOCK(c); /* XXX - Unlock for CHANNEL_INIT() malloc() call */ c->devinfo = CHANNEL_INIT(c->methods, devinfo, b, c, direction); CHN_LOCK(c); if (c->devinfo == NULL) goto out; ret = ENOMEM; if ((sndbuf_getsize(b) == 0) && ((c->flags & CHN_F_VIRTUAL) == 0)) goto out; ret = chn_setdir(c, direction); if (ret) goto out; ret = sndbuf_setfmt(b, AFMT_U8); if (ret) goto out; ret = sndbuf_setfmt(bs, AFMT_U8); if (ret) goto out; out: CHN_UNLOCK(c); if (ret) { if (c->devinfo) { if (CHANNEL_FREE(c->methods, c->devinfo)) sndbuf_free(b); } if (bs) sndbuf_destroy(bs); if (b) sndbuf_destroy(b); c->flags |= CHN_F_DEAD; chn_lockdestroy(c); return ret; } return 0; } int chn_kill(struct pcm_channel *c) { struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; if (c->flags & CHN_F_TRIGGERED) chn_trigger(c, PCMTRIG_ABORT); while (chn_removefeeder(c) == 0); if (CHANNEL_FREE(c->methods, c->devinfo)) sndbuf_free(b); c->flags |= CHN_F_DEAD; sndbuf_destroy(bs); sndbuf_destroy(b); chn_lockdestroy(c); return 0; } int chn_setdir(struct pcm_channel *c, int dir) { struct snd_dbuf *b = c->bufhard; int r; CHN_LOCKASSERT(c); c->direction = dir; r = CHANNEL_SETDIR(c->methods, c->devinfo, c->direction); if (!r && SND_DMA(b)) sndbuf_dmasetdir(b, c->direction); return r; } int chn_setvolume(struct pcm_channel *c, int left, int right) { CHN_LOCKASSERT(c); /* should add a feeder for volume changing if channel returns -1 */ c->volume = (left << 8) | right; return 0; } static int chn_tryspeed(struct pcm_channel *c, int speed) { struct pcm_feeder *f; struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; struct snd_dbuf *x; int r, delta; CHN_LOCKASSERT(c); DEB(printf("setspeed, channel %s\n", c->name)); DEB(printf("want speed %d, ", speed)); if (speed <= 0) return EINVAL; if (CANCHANGE(c)) { r = 0; c->speed = speed; sndbuf_setspd(bs, speed); RANGE(speed, chn_getcaps(c)->minspeed, chn_getcaps(c)->maxspeed); DEB(printf("try speed %d, ", speed)); sndbuf_setspd(b, CHANNEL_SETSPEED(c->methods, c->devinfo, speed)); DEB(printf("got speed %d\n", sndbuf_getspd(b))); delta = sndbuf_getspd(b) - sndbuf_getspd(bs); if (delta < 0) delta = -delta; c->feederflags &= ~(1 << FEEDER_RATE); if (delta > 500) c->feederflags |= 1 << FEEDER_RATE; else sndbuf_setspd(bs, sndbuf_getspd(b)); r = chn_buildfeeder(c); DEB(printf("r = %d\n", r)); if (r) goto out; r = chn_setblocksize(c, 0, 0); if (r) goto out; if (!(c->feederflags & (1 << FEEDER_RATE))) goto out; r = EINVAL; f = chn_findfeeder(c, FEEDER_RATE); DEB(printf("feedrate = %p\n", f)); if (f == NULL) goto out; x = (c->direction == PCMDIR_REC)? b : bs; r = FEEDER_SET(f, FEEDRATE_SRC, sndbuf_getspd(x)); DEB(printf("feeder_set(FEEDRATE_SRC, %d) = %d\n", sndbuf_getspd(x), r)); if (r) goto out; x = (c->direction == PCMDIR_REC)? bs : b; r = FEEDER_SET(f, FEEDRATE_DST, sndbuf_getspd(x)); DEB(printf("feeder_set(FEEDRATE_DST, %d) = %d\n", sndbuf_getspd(x), r)); out: DEB(printf("setspeed done, r = %d\n", r)); return r; } else return EINVAL; } int chn_setspeed(struct pcm_channel *c, int speed) { int r, oldspeed = c->speed; r = chn_tryspeed(c, speed); if (r) { DEB(printf("Failed to set speed %d falling back to %d\n", speed, oldspeed)); r = chn_tryspeed(c, oldspeed); } return r; } static int chn_tryformat(struct pcm_channel *c, u_int32_t fmt) { struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; int r; CHN_LOCKASSERT(c); if (CANCHANGE(c)) { DEB(printf("want format %d\n", fmt)); c->format = fmt; r = chn_buildfeeder(c); if (r == 0) { sndbuf_setfmt(bs, c->format); chn_resetbuf(c); r = CHANNEL_SETFORMAT(c->methods, c->devinfo, sndbuf_getfmt(b)); if (r == 0) r = chn_tryspeed(c, c->speed); } return r; } else return EINVAL; } int chn_setformat(struct pcm_channel *c, u_int32_t fmt) { u_int32_t oldfmt = c->format; int r; r = chn_tryformat(c, fmt); if (r) { DEB(printf("Format change %d failed, reverting to %d\n", fmt, oldfmt)); chn_tryformat(c, oldfmt); } return r; } int chn_setblocksize(struct pcm_channel *c, int blkcnt, int blksz) { struct snd_dbuf *b = c->bufhard; struct snd_dbuf *bs = c->bufsoft; int irqhz, tmp, ret, maxsize, reqblksz, tmpblksz; CHN_LOCKASSERT(c); if (!CANCHANGE(c) || (c->flags & CHN_F_MAPPED)) { KASSERT(sndbuf_getsize(bs) == 0 || sndbuf_getsize(bs) >= sndbuf_getsize(b), ("%s(%s): bufsoft size %d < bufhard size %d", __func__, c->name, sndbuf_getsize(bs), sndbuf_getsize(b))); return EINVAL; } c->flags |= CHN_F_SETBLOCKSIZE; ret = 0; DEB(printf("%s(%d, %d)\n", __func__, blkcnt, blksz)); if (blksz == 0 || blksz == -1) { if (blksz == -1) c->flags &= ~CHN_F_HAS_SIZE; if (!(c->flags & CHN_F_HAS_SIZE)) { blksz = (sndbuf_getbps(bs) * sndbuf_getspd(bs)) / chn_targetirqrate; tmp = 32; while (tmp <= blksz) tmp <<= 1; tmp >>= 1; blksz = tmp; blkcnt = CHN_2NDBUFMAXSIZE / blksz; RANGE(blksz, 16, CHN_2NDBUFMAXSIZE / 2); RANGE(blkcnt, 2, CHN_2NDBUFMAXSIZE / blksz); DEB(printf("%s: defaulting to (%d, %d)\n", __func__, blkcnt, blksz)); } else { blkcnt = sndbuf_getblkcnt(bs); blksz = sndbuf_getblksz(bs); DEB(printf("%s: updating (%d, %d)\n", __func__, blkcnt, blksz)); } } else { ret = EINVAL; if ((blksz < 16) || (blkcnt < 2) || (blkcnt * blksz > CHN_2NDBUFMAXSIZE)) goto out; ret = 0; c->flags |= CHN_F_HAS_SIZE; } reqblksz = blksz; /* adjust for different hw format/speed */ irqhz = (sndbuf_getbps(bs) * sndbuf_getspd(bs)) / blksz; DEB(printf("%s: soft bps %d, spd %d, irqhz == %d\n", __func__, sndbuf_getbps(bs), sndbuf_getspd(bs), irqhz)); RANGE(irqhz, 16, 512); tmpblksz = (sndbuf_getbps(b) * sndbuf_getspd(b)) / irqhz; /* round down to 2^x */ blksz = 32; while (blksz <= tmpblksz) blksz <<= 1; blksz >>= 1; /* round down to fit hw buffer size */ if (sndbuf_getmaxsize(b) > 0) RANGE(blksz, 16, sndbuf_getmaxsize(b) / 2); else /* virtual channels don't appear to allocate bufhard */ RANGE(blksz, 16, CHN_2NDBUFMAXSIZE / 2); DEB(printf("%s: hard blksz requested %d (maxsize %d), ", __func__, blksz, sndbuf_getmaxsize(b))); /* Increase the size of bufsoft if before increasing bufhard. */ maxsize = sndbuf_getsize(b); if (sndbuf_getsize(bs) > maxsize) maxsize = sndbuf_getsize(bs); if (reqblksz * blkcnt > maxsize) maxsize = reqblksz * blkcnt; if (sndbuf_getsize(bs) != maxsize || sndbuf_getblksz(bs) != reqblksz) { ret = sndbuf_remalloc(bs, maxsize/reqblksz, reqblksz); if (ret) goto out1; } CHN_UNLOCK(c); sndbuf_setblksz(b, CHANNEL_SETBLOCKSIZE(c->methods, c->devinfo, blksz)); CHN_LOCK(c); /* Decrease the size of bufsoft after decreasing bufhard. */ maxsize = sndbuf_getsize(b); if (reqblksz * blkcnt > maxsize) maxsize = reqblksz * blkcnt; if (maxsize > sndbuf_getsize(bs)) printf("Danger! %s bufsoft size increasing from %d to %d after CHANNEL_SETBLOCKSIZE()\n", c->name, sndbuf_getsize(bs), maxsize); if (sndbuf_getsize(bs) != maxsize || sndbuf_getblksz(bs) != reqblksz) { ret = sndbuf_remalloc(bs, maxsize/reqblksz, reqblksz); if (ret) goto out1; } irqhz = (sndbuf_getbps(b) * sndbuf_getspd(b)) / sndbuf_getblksz(b); DEB(printf("got %d, irqhz == %d\n", sndbuf_getblksz(b), irqhz)); chn_resetbuf(c); out1: KASSERT(sndbuf_getsize(bs) == 0 || sndbuf_getsize(bs) >= sndbuf_getsize(b), ("%s(%s): bufsoft size %d < bufhard size %d, reqblksz=%d blksz=%d maxsize=%d blkcnt=%d", __func__, c->name, sndbuf_getsize(bs), sndbuf_getsize(b), reqblksz, blksz, maxsize, blkcnt)); out: c->flags &= ~CHN_F_SETBLOCKSIZE; return ret; } int chn_trigger(struct pcm_channel *c, int go) { struct snd_dbuf *b = c->bufhard; int ret; CHN_LOCKASSERT(c); if (SND_DMA(b) && (go == PCMTRIG_EMLDMAWR || go == PCMTRIG_EMLDMARD)) sndbuf_dmabounce(b); ret = CHANNEL_TRIGGER(c->methods, c->devinfo, go); return ret; } int chn_getptr(struct pcm_channel *c) { int hwptr; int a = (1 << c->align) - 1; CHN_LOCKASSERT(c); hwptr = (c->flags & CHN_F_TRIGGERED)? CHANNEL_GETPTR(c->methods, c->devinfo) : 0; /* don't allow unaligned values in the hwa ptr */ #if 1 hwptr &= ~a ; /* Apply channel align mask */ #endif hwptr &= DMA_ALIGN_MASK; /* Apply DMA align mask */ return hwptr; } struct pcmchan_caps * chn_getcaps(struct pcm_channel *c) { CHN_LOCKASSERT(c); return CHANNEL_GETCAPS(c->methods, c->devinfo); } u_int32_t chn_getformats(struct pcm_channel *c) { u_int32_t *fmtlist, fmts; int i; fmtlist = chn_getcaps(c)->fmtlist; fmts = 0; for (i = 0; fmtlist[i]; i++) fmts |= fmtlist[i]; /* report software-supported formats */ if (report_soft_formats) fmts |= AFMT_MU_LAW|AFMT_A_LAW|AFMT_U16_LE|AFMT_U16_BE| AFMT_S16_LE|AFMT_S16_BE|AFMT_U8|AFMT_S8; return fmts; } static int chn_buildfeeder(struct pcm_channel *c) { struct feeder_class *fc; struct pcm_feederdesc desc; u_int32_t tmp[2], type, flags, hwfmt; int err; CHN_LOCKASSERT(c); while (chn_removefeeder(c) == 0); KASSERT((c->feeder == NULL), ("feeder chain not empty")); c->align = sndbuf_getalign(c->bufsoft); if (SLIST_EMPTY(&c->children)) { fc = feeder_getclass(NULL); KASSERT(fc != NULL, ("can't find root feeder")); err = chn_addfeeder(c, fc, NULL); if (err) { DEB(printf("can't add root feeder, err %d\n", err)); return err; } c->feeder->desc->out = c->format; } else { desc.type = FEEDER_MIXER; desc.in = 0; desc.out = c->format; desc.flags = 0; fc = feeder_getclass(&desc); if (fc == NULL) { DEB(printf("can't find vchan feeder\n")); return EOPNOTSUPP; } err = chn_addfeeder(c, fc, &desc); if (err) { DEB(printf("can't add vchan feeder, err %d\n", err)); return err; } } flags = c->feederflags; DEB(printf("feederflags %x\n", flags)); for (type = FEEDER_RATE; type <= FEEDER_LAST; type++) { if (flags & (1 << type)) { desc.type = type; desc.in = 0; desc.out = 0; desc.flags = 0; DEB(printf("find feeder type %d, ", type)); fc = feeder_getclass(&desc); DEB(printf("got %p\n", fc)); if (fc == NULL) { DEB(printf("can't find required feeder type %d\n", type)); return EOPNOTSUPP; } if (c->feeder->desc->out != fc->desc->in) { DEB(printf("build fmtchain from 0x%x to 0x%x: ", c->feeder->desc->out, fc->desc->in)); tmp[0] = fc->desc->in; tmp[1] = 0; if (chn_fmtchain(c, tmp) == 0) { DEB(printf("failed\n")); return ENODEV; } DEB(printf("ok\n")); } err = chn_addfeeder(c, fc, fc->desc); if (err) { DEB(printf("can't add feeder %p, output 0x%x, err %d\n", fc, fc->desc->out, err)); return err; } DEB(printf("added feeder %p, output 0x%x\n", fc, c->feeder->desc->out)); } } if (fmtvalid(c->feeder->desc->out, chn_getcaps(c)->fmtlist)) { hwfmt = c->feeder->desc->out; } else { if (c->direction == PCMDIR_REC) { tmp[0] = c->format; tmp[1] = 0; hwfmt = chn_fmtchain(c, tmp); } else { #if 0 u_int32_t *x = chn_getcaps(c)->fmtlist; printf("acceptable formats for %s:\n", c->name); while (*x) { printf("[0x%8x] ", *x); x++; } #endif hwfmt = chn_fmtchain(c, chn_getcaps(c)->fmtlist); } } if (hwfmt == 0) return ENODEV; sndbuf_setfmt(c->bufhard, hwfmt); return 0; } int chn_notify(struct pcm_channel *c, u_int32_t flags) { struct pcmchan_children *pce; struct pcm_channel *child; int run; CHN_LOCK(c); if (SLIST_EMPTY(&c->children)) { CHN_UNLOCK(c); return ENODEV; } run = (c->flags & CHN_F_TRIGGERED)? 1 : 0; /* * if the hwchan is running, we can't change its rate, format or * blocksize */ if (run) flags &= CHN_N_VOLUME | CHN_N_TRIGGER; if (flags & CHN_N_RATE) { /* * we could do something here, like scan children and decide on * the most appropriate rate to mix at, but we don't for now */ } if (flags & CHN_N_FORMAT) { /* * we could do something here, like scan children and decide on * the most appropriate mixer feeder to use, but we don't for now */ } if (flags & CHN_N_VOLUME) { /* * we could do something here but we don't for now */ } if (flags & CHN_N_BLOCKSIZE) { int blksz; /* * scan the children, find the lowest blocksize and use that * for the hard blocksize */ blksz = sndbuf_getmaxsize(c->bufhard) / 2; SLIST_FOREACH(pce, &c->children, link) { child = pce->channel; CHN_LOCK(child); if (sndbuf_getblksz(child->bufhard) < blksz) blksz = sndbuf_getblksz(child->bufhard); CHN_UNLOCK(child); } chn_setblocksize(c, 2, blksz); } if (flags & CHN_N_TRIGGER) { int nrun; /* * scan the children, and figure out if any are running * if so, we need to be running, otherwise we need to be stopped * if we aren't in our target sstate, move to it */ nrun = 0; SLIST_FOREACH(pce, &c->children, link) { child = pce->channel; CHN_LOCK(child); if (child->flags & CHN_F_TRIGGERED) nrun = 1; CHN_UNLOCK(child); } if (nrun && !run) chn_start(c, 1); if (!nrun && run) chn_abort(c); } CHN_UNLOCK(c); return 0; } void chn_lock(struct pcm_channel *c) { CHN_LOCK(c); } void chn_unlock(struct pcm_channel *c) { CHN_UNLOCK(c); }