[RFC PATCH v5 6/8] ALSA: usb: babyfacepro: add the front-panel poll and controls
Ismaïl Bahloul <[email protected]>
| Newsgroups | org.kernel.vger.linux-doc,org.kernel.vger.linux-kernel,org.kernel.vger.linux-sound,org.kernel.vger.linux-usb |
|---|---|
| Message-ID | <[email protected]> |
Adds the 0x17 readback poll (buttons, wheel, IN/OUT/SELECT, MIX, DIM) as read-only ALSA controls, plus the host-side emulation of the physical wheel/button behaviour (TotalMix's own role): the OUT/IN/MIX wheels, SET's phantom toggle and DIM. Suspend/resume now also stop/start the poll around a system sleep. On an original Babyface Pro, front-panel OUT selector values 0/1/2 correspond to Ch 1/2, Phones and Opt; correct the mapping, which left Ch 1/2 at the previous selection and assigned the other two values to the wrong outputs. Verified against the physical LEDs and knob targeting on both a non-FS unit and the Pro FS. Turning the OUT wheel also made heavy zipper noise on the main and headphone outputs. A loopback measurement (a tone from the headphone output cabled into IN3/IN4) showed why: the firmware moves the analog output level itself on each click, smoothed over about 12 ms, but the driver also wrote the 8-bit (analog) master on every ~20 ms poll from its own flat 1 dB/click count, overriding the firmware's own move each time and saw-toothing the level by 1-3 dB while the wheel turned. The firmware's actual step also depends on the level and on how fast the wheel turns (0.5 to 3 dB per click, doubled below a ~62 ms gap between clicks). Write only the 16-bit (digital) master during the gesture, track the wheel's own level per side in half-dB so the ALSA controls stay accurate, poll the panel every 5 ms for 200 ms after a click to tell fast clicks apart, and reconcile the real 8-bit master once the wheel has rested for 150 ms, the way TotalMix does at the end of a gesture. A muted output now stays muted during the gesture instead of being unmuted by the 8-bit write, and an existing balance (hold-SELECT) is preserved since both sides move by the louder side's step. Co-developed-by: David Fredman <[email protected]> Signed-off-by: David Fredman <[email protected]> Signed-off-by: Ismaïl Bahloul <[email protected]> --- sound/usb/babyfacepro/babyfacepro-ctl.c | 1081 +++++++++++++++++++++++ sound/usb/babyfacepro/babyfacepro.c | 20 + sound/usb/babyfacepro/babyfacepro.h | 100 +++ 3 files changed, 1201 insertions(+) diff --git a/sound/usb/babyfacepro/babyfacepro-ctl.c b/sound/usb/babyfacepro/babyfacepro-ctl.c index 2b4dd471d..eac97c895 100644 --- a/sound/usb/babyfacepro/babyfacepro-ctl.c +++ b/sound/usb/babyfacepro/babyfacepro-ctl.c @@ -2117,3 +2117,1084 @@ int babyface_create_flags(struct snd_usb_babyface *chip) return 0; } +/* Control indices in chip->panel_kctl[] (for snd_ctl_notify). */ +enum { + BF_PANEL_KCTL_BUTTON, + BF_PANEL_KCTL_WHEEL, + BF_PANEL_KCTL_IN, + BF_PANEL_KCTL_OUT, + BF_PANEL_KCTL_MIX, + BF_PANEL_KCTL_DIM, + BF_PANEL_KCTL_SELECT, + BF_PANEL_KCTL_NUM, +}; + +static const char *const bf_panel_in_texts[] = { + "Unknown", "Ch 1/2", "Ch 3/4", "Opt", NULL +}; + +static const char *const bf_panel_out_texts[] = { + "Unknown", "Ch 1/2", "Phones", "Opt", NULL +}; + +static const char *const bf_panel_select_texts[] = { + "Left", "Right", "Both", "None", NULL +}; + +/* byte3 button flash -> event code (0 = none). The idle byte3 is 0x40; + * a press flashes the value below the base for one or two poll frames. + */ +static int bf_panel_button_decode(u8 flash) +{ + switch (flash) { + case BF_PANEL_FLASH_IN: return BF_PANEL_BTN_IN; + case BF_PANEL_FLASH_SET: return BF_PANEL_BTN_SET; + case BF_PANEL_FLASH_MIX: return BF_PANEL_BTN_MIX; + case BF_PANEL_FLASH_OUT: return BF_PANEL_BTN_OUT; + case BF_PANEL_FLASH_SELECT: return BF_PANEL_BTN_SELECT; + case BF_PANEL_FLASH_DIM: return BF_PANEL_BTN_DIM; + default: return BF_PANEL_BTN_NONE; + } +} + +/* (byte2 >> 4) & 7 = IN position 4/5/6 -> enum index (0 = not in range). */ +static int bf_panel_in_decode(u8 nib) +{ + switch (nib) { + case BF_PANEL_IN_CH12: return 1; + case BF_PANEL_IN_CH34: return 2; + case BF_PANEL_IN_OPT: return 3; + default: return 0; + } +} + +/* byte1 & 7 = OUT position. 0/1/2 for physical Ch 1/2 / Phones / Opt, + * LED-correlated capture, hardware-verified on both an original + * Babyface Pro and the Pro FS. + */ +static int bf_panel_out_decode(u8 v) +{ + switch (v) { + case 0x00: + case BF_PANEL_OUT_CH12: return 1; + case 0x01: + case BF_PANEL_OUT_PHONES: return 2; + case 0x02: + case BF_PANEL_OUT_OPT: return 3; + default: return 0; + } +} + +/* MIX-mode VU display law - monitoring dBx2 -> the 0x1A 0x000A display + * value. Piecewise-linear through the captured (dB, display) points + * (cap_mix.pcap 2026-08-23: (-62,0) (-54,1) (-48,2) (-42.5,3) + * (-35,4) (-28.4,5); cap_panel.pcap: (-7.4,10) (-6.7,11) + * (-4.6,12)) - a log-ish VU scale (coarse at the bottom, ~1.4 dB/step + * near 0). The -28..-8 dB middle is interpolated; the exact law is + * pending the cap_mixdisp.pcap full-range sweep (TODO 0g). + */ +static int bf_mix_display(int db2) +{ + static const struct { + s16 db2; + u8 disp; + } pts[] = { + { -124, 0 }, { -108, 1 }, { -96, 2 }, { -85, 3 }, + { -70, 4 }, { -57, 5 }, { -15, 10 }, { -13, 11 }, + { -9, 12 }, + }; + int i; + + if (db2 <= pts[0].db2) + return 0; + for (i = 0; i < ARRAY_SIZE(pts) - 1; i++) { + if (db2 <= pts[i + 1].db2) { + u32 num = (u32)(db2 - pts[i].db2) * + (u32)(pts[i + 1].disp - pts[i].disp); + u32 den = pts[i + 1].db2 - pts[i].db2; + + return pts[i].disp + (int)((num + den / 2) / den); + } + } + /* Above -4.6 dB: keep the last slope (2 dB/step) up to +6 dB. */ + return pts[ARRAY_SIZE(pts) - 1].disp + + clamp((db2 - pts[ARRAY_SIZE(pts) - 1].db2) / 4, 0, 12); +} + +/* The kernel driver plays the TotalMix role for the MIX button (the + * standalone emulator is hardware-validated in tuxmix-core/src/panel.rs + * + usb.rs): one wheel click in fader mode = +/-0.5 dB on the SELECT- + * chosen channel(s) of the IN-selected pair, into the OUT-selected + * output's crosspoint block - the STANDARD map only (cap_mix.pcap / + * cap_select2.pcap, no low-map mirror). Mirrors the change into the + * xpoint cache so the ALSA controls follow the wheel. Takes the mutex + * (the 0x12 writes cycle the transaction flag like the mixer puts). + */ +static void bf_panel_mix_wheel(struct snd_usb_babyface *chip, int delta) +{ + /* Canonical output of the OUT selection (enum 1 = Ch1/2, + * 2 = Phones, 3 = Opt): AN1/2, PH3/4, ADAT7/8 (the optical + * output) respectively. + */ + int out = chip->panel_out == 3 ? 5 : + chip->panel_out == 2 ? 1 : 0; + unsigned int blk = bf_xpoint_block[out]; + u8 targets[2]; + int n = 0; + int db2; + u16 raw, flag; + int i; + + /* SELECT-chosen channel(s) of the IN pair (manual sec. 5.1: SELECT + * steps left/right/both; none = nothing selected = no-op wheel). + * Source indices: AN1/AN2 = 0/1, AN3/AN4 = 2/3, AS1/2 = 4. + */ + if (chip->panel_in == 3) { + targets[0] = 4; /* Opt: the AS1/2 pair */ + n = 1; + } else if (chip->panel_select != 3) { + int base = chip->panel_in == 2 ? 2 : 0; + + targets[0] = base + (chip->panel_select == 1 ? 1 : 0); + n = 1; + if (chip->panel_select == 2) + targets[n++] = base + 1; + } + + mutex_lock(&chip->mutex); + db2 = bf_fader_raw_to_db2(chip->panel_mix_raw); + db2 = clamp(db2 + delta, BF_FADER_DB2_INF, 12); + raw = bf_fader_db2_to_raw(db2); + chip->panel_mix_raw = raw; + for (i = 0; i < n; i++) { + const struct bf_source *s = &bf_sources[targets[i]]; + + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw, + (BF_REG_CROSS_BASE_L + BF_REG_CROSS_STRIDE * blk + + s->idx_l) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, raw, + (BF_REG_CROSS_BASE_R + BF_REG_CROSS_STRIDE * blk + + s->idx_r) | flag); + chip->xpoint[out][targets[i]][0] = raw; + chip->xpoint[out][targets[i]][1] = raw; + /* MIX-mode VU display shadow (0x1A 0x000A+mic): TotalMix + * mirrors the monitoring level into the panel display family + * (cap_mix/cap_panel.pcap) - the input VU segments follow it. + * Written only on change (the captures show TotalMix updating + * it on segment crossings). Law = bf_mix_display (TODO 0g + * pending the exact full-range capture). + */ + if (targets[i] < 4) { + int disp = bf_mix_display(db2); + + if (disp != chip->panel_mix_disp[targets[i]]) { + bf_vendor_write(chip, BF_REQ_GAIN, + (u16)disp, + BF_REG_PANEL_GAIN + targets[i]); + chip->panel_mix_disp[targets[i]] = disp; + } + } + } + mutex_unlock(&chip->mutex); +} + +/* Write an output's L/R masters (8-bit companions + 16-bit with the + * transaction flag) and mirror into the cache - shared by the OUT + * volume wheel and the balance wheel. Caller holds the mutex. + */ +static void bf_panel_write_master(struct snd_usb_babyface *chip, int out, + u16 l, u16 r) +{ + u16 flag; + + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(l), + BF_REG_MASTER_8 + 2 * out); + bf_vendor_write(chip, BF_REQ_GAIN, bf_master_8bit(r), + BF_REG_MASTER_8 + 2 * out + 1); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, + (BF_REG_MASTER_16 + 2 * out) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + chip->master[out][0] = l; + chip->master[out][1] = r; + chip->muted[out] = false; + /* A Phones change while DIM is engaged re-bases the restore. */ + if (chip->dim && out == 1) { + chip->dim_saved[0] = l; + chip->dim_saved[1] = r; + } +} + +/* Front-panel OUT wheel, measured on hardware 2026-09-17 with a tone + * looped from the headphone output into IN3/IN4: + * + * - The firmware moves an analog output's level by itself when the + * wheel turns, with its own ~12 ms smoothing, even when the host + * writes nothing. See bf_out_wheel_step() for the step size. + * - A host write to the 8-bit master (the analog gain) overrides that. + * Writing it on every poll from the host's own count made the level + * saw-tooth by 1-3 dB while turning, heard as heavy zipper noise. + * - The 16-bit master does not affect the analog level, but it is the + * digital outputs' level. TotalMix writes only the 16-bit during a + * wheel gesture, and the 8-bit once the wheel is at rest. + * + * So while the wheel turns, only the 16-bit is written, and the cache + * follows the firmware's own count so the ALSA controls read the real + * level. A muted output stays muted: only the cache moves. Both + * sides move by the louder side's step, so a balance (hold-SELECT) is + * kept. Same output mapping as the MIX wheel (Phones = canon 1, + * Opt = ADAT7/8 = canon 5, else AN1/2). + */ +static void bf_panel_out_wheel_write(struct snd_usb_babyface *chip, int out, + u16 l, u16 r) +{ + u16 flag; + + if (!chip->muted[out]) { + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_CROSSPOINT, l, + (BF_REG_MASTER_16 + 2 * out) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, r, + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + } + chip->master[out][0] = l; + chip->master[out][1] = r; + /* A Phones change while DIM is engaged re-bases the restore. */ + if (chip->dim && out == 1) { + chip->dim_saved[0] = l; + chip->dim_saved[1] = r; + } +} + +/* A click that follows the previous one within this gap moves twice as + * far. In the recordings the firmware doubled clicks up to 58 ms apart + * and not from 67 ms. A normal poll every ~21 ms can't tell those + * apart, so after an OUT wheel click the panel is polled every + * BF_PANEL_FAST_POLL_MS for BF_PANEL_FAST_HOLD_MS, and each click is + * dated at the middle of the interval it was seen in. A wrong guess is + * fixed by the resync at rest below. + */ +#define BF_OUT_WHEEL_ACCEL_MS 62 +#define BF_PANEL_FAST_POLL_MS 5 +#define BF_PANEL_FAST_HOLD_MS 200 + +/* Quiet time after the last click before the resync write. */ +#define BF_OUT_WHEEL_RESYNC_MS 150 + +/* The device's own OUT wheel range: below -90 dB a side is muted. + * BF_OUT_WHEEL_MUTED is where a muted louder side is kept. + */ +#define BF_OUT_WHEEL_FLOOR (-180) /* half-dB */ +#define BF_OUT_WHEEL_MUTED (BF_OUT_WHEEL_FLOOR - 1) +/* Lowest level the 8-bit master is known to take (BF_MASTER_8_MIN). */ +#define BF_OUT_WHEEL_8BIT_MIN (BF_MASTER_8_MIN - BF_MASTER_8_0DB) + +/** + * bf_out_wheel_step - the firmware's OUT wheel step, in half-dB + * @half_db: the louder side's level before the click, in half-dB + * + * Measured on the headphone output (a tone looped into IN3/IN4, the + * driver writing nothing): + * + * -9.5 dB and up 0.5 dB per click + * -10 to -25.5 dB 1 dB + * -26 to -41.5 dB 1.5 dB + * -42 to -57.5 dB 2 dB + * -58 dB and down 3 dB + * + * A fast click moves twice as far. Both sides move by the step of the + * louder one, so a balance is kept. + */ +static int bf_out_wheel_step(int half_db) +{ + if (half_db >= -19) + return 1; + if (half_db >= -51) + return 2; + if (half_db >= -83) + return 3; + if (half_db >= -115) + return 4; + return 6; +} + +/* The wheel's view of one side: its own tracked level while the master + * is still what the wheel wrote, else the master's level. + */ +static int bf_out_wheel_level(struct snd_usb_babyface *chip, int out, int ch) +{ + u16 raw = chip->master[out][ch]; + + if (raw == chip->panel_master_last[out][ch]) + return chip->panel_out_hdb[out][ch]; + if (!raw) + return BF_OUT_WHEEL_MUTED; + return bf_master_half_db(raw); +} + +/* One click on both sides. They move together, so a balance is kept. + * When the louder side would go below the floor, both stop there and the + * louder side mutes; from mute, turning up continues from the floor + * (-90 dB up one step is -87). A quieter side below the floor is muted + * but keeps its offset, so the balance comes back as the level rises. + */ +static void bf_out_wheel_click(int db[2], int step) +{ + int louder = max(db[0], db[1]); + int shift = step; + + if (louder < BF_OUT_WHEEL_FLOOR) { + if (step < 0) + return; + shift = BF_OUT_WHEEL_FLOOR - louder + step; + } else if (louder + step < BF_OUT_WHEEL_FLOOR) { + shift = BF_OUT_WHEEL_MUTED - louder; + } else if (louder + step > 12) { + shift = 12 - louder; + } + db[0] += shift; + db[1] += shift; +} + +static void bf_panel_out_wheel(struct snd_usb_babyface *chip, int delta) +{ + int out = chip->panel_out == 3 ? 5 : + chip->panel_out == 2 ? 1 : 0; + int dir = delta > 0 ? 1 : -1; + int clicks = abs(delta); + ktime_t now = ktime_get(); + bool fast; + int ch, i, step, louder; + int db[2]; + u16 next[2]; + + chip->panel_fast_until = jiffies + + msecs_to_jiffies(BF_PANEL_FAST_HOLD_MS); + /* The click came some time since the previous poll: take the + * middle. Clicks after the first in this poll came within one + * interval, so only the first can be slow. A reversal starts a + * new turn. + */ + if (chip->panel_poll_t) + now = ktime_sub(now, + ktime_divns(ktime_sub(now, chip->panel_poll_t), 2)); + + mutex_lock(&chip->mutex); + fast = chip->panel_out_wheel_t && + dir == chip->panel_out_wheel_dir && + out == chip->panel_out_wheel_out && + ktime_ms_delta(now, chip->panel_out_wheel_t) < + BF_OUT_WHEEL_ACCEL_MS; + for (ch = 0; ch < 2; ch++) + db[ch] = bf_out_wheel_level(chip, out, ch); + for (i = 0; i < clicks; i++) { + /* Muted, the louder side counts as the floor. */ + louder = max3(db[0], db[1], BF_OUT_WHEEL_FLOOR); + step = dir * bf_out_wheel_step(louder); + if (fast || i) + step *= 2; + bf_out_wheel_click(db, step); + } + for (ch = 0; ch < 2; ch++) + next[ch] = db[ch] < BF_OUT_WHEEL_FLOOR ? 0 : + bf_master_16bit(db[ch]); + bf_panel_out_wheel_write(chip, out, next[0], next[1]); + for (ch = 0; ch < 2; ch++) { + chip->panel_master_last[out][ch] = next[ch]; + chip->panel_out_hdb[out][ch] = db[ch]; + } + chip->panel_out_wheel_t = now; + chip->panel_out_wheel_dir = dir; + chip->panel_out_wheel_out = out; + chip->panel_out_resync = true; + mutex_unlock(&chip->mutex); +} + +/* Once the wheel has rested, write the wheel's level to the 8-bit (and + * 16-bit) master, as TotalMix does at the end of a gesture. The level + * already follows the firmware's step rule, so this normally changes + * nothing audible. It fixes the rare click whose speed the driver + * guessed wrong: one zone step per such click, smoothed by the + * firmware. Nothing is written below -64 dB, where the 8-bit codes the + * device takes are unknown, nor after a mixer application has set the + * master since (that write already set the device). + */ +static void bf_panel_out_resync(struct snd_usb_babyface *chip) +{ + int out, ch; + int db[2]; + u16 flag; + + if (!chip->panel_out_resync || + ktime_ms_delta(ktime_get(), chip->panel_out_wheel_t) < + BF_OUT_WHEEL_RESYNC_MS) + return; + + mutex_lock(&chip->mutex); + chip->panel_out_resync = false; + out = chip->panel_out_wheel_out; + for (ch = 0; ch < 2; ch++) { + if (chip->master[out][ch] != chip->panel_master_last[out][ch]) + goto unlock; + db[ch] = chip->panel_out_hdb[out][ch]; + if (db[ch] < BF_OUT_WHEEL_8BIT_MIN) + goto unlock; + } + if (!chip->muted[out]) { + flag = bf_flag_cycle[chip->flag_cnt]; + chip->flag_cnt = (chip->flag_cnt + 1) & 3; + bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[0], + BF_REG_MASTER_8 + 2 * out); + bf_vendor_write(chip, BF_REQ_GAIN, BF_MASTER_8_0DB + db[1], + BF_REG_MASTER_8 + 2 * out + 1); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][0], + (BF_REG_MASTER_16 + 2 * out) | flag); + bf_vendor_write(chip, BF_REQ_CROSSPOINT, chip->master[out][1], + (BF_REG_MASTER_16 + 2 * out + 1) | flag); + } +unlock: + mutex_unlock(&chip->mutex); +} + +/* IN-mode wheel: the gain of the SELECT-chosen channel(s) of the + * IN-selected pair, +/-1 dB per click (manual sec. 5.1: SELECT steps + * left/right/both, then the wheel changes the gain). Writes the PANEL + * gain registers 0x1A 0x000A+mic (cap_select.pcap 2026-08-24 - the + * "ADC gain" family, which drives the same preamp as the GUI + * 0x0000+mic; the cache tracks the raw either way). Opt has no + * preamp and SELECT None = no target. + */ +static void bf_panel_gain_wheel(struct snd_usb_babyface *chip, int delta) +{ + u8 mics[2]; + int n = 0; + int i; + + if (chip->panel_in == 3 || chip->panel_select == 3) + return; + { + int base = chip->panel_in == 2 ? 2 : 0; + + mics[0] = base + (chip->panel_select == 1 ? 1 : 0); + n = 1; + if (chip->panel_select == 2) + mics[n++] = base + 1; + } + + mutex_lock(&chip->mutex); + for (i = 0; i < n; i++) { + int mic = mics[i]; + int db = clamp((int)chip->gain[mic] + delta, + 0, bf_gain_max_db(mic)); + u8 raw = bf_gain_raw(mic, db); + + bf_vendor_write(chip, BF_REQ_GAIN, raw, BF_REG_PANEL_GAIN + mic); + chip->gain[mic] = db; + } + mutex_unlock(&chip->mutex); +} + +/* OUT-balance wheel (hold SELECT + wheel - manual sec. 5.1 "Output + * Balance"): moves the stereo image of the OUT-selected output by + * attenuating ONE side, linear in raw (cap_pan_stereo.pcap: the varied + * side = fixed*(1-|pan|), ~0x9C raw step per click at 0 dB - the PAN + * of the stereo hardware output in TotalMix). The balance position is + * derived from the L/R master ratio (the louder side is the fixed + * one), so the gesture needs no extra state - and the OUT wheel below + * moves both sides by the same dB to preserve an existing balance. + */ +static void bf_panel_balance_wheel(struct snd_usb_babyface *chip, int delta) +{ + int out = chip->panel_out == 3 ? 5 : + chip->panel_out == 2 ? 1 : 0; + u16 l, r; + int bal; /* -100..+100; + = image right (left varies) */ + u16 fixed, varied; + + mutex_lock(&chip->mutex); + /* Read under the lock so the L/R pair is consistent with the + * master/mute/dim writers (they update chip->master[] under the + * same mutex). + */ + l = chip->master[out][0]; + r = chip->master[out][1]; + /* Balance from the L/R ratio: the louder side is the fixed one. */ + if (l >= r) { + bal = r ? -(100 - (100 * r) / l) : -100; + fixed = l; + } else { + bal = l ? (100 - (100 * l) / r) : 100; + fixed = r; + } + bal = clamp(bal + delta * 2, -100, 100); + varied = (u16)((u32)fixed * (100 - abs(bal)) / 100); + l = bal >= 0 ? varied : fixed; + r = bal >= 0 ? fixed : varied; + + bf_panel_write_master(chip, out, l, r); + mutex_unlock(&chip->mutex); +} + +/* SET press (byte3 0x42 flash): toggle 48V phantom on the + * SELECT-chosen mic(s) of the IN-selected pair. The hardware only + * does this in standalone mode (online, TotalMix ignores SET - no USB + * write in the captures), but the driver IS the host: it writes the + * preamp state itself and the P48 LEDs follow (the tuxmix-core + * emulator, hardware-verified). Restricted to IN mode + Ch1/2 (the + * phantom-capable pair); Opt/Ch3/4 and SELECT None = no target. + */ +static void bf_panel_set_phantom(struct snd_usb_babyface *chip) +{ + u16 bits = 0; + int m; + + if (chip->panel_mix || chip->panel_in != 1 || + chip->panel_select == 3) + return; + if (chip->panel_select != 1) + bits |= BF_PREAMP_48V_MIC1; + if (chip->panel_select != 0) + bits |= BF_PREAMP_48V_MIC2; + + mutex_lock(&chip->mutex); + /* One channel selected: toggle it. Both selected: ALIGN both to + * the same state, so repeated SET presses cycle all-on <-> all-off + * (a mixed phantom state cannot persist with both selected). + */ + if (chip->panel_select == 2) { + if ((chip->preamp & bits) == bits) + chip->preamp &= ~bits; + else + chip->preamp |= bits; + } else { + chip->preamp ^= bits; + } + bf_preamp_state_write(chip); + for (m = 0; m < 4; m++) + chip->panel_mix_disp[m] = 0; + mutex_unlock(&chip->mutex); +} + +static void bf_panel_notify(struct snd_usb_babyface *chip, int ctl) +{ + if (chip->panel_kctl[ctl]) + snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE, + &chip->panel_kctl[ctl]->id); +} + +/* One 0x17 read + decode. Called from the poll work; no locking needed - + * the worker is the only writer and the control get callbacks run under + * the ALSA controls lock (chip->panel_button/wheel are consumed there). + */ +static void bf_panel_tick(struct snd_usb_babyface *chip) +{ + u8 st[4]; + int delta, in, out; + bool dim; + u8 cls, pcls; + int btn; + bool mix_flash, fader_now; + + if (bf_vendor_read(chip, BF_REQ_PREAMP, BF_REG_PANEL_READ, st) < 0) + return; /* device gone / busy - retry next tick */ + + if (!chip->panel_seen) { + chip->panel_seen = true; + memcpy(chip->panel_prev, st, sizeof(st)); + /* Seed the state controls from the first snapshot. */ + in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7); + if (in) + chip->panel_in = in; + out = bf_panel_out_decode(st[1] & 0x07); + if (out) + chip->panel_out = out; + chip->panel_mix = !!(st[0] & 0x80); + chip->panel_saw_fader = (st[2] >> 4) == 0x0; + chip->panel_dim = !!(st[1] & 0x20); + return; + } + + /* The udev alsactl restore (~100 ms after probe) clobbers the host + * SELECT with a stale stored value (the control is VOLATILE but + * this alsactl stores/restores it anyway) - re-assert the device's + * power-on state (nothing selected, cycle ARMED) for the first + * ~3 s so the boot always starts in sync. + */ + if (time_is_after_jiffies(chip->panel_start + 3 * HZ)) + chip->panel_select = 3; + + /* Button flash (byte3 over the 0x40 idle base). */ + btn = bf_panel_button_decode(st[3]); + if (btn) + chip->panel_button = btn; + + /* Wheel: signed 4-bit wrap delta of the byte2 low nibble - only + * while the mode class is unchanged. A mode switch (IN 0x4x -> + * fader 0x0x on a MIX press, or the OUT counter carrying 0x8F -> + * 0x90 - the OUT counter is a full byte, cap_set2.pcap) must not + * be read as a wheel jump. Class: 0 = fader (0x0x), 1 = OUT + * (0x8x/0x9x), 2 = IN (0x4x/0x5x/0x6x). + */ + cls = (st[2] >> 4) == 0x8 || (st[2] >> 4) == 0x9 ? 1 : + (st[2] >> 4) == 0x0 ? 0 : 2; + pcls = (chip->panel_prev[2] >> 4) == 0x8 || + (chip->panel_prev[2] >> 4) == 0x9 ? 1 : + (chip->panel_prev[2] >> 4) == 0x0 ? 0 : 2; + delta = (int)(st[2] & 0x0f) - (int)(chip->panel_prev[2] & 0x0f); + if (delta > 8) + delta -= 16; + else if (delta < -8) + delta += 16; + if (delta && cls == pcls) { + chip->panel_wheel = clamp(chip->panel_wheel + delta, + SHRT_MIN, SHRT_MAX); + bf_panel_notify(chip, BF_PANEL_KCTL_WHEEL); + /* Wheel by mode (LINUX-VALIDATION sec. 12, the TotalMix + * emulator): MIX -> monitoring level, OUT (0x8x/0x9x) -> the + * selected output master (or its balance while SELECT is + * held), IN (0x4x/0x5x/0x6x) -> the SELECT-chosen preamp + * gain. + */ + if (chip->panel_mix) + bf_panel_mix_wheel(chip, delta); + else if (chip->panel_sel_hold >= 10 && cls == 1) + bf_panel_balance_wheel(chip, delta); + else if (cls == 1) + bf_panel_out_wheel(chip, delta); + else if (cls == 2) + bf_panel_gain_wheel(chip, delta); + } + bf_panel_out_resync(chip); + + /* Selections - keep the previous when the field is not in range + * (the fader-mode readback drops the IN position bits). + */ + in = bf_panel_in_decode((st[2] >> BF_PANEL_IN_SHIFT) & 0x7); + if (in && in != chip->panel_in) { + chip->panel_in = in; + /* The card CLEARS its L/R/both selection on an IN pair + * switch (user-verified 2026-08-27): re-sync the host- + * tracked SELECT so SET / the wheel / MIX target nothing + * until the user picks a channel again. This is the main + * anti-desync hook (the physical state is not readable). + */ + if (chip->panel_select != 3) { + chip->panel_select = 3; + bf_panel_notify(chip, BF_PANEL_KCTL_SELECT); + } + /* An IN-pair switch disarms the device's SELECT cycle: the + * next press only re-arms it (no step), the one after that + * cycles (device behavior, user-verified 2026-08-28). + */ + chip->panel_select_armed = false; + bf_panel_notify(chip, BF_PANEL_KCTL_IN); + } + out = bf_panel_out_decode(st[1] & 0x07); + if (out && out != chip->panel_out) { + chip->panel_out = out; + bf_panel_notify(chip, BF_PANEL_KCTL_OUT); + } + + /* SELECT press cycles the channel selection L -> R -> both -> none + * -> L (manual sec. 5.1). The state is NOT in the readback + * (panelprobe 2026-08-24), so it is tracked host-side. + */ + if (st[3] == BF_PANEL_FLASH_SELECT && + chip->panel_prev[3] != BF_PANEL_FLASH_SELECT) { + if (!chip->panel_select_armed) { + /* Disarmed (IN switch since the last step): the + * press only re-arms the cycle - the device steps on + * the NEXT press (user-verified 2026-08-28). + */ + chip->panel_select_armed = true; + } else { + chip->panel_select = (chip->panel_select + 1) & 3; + } + bf_panel_notify(chip, BF_PANEL_KCTL_SELECT); + } + /* SELECT hold (the OUT-balance gesture, manual sec. 5.1 "Output + * Balance"): a tap flashes byte3 0x50 for ~2-3 frames at 20 Hz + * (~100-150 ms - selhold_probe2), a hold keeps it sustained, and + * byte0 does NOT gain the 0x80 engaged bit - so the duration is + * the only discriminator: >= 10 ticks (200 ms at 50 Hz) = held. + */ + if (st[3] == BF_PANEL_FLASH_SELECT) + chip->panel_sel_hold++; + else + chip->panel_sel_hold = 0; + + /* SET (A) press: host-side 48V phantom toggle on the + * SELECT-chosen mic(s) (see bf_panel_set_phantom). + */ + if (st[3] == BF_PANEL_FLASH_SET && + chip->panel_prev[3] != BF_PANEL_FLASH_SET) + bf_panel_set_phantom(chip); + + /* DIM press: toggle the host-side dim, same host-in-the-loop + * arrangement as SET above. Decoding the press without acting on + * it made the button look dead with the driver alone. + */ + if (st[3] == BF_PANEL_FLASH_DIM && + chip->panel_prev[3] != BF_PANEL_FLASH_DIM) + bf_panel_toggle_dim(chip); + + /* MIX (fader mode) - HOST-latched, like TotalMix (cap_mix.pcap, + * cap_select2.pcap): the raw press readback is `0D 0D 41 44` - + * byte3 flash 0x44, NO engaged bit, byte2 still in the current + * mode. The host acks the flash with `0x17 0x8480 0x8C80` -> the + * device latches fader mode (byte0/1 gain the 0x80 bit, byte2 = + * 0x00+n counter) and STAYS there after the physical release; the + * SECOND 0x44 flash exits it (`0x17 0x0400 0x8000` + `0x8080`). + * A mode button (IN/OUT/SET) pressed during MIX makes the device + * leave fader mode by itself -> same exit writes (the user: IN + * must return to gain control). `panel_saw_fader` gates the + * device-driven exit so a pre-ack readback (byte2 still 0x4x + * while the 0x44 flash shows) never ends MIX before it started. + */ + mix_flash = st[3] == BF_PANEL_FLASH_MIX && + chip->panel_prev[3] != BF_PANEL_FLASH_MIX; + fader_now = (st[2] >> 4) == 0x0; + + if (mix_flash) { + if (chip->panel_mix) { + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000); + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080); + chip->panel_mix = false; + chip->panel_saw_fader = false; + } else { + int ref, out; + int m; + + bf_vendor_write(chip, BF_REQ_PREAMP, 0x8480, 0x8c80); + chip->panel_mix = true; + /* Seed the monitoring level at the reference + * crosspoint's current value so the first wheel + * click doesn't jump from -inf (the reference = + * the first SELECT-chosen channel of the IN pair; + * Opt = the AS1/2 pair). + */ + out = chip->panel_out == 3 ? 5 : + chip->panel_out == 2 ? 1 : 0; + ref = chip->panel_in == 3 ? 4 : + (chip->panel_in == 2 ? 2 : 0) + + (chip->panel_select == 1 ? 1 : 0); + chip->panel_mix_raw = chip->xpoint[out][ref][0]; + /* Seed the VU display shadow at the CURRENT level + * (cap_panel.pcap: TotalMix writes the display value of + * the current fader on engage - 10 in that session - + * not a hard 0; cap_mix's 0 was because the fader sat + * at the bottom). Only the channels the wheel can move. + */ + for (m = 0; m < 4; m++) + chip->panel_mix_disp[m] = 0; + if (ref < 4) { + int db2 = bf_fader_raw_to_db2(chip->panel_mix_raw); + int disp = bf_mix_display(db2); + + bf_vendor_write(chip, BF_REQ_GAIN, (u16)disp, + BF_REG_PANEL_GAIN + ref); + chip->panel_mix_disp[ref] = disp; + } + } + bf_panel_notify(chip, BF_PANEL_KCTL_MIX); + } + if (fader_now) { + chip->panel_saw_fader = true; + } else if (chip->panel_mix && chip->panel_saw_fader && + st[3] != BF_PANEL_FLASH_MIX) { + /* device left fader mode by itself (IN/OUT/SET press) */ + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8000); + bf_vendor_write(chip, BF_REQ_PREAMP, 0x0400, 0x8080); + chip->panel_mix = false; + chip->panel_saw_fader = false; + bf_panel_notify(chip, BF_PANEL_KCTL_MIX); + } + + dim = !!(st[1] & 0x20); + if (dim != chip->panel_dim) { + chip->panel_dim = dim; + bf_panel_notify(chip, BF_PANEL_KCTL_DIM); + } + + memcpy(chip->panel_prev, st, sizeof(st)); +} + +void babyface_panel_work(struct work_struct *work) +{ + struct snd_usb_babyface *chip = container_of(work, + struct snd_usb_babyface, panel_work.work); + unsigned int ms; + + if (chip->shutdown) + return; + bf_panel_tick(chip); + chip->panel_poll_t = ktime_get(); + /* Poll fast for a moment after an OUT wheel click. */ + ms = time_before(jiffies, chip->panel_fast_until) ? + BF_PANEL_FAST_POLL_MS : chip->panel_poll_ms; + schedule_delayed_work(&chip->panel_work, msecs_to_jiffies(ms)); +} + +void babyface_panel_start(struct snd_usb_babyface *chip) +{ + chip->panel_seen = false; + /* The device boots with NOTHING selected (the SELECT cycle starts + * at none -> AN1 -> AN2 -> both -> none) - the unreadable selection + * must start there too, or every later SET is off by one channel + * (host at AN1 while the LEDs show nothing -> first SELECT makes + * the device blink AN1 but the host believes AN2). + */ + chip->panel_select = 3; /* none */ + chip->panel_select_armed = true; + chip->panel_start = jiffies; + chip->panel_fast_until = jiffies; + chip->panel_poll_t = 0; + /* No master holds 0xffff: the wheel starts from the masters. */ + memset(chip->panel_master_last, 0xff, sizeof(chip->panel_master_last)); + schedule_delayed_work(&chip->panel_work, 0); +} + +void babyface_panel_stop(struct snd_usb_babyface *chip) +{ + cancel_delayed_work_sync(&chip->panel_work); +} + +/* -- controls -------------------------- */ + +/* The button/wheel controls hold the LATEST state and are NOT consumed + * on read: wireplumber subscribes to every notifying control and reads + * it, so a clear-on-get would let another reader eat the event. Each + * consumer tracks its own baseline and acts on changes (the button is a + * last-press code, the wheel an accumulated signed delta). VOLATILE + * keeps alsactl from caching them. + */ +static int bf_panel_button_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = BF_PANEL_BTN_DIM; + uinfo->value.integer.step = 1; + return 0; +} + +static int bf_panel_button_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] = chip->panel_button; + return 0; +} + +static int bf_panel_wheel_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = SHRT_MIN; + uinfo->value.integer.max = SHRT_MAX; + uinfo->value.integer.step = 1; + return 0; +} + +static int bf_panel_wheel_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] = chip->panel_wheel; + return 0; +} + +static int bf_panel_in_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_in_texts); +} + +static int bf_panel_in_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] = chip->panel_in; + return 0; +} + +static int bf_panel_out_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_out_texts); +} + +static int bf_panel_out_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] = chip->panel_out; + return 0; +} + +static int bf_panel_select_info(struct snd_kcontrol *kctl, + struct snd_ctl_elem_info *uinfo) +{ + return snd_ctl_enum_info(uinfo, 1, 4, bf_panel_select_texts); +} + +static int bf_panel_select_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.enumerated.item[0] = chip->panel_select; + return 0; +} + +/* Writable so software (or the user, after a driver reload) can + * re-sync the host-tracked SELECT state to the physical card - the + * L/R/both/none state is NOT in the 0x17 readback, so a reload starts + * at "Left" while the card may sit at any position; a desync makes + * SET / the wheel / MIX target the wrong channel. Writing the + * physical state re-aligns the emulation (TotalMix parity: it also + * lets software select channels directly). + */ +static int bf_panel_select_put(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + unsigned int v = ucontrol->value.enumerated.item[0]; + int ret = 0; + + if (v > 3) + return -EINVAL; + if (v != chip->panel_select) { + chip->panel_select = v; + bf_panel_notify(chip, BF_PANEL_KCTL_SELECT); + ret = 1; + } + return ret; +} + +/* Shared boolean get - private_value selects mix (0) / dim (1). */ +static int bf_panel_bool_get(struct snd_kcontrol *kctl, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_usb_babyface *chip = snd_kcontrol_chip(kctl); + + ucontrol->value.integer.value[0] = + kctl->private_value ? chip->panel_dim : chip->panel_mix; + return 0; +} + +int babyface_create_panel(struct snd_usb_babyface *chip) +{ + struct snd_kcontrol *kctl; + int err; + + memset(chip->panel_kctl, 0, sizeof(chip->panel_kctl)); + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Button", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_button_info, + .get = bf_panel_button_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_BUTTON] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Wheel", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_wheel_info, + .get = bf_panel_wheel_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_WHEEL] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel In", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_in_info, + .get = bf_panel_in_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_IN] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Out", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_out_info, + .get = bf_panel_out_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_OUT] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Mix", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = snd_ctl_boolean_mono_info, + .get = bf_panel_bool_get, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_MIX] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Dim", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = snd_ctl_boolean_mono_info, + .get = bf_panel_bool_get, + .private_value = 1, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_DIM] = kctl; + + kctl = snd_ctl_new1(&(struct snd_kcontrol_new){ + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Front Panel Select", + .access = SNDRV_CTL_ELEM_ACCESS_READ | + SNDRV_CTL_ELEM_ACCESS_WRITE | + SNDRV_CTL_ELEM_ACCESS_VOLATILE, + .info = bf_panel_select_info, + .get = bf_panel_select_get, + .put = bf_panel_select_put, + }, chip); + err = snd_ctl_add(chip->card, kctl); + if (err < 0) + return err; + chip->panel_kctl[BF_PANEL_KCTL_SELECT] = kctl; + + return 0; +} + diff --git a/sound/usb/babyfacepro/babyfacepro.c b/sound/usb/babyfacepro/babyfacepro.c index ccf3dbf36..8cf68e404 100644 --- a/sound/usb/babyfacepro/babyfacepro.c +++ b/sound/usb/babyfacepro/babyfacepro.c @@ -1272,6 +1272,7 @@ static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX; static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR; static int frames_per_urb = BF_FRAMES_PER_URB_DEFAULT; static int nurbs = BF_NURBS_DEFAULT; +static int panel_poll_ms = BF_PANEL_POLL_MS_DEFAULT; module_param_array(index, int, NULL, 0444); MODULE_PARM_DESC(index, "Index value for the Babyface Pro sound card."); @@ -1281,6 +1282,8 @@ module_param(frames_per_urb, int, 0644); MODULE_PARM_DESC(frames_per_urb, "Audio frames per URB, 8..1024 (16 = low-latency floor, 256 = default)."); module_param(nurbs, int, 0644); MODULE_PARM_DESC(nurbs, "URBs in flight per direction, 1..16 (16 = low-latency)."); +module_param(panel_poll_ms, int, 0644); +MODULE_PARM_DESC(panel_poll_ms, "Front-panel poll interval in ms, 10..1000 (20 = default, matches Windows' ~50 Hz)."); /* -- USB driver ------------------------- */ @@ -1366,10 +1369,12 @@ static int babyface_probe(struct usb_interface *intf, chip->alt = BF_ALT_1; chip->frame_bytes = 56; chip->preamp = BF_PREAMP_BASE; + chip->panel_poll_ms = clamp(panel_poll_ms, 10, 1000); mutex_init(&chip->mutex); spin_lock_init(&chip->lock); atomic_set(&chip->urb_err, 0); INIT_WORK(&chip->stream_work, babyface_stream_work); + INIT_DELAYED_WORK(&chip->panel_work, babyface_panel_work); chip->card->private_free = babyface_private_free; /* Model-neutral on purpose. The FS and the original (2015) @@ -1524,12 +1529,23 @@ static int babyface_probe(struct usb_interface *intf, goto error; } + err = babyface_create_panel(chip); + if (err < 0) { + dev_err(&intf->dev, "front-panel control creation failed: %d\n", err); + goto error; + } + err = snd_card_register(chip->card); if (err < 0) { dev_err(&intf->dev, "snd_card_register failed: %d\n", err); goto error; } + /* The panel poll mirrors the physical buttons/wheel into the + * Front Panel controls; it runs for the whole card lifetime. + */ + babyface_panel_start(chip); + usb_set_intfdata(intf, chip); dev_info(&intf->dev, "Babyface Pro: card %i, %u frames/URB, %u URBs/direction\n", @@ -1569,6 +1585,7 @@ static void babyface_disconnect(struct usb_interface *intf) chip->shutdown = true; cancel_work_sync(&chip->stream_work); + babyface_panel_stop(chip); /* Balance the probe()-time usb_disable_autosuspend(): the usb_device * outlives this interface claim (a usbfs detach re-probes without * the physical device ever disconnecting), so leaving autosuspend @@ -1605,6 +1622,7 @@ static int babyface_suspend(struct usb_interface *intf, pm_message_t message) snd_pcm_suspend_all(sdev->device_data); } cancel_work_sync(&chip->stream_work); + babyface_panel_stop(chip); mutex_lock(&chip->mutex); if (chip->streaming) babyface_stream_kill(chip); @@ -1635,6 +1653,8 @@ static int babyface_resume(struct usb_interface *intf) err = babyface_restore_state(chip); out: mutex_unlock(&chip->mutex); + if (!err) + babyface_panel_start(chip); return err; } diff --git a/sound/usb/babyfacepro/babyfacepro.h b/sound/usb/babyfacepro/babyfacepro.h index 9822db312..ba1225de6 100644 --- a/sound/usb/babyfacepro/babyfacepro.h +++ b/sound/usb/babyfacepro/babyfacepro.h @@ -45,6 +45,7 @@ * bf_clock_write / bf_pitch_write. */ +#include <linux/ktime.h> #include <linux/module.h> #include <linux/mutex.h> #include <linux/unaligned.h> @@ -182,6 +183,53 @@ */ #define BF_LOOPBACK_CHANNELS 30 +/* Front-panel poll interval default - Windows polls the 5-register + * status set at ~50 cycles/s (20 ms); match that. Tunable via the + * panel_poll_ms module param for reviewers/distros who want a slower + * (or faster) rate than the Windows-matching default. + */ +#define BF_PANEL_POLL_MS_DEFAULT 20 + +/* Front-panel readback (babyfacepro-ctl.c): 0x17 read at wIdx 0x0000 - the index + * the Windows driver polls (cap_buttons2.pcap). byte0 = preamp 48V/PAD, + * byte1 = OUT sel + DIM/MIX bits, byte2 = IN sel + wheel counter, + * byte3 = button flash (see babyfacepro-ctl.c for the full layout). + */ +#define BF_REG_PANEL_READ 0x0000 +#define BF_PANEL_IN_SHIFT 4 +#define BF_PANEL_IN_CH12 0x04 +#define BF_PANEL_IN_CH34 0x05 +#define BF_PANEL_IN_OPT 0x06 +/* OUT selection - the gain-display-mode encoding (cap_dim.pcap); + * babyfacepro-ctl.c also accepts the base-mode 0x01/0x02 (cap_buttons.pcap). + */ +#define BF_PANEL_OUT_CH12 0x04 +#define BF_PANEL_OUT_PHONES 0x05 +#define BF_PANEL_OUT_OPT 0x06 +#define BF_PANEL_FLASH_IN 0x41 +#define BF_PANEL_FLASH_SET 0x42 +#define BF_PANEL_FLASH_MIX 0x44 +#define BF_PANEL_FLASH_OUT 0x48 +#define BF_PANEL_FLASH_SELECT 0x50 +#define BF_PANEL_FLASH_DIM 0x60 +#define BF_PANEL_BTN_NONE 0 +#define BF_PANEL_BTN_IN 1 +#define BF_PANEL_BTN_SET 2 +#define BF_PANEL_BTN_MIX 3 +#define BF_PANEL_BTN_OUT 4 +#define BF_PANEL_BTN_SELECT 5 +#define BF_PANEL_BTN_DIM 6 + +/* The front-panel gain/display family (0x1A, wIdx 0x000A + mic 0-3; + * cap_panel/cap_mix.pcap): in gain mode the wheel writes the "ADC + * gain" here (drives the same preamp as the GUI 0x0000+mic); in MIX + * (fader) mode the same registers carry the VU DISPLAY shadow - + * TotalMix writes the monitoring level display value (0..~31) and the + * card lights the input VU segments accordingly (hardware-verified + * 2026-08-26 live: sweeping 0x1A values moved the input VU). + */ +#define BF_REG_PANEL_GAIN 0x000a + /* The "cross" register block within each output: the L-registers sit at * odd offsets 5..23 and the R-registers at even offsets 4..22 (the stereo * source pairs that can be cross-linked). bf_crosspoint_clear_cross() @@ -309,6 +357,54 @@ struct snd_usb_babyface { u16 dim_saved[2]; /* pre-DIM Phones master (out 1 L/R) */ bool dim; /* DIM engaged (fixed -20 dB on Phones) */ struct snd_kcontrol *dim_kctl; /* for snd_ctl_notify */ + + /* front panel (babyfacepro-ctl.c) - 0x17 readback poll */ + struct delayed_work panel_work; + unsigned int panel_poll_ms; /* front-panel poll interval, module param */ + u8 panel_prev[4]; /* last 0x17 snapshot */ + bool panel_seen; /* first snapshot taken */ + bool panel_select_armed; /* device SELECT cycle armed (IN switch disarms) */ + unsigned long panel_start; /* jiffies at panel_start (boot re-assert) */ + int panel_button; /* latched button event (consumed on get) */ + int panel_wheel; /* accumulated wheel delta (consumed on get) */ + int panel_in; /* enum: 0 unknown, 1 Ch1/2, 2 Ch3/4, 3 Opt */ + int panel_out; /* enum: 0 unknown, 1 Ch1/2, 2 Phones, 3 Opt */ + bool panel_mix; /* MIX engaged - HOST-latched (like TotalMix): + * set by the 0x44 flash ack, NOT by the readback + * 0x80 bit (the raw press has none) + */ + bool panel_dim; /* DIM sticky (byte1 bit 0x20) */ + bool panel_saw_fader; /* device observed in fader mode (byte2 0x0x) + * - gates the device-driven MIX exit + */ + int panel_select; /* SELECT state: 0 L, 1 R, 2 both, 3 none + * (host-tracked - not in the readback) + */ + int panel_sel_hold; /* consecutive ticks with byte3 = 0x50 + * (SELECT held > 200 ms = the OUT-balance + * gesture; a tap flashes only ~100-150 ms, + * selhold_probe2 - no engaged bit) + */ + /* The OUT wheel's own level per side, in half-dB, valid while the + * master still holds the value the wheel last wrote (the 16-bit + * value can't hold every half-dB step, nor the device's range + * below -64 dB, nor its mute). 0xffff = nothing written yet. + */ + u16 panel_master_last[6][2]; + s16 panel_out_hdb[6][2]; + /* OUT wheel acceleration and resync at rest (babyfacepro-ctl.c) */ + ktime_t panel_out_wheel_t; /* estimated time of the last count */ + ktime_t panel_poll_t; /* when the previous poll ran */ + unsigned long panel_fast_until; /* jiffies: poll fast until then */ + int panel_out_wheel_dir; /* its direction, +1 or -1 */ + int panel_out_wheel_out; /* the output it moved */ + bool panel_out_resync; /* 8-bit write due once at rest */ + u16 panel_mix_raw; /* MIX-mode monitoring level (fader raw) */ + u8 panel_mix_disp[4]; /* MIX-mode VU display shadow per mic + * (0x1A 0x000A+mic - written on change + * so the input VU follows the wheel) + */ + struct snd_kcontrol *panel_kctl[7]; /* for snd_ctl_notify */ }; /* The mixer state cached across interface re-probes/resume (see @@ -394,6 +490,10 @@ u8 bf_gain_raw(int mic, int db); int bf_loopback_write_map(struct snd_usb_babyface *chip, int out, bool on); void bf_panel_toggle_dim(struct snd_usb_babyface *chip); int babyface_create_flags(struct snd_usb_babyface *chip); +int babyface_create_panel(struct snd_usb_babyface *chip); +void babyface_panel_start(struct snd_usb_babyface *chip); +void babyface_panel_stop(struct snd_usb_babyface *chip); +void babyface_panel_work(struct work_struct *work); /* Master gain-law helpers - shared with the front-panel wheels once * the front panel lands. -- 2.55.0