Skip to content

Commit

Permalink
Async mode fix (#29)
Browse files Browse the repository at this point in the history
* feat : NavaThread added

* fix : linux and mac async functions removed

* fix : __play_win function updated

* feat : sound_id_gen function added

* feat : stop and stop_all functions added

* fix : use NavaThread instead of Thread

* fix : minor bug in play function windows section fixed

* fix : cleanup_processes function removed

* fix : minor bug in subprocess call fixed

* fix : minor edit in tests

* fix : try to fix windows test bug

* fix : try to fix windows test bug

* doc : NavaThread class docstring updated

* doc : functions docstrings updated

* doc : functions docstrings updated

* fix : __play_win_by_flags functions renamed to __play_win_flags

* fix : is_async renamed to async_mode

* doc : async_mode docstring updated

* fix : autopep8 scripts updated

* fix : autopep8

* doc : NavaThread docstring bug fixed

* fix : stop function error added

* fix : error_test updated

* fix : function_test updated

* doc : CHANGELOG.md updated

* doc : README.md updated

* fix : async_mode default value set to False

* fix : minor edit in async_mode test

* doc : README.md async section updated

* fix : minor bug in linux/mac sync mode fixed

* fix : minor edit in __paly_win function

* doc : CHANGELOG updated
  • Loading branch information
sepandhaghighi authored Jan 18, 2024
1 parent 78992c6 commit 9b166c4
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 105 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/windows_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ jobs:
- name: Install Scream
shell: powershell
run: |
Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
Install-Module -Name 7Zip4PowerShell -Force
Start-Service audio*
Invoke-WebRequest https://github.com/duncanthrax/scream/releases/download/3.6/Scream3.6.zip -OutFile C:\Scream3.6.zip
Extract-7Zip -Path C:\Scream3.6.zip -DestinationPath C:\Scream
Expand-7Zip -ArchiveFileName C:\Scream3.6.zip -TargetPath C:\Scream
$cert = (Get-AuthenticodeSignature C:\Scream\Install\driver\Scream.sys).SignerCertificate
$store = [System.Security.Cryptography.X509Certificates.X509Store]::new("TrustedPublisher", "LocalMachine")
$store.Open("ReadWrite")
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `NavaThread` class
- `stop` function
- `stop_all` function
### Changed
- `async_mode` parameter added to `play` function
## [0.2] - 2023-07-10
### Added
- Logo
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,24 @@ Nava is a Python library that allows users to play sound in Python without any d
## Usage

### Basic

```python
from nava import play
play("alarm.wav")
```

### Async mode

⚠️ The `async_mode` parameter has a default value of `False`

```python
import time
from nava import play, stop
sound_id = play("alarm.wav", async_mode=True)
time.sleep(4)
stop(sound_id)
```

### Error

```python
Expand Down
6 changes: 3 additions & 3 deletions autopep8.bat
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
python -m autopep8 nava --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --verbose
python -m autopep8 setup.py --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --verbose
python -m autopep8 others --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --verbose
python -m autopep8 nava --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose
python -m autopep8 setup.py --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose
python -m autopep8 others --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose
6 changes: 3 additions & 3 deletions autopep8.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
python -m autopep8 nava --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --verbose
python -m autopep8 setup.py --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --verbose
python -m autopep8 others --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --verbose
python -m autopep8 nava --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose
python -m autopep8 setup.py --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose
python -m autopep8 others --recursive --aggressive --aggressive --in-place --pep8-passes 2000 --max-line-length 120 --verbose
7 changes: 3 additions & 4 deletions nava/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
"""Nava modules."""
from .params import NAVA_VERSION
from .errors import NavaBaseError
from .functions import play, cleanup_processes

from .functions import play, stop, stop_all
import atexit
# Async play processes clean up
atexit.register(cleanup_processes)

atexit.register(stop_all)

__version__ = NAVA_VERSION
200 changes: 108 additions & 92 deletions nava/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,46 @@
import os
import shlex
from functools import wraps
import threading

from .thread import NavaThread
from .params import OVERVIEW
from .params import SOUND_FILE_PLAY_ERROR, SOUND_FILE_EXIST_ERROR
from .params import SOUND_FILE_PATH_TYPE_ERROR
from .params import SOUND_FILE_PATH_TYPE_ERROR, SOUND_ID_EXIST_ERROR
from .errors import NavaBaseError
from . import params

"""
List of all aplay processes
"""
play_processes = []

def stop(sound_id):
"""
Stop sound.
:param sound_id: sound id
:type sound_id: int
:return: None
"""
if sound_id not in params._play_threads_map:
raise NavaBaseError(SOUND_ID_EXIST_ERROR)
params._play_threads_map[sound_id].stop()


def stop_all():
"""
Stop all sounds.
:return: None
"""
for thread in params._play_threads_map.values():
thread.stop()


def sound_id_gen():
"""
Sound id generator.
:return: sound id as int
"""
params._play_threads_counter += 1
sound_id = params._play_threads_counter + 1000
return sound_id


def nava_help():
Expand Down Expand Up @@ -53,137 +82,124 @@ def quoter(sound_path, *args, **kwargs):
return quoter


def cleanup_processes():
def __play_win(sound_path, async_mode=False):
"""
Cleanup undead play processes after module exit.
Play sound in Windows.
:return: None
:param sound_path: sound path
:type sound_path: str
:param async_mode: async mode flag
:type async_mode: bool
:return: None or sound id
"""
for proc in play_processes:
proc.kill()
proc.terminate()
import winsound
play_flags = winsound.SND_FILENAME | (async_mode & winsound.SND_ASYNC)

if async_mode:
sound_thread = NavaThread(target=__play_win_flags,
args=(sound_path, play_flags), daemon=True)
sound_thread.start()
sound_id = sound_id_gen()
params._play_threads_map[sound_id] = sound_thread
return sound_id
else:
winsound.PlaySound(sound_path, play_flags)


def __play_win(sound_path, is_async=True):
def __play_win_flags(sound_path, flags):
"""
Play sound in Windows.
Play sound in Windows using different flags.
:param sound_path: sound path
:type sound_path: str
:param is_async: play async or not
:type is_async: bool
:param flags: different mode flags
:type flags: winsound flags
:return: None
"""
import winsound
# If is_async is ture, play async
play_flags = winsound.SND_FILENAME | (is_async & winsound.SND_ASYNC)
winsound.PlaySound(sound_path, play_flags)
winsound.PlaySound(sound_path, flags)


@quote
def __play_linux(sound_path, is_async=True):
def __play_linux(sound_path, async_mode=False):
"""
Play sound in Linux.
:param sound_path: sound path to be played
:type sound_path: str
:param is_async: play async or not
:type is_async: bool
:return: None or sound thread (depending on async flag)
"""
if is_async:
sound_thread = threading.Thread(target=__play_async_linux,
args=(sound_path,),
daemon=True)
:param async_mode: async mode flag
:type async_mode: bool
:return: None or sound id
"""
if async_mode:
sound_thread = NavaThread(target=__play_proc_linux,
args=(sound_path,),
daemon=True)
sound_thread.start()
return sound_thread
sound_id = sound_id_gen()
params._play_threads_map[sound_id] = sound_thread
return sound_id
else:
__play_sync_linux(sound_path)


def __play_sync_linux(sound_path):
"""
Play sound synchronously in Linux.
proc = __play_proc_linux(sound_path)
proc.wait()

:param sound_path: sound path to be played
:type sound_path: str
:return: None
"""
_ = subprocess.check_call(["aplay",
sound_path],
shell=False,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)


def __play_async_linux(sound_path):
def __play_proc_linux(sound_path):
"""
Play sound asynchronously in Linux.
Create sound playing process in Linux.
:param sound_path: sound path to be played
:type sound_path: str
:return: None
:return: process
"""
proc = subprocess.Popen(["aplay",
sound_path],
sound_path],
shell=False,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
play_processes.append(proc)
return proc


@quote
def __play_mac(sound_path, is_async=True):
def __play_mac(sound_path, async_mode=False):
"""
Play sound in macOS.
:param sound_path: sound path
:type sound_path: str
:param is_async: play sound in async mode
:type is_async: bool
:return: None or sound thread, depending on the flag
"""
if is_async:
sound_thread = threading.Thread(target=__play_async_mac,
args=(sound_path,),
daemon=True)
:param async_mode: async mode flag
:type async_mode: bool
:return: None or sound id
"""
if async_mode:
sound_thread = NavaThread(target=__play_proc_mac,
args=(sound_path,),
daemon=True)
sound_thread.start()
return sound_thread
sound_id = sound_id_gen()
params._play_threads_map[sound_id] = sound_thread
return sound_id
else:
__play_sync_mac(sound_path)


def __play_sync_mac(sound_path):
"""
Play sound synchronously in macOS.
:param sound_path: sound path to be played
:type sound_path: str
:return: None
"""
_ = subprocess.check_call(["afplay",
sound_path],
shell=False,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
proc = __play_proc_mac(sound_path)
proc.wait()


def __play_async_mac(sound_path):
def __play_proc_mac(sound_path):
"""
Play sound asynchronously in macOS.
Create sound playing process in macOS.
:param sound_path: sound path to be played
:type sound_path: str
:return: None
:return: process
"""
proc = subprocess.Popen(["afplay",
sound_path],
sound_path],
shell=False,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
play_processes.append(proc)
return proc


def path_check(func):
Expand Down Expand Up @@ -215,23 +231,23 @@ def path_checker(sound_path, *args, **kwargs):


@path_check
def play(sound_path, is_async=True):
def play(sound_path, async_mode=False):
"""
Play sound.
:param sound_path: sound path
:type sound_path: str
:param is_async: play synchronously or asynchronously (async by default)
:type is_async: bool
:return: None or sound thread for futher handlings
:param async_mode: async mode flag
:type async_mode: bool
:return: None or sound id
"""
try:
sys_platform = sys.platform
if sys_platform == "win32":
__play_win(sound_path, is_async)
return __play_win(sound_path, async_mode)
elif sys_platform == "darwin":
return __play_mac(sound_path, is_async)
return __play_mac(sound_path, async_mode)
else:
return __play_linux(sound_path, is_async)
except Exception: # pragma: no cover
return __play_linux(sound_path, async_mode)
except Exception: # pragma: no cover
raise NavaBaseError(SOUND_FILE_PLAY_ERROR)
4 changes: 4 additions & 0 deletions nava/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
SOUND_FILE_PLAY_ERROR = "Sound can not play due to some issues."
SOUND_FILE_EXIST_ERROR = "Given sound file doesn't exist."
SOUND_FILE_PATH_TYPE_ERROR = "Sound file's path should be a string."
SOUND_ID_EXIST_ERROR = "Given sound id doesn't exist."

_play_threads_map = dict()
_play_threads_counter = 0
Loading

0 comments on commit 9b166c4

Please sign in to comment.