Play idents in stereo on channels 15/16 with configurable ident_channel setting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3677,6 +3677,7 @@ class AudioDeviceSettings(BaseModel):
|
|||||||
music_channel: Optional[int] = None
|
music_channel: Optional[int] = None
|
||||||
sfx_channel: Optional[int] = None
|
sfx_channel: Optional[int] = None
|
||||||
ad_channel: Optional[int] = None
|
ad_channel: Optional[int] = None
|
||||||
|
ident_channel: Optional[int] = None
|
||||||
monitor_device: Optional[int] = None
|
monitor_device: Optional[int] = None
|
||||||
monitor_channel: Optional[int] = None
|
monitor_channel: Optional[int] = None
|
||||||
phone_filter: Optional[bool] = None
|
phone_filter: Optional[bool] = None
|
||||||
@@ -3716,6 +3717,7 @@ async def set_audio_settings(settings: AudioDeviceSettings):
|
|||||||
music_channel=settings.music_channel,
|
music_channel=settings.music_channel,
|
||||||
sfx_channel=settings.sfx_channel,
|
sfx_channel=settings.sfx_channel,
|
||||||
ad_channel=settings.ad_channel,
|
ad_channel=settings.ad_channel,
|
||||||
|
ident_channel=settings.ident_channel,
|
||||||
monitor_device=settings.monitor_device,
|
monitor_device=settings.monitor_device,
|
||||||
monitor_channel=settings.monitor_channel,
|
monitor_channel=settings.monitor_channel,
|
||||||
phone_filter=settings.phone_filter
|
phone_filter=settings.phone_filter
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class AudioService:
|
|||||||
self.music_channel: int = 5 # Channel for music
|
self.music_channel: int = 5 # Channel for music
|
||||||
self.sfx_channel: int = 3 # Channel for SFX
|
self.sfx_channel: int = 3 # Channel for SFX
|
||||||
self.ad_channel: int = 11 # Channel for ads
|
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_device: Optional[int] = 14 # Babyface Pro (headphone monitoring)
|
||||||
self.monitor_channel: int = 1 # Channel for mic monitoring on monitor device
|
self.monitor_channel: int = 1 # Channel for mic monitoring on monitor device
|
||||||
self.phone_filter: bool = False # Phone filter on caller voices
|
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.music_channel = data.get("music_channel", 2)
|
||||||
self.sfx_channel = data.get("sfx_channel", 3)
|
self.sfx_channel = data.get("sfx_channel", 3)
|
||||||
self.ad_channel = data.get("ad_channel", 11)
|
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_device = data.get("monitor_device")
|
||||||
self.monitor_channel = data.get("monitor_channel", 1)
|
self.monitor_channel = data.get("monitor_channel", 1)
|
||||||
self.phone_filter = data.get("phone_filter", False)
|
self.phone_filter = data.get("phone_filter", False)
|
||||||
@@ -131,6 +133,7 @@ class AudioService:
|
|||||||
"music_channel": self.music_channel,
|
"music_channel": self.music_channel,
|
||||||
"sfx_channel": self.sfx_channel,
|
"sfx_channel": self.sfx_channel,
|
||||||
"ad_channel": self.ad_channel,
|
"ad_channel": self.ad_channel,
|
||||||
|
"ident_channel": self.ident_channel,
|
||||||
"monitor_device": self.monitor_device,
|
"monitor_device": self.monitor_device,
|
||||||
"monitor_channel": self.monitor_channel,
|
"monitor_channel": self.monitor_channel,
|
||||||
"phone_filter": self.phone_filter,
|
"phone_filter": self.phone_filter,
|
||||||
@@ -165,6 +168,7 @@ class AudioService:
|
|||||||
music_channel: Optional[int] = None,
|
music_channel: Optional[int] = None,
|
||||||
sfx_channel: Optional[int] = None,
|
sfx_channel: Optional[int] = None,
|
||||||
ad_channel: Optional[int] = None,
|
ad_channel: Optional[int] = None,
|
||||||
|
ident_channel: Optional[int] = None,
|
||||||
monitor_device: Optional[int] = None,
|
monitor_device: Optional[int] = None,
|
||||||
monitor_channel: Optional[int] = None,
|
monitor_channel: Optional[int] = None,
|
||||||
phone_filter: Optional[bool] = None
|
phone_filter: Optional[bool] = None
|
||||||
@@ -186,6 +190,8 @@ class AudioService:
|
|||||||
self.sfx_channel = sfx_channel
|
self.sfx_channel = sfx_channel
|
||||||
if ad_channel is not None:
|
if ad_channel is not None:
|
||||||
self.ad_channel = ad_channel
|
self.ad_channel = ad_channel
|
||||||
|
if ident_channel is not None:
|
||||||
|
self.ident_channel = ident_channel
|
||||||
if monitor_device is not None:
|
if monitor_device is not None:
|
||||||
self.monitor_device = monitor_device
|
self.monitor_device = monitor_device
|
||||||
if monitor_channel is not None:
|
if monitor_channel is not None:
|
||||||
@@ -207,6 +213,7 @@ class AudioService:
|
|||||||
"music_channel": self.music_channel,
|
"music_channel": self.music_channel,
|
||||||
"sfx_channel": self.sfx_channel,
|
"sfx_channel": self.sfx_channel,
|
||||||
"ad_channel": self.ad_channel,
|
"ad_channel": self.ad_channel,
|
||||||
|
"ident_channel": self.ident_channel,
|
||||||
"monitor_device": self.monitor_device,
|
"monitor_device": self.monitor_device,
|
||||||
"monitor_channel": self.monitor_channel,
|
"monitor_channel": self.monitor_channel,
|
||||||
"phone_filter": self.phone_filter,
|
"phone_filter": self.phone_filter,
|
||||||
@@ -1014,7 +1021,7 @@ class AudioService:
|
|||||||
self._ad_position = 0
|
self._ad_position = 0
|
||||||
|
|
||||||
def play_ident(self, file_path: str):
|
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
|
import librosa
|
||||||
|
|
||||||
path = Path(file_path)
|
path = Path(file_path)
|
||||||
@@ -1026,8 +1033,11 @@ class AudioService:
|
|||||||
self.stop_ad()
|
self.stop_ad()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
audio, sr = librosa.load(str(path), sr=self.output_sample_rate, mono=True)
|
audio, sr = librosa.load(str(path), sr=self.output_sample_rate, mono=False)
|
||||||
self._ident_data = audio.astype(np.float32)
|
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:
|
except Exception as e:
|
||||||
print(f"Failed to load ident: {e}")
|
print(f"Failed to load ident: {e}")
|
||||||
return
|
return
|
||||||
@@ -1039,18 +1049,21 @@ class AudioService:
|
|||||||
num_channels = 2
|
num_channels = 2
|
||||||
device = None
|
device = None
|
||||||
device_sr = self.output_sample_rate
|
device_sr = self.output_sample_rate
|
||||||
channel_idx = 0
|
ch_l = 0
|
||||||
|
ch_r = 1
|
||||||
else:
|
else:
|
||||||
device_info = sd.query_devices(self.output_device)
|
device_info = sd.query_devices(self.output_device)
|
||||||
num_channels = device_info['max_output_channels']
|
num_channels = device_info['max_output_channels']
|
||||||
device_sr = int(device_info['default_samplerate'])
|
device_sr = int(device_info['default_samplerate'])
|
||||||
device = self.output_device
|
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:
|
if self.output_sample_rate != device_sr:
|
||||||
self._ident_resampled = librosa.resample(
|
self._ident_resampled = np.stack([
|
||||||
self._ident_data, orig_sr=self.output_sample_rate, target_sr=device_sr
|
librosa.resample(self._ident_data[0], orig_sr=self.output_sample_rate, target_sr=device_sr),
|
||||||
).astype(np.float32)
|
librosa.resample(self._ident_data[1], orig_sr=self.output_sample_rate, target_sr=device_sr),
|
||||||
|
]).astype(np.float32)
|
||||||
else:
|
else:
|
||||||
self._ident_resampled = self._ident_data
|
self._ident_resampled = self._ident_data
|
||||||
|
|
||||||
@@ -1059,16 +1072,21 @@ class AudioService:
|
|||||||
if not self._ident_playing or self._ident_resampled is None:
|
if not self._ident_playing or self._ident_resampled is None:
|
||||||
return
|
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:
|
if remaining >= frames:
|
||||||
chunk = self._ident_resampled[self._ident_position:self._ident_position + frames]
|
chunk_l = self._ident_resampled[0, self._ident_position:self._ident_position + frames]
|
||||||
outdata[:, channel_idx] = chunk
|
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:
|
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
|
self._ident_position += frames
|
||||||
else:
|
else:
|
||||||
if remaining > 0:
|
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
|
self._ident_playing = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1081,7 +1099,7 @@ class AudioService:
|
|||||||
blocksize=2048
|
blocksize=2048
|
||||||
)
|
)
|
||||||
self._ident_stream.start()
|
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:
|
except Exception as e:
|
||||||
print(f"Ident playback error: {e}")
|
print(f"Ident playback error: {e}")
|
||||||
self._ident_playing = False
|
self._ident_playing = False
|
||||||
|
|||||||
@@ -174,6 +174,7 @@
|
|||||||
<label>Music Ch <input type="number" id="music-channel" value="5" min="1" max="16" class="channel-input"></label>
|
<label>Music Ch <input type="number" id="music-channel" value="5" min="1" max="16" class="channel-input"></label>
|
||||||
<label>SFX Ch <input type="number" id="sfx-channel" value="7" min="1" max="16" class="channel-input"></label>
|
<label>SFX Ch <input type="number" id="sfx-channel" value="7" min="1" max="16" class="channel-input"></label>
|
||||||
<label>Ad Ch <input type="number" id="ad-channel" value="11" min="1" max="16" class="channel-input"></label>
|
<label>Ad Ch <input type="number" id="ad-channel" value="11" min="1" max="16" class="channel-input"></label>
|
||||||
|
<label>Ident Ch <input type="number" id="ident-channel" value="15" min="1" max="16" class="channel-input"></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ async function loadAudioDevices() {
|
|||||||
const musicCh = document.getElementById('music-channel');
|
const musicCh = document.getElementById('music-channel');
|
||||||
const sfxCh = document.getElementById('sfx-channel');
|
const sfxCh = document.getElementById('sfx-channel');
|
||||||
const adCh = document.getElementById('ad-channel');
|
const adCh = document.getElementById('ad-channel');
|
||||||
|
const identCh = document.getElementById('ident-channel');
|
||||||
|
|
||||||
if (inputCh) inputCh.value = settings.input_channel || 1;
|
if (inputCh) inputCh.value = settings.input_channel || 1;
|
||||||
if (callerCh) callerCh.value = settings.caller_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 (musicCh) musicCh.value = settings.music_channel || 2;
|
||||||
if (sfxCh) sfxCh.value = settings.sfx_channel || 3;
|
if (sfxCh) sfxCh.value = settings.sfx_channel || 3;
|
||||||
if (adCh) adCh.value = settings.ad_channel || 11;
|
if (adCh) adCh.value = settings.ad_channel || 11;
|
||||||
|
if (identCh) identCh.value = settings.ident_channel || 15;
|
||||||
|
|
||||||
// Phone filter setting
|
// Phone filter setting
|
||||||
const phoneFilterEl = document.getElementById('phone-filter');
|
const phoneFilterEl = document.getElementById('phone-filter');
|
||||||
@@ -374,6 +376,7 @@ async function saveAudioDevices() {
|
|||||||
const musicChannel = document.getElementById('music-channel')?.value;
|
const musicChannel = document.getElementById('music-channel')?.value;
|
||||||
const sfxChannel = document.getElementById('sfx-channel')?.value;
|
const sfxChannel = document.getElementById('sfx-channel')?.value;
|
||||||
const adChannel = document.getElementById('ad-channel')?.value;
|
const adChannel = document.getElementById('ad-channel')?.value;
|
||||||
|
const identChannel = document.getElementById('ident-channel')?.value;
|
||||||
const phoneFilterChecked = document.getElementById('phone-filter')?.checked ?? false;
|
const phoneFilterChecked = document.getElementById('phone-filter')?.checked ?? false;
|
||||||
|
|
||||||
await fetch('/api/audio/settings', {
|
await fetch('/api/audio/settings', {
|
||||||
@@ -388,6 +391,7 @@ async function saveAudioDevices() {
|
|||||||
music_channel: musicChannel ? parseInt(musicChannel) : 2,
|
music_channel: musicChannel ? parseInt(musicChannel) : 2,
|
||||||
sfx_channel: sfxChannel ? parseInt(sfxChannel) : 3,
|
sfx_channel: sfxChannel ? parseInt(sfxChannel) : 3,
|
||||||
ad_channel: adChannel ? parseInt(adChannel) : 11,
|
ad_channel: adChannel ? parseInt(adChannel) : 11,
|
||||||
|
ident_channel: identChannel ? parseInt(identChannel) : 15,
|
||||||
phone_filter: phoneFilterChecked
|
phone_filter: phoneFilterChecked
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user