--global-fix
This commit is contained in:
174
my3dengine/audio.py
Normal file
174
my3dengine/audio.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import numpy as np
|
||||
import sounddevice as sd
|
||||
import soundfile as sf
|
||||
import threading
|
||||
|
||||
class AudioListener:
|
||||
def __init__(self, position):
|
||||
self.position = np.array(position, dtype=float)
|
||||
|
||||
class AudioSource:
|
||||
def __init__(self, position, sound_path, emit_radius=15.0, volume=1.0, loop=False, fade_duration=1.0):
|
||||
self.position = np.array(position, dtype=float)
|
||||
self.sound_path = sound_path
|
||||
self.emit_radius = emit_radius
|
||||
self.base_volume = volume
|
||||
self.loop = loop
|
||||
|
||||
self._data, self._fs = sf.read(self.sound_path, dtype='float32')
|
||||
self._stream = None
|
||||
self._stop_flag = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
self._volume = 0.0 # aktualna głośność (płynna)
|
||||
self._target_volume = 0.0 # docelowa głośność (fade target)
|
||||
self._fade_duration = fade_duration # czas fade w sekundach
|
||||
|
||||
self._pos = 0
|
||||
self.playing = False
|
||||
self._paused = False
|
||||
|
||||
self.MIN_VOLUME_TO_PLAY = 0.01
|
||||
|
||||
def _callback(self, outdata, frames, time, status):
|
||||
with self._lock:
|
||||
if self._stop_flag.is_set():
|
||||
raise sd.CallbackStop()
|
||||
|
||||
chunk_start = self._pos
|
||||
chunk_end = self._pos + frames
|
||||
|
||||
if chunk_end > len(self._data):
|
||||
if self.loop:
|
||||
part1 = self._data[chunk_start:]
|
||||
part2 = self._data[:chunk_end - len(self._data)]
|
||||
chunk = np.concatenate((part1, part2))
|
||||
self._pos = chunk_end - len(self._data)
|
||||
else:
|
||||
chunk = self._data[chunk_start:]
|
||||
chunk = np.pad(chunk, ((0, frames - len(chunk)), (0, 0)), mode='constant')
|
||||
self._pos = len(self._data)
|
||||
else:
|
||||
chunk = self._data[chunk_start:chunk_end]
|
||||
self._pos = chunk_end
|
||||
|
||||
current_vol = self._volume
|
||||
target_vol = self._target_volume
|
||||
|
||||
fade_step = 1.0 / (self._fs * self._fade_duration) # ile głośności na 1 próbkę
|
||||
|
||||
steps = np.arange(frames, dtype=np.float32)
|
||||
|
||||
if target_vol > current_vol:
|
||||
volumes = current_vol + fade_step * steps * (target_vol - current_vol)
|
||||
volumes = np.clip(volumes, 0.0, target_vol)
|
||||
else:
|
||||
volumes = current_vol - fade_step * steps * (current_vol - target_vol)
|
||||
volumes = np.clip(volumes, target_vol, 1.0)
|
||||
|
||||
self._volume = volumes[-1]
|
||||
|
||||
if self._paused or self._volume < self.MIN_VOLUME_TO_PLAY:
|
||||
outdata[:] = 0
|
||||
else:
|
||||
if chunk.ndim > 1:
|
||||
volumes = volumes[:, np.newaxis]
|
||||
outdata[:] = chunk * volumes
|
||||
|
||||
if not self.loop and self._pos >= len(self._data):
|
||||
raise sd.CallbackStop()
|
||||
|
||||
def _on_finished(self):
|
||||
with self._lock:
|
||||
self._stream = None
|
||||
self.playing = False
|
||||
self._volume = 0.0
|
||||
self._target_volume = 0.0
|
||||
self._pos = 0
|
||||
self._paused = False
|
||||
|
||||
def play(self, volume):
|
||||
with self._lock:
|
||||
if self.playing:
|
||||
if self._paused:
|
||||
# Jeśli jest pauza, wznow i ustaw docelową głośność
|
||||
self._paused = False
|
||||
self._target_volume = volume
|
||||
return
|
||||
else:
|
||||
# Już gra, więc ustaw tylko docelową głośność
|
||||
self._target_volume = volume
|
||||
return
|
||||
|
||||
# Nowe odtwarzanie od zera z głośnością 0 i fade-in
|
||||
self._volume = 0.0
|
||||
self._target_volume = volume
|
||||
self._pos = 0
|
||||
self._stop_flag.clear()
|
||||
|
||||
self._stream = sd.OutputStream(
|
||||
samplerate=self._fs,
|
||||
channels=self._data.shape[1] if self._data.ndim > 1 else 1,
|
||||
callback=self._callback,
|
||||
finished_callback=self._on_finished
|
||||
)
|
||||
self._stream.start()
|
||||
self.playing = True
|
||||
self._paused = False
|
||||
|
||||
def pause(self):
|
||||
with self._lock:
|
||||
if self.playing and not self._paused:
|
||||
self._target_volume = 0.0 # fade out do 0, ale stream działa
|
||||
# _paused ustawiane w callbacku, gdy głośność spadnie do zera
|
||||
|
||||
def resume(self):
|
||||
with self._lock:
|
||||
if self.playing and self._paused:
|
||||
self._paused = False
|
||||
self._volume = 0.0 # zaczynamy z 0, fade-in w callbacku
|
||||
self._target_volume = self.base_volume # docelowa głośność
|
||||
|
||||
def stop(self):
|
||||
with self._lock:
|
||||
if self._stream:
|
||||
self._stop_flag.set()
|
||||
self.playing = False
|
||||
self._volume = 0.0
|
||||
self._target_volume = 0.0
|
||||
self._pos = 0
|
||||
self._paused = False
|
||||
|
||||
def set_target_volume(self, volume):
|
||||
with self._lock:
|
||||
self._target_volume = max(0.0, min(volume, 1.0))
|
||||
|
||||
def update_audio_system(listener, sources, debug=False):
|
||||
for source in sources:
|
||||
dist = np.linalg.norm(listener.position - source.position)
|
||||
|
||||
if dist <= source.emit_radius:
|
||||
attenuation = 1.0 - dist / source.emit_radius
|
||||
volume = source.base_volume * attenuation
|
||||
|
||||
if debug:
|
||||
print(f"[Audio] Distance: {dist:.2f}, Volume: {volume:.2f}")
|
||||
|
||||
if not source.playing:
|
||||
source.play(volume)
|
||||
else:
|
||||
if source._paused:
|
||||
# Jeśli był pauzowany, startuj od nowa z głośnością 0 i fade-in
|
||||
source.stop()
|
||||
source.play(volume)
|
||||
else:
|
||||
source.set_target_volume(volume)
|
||||
|
||||
else:
|
||||
if source.playing:
|
||||
source.set_target_volume(0.0)
|
||||
# Poczekaj, aż głośność spadnie prawie do zera, wtedy pauzuj
|
||||
if source._volume <= 0.01 and not source._paused:
|
||||
if debug:
|
||||
print(f"[PAUSE] {source.sound_path} - volume zero")
|
||||
source.pause()
|
||||
Reference in New Issue
Block a user