diff --git a/backend/main.py b/backend/main.py
index 61088e0..711b1ec 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -3677,6 +3677,7 @@ class AudioDeviceSettings(BaseModel):
music_channel: Optional[int] = None
sfx_channel: Optional[int] = None
ad_channel: Optional[int] = None
+ ident_channel: Optional[int] = None
monitor_device: Optional[int] = None
monitor_channel: Optional[int] = None
phone_filter: Optional[bool] = None
@@ -3716,6 +3717,7 @@ async def set_audio_settings(settings: AudioDeviceSettings):
music_channel=settings.music_channel,
sfx_channel=settings.sfx_channel,
ad_channel=settings.ad_channel,
+ ident_channel=settings.ident_channel,
monitor_device=settings.monitor_device,
monitor_channel=settings.monitor_channel,
phone_filter=settings.phone_filter
diff --git a/backend/services/audio.py b/backend/services/audio.py
index d3afeda..d5b8762 100644
--- a/backend/services/audio.py
+++ b/backend/services/audio.py
@@ -28,6 +28,7 @@ class AudioService:
self.music_channel: int = 5 # Channel for music
self.sfx_channel: int = 3 # Channel for SFX
self.ad_channel: int = 11 # Channel for ads
+ self.ident_channel: int = 15 # Channel for idents (stereo: ch 15+16)
self.monitor_device: Optional[int] = 14 # Babyface Pro (headphone monitoring)
self.monitor_channel: int = 1 # Channel for mic monitoring on monitor device
self.phone_filter: bool = False # Phone filter on caller voices
@@ -112,6 +113,7 @@ class AudioService:
self.music_channel = data.get("music_channel", 2)
self.sfx_channel = data.get("sfx_channel", 3)
self.ad_channel = data.get("ad_channel", 11)
+ self.ident_channel = data.get("ident_channel", 15)
self.monitor_device = data.get("monitor_device")
self.monitor_channel = data.get("monitor_channel", 1)
self.phone_filter = data.get("phone_filter", False)
@@ -131,6 +133,7 @@ class AudioService:
"music_channel": self.music_channel,
"sfx_channel": self.sfx_channel,
"ad_channel": self.ad_channel,
+ "ident_channel": self.ident_channel,
"monitor_device": self.monitor_device,
"monitor_channel": self.monitor_channel,
"phone_filter": self.phone_filter,
@@ -165,6 +168,7 @@ class AudioService:
music_channel: Optional[int] = None,
sfx_channel: Optional[int] = None,
ad_channel: Optional[int] = None,
+ ident_channel: Optional[int] = None,
monitor_device: Optional[int] = None,
monitor_channel: Optional[int] = None,
phone_filter: Optional[bool] = None
@@ -186,6 +190,8 @@ class AudioService:
self.sfx_channel = sfx_channel
if ad_channel is not None:
self.ad_channel = ad_channel
+ if ident_channel is not None:
+ self.ident_channel = ident_channel
if monitor_device is not None:
self.monitor_device = monitor_device
if monitor_channel is not None:
@@ -207,6 +213,7 @@ class AudioService:
"music_channel": self.music_channel,
"sfx_channel": self.sfx_channel,
"ad_channel": self.ad_channel,
+ "ident_channel": self.ident_channel,
"monitor_device": self.monitor_device,
"monitor_channel": self.monitor_channel,
"phone_filter": self.phone_filter,
@@ -1014,7 +1021,7 @@ class AudioService:
self._ad_position = 0
def play_ident(self, file_path: str):
- """Load and play an ident file once (no loop) on the ad channel"""
+ """Load and play an ident file once (no loop) in stereo on ident_channel/ident_channel+1"""
import librosa
path = Path(file_path)
@@ -1026,8 +1033,11 @@ class AudioService:
self.stop_ad()
try:
- audio, sr = librosa.load(str(path), sr=self.output_sample_rate, mono=True)
- self._ident_data = audio.astype(np.float32)
+ audio, sr = librosa.load(str(path), sr=self.output_sample_rate, mono=False)
+ if audio.ndim == 1:
+ # Mono file — duplicate to stereo
+ audio = np.stack([audio, audio])
+ self._ident_data = audio.astype(np.float32) # shape: (2, samples)
except Exception as e:
print(f"Failed to load ident: {e}")
return
@@ -1039,18 +1049,21 @@ class AudioService:
num_channels = 2
device = None
device_sr = self.output_sample_rate
- channel_idx = 0
+ ch_l = 0
+ ch_r = 1
else:
device_info = sd.query_devices(self.output_device)
num_channels = device_info['max_output_channels']
device_sr = int(device_info['default_samplerate'])
device = self.output_device
- channel_idx = min(self.ad_channel, num_channels) - 1
+ ch_l = min(self.ident_channel, num_channels) - 1
+ ch_r = min(self.ident_channel + 1, num_channels) - 1
if self.output_sample_rate != device_sr:
- self._ident_resampled = librosa.resample(
- self._ident_data, orig_sr=self.output_sample_rate, target_sr=device_sr
- ).astype(np.float32)
+ self._ident_resampled = np.stack([
+ librosa.resample(self._ident_data[0], orig_sr=self.output_sample_rate, target_sr=device_sr),
+ librosa.resample(self._ident_data[1], orig_sr=self.output_sample_rate, target_sr=device_sr),
+ ]).astype(np.float32)
else:
self._ident_resampled = self._ident_data
@@ -1059,16 +1072,21 @@ class AudioService:
if not self._ident_playing or self._ident_resampled is None:
return
- remaining = len(self._ident_resampled) - self._ident_position
+ n_samples = self._ident_resampled.shape[1]
+ remaining = n_samples - self._ident_position
if remaining >= frames:
- chunk = self._ident_resampled[self._ident_position:self._ident_position + frames]
- outdata[:, channel_idx] = chunk
+ chunk_l = self._ident_resampled[0, self._ident_position:self._ident_position + frames]
+ chunk_r = self._ident_resampled[1, self._ident_position:self._ident_position + frames]
+ outdata[:, ch_l] = chunk_l
+ outdata[:, ch_r] = chunk_r
if self.stem_recorder:
- self.stem_recorder.write_sporadic("idents", chunk.copy(), device_sr)
+ mono_mix = (chunk_l + chunk_r) * 0.5
+ self.stem_recorder.write_sporadic("idents", mono_mix.copy(), device_sr)
self._ident_position += frames
else:
if remaining > 0:
- outdata[:remaining, channel_idx] = self._ident_resampled[self._ident_position:]
+ outdata[:remaining, ch_l] = self._ident_resampled[0, self._ident_position:]
+ outdata[:remaining, ch_r] = self._ident_resampled[1, self._ident_position:]
self._ident_playing = False
try:
@@ -1081,7 +1099,7 @@ class AudioService:
blocksize=2048
)
self._ident_stream.start()
- print(f"Ident playback started on ch {self.ad_channel} @ {device_sr}Hz")
+ print(f"Ident playback started on ch {self.ident_channel}/{self.ident_channel + 1} @ {device_sr}Hz")
except Exception as e:
print(f"Ident playback error: {e}")
self._ident_playing = False
diff --git a/frontend/index.html b/frontend/index.html
index f125b9d..14e9bfe 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -174,6 +174,7 @@
+
diff --git a/frontend/js/app.js b/frontend/js/app.js
index 94914e1..5635cc1 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -343,6 +343,7 @@ async function loadAudioDevices() {
const musicCh = document.getElementById('music-channel');
const sfxCh = document.getElementById('sfx-channel');
const adCh = document.getElementById('ad-channel');
+ const identCh = document.getElementById('ident-channel');
if (inputCh) inputCh.value = settings.input_channel || 1;
if (callerCh) callerCh.value = settings.caller_channel || 1;
@@ -350,6 +351,7 @@ async function loadAudioDevices() {
if (musicCh) musicCh.value = settings.music_channel || 2;
if (sfxCh) sfxCh.value = settings.sfx_channel || 3;
if (adCh) adCh.value = settings.ad_channel || 11;
+ if (identCh) identCh.value = settings.ident_channel || 15;
// Phone filter setting
const phoneFilterEl = document.getElementById('phone-filter');
@@ -374,6 +376,7 @@ async function saveAudioDevices() {
const musicChannel = document.getElementById('music-channel')?.value;
const sfxChannel = document.getElementById('sfx-channel')?.value;
const adChannel = document.getElementById('ad-channel')?.value;
+ const identChannel = document.getElementById('ident-channel')?.value;
const phoneFilterChecked = document.getElementById('phone-filter')?.checked ?? false;
await fetch('/api/audio/settings', {
@@ -388,6 +391,7 @@ async function saveAudioDevices() {
music_channel: musicChannel ? parseInt(musicChannel) : 2,
sfx_channel: sfxChannel ? parseInt(sfxChannel) : 3,
ad_channel: adChannel ? parseInt(adChannel) : 11,
+ ident_channel: identChannel ? parseInt(identChannel) : 15,
phone_filter: phoneFilterChecked
})
});