Add mic ON/OFF toggle, headphone and studio output busses
- Channels 0-1 are now dedicated Mic 1/Mic 2 with mic_on toggle - Mic audio only routes to main when mic_on=True, never to headphone - New headphone_out_L/R JACK ports (all channels except mics) - New studio_out_L/R JACK ports (copies main, mutes when any mic is on) - Mic ON/OFF toggle button in mixer UI (hidden on non-mic channels) - Session save/load includes mic_on state
This commit is contained in:
+67
-4
@@ -15,8 +15,8 @@ FFMPEG_FORMATS = {'.mp3', '.m4a', '.mp4', '.opus', '.aac', '.wma'}
|
||||
|
||||
NUM_MIXER_CHANNELS = 8
|
||||
DEFAULT_CHANNEL_NAMES = [
|
||||
"Turntable 1", "Turntable 2", "Mic", "Aux 1",
|
||||
"Aux 2", "Aux 3", "Aux 4", "Aux 5",
|
||||
"Mic 1", "Mic 2", "Aux 1",
|
||||
"Aux 2", "Aux 3", "Aux 4", "Aux 5", "Aux 6",
|
||||
]
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ class MixerChannel:
|
||||
self.muted = False
|
||||
self.solo = False
|
||||
self.pfl = False
|
||||
self.mic_on = False
|
||||
self.vu_peak = 0.0
|
||||
self.in_port_L = None
|
||||
self.in_port_R = None
|
||||
@@ -285,6 +286,18 @@ class AudioEngine:
|
||||
self.client.outports.register("master_out_R"),
|
||||
)
|
||||
|
||||
# Headphone output ports (all channels except mics)
|
||||
self.headphone_out = (
|
||||
self.client.outports.register("headphone_out_L"),
|
||||
self.client.outports.register("headphone_out_R"),
|
||||
)
|
||||
|
||||
# Studio output ports (same as main, mutes when any mic is on)
|
||||
self.studio_out = (
|
||||
self.client.outports.register("studio_out_L"),
|
||||
self.client.outports.register("studio_out_R"),
|
||||
)
|
||||
|
||||
# PFL output ports
|
||||
self.pfl_out = (
|
||||
self.client.outports.register("pfl_out_L"),
|
||||
@@ -325,6 +338,15 @@ class AudioEngine:
|
||||
if 0 <= index < NUM_MIXER_CHANNELS:
|
||||
self.mixer['channels'][index].pfl = pfl
|
||||
|
||||
def set_channel_mic_on(self, index: int, on: bool):
|
||||
if 0 <= index < NUM_MIXER_CHANNELS:
|
||||
self.mixer['channels'][index].mic_on = on
|
||||
|
||||
def get_channel_mic_on(self, index: int) -> bool:
|
||||
if 0 <= index < NUM_MIXER_CHANNELS:
|
||||
return self.mixer['channels'][index].mic_on
|
||||
return False
|
||||
|
||||
def _fill_port(self, port_L, port_R, deck: DeckState, frames: int):
|
||||
buf_L = port_L.get_array()
|
||||
buf_R = port_R.get_array()
|
||||
@@ -423,7 +445,18 @@ class AudioEngine:
|
||||
master_buf_L.fill(0.0)
|
||||
master_buf_R.fill(0.0)
|
||||
|
||||
headphone_buf_L = self.headphone_out[0].get_array()
|
||||
headphone_buf_R = self.headphone_out[1].get_array()
|
||||
headphone_buf_L.fill(0.0)
|
||||
headphone_buf_R.fill(0.0)
|
||||
|
||||
studio_buf_L = self.studio_out[0].get_array()
|
||||
studio_buf_R = self.studio_out[1].get_array()
|
||||
studio_buf_L.fill(0.0)
|
||||
studio_buf_R.fill(0.0)
|
||||
|
||||
any_solo = any(ch.solo for ch in self.mixer['channels'])
|
||||
any_mic_on = any(ch.mic_on for ch in self.mixer['channels'][:2])
|
||||
|
||||
# PFL bus
|
||||
pfl_buf_L = self.pfl_out[0].get_array()
|
||||
@@ -436,6 +469,7 @@ class AudioEngine:
|
||||
in_R = ch.in_port_R.get_array()
|
||||
|
||||
mute = ch.muted or (any_solo and not ch.solo)
|
||||
is_mic = ch.index < 2
|
||||
|
||||
# PFL: pre-fader, pre-mute signal to PFL bus
|
||||
if ch.pfl:
|
||||
@@ -447,16 +481,31 @@ class AudioEngine:
|
||||
continue
|
||||
|
||||
vol = ch.volume
|
||||
goes_to_main = not is_mic or ch.mic_on
|
||||
goes_to_headphone = not is_mic
|
||||
|
||||
peak = 0.0
|
||||
for i in range(frames):
|
||||
s_L = in_L[i] * vol
|
||||
s_R = in_R[i] * vol
|
||||
master_buf_L[i] += s_L
|
||||
master_buf_R[i] += s_R
|
||||
if goes_to_main:
|
||||
master_buf_L[i] += s_L
|
||||
master_buf_R[i] += s_R
|
||||
if goes_to_headphone:
|
||||
headphone_buf_L[i] += s_L
|
||||
headphone_buf_R[i] += s_R
|
||||
peak = max(peak, abs(s_L), abs(s_R))
|
||||
|
||||
ch.vu_peak = peak
|
||||
|
||||
# ── Studio bus: same as main, muted when any mic is on ────────
|
||||
if any_mic_on:
|
||||
studio_buf_L.fill(0.0)
|
||||
studio_buf_R.fill(0.0)
|
||||
else:
|
||||
studio_buf_L[:] = master_buf_L[:]
|
||||
studio_buf_R[:] = master_buf_R[:]
|
||||
|
||||
# ── Master DSP ──────────────────────────────────────────────────
|
||||
master_audio = np.array([master_buf_L, master_buf_R])
|
||||
|
||||
@@ -466,6 +515,20 @@ class AudioEngine:
|
||||
master_buf_L[:] = master_audio[0]
|
||||
master_buf_R[:] = master_audio[1]
|
||||
|
||||
# Apply same DSP to headphone (without mics)
|
||||
hp_audio = np.array([headphone_buf_L, headphone_buf_R])
|
||||
self.mixer['master'].compressor.process(hp_audio)
|
||||
self.mixer['master'].limiter.process(hp_audio)
|
||||
headphone_buf_L[:] = hp_audio[0]
|
||||
headphone_buf_R[:] = hp_audio[1]
|
||||
|
||||
# Apply same DSP to studio
|
||||
studio_audio = np.array([studio_buf_L, studio_buf_R])
|
||||
self.mixer['master'].compressor.process(studio_audio)
|
||||
self.mixer['master'].limiter.process(studio_audio)
|
||||
studio_buf_L[:] = studio_audio[0]
|
||||
studio_buf_R[:] = studio_audio[1]
|
||||
|
||||
master_peak = max(np.max(np.abs(master_audio[0])), np.max(np.abs(master_audio[1])))
|
||||
self.mixer['master'].vu_peak = master_peak
|
||||
|
||||
|
||||
@@ -279,6 +279,40 @@ def icon_solo(color: str = "#ffaa00") -> QIcon:
|
||||
return _make_icon(paint, color)
|
||||
|
||||
|
||||
def icon_mic(color: str = "#00ff41") -> QIcon:
|
||||
def paint(p, r, c):
|
||||
p.setPen(Qt.NoPen)
|
||||
p.setBrush(c)
|
||||
# Mic body - pill/rectangle shape
|
||||
bw = r.width() * 0.35
|
||||
bh = r.height() * 0.45
|
||||
bx = r.center().x() - bw / 2
|
||||
by = r.top() + r.height() * 0.05
|
||||
p.drawRoundedRect(QRectF(bx, by, bw, bh), bw * 0.3, bw * 0.3)
|
||||
# Mic arc at top
|
||||
path = QPainterPath()
|
||||
arc_w = bw * 0.6
|
||||
arc_h = r.height() * 0.08
|
||||
path.moveTo(r.center().x() - arc_w / 2, by)
|
||||
path.quadTo(r.center().x(), by - arc_h, r.center().x() + arc_w / 2, by)
|
||||
p.drawPath(path)
|
||||
# Stand lines down from body
|
||||
pen = QPen(c, r.width() * 0.06)
|
||||
pen.setCapStyle(Qt.RoundCap)
|
||||
p.setPen(pen)
|
||||
p.setBrush(Qt.NoBrush)
|
||||
stand_top = by + bh
|
||||
p.drawLine(QPointF(r.center().x(), stand_top),
|
||||
QPointF(r.center().x(), stand_top + r.height() * 0.15))
|
||||
# Base
|
||||
base_w = r.width() * 0.45
|
||||
base_h = r.height() * 0.1
|
||||
p.drawRoundedRect(QRectF(r.center().x() - base_w / 2,
|
||||
stand_top + r.height() * 0.12,
|
||||
base_w, base_h), base_w * 0.15, base_w * 0.15)
|
||||
return _make_icon(paint, color)
|
||||
|
||||
|
||||
def icon_cog(color: str = "#aaaaaa") -> QIcon:
|
||||
def paint(p, r, c):
|
||||
pen = QPen(c, r.width() * 0.1)
|
||||
|
||||
@@ -196,6 +196,7 @@ class RadioPanelWindow(QMainWindow):
|
||||
'muted': ch.muted,
|
||||
'solo': ch.solo,
|
||||
'pfl': ch.pfl,
|
||||
'mic_on': ch.mic_on,
|
||||
})
|
||||
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
@@ -226,6 +227,7 @@ class RadioPanelWindow(QMainWindow):
|
||||
ch.muted = ch_state.get('muted', ch.muted)
|
||||
ch.solo = ch_state.get('solo', ch.solo)
|
||||
ch.pfl = ch_state.get('pfl', ch.pfl)
|
||||
ch.mic_on = ch_state.get('mic_on', ch.mic_on)
|
||||
|
||||
self.mixer.refresh_strips()
|
||||
|
||||
|
||||
+32
-1
@@ -9,7 +9,7 @@ from PySide6.QtCore import Qt, QTimer, QSize
|
||||
from PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
|
||||
|
||||
from audio_engine import NUM_MIXER_CHANNELS
|
||||
from icons import icon_mute, icon_solo
|
||||
from icons import icon_mute, icon_solo, icon_mic
|
||||
import math
|
||||
|
||||
|
||||
@@ -214,6 +214,28 @@ class ChannelStrip(QFrame):
|
||||
""")
|
||||
btn_row.addWidget(self.solo_btn)
|
||||
|
||||
# Mic ON/OFF button for channels 0 and 1
|
||||
self.mic_btn = QPushButton()
|
||||
self.mic_btn.setFixedSize(28, 24)
|
||||
self.mic_btn.setIconSize(QSize(16, 16))
|
||||
self.mic_btn.setIcon(icon_mic("#888"))
|
||||
self.mic_btn.setCheckable(True)
|
||||
self.mic_btn.setToolTip("Mic ON – audio goes to main")
|
||||
self.mic_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #202426;
|
||||
border: 1px solid #303a3a;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #1a3a1a;
|
||||
border: 1px solid #00cc66;
|
||||
}
|
||||
QPushButton:hover { background-color: #303436; }
|
||||
""")
|
||||
self.mic_btn.setVisible(self.channel_index < 2)
|
||||
btn_row.addWidget(self.mic_btn)
|
||||
|
||||
self.pfl_btn = QPushButton("PFL")
|
||||
self.pfl_btn.setFixedSize(28, 24)
|
||||
self.pfl_btn.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
||||
@@ -242,6 +264,7 @@ class ChannelStrip(QFrame):
|
||||
self.volume_slider.valueChanged.connect(self._on_volume_changed)
|
||||
self.mute_btn.toggled.connect(self._on_mute_toggled)
|
||||
self.solo_btn.toggled.connect(self._on_solo_toggled)
|
||||
self.mic_btn.toggled.connect(self._on_mic_toggled)
|
||||
self.pfl_btn.toggled.connect(self._on_pfl_toggled)
|
||||
|
||||
def _rename(self):
|
||||
@@ -266,9 +289,17 @@ class ChannelStrip(QFrame):
|
||||
def _on_pfl_toggled(self, checked: bool):
|
||||
self.engine.set_channel_pfl(self.channel_index, checked)
|
||||
|
||||
def _on_mic_toggled(self, checked: bool):
|
||||
self.engine.set_channel_mic_on(self.channel_index, checked)
|
||||
color = "#00cc66" if checked else "#888"
|
||||
self.mic_btn.setIcon(icon_mic(color))
|
||||
|
||||
def _refresh(self):
|
||||
self.vu_meter.set_value(self._channel.vu_peak)
|
||||
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
||||
if self.channel_index < 2:
|
||||
color = "#00cc66" if self._channel.mic_on else "#888"
|
||||
self.mic_btn.setIcon(icon_mic(color))
|
||||
|
||||
|
||||
class MasterPreSection(QFrame):
|
||||
|
||||
Reference in New Issue
Block a user