Skip to content

Commit

Permalink
fix(audio): add a recovery mechanism for audio service to rebind the …
Browse files Browse the repository at this point in the history
…sound card if it is not available - closes #83
  • Loading branch information
sassanh committed Aug 1, 2024
1 parent 0bd9137 commit f5ee50e
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 43 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Version 0.15.6

- fix(audio): add a recovery mechanism for audio service to rebind the sound card if it is not available - closes #83

## Version 0.15.5

- feat(notifications): add `progress` and `progress_weight` properties to `Notification` object and show the progress on the header of the app
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "ubo-app"
version = "0.15.5"
version = "0.15.6"
description = "Ubo main app, running on device initialization. A platform for running other apps."
authors = ["Sassan Haradji <[email protected]>"]
license = "Apache-2.0"
Expand Down
85 changes: 45 additions & 40 deletions ubo_app/services/000-audio/audio_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@

from __future__ import annotations

import asyncio
import math
import subprocess
import wave

import alsaaudio
import simpleaudio
from simpleaudio import _simpleaudio # pyright: ignore [reportAttributeAccessIssue]

from ubo_app.logging import logger
from ubo_app.store.main import dispatch
from ubo_app.store.services.audio import AudioPlaybackDoneEvent
from ubo_app.utils import IS_RPI
from ubo_app.utils.async_ import create_task
from ubo_app.utils.server import send_command

CHUNK_SIZE = 1024
TRIALS = 3


def _linear_to_logarithmic(volume_linear: float) -> int:
Expand Down Expand Up @@ -47,40 +48,12 @@ async def initialize_audio() -> None:
)
except StopIteration:
logger.exception('No audio card found')
except OSError:
logger.exception('Error while setting default sink')
logger.info('Restarting pulseaudio')

await self.restart_pulse_audio()

await asyncio.sleep(5)
else:
break

create_task(initialize_audio())

async def restart_pulse_audio(self: AudioManager) -> None:
"""Restart pulseaudio."""
if not IS_RPI:
return
process = await asyncio.create_subprocess_exec(
'/usr/bin/env',
'pulseaudio',
'--kill',
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
await process.wait()
process = await asyncio.create_subprocess_exec(
'/usr/bin/env',
'pulseaudio',
'--start',
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
await process.wait()

def play_file(self: AudioManager, filename: str) -> None:
async def play_file(self: AudioManager, filename: str) -> None:
"""Play a waveform audio file.
Parameters
Expand All @@ -91,9 +64,20 @@ def play_file(self: AudioManager, filename: str) -> None:
"""
# open the file for reading.
logger.info('Opening audio file for playback', extra={'filename_': filename})
simpleaudio.WaveObject.from_wave_file(filename).play()
with wave.open(filename, 'rb') as wave_file:
sample_rate = wave_file.getframerate()
channels = wave_file.getnchannels()
sample_width = wave_file.getsampwidth()
audio_data = wave_file.readframes(wave_file.getnframes())

await self.play_sequence(
audio_data,
channels=channels,
rate=sample_rate,
width=sample_width,
)

def play_sequence(
async def play_sequence(
self: AudioManager,
data: bytes,
*,
Expand Down Expand Up @@ -123,12 +107,33 @@ def play_sequence(
"""
if data != b'':
simpleaudio.WaveObject(
audio_data=data,
num_channels=channels,
sample_rate=rate,
bytes_per_sample=width,
).play().wait_done()
for trial in range(TRIALS):
try:
wave_object = simpleaudio.WaveObject(
audio_data=data,
num_channels=channels,
sample_rate=rate,
bytes_per_sample=width,
)
play_object = wave_object.play()
play_object.wait_done()
except _simpleaudio.SimpleaudioError:
logger.exception(
'Error while playing audio file',
extra={'trial': trial},
)
logger.info(
'Reporting the playback issue to ubo-system',
extra={'trial': trial},
)
await send_command('audio failure_report', has_output=True)
else:
break
else:
logger.error(
'Failed to play audio file after multiple trials',
extra={'tried_times': TRIALS},
)
if id is not None:
dispatch(AudioPlaybackDoneEvent(id=id))

Expand Down
20 changes: 20 additions & 0 deletions ubo_app/services/000-audio/setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# ruff: noqa: D100, D101, D102, D103, D104, D107, N999
from __future__ import annotations

import asyncio
from pathlib import Path
from typing import TYPE_CHECKING, ParamSpec

from audio_manager import AudioManager
from constants import AUDIO_MIC_STATE_ICON_ID, AUDIO_MIC_STATE_ICON_PRIORITY
Expand All @@ -12,6 +14,23 @@
from ubo_app.utils.async_ import to_thread
from ubo_app.utils.persistent_store import register_persistent_store

if TYPE_CHECKING:
from collections.abc import Callable, Coroutine

Args = ParamSpec('Args')


def _run_async_in_thread(
async_func: Callable[Args, Coroutine],
*args: Args.args,
**kwargs: Args.kwargs,
) -> None:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(async_func(*args, **kwargs))
loop.close()
return result


def init_service() -> None:
audio_manager = AudioManager()
Expand Down Expand Up @@ -51,6 +70,7 @@ def _(is_mute: bool) -> None: # noqa: FBT001
subscribe_event(
AudioPlayAudioEvent,
lambda event: to_thread(
_run_async_in_thread,
audio_manager.play_sequence,
event.sample,
id=event.id,
Expand Down
4 changes: 2 additions & 2 deletions ubo_app/services/030-ip/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def load_ip_addresses() -> None:
async def is_connected() -> bool:
results = await asyncio.gather(
*(
asyncio.wait_for(asyncio.open_connection(ip, 53), timeout=0.1)
asyncio.wait_for(asyncio.open_connection(ip, 53), timeout=1)
for ip in ('1.1.1.1', '8.8.8.8')
),
return_exceptions=True,
Expand Down Expand Up @@ -113,7 +113,7 @@ async def check_connection() -> bool:
),
IpSetIsConnectedAction(is_connected=False),
)
await asyncio.sleep(0.1)
await asyncio.sleep(1)


IpMainMenu = SubMenuItem(
Expand Down
31 changes: 31 additions & 0 deletions ubo_app/system/system_manager/audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""handle audio commands."""

from __future__ import annotations

import time
from pathlib import Path

from ubo_app.logging import get_logger

DEVICE = '1-001a'
DRIVER_PATH = Path('/sys/bus/i2c/drivers/wm8960')


logger = get_logger('system-manager')


def audio_handler(command: str) -> str | None:
"""Install and start Docker on the host machine."""
if command == 'failure_report':
logger.info('Audio failure report received, rebinding device...')
try:
(DRIVER_PATH / 'unbind').write_text(DEVICE)
time.sleep(1)
(DRIVER_PATH / 'bind').write_text(DEVICE)
except Exception as e:
logger.exception('Error rebinding device', exc_info=e)
return 'error'
else:
logger.info('Device has been rebound.')
return 'done'
return None
3 changes: 3 additions & 0 deletions ubo_app/system/system_manager/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ubo_app.constants import USERNAME
from ubo_app.error_handlers import setup_error_handling
from ubo_app.logging import add_file_handler, add_stdout_handler, get_logger
from ubo_app.system.system_manager.audio import audio_handler
from ubo_app.system.system_manager.docker import docker_handler
from ubo_app.system.system_manager.led import LEDManager
from ubo_app.system.system_manager.reset_button import setup_reset_button
Expand Down Expand Up @@ -44,6 +45,8 @@ def handle_command(command: str) -> str | None:
thread.start()
elif header == 'service':
return service_handler(incoming[0], incoming[1])
elif header == 'audio':
return audio_handler(incoming[0])
return None


Expand Down

0 comments on commit f5ee50e

Please sign in to comment.