From 4ae259c2357bcfbbbf46570c21dc2fad6bce0c8d Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Fri, 16 Jun 2023 23:42:30 +0330 Subject: [PATCH 01/12] Add async to dev - test async feature --- AUTHORS.md | 1 + dev/clear.wav | Bin 0 -> 15058 bytes dev/playsound.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ dev/test.py | 84 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 dev/clear.wav create mode 100644 dev/playsound.py create mode 100644 dev/test.py diff --git a/AUTHORS.md b/AUTHORS.md index 27f620d..e1540c2 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -11,3 +11,4 @@ # Other Contributors # ---------- - Zahra Mobasher ([Instagram](https://www.instagram.com/littleblackoyster/?hl=en)) +- Nima Samadi ([Github](https://github.com/NimaSamadi007)) \ No newline at end of file diff --git a/dev/clear.wav b/dev/clear.wav new file mode 100644 index 0000000000000000000000000000000000000000..f825b257633d6ca6bc97f1e05c3b18eb3f248cc7 GIT binary patch literal 15058 zcmeHNv5wp}5M6gE^9eydBV7s`NSnfGT$};HV8H+^RH#rvfQ1SbTe$ESgnwyujAl$fm)8ZO(t+Cj`6fY)77pOlZKrLik0eZ{>YC1YtRxmkK$jcgd7jg7Eg zfl^l}jp#yQMANo5Zbs5(AqB%bNx%1uLGxqU9Hwv-4(MX4>^u;B2lQdMC-g61HCalT z>k54bwQ#boBS8R*MM#Qp!$2nh5dXjdP^dN1ZRG1<2x$(8+V>h55lH|O5DpwWEK48< z3qTIJh=6`@AtKOZ{e+ew7y&ES&&IH^05*q&I=;+aXOn^|_FlZ0L_rY&EP7eVDcSax zOSa973L8`k!}8K#+Cc#xvq*Hdj3jfj-h+*7o#N9xQLqgnF;FQT;QoM94wW#nS?(iC z94@ly&FkvxLbqkqPmlArW4038^l9&qCMWOy--0SuSB$A-4VAgf@;H<m^uT_~9?@XHi-4?BRwCC@FYrYYuf0w8^paz;4zh5r zJ5$mK*SUv!xWU~B*SQBnw^pm`?D4zMY@39W&^a$Bov->3e)`M9=InyY4!UEx)p>ON z#VFI}QnrW74Lww*hiZW@5BHL_j2*h*J5rafQMSIyQ0wQ}iRN0Fa7}4-2C`jslPcrC zcOom1b%PVdt63*@n{c~=g{qmI2D^>>Nhc1vaHMQ~R;gC(BO$xr3Q$(Gh_~`zH?DH$ zX8NGZ_53B{*LnSHd#NeeE@@1>p6rCAR;n}Y;YIwZ9bEyF5Z`JOuRX?6lacX zby?nP($zIpl{_p}SEo}S@tIP2rM#y+mUluh{uW{$D-h4z_7BJk;YZQOqidx6r1%>Pka?#zpCqtLI`lA?_HQ`5`^QZ7 zPt$P8IybR*Q-1SkZRX>P1$z(2i?*N9+2cDyOizy>&#!LA2^i~?8~qI$*NR5#CSCi? zHnJELYE3O!`)1f~yf6GSGY$?)`?cgBcvu)qM<9FLN(<_(v(-HlN~eQN&xH?$pu>!d z7YG^F&AWJKd7}dwTmtZ>7tfGG3~$cy|jg-2ep@ zvFW}>G*vcRemO(CBVqt&Pkn34Vqgpzr+WL!54j3MJ{_BFuOWFAj None: + """ + Start playing given sound + + :file_name: sound file name to play + :type: str + :is_async: play sound synchronously or asynchronously + :type: bool + :loop: play sound on loop + :type: bool + :return: None + """ + if is_async: + asyncio.run(play_sound_async(file_name, loop)) + else: + play_sound_sync(file_name, loop) + +def test_sound_playing(): + """ + Example function testing sound player + """ + start_sound("clear.wav", True, True) + +if __name__ == "__main__": + test_sound_playing() From 2271c8fcb604f0ce68398250b7c58d870c63a035 Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Sat, 24 Jun 2023 18:05:07 +0330 Subject: [PATCH 02/12] Test nava play --- test/__init__.py | 0 {dev => test}/clear.wav | Bin test/main.py | 3 +++ 3 files changed, 3 insertions(+) create mode 100644 test/__init__.py rename {dev => test}/clear.wav (100%) create mode 100644 test/main.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev/clear.wav b/test/clear.wav similarity index 100% rename from dev/clear.wav rename to test/clear.wav diff --git a/test/main.py b/test/main.py new file mode 100644 index 0000000..696ca32 --- /dev/null +++ b/test/main.py @@ -0,0 +1,3 @@ +import nava.functions as nvf + +nvf.play("test/clear.wav") From 1310e1d31ba08b692dd101af624155fe7f599a89 Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Sun, 25 Jun 2023 12:20:25 +0330 Subject: [PATCH 03/12] Add async and loop feature --- .gitignore | 2 ++ nava/functions.py | 50 +++++++++++++++++++++++++++++++++++++++-------- test/main.py | 2 +- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 73358ad..664f91e 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,5 @@ dmypy.json # Cython debug symbols cython_debug/ +# Dev realated files +dev/ diff --git a/nava/functions.py b/nava/functions.py index ea204f1..f3ea174 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -5,6 +5,7 @@ import os import shlex from functools import wraps +import asyncio from .params import OVERVIEW from .params import SOUND_FILE_PLAY_ERROR, SOUND_FILE_EXIST_ERROR @@ -60,7 +61,7 @@ def __play_win(sound_path): @quote -def __play_linux(sound_path): +def __play_linux(sound_path, loop=False, is_async=True): """ Play sound in Linux. @@ -68,12 +69,41 @@ def __play_linux(sound_path): :type sound_path: str :return: None """ + if is_async: + if loop: + while True: + asyncio.run(__play_async_linx(sound_path)) + else: + asyncio.run(__play_async_linx(sound_path)) + else: + if loop: + while True: + __play_sync_linux(sound_path) + else: + __play_sync_linux(sound_path) + +def __play_sync_linux(sound_path): + """ + Play sound synch in Linux + """ _ = subprocess.check_call(["aplay", - sound_path], - shell=False, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) + sound_path], + shell=False, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + +async def __play_async_linx(sound_path): + """ + Play sound async in Linux + """ + play_cmd = f"aplay {sound_path}" + proc = await asyncio.subprocess.create_subprocess_shell( + play_cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() @quote @@ -122,12 +152,16 @@ def path_checker(sound_path, *args, **kwargs): @path_check -def play(sound_path): +def play(sound_path, loop=False, is_async=True): """ Play sound. :param sound_path: sound path :type sound_path: str + :param loop: play sound on loop (False by default) + :type loop: bool + :param is_async: play synchronously or asynchronously (True by default) + :type is_async: bool :return: None """ try: @@ -137,6 +171,6 @@ def play(sound_path): elif sys_platform == "darwin": __play_mac(sound_path) else: - __play_linux(sound_path) + __play_linux(sound_path, loop, is_async) except Exception: raise NavaBaseError(SOUND_FILE_PLAY_ERROR) diff --git a/test/main.py b/test/main.py index 696ca32..4f2b91c 100644 --- a/test/main.py +++ b/test/main.py @@ -1,3 +1,3 @@ import nava.functions as nvf -nvf.play("test/clear.wav") +nvf.play("test/clear.wav", is_async=False, loop=False) From 2a3c6b326c6ad0d5cb17ec60b026b208592a0b84 Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Mon, 10 Jul 2023 21:54:36 +0330 Subject: [PATCH 04/12] Fix async bugs - Update documentation and test script * Async bugs: call the play function async from test script * Test script: more intuitive example of test script --- .gitignore | 3 -- dev/playsound.py | 80 -------------------------------------------- dev/test.py | 84 ----------------------------------------------- nava/functions.py | 42 +++++++++++++----------- test/main.py | 47 ++++++++++++++++++++++++-- 5 files changed, 67 insertions(+), 189 deletions(-) delete mode 100644 dev/playsound.py delete mode 100644 dev/test.py diff --git a/.gitignore b/.gitignore index 664f91e..a81c8ee 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,3 @@ dmypy.json # Cython debug symbols cython_debug/ - -# Dev realated files -dev/ diff --git a/dev/playsound.py b/dev/playsound.py deleted file mode 100644 index 98cba6e..0000000 --- a/dev/playsound.py +++ /dev/null @@ -1,80 +0,0 @@ -# simpleAudio.py -# very basic audio file player without external modules -# for Windows and Mac in Python. Only plays wav files -# on Windows, seems to work with wav, mp3 and other formats on Macs. - -# Provides two functions: -# startSound(filename, async=True, loop=True) -# stopSound() - -import sys, platform, subprocess, time, threading - -def checkForOS(*terms): - platformString = (sys.platform + platform.platform()).lower() - for term in terms: - if (term in platformString): return True - return False -osIsWindows = checkForOS("win32", "win64", "windows") -osIsLinux = checkForOS("linux") - -if (osIsWindows == True): - import winsound - def startSound(filename, async=True, loop=True): - flags = winsound.SND_FILENAME - if (async == True): flags |= winsound.SND_ASYNC - if (loop == True): flags |= winsound.SND_LOOP - winsound.PlaySound(filename, flags) - def stopSound(): - winsound.PlaySound(None, 0) -else: - # For now, just do Mac OS (sorry, Linux...) - soundProcesses = [ ] - soundThreads = [ ] - soundThreadCounter = 0 - def startSound(filename, async=True, loop=True): - if (async == True): startAsyncSound(filename, loop) - else: startSyncSound(filename, loop) - def startSyncSound(filename, loop, thread=None): - while True: - app = "aplay" if osIsLinux else "afplay" - p = subprocess.Popen([app, filename]) - soundProcesses.append(p) - p.wait() - if (p in soundProcesses): soundProcesses.remove(p) - if (loop == False): break - if ((thread != None) and (thread not in soundThreads)): break - def startAsyncSound(filename, loop): - global soundThreadCounter - tc = soundThreadCounter - soundThreadCounter += 1 - soundThreads.append(tc) - thread = threading.Thread(target=startSyncSound, - args=(filename,loop,tc)) - thread.daemon = True - thread.start() - def stopSound(): - global soundThreads - soundThreads = [ ] - while (soundProcesses != [ ]): - try: soundProcesses.pop().terminate() - except: pass - -##################################### -# Simple test -##################################### -""" -def testSoundPlaying(): - print "starting sound!...", - startSound("tetris.wav", async=True, loop=True) - print "done!" - - print "sleeping for 4 seconds...", - time.sleep(4) - print "done!" - - print "stopping sound...", - stopSound() - print "done!" - -testSoundPlaying() -""" \ No newline at end of file diff --git a/dev/test.py b/dev/test.py deleted file mode 100644 index cf44dbf..0000000 --- a/dev/test.py +++ /dev/null @@ -1,84 +0,0 @@ -import asyncio -import subprocess - - -async def count(): - """ - Example function to test async play - - """ - while True: - print("Hi!") - await asyncio.sleep(1) - -async def play_sound_async(file_name, loop = True): - """ - Play sound asynchronously - - :param file_name: input sound file name - :type file_name: str - :param loop: play sound on loop - :type: bool - :return: None - """ - task = asyncio.create_task(count()) - cmd = f"aplay {file_name}" - while True: - proc = await asyncio.subprocess.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - - stdout, stderr = await proc.communicate() - - if not loop: - task.cancel() - break - -def play_sound_sync(file_name, loop = True): - """ - Play sound synchronously - - :param file_name: input sound file name - :type file_name: str - :param loop: play sound on loop - :type: bool - :return: None - """ - while True: - _ = subprocess.check_call(["aplay", - file_name], - shell=False, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - if not loop: - break - - -def start_sound(file_name: str, is_async: bool = True, loop: bool = True) -> None: - """ - Start playing given sound - - :file_name: sound file name to play - :type: str - :is_async: play sound synchronously or asynchronously - :type: bool - :loop: play sound on loop - :type: bool - :return: None - """ - if is_async: - asyncio.run(play_sound_async(file_name, loop)) - else: - play_sound_sync(file_name, loop) - -def test_sound_playing(): - """ - Example function testing sound player - """ - start_sound("clear.wav", True, True) - -if __name__ == "__main__": - test_sound_playing() diff --git a/nava/functions.py b/nava/functions.py index f3ea174..f507bb4 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -61,30 +61,29 @@ def __play_win(sound_path): @quote -def __play_linux(sound_path, loop=False, is_async=True): +async def __play_linux(sound_path, is_async=True): """ Play sound in Linux. - :param sound_path: sound path + :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 """ if is_async: - if loop: - while True: - asyncio.run(__play_async_linx(sound_path)) - else: - asyncio.run(__play_async_linx(sound_path)) + task = asyncio.create_task(__play_async_linux(sound_path)) + await task else: - if loop: - while True: - __play_sync_linux(sound_path) - else: - __play_sync_linux(sound_path) + __play_sync_linux(sound_path) + def __play_sync_linux(sound_path): """ - Play sound synch in Linux + Play sound synchronously in Linux + :param sound_path: sound path to be played + :type sound_path: str + :return: None """ _ = subprocess.check_call(["aplay", sound_path], @@ -93,9 +92,13 @@ def __play_sync_linux(sound_path): stdin=subprocess.PIPE, stdout=subprocess.PIPE) -async def __play_async_linx(sound_path): + +async def __play_async_linux(sound_path): """ - Play sound async in Linux + Play sound asynchronously in Linux + :param sound_path: sound path to be played + :type sound_path: str + :return: None """ play_cmd = f"aplay {sound_path}" proc = await asyncio.subprocess.create_subprocess_shell( @@ -152,15 +155,13 @@ def path_checker(sound_path, *args, **kwargs): @path_check -def play(sound_path, loop=False, is_async=True): +async def play(sound_path, is_async=True): """ Play sound. :param sound_path: sound path :type sound_path: str - :param loop: play sound on loop (False by default) - :type loop: bool - :param is_async: play synchronously or asynchronously (True by default) + :param is_async: play synchronously or asynchronously (async by default) :type is_async: bool :return: None """ @@ -171,6 +172,7 @@ def play(sound_path, loop=False, is_async=True): elif sys_platform == "darwin": __play_mac(sound_path) else: - __play_linux(sound_path, loop, is_async) + task = asyncio.create_task(__play_linux(sound_path, is_async)) + await task except Exception: raise NavaBaseError(SOUND_FILE_PLAY_ERROR) diff --git a/test/main.py b/test/main.py index 4f2b91c..571c407 100644 --- a/test/main.py +++ b/test/main.py @@ -1,3 +1,46 @@ -import nava.functions as nvf +import asyncio +import nava.functions as nv -nvf.play("test/clear.wav", is_async=False, loop=False) + +async def test_print(): + """ + Simple function printing hello world async + + :return: None + """ + while True: + print("Hello, World!") + await asyncio.sleep(0.1) + +async def test_play(): + """ + Simple function playing sound async (must provide path to file) + + :return: None + """ + play_task = asyncio.create_task(nv.play("test/clear.wav", is_async=True)) + print_task = asyncio.create_task(test_print()) + + await play_task + +if __name__ == "__main__": + asyncio.run(test_play()) + """ + How to run? Make sure you're in the project root + directory. Then, pass the script to Python interpreter + like below: + > python -m test.main + The result in async mode with 0.1 second delay is as follows: + Hello, World! + Hello, World! + Hello, World! + Hello, World! + Hello, World! + Hello, World! + Hello, World! + Hello, World! + Hello, World! + + In the meantime you should be able to hear the sound. The actual + number of hello worlds depend on the voice length and the delay value. + """ From de92990dbc2c5585c5047b56f3b99c0ebade901f Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Mon, 10 Jul 2023 22:01:52 +0330 Subject: [PATCH 05/12] Remove clear.wav - Fix documentation spacing issue --- nava/functions.py | 2 ++ test/clear.wav | Bin 15058 -> 0 bytes 2 files changed, 2 insertions(+) delete mode 100644 test/clear.wav diff --git a/nava/functions.py b/nava/functions.py index f507bb4..0883d0e 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -81,6 +81,7 @@ async def __play_linux(sound_path, is_async=True): def __play_sync_linux(sound_path): """ Play sound synchronously in Linux + :param sound_path: sound path to be played :type sound_path: str :return: None @@ -96,6 +97,7 @@ def __play_sync_linux(sound_path): async def __play_async_linux(sound_path): """ Play sound asynchronously in Linux + :param sound_path: sound path to be played :type sound_path: str :return: None diff --git a/test/clear.wav b/test/clear.wav deleted file mode 100644 index f825b257633d6ca6bc97f1e05c3b18eb3f248cc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15058 zcmeHNv5wp}5M6gE^9eydBV7s`NSnfGT$};HV8H+^RH#rvfQ1SbTe$ESgnwyujAl$fm)8ZO(t+Cj`6fY)77pOlZKrLik0eZ{>YC1YtRxmkK$jcgd7jg7Eg zfl^l}jp#yQMANo5Zbs5(AqB%bNx%1uLGxqU9Hwv-4(MX4>^u;B2lQdMC-g61HCalT z>k54bwQ#boBS8R*MM#Qp!$2nh5dXjdP^dN1ZRG1<2x$(8+V>h55lH|O5DpwWEK48< z3qTIJh=6`@AtKOZ{e+ew7y&ES&&IH^05*q&I=;+aXOn^|_FlZ0L_rY&EP7eVDcSax zOSa973L8`k!}8K#+Cc#xvq*Hdj3jfj-h+*7o#N9xQLqgnF;FQT;QoM94wW#nS?(iC z94@ly&FkvxLbqkqPmlArW4038^l9&qCMWOy--0SuSB$A-4VAgf@;H<m^uT_~9?@XHi-4?BRwCC@FYrYYuf0w8^paz;4zh5r zJ5$mK*SUv!xWU~B*SQBnw^pm`?D4zMY@39W&^a$Bov->3e)`M9=InyY4!UEx)p>ON z#VFI}QnrW74Lww*hiZW@5BHL_j2*h*J5rafQMSIyQ0wQ}iRN0Fa7}4-2C`jslPcrC zcOom1b%PVdt63*@n{c~=g{qmI2D^>>Nhc1vaHMQ~R;gC(BO$xr3Q$(Gh_~`zH?DH$ zX8NGZ_53B{*LnSHd#NeeE@@1>p6rCAR;n}Y;YIwZ9bEyF5Z`JOuRX?6lacX zby?nP($zIpl{_p}SEo}S@tIP2rM#y+mUluh{uW{$D-h4z_7BJk;YZQOqidx6r1%>Pka?#zpCqtLI`lA?_HQ`5`^QZ7 zPt$P8IybR*Q-1SkZRX>P1$z(2i?*N9+2cDyOizy>&#!LA2^i~?8~qI$*NR5#CSCi? zHnJELYE3O!`)1f~yf6GSGY$?)`?cgBcvu)qM<9FLN(<_(v(-HlN~eQN&xH?$pu>!d z7YG^F&AWJKd7}dwTmtZ>7tfGG3~$cy|jg-2ep@ zvFW}>G*vcRemO(CBVqt&Pkn34Vqgpzr+WL!54j3MJ{_BFuOWFAj Date: Mon, 10 Jul 2023 22:03:36 +0330 Subject: [PATCH 06/12] Fix period issue in documentations --- nava/functions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nava/functions.py b/nava/functions.py index 0883d0e..9b7bda4 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -80,7 +80,7 @@ async def __play_linux(sound_path, is_async=True): def __play_sync_linux(sound_path): """ - Play sound synchronously in Linux + Play sound synchronously in Linux. :param sound_path: sound path to be played :type sound_path: str @@ -96,8 +96,8 @@ def __play_sync_linux(sound_path): async def __play_async_linux(sound_path): """ - Play sound asynchronously in Linux - + Play sound asynchronously in Linux. + :param sound_path: sound path to be played :type sound_path: str :return: None From d7aaa54f00e92d008b4f705e0d0121bff799b830 Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Sun, 16 Jul 2023 23:34:44 +0330 Subject: [PATCH 07/12] Implement async feature with threading module * Async feature: Now user will get a handling thread which then could be used for waiting for the sound playing to be ended or do other tasks in the meantime. * Update documentations --- nava/__init__.py | 4 ++++ nava/functions.py | 59 ++++++++++++++++++++++++++++++++--------------- test/main.py | 49 +++++++++++---------------------------- 3 files changed, 59 insertions(+), 53 deletions(-) diff --git a/nava/__init__.py b/nava/__init__.py index 3f535d0..21049d1 100644 --- a/nava/__init__.py +++ b/nava/__init__.py @@ -4,4 +4,8 @@ from .errors import NavaBaseError from .functions import play +import atexit +# Async play processes clean up +atexit.register(functions.__cleanup_processes) + __version__ = NAVA_VERSION diff --git a/nava/functions.py b/nava/functions.py index cdb9edb..1f39889 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -5,13 +5,18 @@ import os import shlex from functools import wraps -import asyncio +import threading from .params import OVERVIEW from .params import SOUND_FILE_PLAY_ERROR, SOUND_FILE_EXIST_ERROR from .params import SOUND_FILE_PATH_TYPE_ERROR from .errors import NavaBaseError +""" +List of all aplay processes +""" +play_processes = [] + def nava_help(): """ @@ -48,6 +53,17 @@ def quoter(sound_path, *args, **kwargs): return quoter +def __cleanup_processes(): + """ + Cleanup undead play processes after module exit. + + :return: None + """ + for proc in play_processes: + proc.terminate() + proc.kill() + + def __play_win(sound_path): """ Play sound in Windows. @@ -61,7 +77,7 @@ def __play_win(sound_path): @quote -async def __play_linux(sound_path, is_async=True): +def __play_linux(sound_path, is_async=True): """ Play sound in Linux. @@ -69,13 +85,12 @@ async def __play_linux(sound_path, is_async=True): :type sound_path: str :param is_async: play async or not :type is_async: bool - :return: None + :return: None or sound thread """ if is_async: - task = asyncio.create_task(__play_async_linux(sound_path)) - await task + return __play_async_linux(sound_path) else: - __play_sync_linux(sound_path) + return __play_sync_linux(sound_path) def __play_sync_linux(sound_path): @@ -92,23 +107,31 @@ def __play_sync_linux(sound_path): stderr=subprocess.PIPE, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + return None -async def __play_async_linux(sound_path): +def __play_non_blocking_linux(sound_path): + proc = subprocess.Popen(["aplay", + sound_path], + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + play_processes.append(proc) + + +def __play_async_linux(sound_path): """ Play sound asynchronously in Linux. :param sound_path: sound path to be played :type sound_path: str - :return: None + :return: sound thread for further handalings """ - play_cmd = f"aplay {sound_path}" - proc = await asyncio.subprocess.create_subprocess_shell( - play_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await proc.communicate() + sound_thread = threading.Thread(target=__play_non_blocking_linux, + args=(sound_path,), + daemon=True) + sound_thread.start() + return sound_thread @quote @@ -157,7 +180,7 @@ def path_checker(sound_path, *args, **kwargs): @path_check -async def play(sound_path, is_async=True): +def play(sound_path, is_async=True): """ Play sound. @@ -165,7 +188,7 @@ async def play(sound_path, is_async=True): :type sound_path: str :param is_async: play synchronously or asynchronously (async by default) :type is_async: bool - :return: None + :return: None or sound thread for futher handlings """ try: sys_platform = sys.platform @@ -174,6 +197,6 @@ async def play(sound_path, is_async=True): elif sys_platform == "darwin": __play_mac(sound_path) else: - __play_linux(sound_path) + return __play_linux(sound_path, is_async) except Exception: # pragma: no cover raise NavaBaseError(SOUND_FILE_PLAY_ERROR) diff --git a/test/main.py b/test/main.py index 571c407..d7f66c2 100644 --- a/test/main.py +++ b/test/main.py @@ -1,46 +1,25 @@ -import asyncio +import time import nava.functions as nv +if __name__ == "__main__": + sound_thread = nv.play("test/test.wav", is_async=True) -async def test_print(): - """ - Simple function printing hello world async - - :return: None - """ - while True: - print("Hello, World!") - await asyncio.sleep(0.1) - -async def test_play(): - """ - Simple function playing sound async (must provide path to file) - - :return: None - """ - play_task = asyncio.create_task(nv.play("test/clear.wav", is_async=True)) - print_task = asyncio.create_task(test_print()) - - await play_task + for _ in range(10): + print("Hello World!") + time.sleep(0.25) -if __name__ == "__main__": - asyncio.run(test_play()) """ How to run? Make sure you're in the project root directory. Then, pass the script to Python interpreter like below: > python -m test.main - The result in async mode with 0.1 second delay is as follows: - Hello, World! - Hello, World! - Hello, World! - Hello, World! - Hello, World! - Hello, World! - Hello, World! - Hello, World! - Hello, World! - In the meantime you should be able to hear the sound. The actual - number of hello worlds depend on the voice length and the delay value. + Expected behaviours: + * Async mode: If you don't wait for the thread, you + can only hear about 2.5 seconds of the voice. In the + meantime you will see "Hello World!" messages printing + on the screen + * Sync mode: You will hear the sound completely and then, + for about 2.5 seconds you will see 10 "Hello World!" + messages printing on the screen. """ From 27761c22938585e6ef51466dfa0c39b620d927fd Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Mon, 17 Jul 2023 10:22:55 +0330 Subject: [PATCH 08/12] Document non-blocking sound play function --- nava/functions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nava/functions.py b/nava/functions.py index 1f39889..586c1da 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -111,6 +111,13 @@ def __play_sync_linux(sound_path): def __play_non_blocking_linux(sound_path): + """ + Non-blocking sound playing. + + :param sound_path: sound path to be played + :type sound_path: str + :return: None + """ proc = subprocess.Popen(["aplay", sound_path], stderr=subprocess.PIPE, From 76b2732a82248ada01c26c7cefc3f1b6ad036f7e Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Sat, 22 Jul 2023 15:33:13 +0330 Subject: [PATCH 09/12] Remove repetitive functions - Fix possible import issues in __init__ - Finish Linux implementation * __non_blocking_play() is removed * cleanup_processes function name changed to make sure it's imported * Linux implementation completed and tested --- nava/__init__.py | 4 ++-- nava/functions.py | 36 ++++++++++++------------------------ 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/nava/__init__.py b/nava/__init__.py index 21049d1..6ed050a 100644 --- a/nava/__init__.py +++ b/nava/__init__.py @@ -2,10 +2,10 @@ """Nava modules.""" from .params import NAVA_VERSION from .errors import NavaBaseError -from .functions import play +from .functions import play, cleanup_processes import atexit # Async play processes clean up -atexit.register(functions.__cleanup_processes) +atexit.register(cleanup_processes) __version__ = NAVA_VERSION diff --git a/nava/functions.py b/nava/functions.py index 586c1da..b79f403 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -53,15 +53,15 @@ def quoter(sound_path, *args, **kwargs): return quoter -def __cleanup_processes(): +def cleanup_processes(): """ Cleanup undead play processes after module exit. :return: None """ for proc in play_processes: - proc.terminate() proc.kill() + proc.terminate() def __play_win(sound_path): @@ -85,12 +85,16 @@ def __play_linux(sound_path, is_async=True): :type sound_path: str :param is_async: play async or not :type is_async: bool - :return: None or sound thread + :return: None or sound thread (depending on async flag) """ if is_async: - return __play_async_linux(sound_path) + sound_thread = threading.Thread(target=__play_async_linux, + args=(sound_path,), + daemon=True) + sound_thread.start() + return sound_thread else: - return __play_sync_linux(sound_path) + __play_sync_linux(sound_path) def __play_sync_linux(sound_path): @@ -107,12 +111,11 @@ def __play_sync_linux(sound_path): stderr=subprocess.PIPE, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - return None + - -def __play_non_blocking_linux(sound_path): +def __play_async_linux(sound_path): """ - Non-blocking sound playing. + Play sound asynchronously in Linux. :param sound_path: sound path to be played :type sound_path: str @@ -126,21 +129,6 @@ def __play_non_blocking_linux(sound_path): play_processes.append(proc) -def __play_async_linux(sound_path): - """ - Play sound asynchronously in Linux. - - :param sound_path: sound path to be played - :type sound_path: str - :return: sound thread for further handalings - """ - sound_thread = threading.Thread(target=__play_non_blocking_linux, - args=(sound_path,), - daemon=True) - sound_thread.start() - return sound_thread - - @quote def __play_mac(sound_path): """ From 11828cbacdb0e3340b6760f3bd9422df32539c11 Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Sat, 22 Jul 2023 15:57:55 +0330 Subject: [PATCH 10/12] Implement Windows async play feature - Tested in Windows --- nava/functions.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nava/functions.py b/nava/functions.py index b79f403..659f03b 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -64,16 +64,20 @@ def cleanup_processes(): proc.terminate() -def __play_win(sound_path): +def __play_win(sound_path, is_async): """ Play sound in Windows. :param sound_path: sound path :type sound_path: str + :param is_async: play async or not + :type is_async: bool :return: None """ import winsound - winsound.PlaySound(sound_path, winsound.SND_FILENAME) + # If is_async is ture, play async + play_flags = winsound.SND_FILENAME | (is_async & winsound.SND_ASYNC) + winsound.PlaySound(sound_path, play_flags) @quote @@ -188,7 +192,7 @@ def play(sound_path, is_async=True): try: sys_platform = sys.platform if sys_platform == "win32": - __play_win(sound_path) + __play_win(sound_path, is_async) elif sys_platform == "darwin": __play_mac(sound_path) else: From befee15ad2dd26305ecb0a71827781c11409793a Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Sat, 22 Jul 2023 23:26:18 +0330 Subject: [PATCH 11/12] Implement macOS async feature based on Linux --- nava/functions.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/nava/functions.py b/nava/functions.py index 659f03b..cb2953a 100644 --- a/nava/functions.py +++ b/nava/functions.py @@ -64,7 +64,7 @@ def cleanup_processes(): proc.terminate() -def __play_win(sound_path, is_async): +def __play_win(sound_path, is_async=True): """ Play sound in Windows. @@ -134,12 +134,32 @@ def __play_async_linux(sound_path): @quote -def __play_mac(sound_path): +def __play_mac(sound_path, is_async=True): """ 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) + sound_thread.start() + return sound_thread + 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", @@ -150,6 +170,22 @@ def __play_mac(sound_path): stdout=subprocess.PIPE) +def __play_async_mac(sound_path): + """ + Play sound asynchronously in macOS. + + :param sound_path: sound path to be played + :type sound_path: str + :return: None + """ + proc = subprocess.Popen(["afplay", + sound_path], + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + play_processes.append(proc) + + def path_check(func): """ Check the given path to be a string and a valid file directory. @@ -194,7 +230,7 @@ def play(sound_path, is_async=True): if sys_platform == "win32": __play_win(sound_path, is_async) elif sys_platform == "darwin": - __play_mac(sound_path) + return __play_mac(sound_path, is_async) else: return __play_linux(sound_path, is_async) except Exception: # pragma: no cover From fb5b2596ccac1a368e41982928aa3b3dd17ee792 Mon Sep 17 00:00:00 2001 From: Nima Samadi Date: Tue, 25 Jul 2023 12:15:59 +0330 Subject: [PATCH 12/12] Remove test/main and test/__init__.py --- test/__init__.py | 0 test/main.py | 25 ------------------------- 2 files changed, 25 deletions(-) delete mode 100644 test/__init__.py delete mode 100644 test/main.py diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/main.py b/test/main.py deleted file mode 100644 index d7f66c2..0000000 --- a/test/main.py +++ /dev/null @@ -1,25 +0,0 @@ -import time -import nava.functions as nv - -if __name__ == "__main__": - sound_thread = nv.play("test/test.wav", is_async=True) - - for _ in range(10): - print("Hello World!") - time.sleep(0.25) - - """ - How to run? Make sure you're in the project root - directory. Then, pass the script to Python interpreter - like below: - > python -m test.main - - Expected behaviours: - * Async mode: If you don't wait for the thread, you - can only hear about 2.5 seconds of the voice. In the - meantime you will see "Hello World!" messages printing - on the screen - * Sync mode: You will hear the sound completely and then, - for about 2.5 seconds you will see 10 "Hello World!" - messages printing on the screen. - """