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
|
NUM_MIXER_CHANNELS = 8
|
||||||
DEFAULT_CHANNEL_NAMES = [
|
DEFAULT_CHANNEL_NAMES = [
|
||||||
"Turntable 1", "Turntable 2", "Mic", "Aux 1",
|
"Mic 1", "Mic 2", "Aux 1",
|
||||||
"Aux 2", "Aux 3", "Aux 4", "Aux 5",
|
"Aux 2", "Aux 3", "Aux 4", "Aux 5", "Aux 6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -163,6 +163,7 @@ class MixerChannel:
|
|||||||
self.muted = False
|
self.muted = False
|
||||||
self.solo = False
|
self.solo = False
|
||||||
self.pfl = False
|
self.pfl = False
|
||||||
|
self.mic_on = False
|
||||||
self.vu_peak = 0.0
|
self.vu_peak = 0.0
|
||||||
self.in_port_L = None
|
self.in_port_L = None
|
||||||
self.in_port_R = None
|
self.in_port_R = None
|
||||||
@@ -285,6 +286,18 @@ class AudioEngine:
|
|||||||
self.client.outports.register("master_out_R"),
|
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
|
# PFL output ports
|
||||||
self.pfl_out = (
|
self.pfl_out = (
|
||||||
self.client.outports.register("pfl_out_L"),
|
self.client.outports.register("pfl_out_L"),
|
||||||
@@ -325,6 +338,15 @@ class AudioEngine:
|
|||||||
if 0 <= index < NUM_MIXER_CHANNELS:
|
if 0 <= index < NUM_MIXER_CHANNELS:
|
||||||
self.mixer['channels'][index].pfl = pfl
|
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):
|
def _fill_port(self, port_L, port_R, deck: DeckState, frames: int):
|
||||||
buf_L = port_L.get_array()
|
buf_L = port_L.get_array()
|
||||||
buf_R = port_R.get_array()
|
buf_R = port_R.get_array()
|
||||||
@@ -423,7 +445,18 @@ class AudioEngine:
|
|||||||
master_buf_L.fill(0.0)
|
master_buf_L.fill(0.0)
|
||||||
master_buf_R.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_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 bus
|
||||||
pfl_buf_L = self.pfl_out[0].get_array()
|
pfl_buf_L = self.pfl_out[0].get_array()
|
||||||
@@ -436,6 +469,7 @@ class AudioEngine:
|
|||||||
in_R = ch.in_port_R.get_array()
|
in_R = ch.in_port_R.get_array()
|
||||||
|
|
||||||
mute = ch.muted or (any_solo and not ch.solo)
|
mute = ch.muted or (any_solo and not ch.solo)
|
||||||
|
is_mic = ch.index < 2
|
||||||
|
|
||||||
# PFL: pre-fader, pre-mute signal to PFL bus
|
# PFL: pre-fader, pre-mute signal to PFL bus
|
||||||
if ch.pfl:
|
if ch.pfl:
|
||||||
@@ -447,16 +481,31 @@ class AudioEngine:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
vol = ch.volume
|
vol = ch.volume
|
||||||
|
goes_to_main = not is_mic or ch.mic_on
|
||||||
|
goes_to_headphone = not is_mic
|
||||||
|
|
||||||
peak = 0.0
|
peak = 0.0
|
||||||
for i in range(frames):
|
for i in range(frames):
|
||||||
s_L = in_L[i] * vol
|
s_L = in_L[i] * vol
|
||||||
s_R = in_R[i] * vol
|
s_R = in_R[i] * vol
|
||||||
master_buf_L[i] += s_L
|
if goes_to_main:
|
||||||
master_buf_R[i] += s_R
|
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))
|
peak = max(peak, abs(s_L), abs(s_R))
|
||||||
|
|
||||||
ch.vu_peak = peak
|
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 DSP ──────────────────────────────────────────────────
|
||||||
master_audio = np.array([master_buf_L, master_buf_R])
|
master_audio = np.array([master_buf_L, master_buf_R])
|
||||||
|
|
||||||
@@ -466,6 +515,20 @@ class AudioEngine:
|
|||||||
master_buf_L[:] = master_audio[0]
|
master_buf_L[:] = master_audio[0]
|
||||||
master_buf_R[:] = master_audio[1]
|
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])))
|
master_peak = max(np.max(np.abs(master_audio[0])), np.max(np.abs(master_audio[1])))
|
||||||
self.mixer['master'].vu_peak = master_peak
|
self.mixer['master'].vu_peak = master_peak
|
||||||
|
|
||||||
|
|||||||
@@ -279,6 +279,40 @@ def icon_solo(color: str = "#ffaa00") -> QIcon:
|
|||||||
return _make_icon(paint, color)
|
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 icon_cog(color: str = "#aaaaaa") -> QIcon:
|
||||||
def paint(p, r, c):
|
def paint(p, r, c):
|
||||||
pen = QPen(c, r.width() * 0.1)
|
pen = QPen(c, r.width() * 0.1)
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ class RadioPanelWindow(QMainWindow):
|
|||||||
'muted': ch.muted,
|
'muted': ch.muted,
|
||||||
'solo': ch.solo,
|
'solo': ch.solo,
|
||||||
'pfl': ch.pfl,
|
'pfl': ch.pfl,
|
||||||
|
'mic_on': ch.mic_on,
|
||||||
})
|
})
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
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.muted = ch_state.get('muted', ch.muted)
|
||||||
ch.solo = ch_state.get('solo', ch.solo)
|
ch.solo = ch_state.get('solo', ch.solo)
|
||||||
ch.pfl = ch_state.get('pfl', ch.pfl)
|
ch.pfl = ch_state.get('pfl', ch.pfl)
|
||||||
|
ch.mic_on = ch_state.get('mic_on', ch.mic_on)
|
||||||
|
|
||||||
self.mixer.refresh_strips()
|
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 PySide6.QtGui import QFont, QPainter, QColor, QLinearGradient, QPen
|
||||||
|
|
||||||
from audio_engine import NUM_MIXER_CHANNELS
|
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
|
import math
|
||||||
|
|
||||||
|
|
||||||
@@ -214,6 +214,28 @@ class ChannelStrip(QFrame):
|
|||||||
""")
|
""")
|
||||||
btn_row.addWidget(self.solo_btn)
|
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 = QPushButton("PFL")
|
||||||
self.pfl_btn.setFixedSize(28, 24)
|
self.pfl_btn.setFixedSize(28, 24)
|
||||||
self.pfl_btn.setFont(QFont("Segoe UI", 7, QFont.Bold))
|
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.volume_slider.valueChanged.connect(self._on_volume_changed)
|
||||||
self.mute_btn.toggled.connect(self._on_mute_toggled)
|
self.mute_btn.toggled.connect(self._on_mute_toggled)
|
||||||
self.solo_btn.toggled.connect(self._on_solo_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)
|
self.pfl_btn.toggled.connect(self._on_pfl_toggled)
|
||||||
|
|
||||||
def _rename(self):
|
def _rename(self):
|
||||||
@@ -266,9 +289,17 @@ class ChannelStrip(QFrame):
|
|||||||
def _on_pfl_toggled(self, checked: bool):
|
def _on_pfl_toggled(self, checked: bool):
|
||||||
self.engine.set_channel_pfl(self.channel_index, checked)
|
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):
|
def _refresh(self):
|
||||||
self.vu_meter.set_value(self._channel.vu_peak)
|
self.vu_meter.set_value(self._channel.vu_peak)
|
||||||
self.volume_slider.setValue(int(math.sqrt(self._channel.volume) * 100))
|
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):
|
class MasterPreSection(QFrame):
|
||||||
|
|||||||
Reference in New Issue
Block a user