fixed most of the audio stuttering

This commit is contained in:
study-faraphel 2025-01-04 14:03:43 +01:00
parent 077d1d3d9d
commit c225a3e27c
3 changed files with 72 additions and 43 deletions

View file

@ -1,25 +1,29 @@
## Installation # M2 Projet Thématique - Diffusion Radio Proche-en-Proche
Un projet visant à créer un réseau de machine capable de diffuser une source
audio à jouer de manière synchronisé.
Les communications du réseau doivent être chiffré et il ne doit pas être possible
d'inséré une machine inconnu pour pertuber le réseau.
## Usage
Debian Debian
```bash ```bash
# Pre-requires # dependencies
sudo apt install chrony sudo apt install ffmpeg
# Download # download the project
sudo apt install -y git
git clone https://git.faraphel.fr/study-faraphel/M2-PT-DRP git clone https://git.faraphel.fr/study-faraphel/M2-PT-DRP
cd M2-PT-DRP cd ./M2-PT-DRP/
# Dependencies # create a virtual environment
sudo apt install -y libmpg123-dev libssl-dev portaudio19-dev python3 -m venv ./.venv/
source ./.venv/bin/activate
# Compile # install python packages
sudo apt install -y build-essential cmake ninja-build pkg-config pip3 install -r ./requirements.txt
cmake -S . -B build -G Ninja
cmake --build build
cd build
# Run # run the application
sudo ./M2-PT-DRP --host ff02::1 --ipv6 python3 -m source
``` ```

View file

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
import pause import pause
import pydub import pydub
from pydub.utils import make_chunks
from source.behaviors.roles import base from source.behaviors.roles import base
from source.managers import Manager from source.managers import Manager
@ -25,27 +26,24 @@ class MasterRole(base.BaseRole):
self.manager.communication.secret_key = os.urandom(32) self.manager.communication.secret_key = os.urandom(32)
# prepare the audio file that will be streamed # prepare the audio file that will be streamed
# TODO(Faraphel): use another audio source
self.audio = pydub.AudioSegment.from_file("../assets/Caravan Palace - Wonderland.mp3") self.audio = pydub.AudioSegment.from_file("../assets/Caravan Palace - Wonderland.mp3")
self.play_time = datetime.now() self.play_time = datetime.now()
# calculate the number of bytes per milliseconds in the audio
bytes_per_ms = self.audio.frame_rate * self.audio.sample_width * self.audio.channels / 1000
# calculate the required chunk duration to reach that size
self.chunk_duration = timedelta(milliseconds=self.TARGET_SIZE / bytes_per_ms)
# split the audio into chunks
self.chunks = make_chunks(self.audio, self.chunk_duration.total_seconds() * 1000)
def handle(self): def handle(self):
# TODO(Faraphel): check if another server is emitting sound in the network. Return to undefined if yes # TODO(Faraphel): check if another server is emitting sound in the network. Return to undefined if yes
# calculate the number of bytes per milliseconds in the audio # get the next chunk
bytes_per_ms = self.audio.frame_rate * self.audio.sample_width * self.audio.channels / 1000 chunk = self.chunks.pop(0)
# calculate the required chunk duration to reach that size
chunk_duration = timedelta(milliseconds=self.TARGET_SIZE / bytes_per_ms)
# calculate the audio time
chunk_start_time = datetime.now() - self.play_time
chunk_end_time = chunk_start_time + chunk_duration
# get the music for that period
chunk = self.audio[
chunk_start_time.total_seconds() * 1000 :
chunk_end_time.total_seconds() * 1000
]
# broadcast it in the network # broadcast it in the network
audio_packet = AudioPacket( audio_packet = AudioPacket(
@ -59,4 +57,4 @@ class MasterRole(base.BaseRole):
# wait for the audio to play # wait for the audio to play
# TODO(Faraphel): should adapt to the compute time above # TODO(Faraphel): should adapt to the compute time above
pause.until(self.play_time + chunk_end_time) pause.until(datetime.now() + self.chunk_duration)

View file

@ -1,4 +1,5 @@
import threading import threading
import typing
from datetime import datetime from datetime import datetime
import numpy import numpy
@ -12,6 +13,8 @@ from source.utils.audio.audio import sample_width_to_type
class AudioManager: class AudioManager:
def __init__(self, manager: "managers.Manager"): def __init__(self, manager: "managers.Manager"):
self.stream: typing.Optional[sounddevice.OutputStream] = None
# buffer containing the set of audio chunk to play. Sort them by their time to play # buffer containing the set of audio chunk to play. Sort them by their time to play
self.buffer = sortedcontainers.SortedList(key=lambda audio: audio.time) self.buffer = sortedcontainers.SortedList(key=lambda audio: audio.time)
@ -31,13 +34,46 @@ class AudioManager:
# trigger the new audio event # trigger the new audio event
self.new_audio_event.set() self.new_audio_event.set()
def play_audio(self, audio: packets.AudioPacket) -> None:
# create a numpy array for our sample
sample = numpy.frombuffer(audio.data, dtype=sample_width_to_type(audio.sample_width))
# reshape it to have a sub-array for each channels
sample = sample.reshape((-1, audio.channels))
# normalize the sample to be between -1 and 1
sample = sample / (2 ** (audio.sample_width * 8 - 1))
# use float32 for the audio library
sample = sample.astype(numpy.float32)
# wait for the audio given time
pause.until(audio.time)
# update the stream if the audio use different settings
if (
self.stream is None or
self.stream.samplerate != audio.sample_rate or
self.stream.channels != audio.channels
):
self.stream = sounddevice.OutputStream(
samplerate=audio.sample_rate,
channels=audio.channels,
)
# play
self.stream.start()
# write the audio to the stream
self.stream.write(sample)
def handle(self) -> None: def handle(self) -> None:
""" """
Play the audio chunk in the buffer at the given time Play the audio chunk in the buffer at the given time
""" """
# wait for a new audio packet # wait for a new audio packet
self.new_audio_event.wait() # TODO(Faraphel): use self.lock ? seem to softlock the application
if len(self.buffer) == 0:
self.new_audio_event.clear()
self.new_audio_event.wait()
# get the most recent audio packet to play # get the most recent audio packet to play
audio: packets.AudioPacket = self.buffer.pop(0) audio: packets.AudioPacket = self.buffer.pop(0)
@ -46,18 +82,9 @@ class AudioManager:
if audio.time < datetime.now(): if audio.time < datetime.now():
return return
# create a numpy array for our sample # play the audio packet
sample = numpy.frombuffer(audio.data, dtype=sample_width_to_type(numpy.int16)) self.play_audio(audio)
# reshape it to have a sub-array for each channels
sample = sample.reshape((-1, audio.channels))
# normalize the sample to be between -1 and 1
sample = sample / (2 ** (audio.sample_width * 8 - 1))
# wait for the audio given time
pause.until(audio.time)
# play the audio
sounddevice.play(sample, audio.sample_rate)
def loop(self) -> None: def loop(self) -> None:
""" """