Ads play once on channel 11, separate from music
- Add dedicated ad playback system (no loop, own channel) - Ad channel defaults to 11, saved/loaded with audio settings - Separate play_ad/stop_ad methods and API endpoints - Frontend stop button now calls /api/ads/stop instead of stopMusic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1298,16 +1298,22 @@ async def get_ads():
|
|||||||
|
|
||||||
@app.post("/api/ads/play")
|
@app.post("/api/ads/play")
|
||||||
async def play_ad(request: MusicRequest):
|
async def play_ad(request: MusicRequest):
|
||||||
"""Play an ad on the music channel"""
|
"""Play an ad once on the ad channel (ch 11)"""
|
||||||
ad_path = settings.ads_dir / request.track
|
ad_path = settings.ads_dir / request.track
|
||||||
if not ad_path.exists():
|
if not ad_path.exists():
|
||||||
raise HTTPException(404, "Ad not found")
|
raise HTTPException(404, "Ad not found")
|
||||||
|
|
||||||
audio_service.load_music(str(ad_path))
|
audio_service.play_ad(str(ad_path))
|
||||||
audio_service.play_music()
|
|
||||||
return {"status": "playing", "track": request.track}
|
return {"status": "playing", "track": request.track}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/ads/stop")
|
||||||
|
async def stop_ad():
|
||||||
|
"""Stop ad playback"""
|
||||||
|
audio_service.stop_ad()
|
||||||
|
return {"status": "stopped"}
|
||||||
|
|
||||||
|
|
||||||
# --- LLM Settings Endpoints ---
|
# --- LLM Settings Endpoints ---
|
||||||
|
|
||||||
@app.get("/api/settings")
|
@app.get("/api/settings")
|
||||||
|
|||||||
@@ -27,8 +27,16 @@ class AudioService:
|
|||||||
self.live_caller_channel: int = 9 # Channel for live caller audio
|
self.live_caller_channel: int = 9 # Channel for live caller audio
|
||||||
self.music_channel: int = 2 # Channel for music
|
self.music_channel: int = 2 # 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.phone_filter: bool = False # Phone filter on caller voices
|
self.phone_filter: bool = False # Phone filter on caller voices
|
||||||
|
|
||||||
|
# Ad playback state
|
||||||
|
self._ad_stream: Optional[sd.OutputStream] = None
|
||||||
|
self._ad_data: Optional[np.ndarray] = None
|
||||||
|
self._ad_resampled: Optional[np.ndarray] = None
|
||||||
|
self._ad_position: int = 0
|
||||||
|
self._ad_playing: bool = False
|
||||||
|
|
||||||
# Recording state
|
# Recording state
|
||||||
self._recording = False
|
self._recording = False
|
||||||
self._record_thread: Optional[threading.Thread] = None
|
self._record_thread: Optional[threading.Thread] = None
|
||||||
@@ -78,8 +86,9 @@ class AudioService:
|
|||||||
self.live_caller_channel = data.get("live_caller_channel", 4)
|
self.live_caller_channel = data.get("live_caller_channel", 4)
|
||||||
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.phone_filter = data.get("phone_filter", False)
|
self.phone_filter = data.get("phone_filter", False)
|
||||||
print(f"Loaded audio settings: output={self.output_device}, channels={self.caller_channel}/{self.live_caller_channel}/{self.music_channel}/{self.sfx_channel}, phone_filter={self.phone_filter}")
|
print(f"Loaded audio settings: output={self.output_device}, channels={self.caller_channel}/{self.live_caller_channel}/{self.music_channel}/{self.sfx_channel}/ad:{self.ad_channel}, phone_filter={self.phone_filter}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to load audio settings: {e}")
|
print(f"Failed to load audio settings: {e}")
|
||||||
|
|
||||||
@@ -94,6 +103,7 @@ class AudioService:
|
|||||||
"live_caller_channel": self.live_caller_channel,
|
"live_caller_channel": self.live_caller_channel,
|
||||||
"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,
|
||||||
"phone_filter": self.phone_filter,
|
"phone_filter": self.phone_filter,
|
||||||
}
|
}
|
||||||
with open(SETTINGS_FILE, "w") as f:
|
with open(SETTINGS_FILE, "w") as f:
|
||||||
@@ -655,6 +665,85 @@ class AudioService:
|
|||||||
self._music_position = 0
|
self._music_position = 0
|
||||||
print("Music stopped")
|
print("Music stopped")
|
||||||
|
|
||||||
|
def play_ad(self, file_path: str):
|
||||||
|
"""Load and play an ad file once (no loop) on the ad channel"""
|
||||||
|
import librosa
|
||||||
|
|
||||||
|
path = Path(file_path)
|
||||||
|
if not path.exists():
|
||||||
|
print(f"Ad file not found: {file_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop_ad()
|
||||||
|
|
||||||
|
try:
|
||||||
|
audio, sr = librosa.load(str(path), sr=self.output_sample_rate, mono=True)
|
||||||
|
self._ad_data = audio.astype(np.float32)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load ad: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._ad_playing = True
|
||||||
|
self._ad_position = 0
|
||||||
|
|
||||||
|
if self.output_device is None:
|
||||||
|
num_channels = 2
|
||||||
|
device = None
|
||||||
|
device_sr = self.output_sample_rate
|
||||||
|
channel_idx = 0
|
||||||
|
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
|
||||||
|
|
||||||
|
if self.output_sample_rate != device_sr:
|
||||||
|
self._ad_resampled = librosa.resample(
|
||||||
|
self._ad_data, orig_sr=self.output_sample_rate, target_sr=device_sr
|
||||||
|
).astype(np.float32)
|
||||||
|
else:
|
||||||
|
self._ad_resampled = self._ad_data
|
||||||
|
|
||||||
|
def callback(outdata, frames, time_info, status):
|
||||||
|
outdata[:] = 0
|
||||||
|
if not self._ad_playing or self._ad_resampled is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
remaining = len(self._ad_resampled) - self._ad_position
|
||||||
|
if remaining >= frames:
|
||||||
|
outdata[:, channel_idx] = self._ad_resampled[self._ad_position:self._ad_position + frames]
|
||||||
|
self._ad_position += frames
|
||||||
|
else:
|
||||||
|
if remaining > 0:
|
||||||
|
outdata[:remaining, channel_idx] = self._ad_resampled[self._ad_position:]
|
||||||
|
# Ad finished — no loop
|
||||||
|
self._ad_playing = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ad_stream = sd.OutputStream(
|
||||||
|
device=device,
|
||||||
|
channels=num_channels,
|
||||||
|
samplerate=device_sr,
|
||||||
|
dtype=np.float32,
|
||||||
|
callback=callback,
|
||||||
|
blocksize=2048
|
||||||
|
)
|
||||||
|
self._ad_stream.start()
|
||||||
|
print(f"Ad playback started on ch {self.ad_channel} @ {device_sr}Hz")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ad playback error: {e}")
|
||||||
|
self._ad_playing = False
|
||||||
|
|
||||||
|
def stop_ad(self):
|
||||||
|
"""Stop ad playback"""
|
||||||
|
self._ad_playing = False
|
||||||
|
if self._ad_stream:
|
||||||
|
self._ad_stream.stop()
|
||||||
|
self._ad_stream.close()
|
||||||
|
self._ad_stream = None
|
||||||
|
self._ad_position = 0
|
||||||
|
|
||||||
def set_music_volume(self, volume: float):
|
def set_music_volume(self, volume: float):
|
||||||
"""Set music volume (0.0 to 1.0)"""
|
"""Set music volume (0.0 to 1.0)"""
|
||||||
self._music_volume = max(0.0, min(1.0, volume))
|
self._music_volume = max(0.0, min(1.0, volume))
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ function initEventListeners() {
|
|||||||
|
|
||||||
// Ads
|
// Ads
|
||||||
document.getElementById('ad-play-btn')?.addEventListener('click', playAd);
|
document.getElementById('ad-play-btn')?.addEventListener('click', playAd);
|
||||||
document.getElementById('ad-stop-btn')?.addEventListener('click', stopMusic);
|
document.getElementById('ad-stop-btn')?.addEventListener('click', stopAd);
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
document.getElementById('settings-btn')?.addEventListener('click', async () => {
|
document.getElementById('settings-btn')?.addEventListener('click', async () => {
|
||||||
@@ -686,6 +686,10 @@ async function playAd() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function stopAd() {
|
||||||
|
await fetch('/api/ads/stop', { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Sound Effects (Server-Side) ---
|
// --- Sound Effects (Server-Side) ---
|
||||||
async function loadSounds() {
|
async function loadSounds() {
|
||||||
|
|||||||
Reference in New Issue
Block a user