/* * Copyright (c) 2026 Abdelkader Boudih * * SPDX-License-Identifier: BSD-2-Clause */ /* * fwisound(4) - Apple FireWire audio driver * * Protocol reverse-engineered by Clemens Ladisch (Linux isight_audio). */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static MALLOC_DEFINE(M_FWISOUND, "fwisound", "Apple FireWire Audio"); static int debug = 0; static int iso_channel = FWISOUND_ISO_CHANNEL; SYSCTL_DECL(_hw_firewire); static SYSCTL_NODE(_hw_firewire, OID_AUTO, fwisound, CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Apple FireWire Audio"); SYSCTL_INT(_hw_firewire_fwisound, OID_AUTO, debug, CTLFLAG_RWTUN, &debug, 0, "fwisound debug level"); SYSCTL_INT(_hw_firewire_fwisound, OID_AUTO, iso_channel, CTLFLAG_RWTUN, &iso_channel, 0, "ISO channel for isochronous receive (default 1)"); #define FWISOUND_DEBUG(lev, fmt, ...) \ do { \ if (debug >= (lev)) \ printf("fwisound: " fmt, ## __VA_ARGS__); \ } while (0) static void fwisound_identify(driver_t *, device_t); static int fwisound_probe(device_t); static int fwisound_attach(device_t); static int fwisound_detach(device_t); static void fwisound_post_busreset(void *); static void fwisound_post_explore(void *); static void fwisound_probe_task(void *, int); static void fwisound_start_task(void *, int); static void fwisound_stop_task(void *, int); static void fwisound_iso_input(struct fw_xferq *); static int fwisound_read_quadlet(struct fwisound_softc *sc, uint32_t offset, uint32_t *val) { uint16_t dst; uint8_t spd; int err; FWISOUND_LOCK(sc); if (sc->fwdev == NULL) { FWISOUND_UNLOCK(sc); return (ENXIO); } dst = FWLOCALBUS | sc->fwdev->dst; spd = min(sc->fwdev->speed, FWSPD_S400); FWISOUND_UNLOCK(sc); err = fw_read_quadlet(sc->fd.fc, M_FWISOUND, dst, spd, sc->cmd_hi, sc->cmd_lo + offset, val); if (err) FWISOUND_DEBUG(1, "read_quadlet: offset=0x%x err=%d\n", offset, err); return (err); } static int fwisound_write_quadlet(struct fwisound_softc *sc, uint32_t offset, uint32_t val) { uint16_t dst; uint8_t spd; int err; FWISOUND_LOCK(sc); if (sc->fwdev == NULL) { FWISOUND_UNLOCK(sc); return (ENXIO); } dst = FWLOCALBUS | sc->fwdev->dst; spd = min(sc->fwdev->speed, FWSPD_S400); FWISOUND_UNLOCK(sc); err = fw_write_quadlet(sc->fd.fc, M_FWISOUND, dst, spd, sc->cmd_hi, sc->cmd_lo + offset, val); if (err) FWISOUND_DEBUG(1, "write_quadlet: offset=0x%x err=%d\n", offset, err); return (err); } static uint32_t fwisound_find_audio_unit(struct fw_device *fwdev) { struct csrhdr *hdr; struct csrdirectory *root, *udir; struct csrreg *reg, *u; int i, j, spec_ok, ver_ok; uint32_t audio_base; hdr = (struct csrhdr *)fwdev->csrrom; if (hdr->info_len <= 1) return (0); root = (struct csrdirectory *)(fwdev->csrrom + 1 + hdr->info_len); for (i = 0; i < root->crc_len; i++) { reg = &root->entry[i]; if (reg->key != CROM_UDIR) continue; udir = (struct csrdirectory *)(reg + reg->val); spec_ok = 0; ver_ok = 0; audio_base = 0; for (j = 0; j < udir->crc_len; j++) { u = &udir->entry[j]; if (u->key == CSRKEY_SPEC && u->val == FWISOUND_SPEC_ID) spec_ok = 1; else if (u->key == CSRKEY_VER && u->val == FWISOUND_VERSION) ver_ok = 1; else if (u->key == IIDC_CROM_CMD_BASE) audio_base = u->val; } if (spec_ok && ver_ok && audio_base != 0) return (audio_base); } return (0); } static void fwisound_probe_task(void *arg, int pending __unused) { struct fwisound_softc *sc = (struct fwisound_softc *)arg; uint32_t val; int err; if (sc->state == FWISOUND_STATE_DETACHING || sc->fwdev == NULL) return; err = fwisound_write_quadlet(sc, FWISOUND_REG_SAMPLE_RATE, FWISOUND_RATE_48000); if (err) { device_printf(sc->dev, "failed to set sample rate: %d\n", err); return; } if (fwisound_read_quadlet(sc, FWISOUND_REG_GAIN_RAW_START, &val) == 0) sc->gain_raw_min = (int32_t)val; if (fwisound_read_quadlet(sc, FWISOUND_REG_GAIN_RAW_END, &val) == 0) sc->gain_raw_max = (int32_t)val; FWISOUND_LOCK(sc); if (sc->state == FWISOUND_STATE_DETACHING) { FWISOUND_UNLOCK(sc); return; } sc->state = FWISOUND_STATE_PROBED; FWISOUND_UNLOCK(sc); } static void fwisound_start_task(void *arg, int pending __unused) { struct fwisound_softc *sc = (struct fwisound_softc *)arg; if (sc->state == FWISOUND_STATE_DETACHING) return; fwisound_iso_start(sc); } static void fwisound_stop_task(void *arg, int pending __unused) { struct fwisound_softc *sc = (struct fwisound_softc *)arg; fwisound_iso_stop(sc); FWISOUND_LOCK(sc); if (sc->state != FWISOUND_STATE_DETACHING) sc->state = FWISOUND_STATE_PROBED; FWISOUND_UNLOCK(sc); } int fwisound_iso_start(struct fwisound_softc *sc) { struct firewire_comm *fc = sc->fd.fc; struct fw_xferq *xferq; uint32_t val; uint8_t speed; int dma_ch, err; mtx_assert(&sc->mtx, MA_NOTOWNED); FWISOUND_LOCK(sc); if (sc->dma_ch >= 0 || sc->state == FWISOUND_STATE_STREAMING) { FWISOUND_UNLOCK(sc); return (0); } if (sc->state == FWISOUND_STATE_DETACHING || sc->fwdev == NULL) { FWISOUND_UNLOCK(sc); return (ENXIO); } speed = min(sc->fwdev->speed, FWSPD_S400); FWISOUND_UNLOCK(sc); dma_ch = fw_open_isodma(fc, 0); if (dma_ch < 0) { device_printf(sc->dev, "no IR DMA channel available\n"); return (EBUSY); } FWISOUND_LOCK(sc); if (sc->dma_ch >= 0) { FWISOUND_UNLOCK(sc); fc->ir[dma_ch]->flag &= ~FWXFERQ_OPEN; return (0); } FWISOUND_UNLOCK(sc); xferq = fc->ir[dma_ch]; xferq->flag |= FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_STREAM; xferq->flag &= ~FWXFERQ_CHTAGMASK; xferq->flag |= sc->iso_channel & FWXFERQ_CHTAGMASK; xferq->sc = (caddr_t)sc; xferq->hand = fwisound_iso_input; xferq->bnchunk = FWISOUND_ISO_NCHUNK; xferq->bnpacket = 1; xferq->psize = FWISOUND_ISO_PKTSIZE; xferq->queued = 0; xferq->buf = NULL; xferq->bulkxfer = malloc( sizeof(struct fw_bulkxfer) * xferq->bnchunk, M_FWISOUND, M_WAITOK | M_ZERO); fw_iso_init_chunks(xferq); sc->sample_total = 0; val = sc->iso_channel | ((uint32_t)speed << 16); err = fwisound_write_quadlet(sc, FWISOUND_REG_ISO_TX_CONFIG, val); if (err) { device_printf(sc->dev, "failed to set ISO_TX_CONFIG: %d\n", err); goto fail; } err = fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, FWISOUND_AUDIO_ENABLE); if (err) { device_printf(sc->dev, "failed to enable audio: %d\n", err); goto fail; } err = fc->irx_enable(fc, dma_ch); if (err) { device_printf(sc->dev, "failed to start IR DMA: %d\n", err); fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0); goto fail; } FWISOUND_LOCK(sc); if (sc->state == FWISOUND_STATE_DETACHING) { FWISOUND_UNLOCK(sc); fc->irx_disable(fc, dma_ch); fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0); err = ENXIO; goto fail; } sc->dma_ch = dma_ch; sc->state = FWISOUND_STATE_STREAMING; FWISOUND_UNLOCK(sc); return (0); fail: fw_iso_free_chunks(xferq, M_FWISOUND); xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_OPEN | FWXFERQ_STREAM | FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK); xferq->hand = NULL; sc->dma_ch = -1; return (err); } void fwisound_iso_stop(struct fwisound_softc *sc) { struct firewire_comm *fc = sc->fd.fc; struct fw_xferq *xferq; int dma_ch; FWISOUND_LOCK(sc); dma_ch = sc->dma_ch; if (dma_ch < 0) { FWISOUND_UNLOCK(sc); return; } sc->dma_ch = -1; FWISOUND_UNLOCK(sc); xferq = fc->ir[dma_ch]; xferq->flag &= ~FWXFERQ_OPEN; xferq->hand = NULL; if (xferq->flag & FWXFERQ_RUNNING) fc->irx_disable(fc, dma_ch); if (sc->fwdev != NULL) fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0); FWISOUND_LOCK(sc); fw_iso_wait_inactive_locked(&sc->mtx, &sc->iso_active, "fwisis"); FWISOUND_UNLOCK(sc); xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_STREAM | FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK); fw_iso_free_chunks(xferq, M_FWISOUND); } static void fwisound_iso_input(struct fw_xferq *xferq) { struct fwisound_softc *sc = (struct fwisound_softc *)xferq->sc; struct fwisound_chan *chan; struct fw_bulkxfer *sxfer; struct fw_pkt *fp; struct fwisound_payload *pay; struct mbuf *m; uint8_t *ring; uint32_t plen, nbytes, sample_count, ring_size, pos, chunk, old_ptr; int dma_ch, need_intr; FWISOUND_LOCK(sc); dma_ch = sc->dma_ch; if (dma_ch < 0) { FWISOUND_UNLOCK(sc); return; } sc->iso_active = 1; chan = sc->chan; FWISOUND_UNLOCK(sc); while ((sxfer = STAILQ_FIRST(&xferq->stvalid)) != NULL) { STAILQ_REMOVE_HEAD(&xferq->stvalid, link); m = fw_iso_dequeue(xferq, sxfer, sc->fd.fc); if (m == NULL) continue; fp = mtod(m, struct fw_pkt *); plen = fp->mode.stream.len; if (plen < 20 || (uint32_t)m->m_len < 4 + plen) { m_freem(m); continue; } pay = (struct fwisound_payload *)(mtod(m, uint8_t *) + 4); if (ntohl(pay->signature) != FWISOUND_SIGNATURE) { m_freem(m); continue; } sample_count = ntohl(pay->sample_count); if (sample_count == 0 || sample_count > 475) { m_freem(m); continue; } nbytes = sample_count * 4; if (plen < 16 + nbytes) { m_freem(m); continue; } if (sc->sample_total != 0 && ntohl(pay->sample_total) != sc->sample_total) sc->dropped++; sc->sample_total = ntohl(pay->sample_total) + sample_count; need_intr = 0; if (chan != NULL && chan->running && chan->buf != NULL && chan->pcm_chan != NULL) { ring = sndbuf_getbufofs(chan->buf, 0); ring_size = chan->buf->bufsize; old_ptr = chan->hw_ptr; uint8_t *src = (uint8_t *)pay->samples; uint32_t rem = nbytes; while (rem > 0) { pos = chan->hw_ptr % ring_size; chunk = MIN(rem, ring_size - pos); memcpy(ring + pos, src, chunk); chan->hw_ptr += chunk; src += chunk; rem -= chunk; } if (chan->buf->blksz == 0 || (old_ptr / chan->buf->blksz) != (chan->hw_ptr / chan->buf->blksz)) need_intr = 1; } m_freem(m); if (need_intr) { chn_intr(chan->pcm_chan); } } fw_iso_rearm_done(xferq, sc->fd.fc, &sc->mtx, &sc->iso_active, &sc->dma_ch, dma_ch); } static void fwisound_post_explore(void *arg) { struct fwisound_softc *sc = (struct fwisound_softc *)arg; struct fw_device *fwdev; uint32_t audio_base; int err; FWISOUND_LOCK(sc); if (sc->state == FWISOUND_STATE_DETACHING) { FWISOUND_UNLOCK(sc); return; } if (sc->fwdev != NULL) { STAILQ_FOREACH(fwdev, &sc->fd.fc->devices, link) { if (fwdev == sc->fwdev && fwdev->status == FWDEVATTACHED) break; } if (fwdev == NULL) { device_printf(sc->dev, "device disconnected\n"); sc->fwdev = NULL; sc->state = FWISOUND_STATE_IDLE; FWISOUND_UNLOCK(sc); fwisound_iso_stop(sc); FWISOUND_LOCK(sc); } } if (sc->fwdev == NULL) { STAILQ_FOREACH(fwdev, &sc->fd.fc->devices, link) { if (fwdev->status != FWDEVATTACHED) continue; audio_base = fwisound_find_audio_unit(fwdev); if (audio_base == 0) continue; sc->fwdev = fwdev; sc->cmd_hi = 0xffff; sc->cmd_lo = 0xf0000000 | (audio_base << 2); FWISOUND_UNLOCK(sc); err = taskqueue_enqueue(taskqueue_thread, &sc->probe_task); if (err != 0) device_printf(sc->dev, "taskqueue_enqueue failed: %d\n", err); return; } } FWISOUND_UNLOCK(sc); } static void fwisound_post_busreset(void *arg __unused) { } static void fwisound_identify(driver_t *driver, device_t parent) { if (device_find_child(parent, "fwisound", DEVICE_UNIT_ANY) == NULL) BUS_ADD_CHILD(parent, 0, "fwisound", DEVICE_UNIT_ANY); } static int fwisound_probe(device_t dev) { device_set_desc(dev, "Apple FireWire Audio"); return (0); } static int fwisound_attach(device_t dev) { struct fwisound_softc *sc; sc = device_get_softc(dev); sc->fd.dev = dev; sc->fd.fc = device_get_ivars(dev); sc->dev = dev; mtx_init(&sc->mtx, "fwisound", NULL, MTX_DEF); sc->fwdev = NULL; sc->state = FWISOUND_STATE_IDLE; sc->dma_ch = -1; sc->iso_active = 0; sc->iso_channel = (uint8_t)(iso_channel & FWXFERQ_CHTAGMASK); sc->chan = NULL; TASK_INIT(&sc->probe_task, 0, fwisound_probe_task, sc); TASK_INIT(&sc->start_task, 0, fwisound_start_task, sc); TASK_INIT(&sc->stop_task, 0, fwisound_stop_task, sc); sc->fd.post_busreset = fwisound_post_busreset; sc->fd.post_explore = fwisound_post_explore; sc->pcm_dev = device_add_child(dev, "pcm", DEVICE_UNIT_ANY); if (sc->pcm_dev == NULL) { device_printf(dev, "failed to add pcm child\n"); mtx_destroy(&sc->mtx); return (ENXIO); } bus_attach_children(dev); fwisound_post_explore(sc); return (0); } static int fwisound_detach(device_t dev) { struct fwisound_softc *sc; sc = device_get_softc(dev); FWISOUND_LOCK(sc); sc->state = FWISOUND_STATE_DETACHING; FWISOUND_UNLOCK(sc); taskqueue_drain(taskqueue_thread, &sc->probe_task); taskqueue_drain(taskqueue_thread, &sc->start_task); taskqueue_drain(taskqueue_thread, &sc->stop_task); fwisound_iso_stop(sc); if (sc->pcm_dev != NULL) device_delete_children(dev); FWISOUND_LOCK(sc); sc->fwdev = NULL; FWISOUND_UNLOCK(sc); mtx_destroy(&sc->mtx); return (0); } static device_method_t fwisound_methods[] = { DEVMETHOD(device_identify, fwisound_identify), DEVMETHOD(device_probe, fwisound_probe), DEVMETHOD(device_attach, fwisound_attach), DEVMETHOD(device_detach, fwisound_detach), DEVMETHOD_END }; static driver_t fwisound_driver = { "fwisound", fwisound_methods, sizeof(struct fwisound_softc), }; DRIVER_MODULE(fwisound, firewire, fwisound_driver, 0, 0); MODULE_VERSION(fwisound, 1); MODULE_DEPEND(fwisound, firewire, 1, 1, 1); MODULE_DEPEND(fwisound, sound, SOUND_MINVER, SOUND_PREFVER, SOUND_MAXVER);