diff -pruN 2.5.2-1/.github/workflows/check-code-quality.yaml 3.3.0-1/.github/workflows/check-code-quality.yaml
--- 2.5.2-1/.github/workflows/check-code-quality.yaml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.github/workflows/check-code-quality.yaml	1970-01-01 00:00:00.000000000 +0000
@@ -1,39 +0,0 @@
-name: Check code quality with Python 3.13
-
-on:
-  push:
-    branches: ["main"]
-  pull_request:
-    branches: ["main"]
-
-permissions:
-  contents: read
-
-jobs:
-  build:
-
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v4
-      - name: Set up Python 3.13
-        uses: actions/setup-python@v5
-        with:
-          python-version: "3.13"
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install Flake8-pyproject pytest mypy black isort
-
-      - name: Lint with flake8
-        run: |
-          flake8 .
-      - name: Lint with mypy
-        run: |
-          mypy .
-      - name: Check style with isort
-        run: |
-          isort --check .
-      - name: Check style with black
-        run: |
-          black --check .
diff -pruN 2.5.2-1/.github/workflows/check-with-pytest-linux.yaml 3.3.0-1/.github/workflows/check-with-pytest-linux.yaml
--- 2.5.2-1/.github/workflows/check-with-pytest-linux.yaml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.github/workflows/check-with-pytest-linux.yaml	1970-01-01 00:00:00.000000000 +0000
@@ -1,70 +0,0 @@
-name: Install locally and run pytest on Linux
-
-on:
-  push:
-    branches: ["main"]
-  pull_request:
-    branches: ["main"]
-
-jobs:
-  build:
-
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
-
-    steps:
-      #      - name: Setup GStreamer
-      #        id: setup_gstreamer
-      #        uses: blinemedical/setup-gstreamer@v1.4.0
-      # TODO(Martin): Migrate to pipewire-pulse
-      - name: Start PulseAudio server - TODO migrate to pipewire-pulse!
-        run: |
-          sudo apt-get update
-          sudo apt-get -y install pulseaudio pulseaudio-utils
-          pulseaudio --start
-          pactl load-module module-null-sink sink_name=virtual-sink sink_properties=device.description="Virtual_Sink"
-          pactl set-default-sink virtual-sink
-      - name: Check PulseAudio status
-        run: pulseaudio --check
-      - name: List PulseAudio sinks
-        run: pactl list short sinks
-      - name: Setup GStreamer
-        run: |
-          sudo apt-get update
-          sudo apt-get -y install \
-              libunwind-dev \
-              libgirepository1.0-dev \
-              gstreamer1.0 \
-              gstreamer1.0-pulseaudio \
-              gstreamer1.0-alsa \
-              libgstreamer1.0-dev \
-              libgstreamer-plugins-base1.0-dev \
-              libgstreamer-plugins-bad1.0-dev \
-              gstreamer1.0-plugins-base \
-              gstreamer1.0-plugins-good \
-              gstreamer1.0-plugins-bad \
-              gstreamer1.0-plugins-ugly \
-              gstreamer1.0-libav \
-              gstreamer1.0-tools \
-              gstreamer1.0-x \
-              gstreamer1.0-alsa \
-              gstreamer1.0-gl \
-              gstreamer1.0-gtk3 \
-              gstreamer1.0-qt5 \
-              gstreamer1.0-pulseaudio
-          gst-inspect-1.0
-      - uses: actions/checkout@v4
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v5
-        with:
-          python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
-        run: |
-          pip install .
-          pip install pytest
-
-      - name: Test with pytest
-        run: |
-          timeout 60 pytest tests --log-cli-level=WARNING -vv
diff -pruN 2.5.2-1/.github/workflows/check-with-pytest-macos.yaml 3.3.0-1/.github/workflows/check-with-pytest-macos.yaml
--- 2.5.2-1/.github/workflows/check-with-pytest-macos.yaml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.github/workflows/check-with-pytest-macos.yaml	1970-01-01 00:00:00.000000000 +0000
@@ -1,30 +0,0 @@
-name: Install locally and run pytest on macOS
-
-on:
-  push:
-    branches: ["main"]
-  pull_request:
-    branches: ["main"]
-
-jobs:
-  build:
-
-    runs-on: macos-latest
-    strategy:
-      matrix:
-        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
-
-    steps:
-      - uses: actions/checkout@v4
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v5
-        with:
-          python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
-        run: |
-          pip install .
-          pip install pytest
-
-      - name: Test with pytest
-        run: |
-          pytest tests --log-cli-level=WARNING
diff -pruN 2.5.2-1/.github/workflows/check-with-pytest-windows.yaml 3.3.0-1/.github/workflows/check-with-pytest-windows.yaml
--- 2.5.2-1/.github/workflows/check-with-pytest-windows.yaml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.github/workflows/check-with-pytest-windows.yaml	1970-01-01 00:00:00.000000000 +0000
@@ -1,36 +0,0 @@
-name: Install locally and run pytest on Windows
-
-# This test fails on windows server without audio devices
-# So we don't run it automatically
-
-on:
-  workflow_dispatch:
-
-#on:
-#  push:
-#    branches: ["main"]
-#  pull_request:
-#    branches: ["main"]
-
-jobs:
-  build:
-
-    runs-on: windows-2019
-    strategy:
-      matrix:
-        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
-
-    steps:
-      - uses: actions/checkout@v4
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v5
-        with:
-          python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
-        run: |
-          pip install .
-          pip install pytest
-
-      - name: Test with pytest
-        run: |
-          pytest tests --log-cli-level=WARNING
diff -pruN 2.5.2-1/.gitignore 3.3.0-1/.gitignore
--- 2.5.2-1/.gitignore	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.gitignore	2020-02-02 00:00:00.000000000 +0000
@@ -59,5 +59,6 @@ docs/_build/
 target/
 
 # Custom
+.idea
 devel/
 **/script*
diff -pruN 2.5.2-1/.idea/.gitignore 3.3.0-1/.idea/.gitignore
--- 2.5.2-1/.idea/.gitignore	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/.gitignore	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff -pruN 2.5.2-1/.idea/inspectionProfiles/Project_Default.xml 3.3.0-1/.idea/inspectionProfiles/Project_Default.xml
--- 2.5.2-1/.idea/inspectionProfiles/Project_Default.xml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/inspectionProfiles/Project_Default.xml	1970-01-01 00:00:00.000000000 +0000
@@ -1,100 +0,0 @@
-<component name="InspectionProjectProfileManager">
-  <profile version="1.0">
-    <option name="myName" value="Project Default" />
-    <option name="scopesOrder">
-      <list>
-        <option value="All Changed Files" />
-        <option value="Open Files" />
-        <option value="Project Files" />
-        <option value="Scratches and Consoles" />
-      </list>
-    </option>
-    <inspection_tool class="ClangTidy" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="ConstevalIfIsAlwaysConstant" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppCStyleCast" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppClassCanBeFinal" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppCompileTimeConstantCanBeReplacedWithBooleanConstant" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppConstValueFunctionReturnType" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppDiscardedPostfixOperatorResult" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppForLoopCanBeReplacedWithWhile" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppFunctionalStyleCast" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppIfCanBeReplacedByConstexprIf" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppJoinDeclarationAndAssignment" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppLocalVariableMayBeConst" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppLocalVariableMightNotBeInitialized" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppMemberFunctionMayBeConst" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppMemberFunctionMayBeStatic" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppNonExplicitConversionOperator" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppNonExplicitConvertingConstructor" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppParameterMayBeConst" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppParameterMayBeConstPtrOrRef" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppPassValueParameterByConstReference" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppRedundantNamespaceDefinition" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppReinterpretCastFromVoidPtr" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppReplaceMemsetWithZeroInitialization" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppReplaceTieWithStructuredBinding" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppSmartPointerVsMakeFunction" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppStringLiteralToCharPointerConversion" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppThrowExpressionCanBeReplacedWithRethrow" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppTooWideScope" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppTooWideScopeInitStatement" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppUseAlgorithmWithCount" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppUseAssociativeContains" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppUseAuto" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppUseElementsView" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppUseEraseAlgorithm" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppUseFamiliarTemplateSyntaxForGenericLambdas" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppUseRangeAlgorithm" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppUseStdSize" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppUseStructuredBinding" enabled="false" level="HINT" enabled_by_default="false" />
-    <inspection_tool class="CppUseTypeTraitAlias" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppVariableCanBeMadeConstexpr" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="CppZeroConstantCanBeReplacedWithNullptr" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
-    <inspection_tool class="GrazieInspection" enabled="false" level="TYPO" enabled_by_default="false" />
-    <inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="IfStdIsConstantEvaluatedCanBeReplaced" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyAttributeOutsideInitInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyBroadExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyDefaultArgumentInspection" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyDictCreationInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyListCreationInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyMethodMayBeStaticInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyMethodOverridingInspection" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
-      <option name="ignoredPackages">
-        <value>
-          <list size="7">
-            <item index="0" class="java.lang.String" itemvalue="tensorflow" />
-            <item index="1" class="java.lang.String" itemvalue="scikit-image" />
-            <item index="2" class="java.lang.String" itemvalue="shapely" />
-            <item index="3" class="java.lang.String" itemvalue="timm" />
-            <item index="4" class="java.lang.String" itemvalue="cython" />
-            <item index="5" class="java.lang.String" itemvalue="h5py" />
-            <item index="6" class="java.lang.String" itemvalue="submitit" />
-          </list>
-        </value>
-      </option>
-    </inspection_tool>
-    <inspection_tool class="PyPep8Inspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyPep8NamingInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyProtectedMemberInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyRedeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyRelativeImportInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyShadowingBuiltinsInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyShadowingNamesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="PyUnusedLocalInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
-    <inspection_tool class="ShellCheck" enabled="true" level="ERROR" enabled_by_default="true">
-      <shellcheck_settings value="SC2086,SC2153" />
-    </inspection_tool>
-    <inspection_tool class="Simplify" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
-      <option name="processCode" value="false" />
-      <option name="processLiterals" value="true" />
-      <option name="processComments" value="true" />
-    </inspection_tool>
-    <inspection_tool class="StdIsConstantEvaluatedWillAlwaysEvaluateToConstant" enabled="false" level="WARNING" enabled_by_default="false" />
-    <inspection_tool class="requirements.InstalledPackageInspection" enabled="false" level="WARNING" enabled_by_default="false" />
-  </profile>
-</component>
\ No newline at end of file
diff -pruN 2.5.2-1/.idea/inspectionProfiles/profiles_settings.xml 3.3.0-1/.idea/inspectionProfiles/profiles_settings.xml
--- 2.5.2-1/.idea/inspectionProfiles/profiles_settings.xml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/inspectionProfiles/profiles_settings.xml	1970-01-01 00:00:00.000000000 +0000
@@ -1,6 +0,0 @@
-<component name="InspectionProjectProfileManager">
-  <settings>
-    <option name="USE_PROJECT_PROFILE" value="false" />
-    <version value="1.0" />
-  </settings>
-</component>
\ No newline at end of file
diff -pruN 2.5.2-1/.idea/misc.xml 3.3.0-1/.idea/misc.xml
--- 2.5.2-1/.idea/misc.xml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/misc.xml	1970-01-01 00:00:00.000000000 +0000
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="Black">
-    <option name="sdkName" value="main" />
-  </component>
-  <component name="ProjectRootManager" version="2" project-jdk-name="main" project-jdk-type="Python SDK" />
-</project>
\ No newline at end of file
diff -pruN 2.5.2-1/.idea/modules.xml 3.3.0-1/.idea/modules.xml
--- 2.5.2-1/.idea/modules.xml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/modules.xml	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectModuleManager">
-    <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/playsound3.iml" filepath="$PROJECT_DIR$/.idea/playsound3.iml" />
-    </modules>
-  </component>
-</project>
\ No newline at end of file
diff -pruN 2.5.2-1/.idea/playsound3.iml 3.3.0-1/.idea/playsound3.iml
--- 2.5.2-1/.idea/playsound3.iml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/playsound3.iml	1970-01-01 00:00:00.000000000 +0000
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="PYTHON_MODULE" version="4">
-  <component name="NewModuleRootManager">
-    <content url="file://$MODULE_DIR$" />
-    <orderEntry type="jdk" jdkName="main" jdkType="Python SDK" />
-    <orderEntry type="sourceFolder" forTests="false" />
-  </component>
-  <component name="PyDocumentationSettings">
-    <option name="format" value="GOOGLE" />
-    <option name="myDocStringFormat" value="Google" />
-  </component>
-</module>
\ No newline at end of file
diff -pruN 2.5.2-1/.idea/vcs.xml 3.3.0-1/.idea/vcs.xml
--- 2.5.2-1/.idea/vcs.xml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/vcs.xml	1970-01-01 00:00:00.000000000 +0000
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="VcsDirectoryMappings">
-    <mapping directory="" vcs="Git" />
-  </component>
-</project>
\ No newline at end of file
diff -pruN 2.5.2-1/.idea/workspace.xml 3.3.0-1/.idea/workspace.xml
--- 2.5.2-1/.idea/workspace.xml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/.idea/workspace.xml	1970-01-01 00:00:00.000000000 +0000
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="AutoImportSettings">
-    <option name="autoReloadType" value="SELECTIVE" />
-  </component>
-  <component name="ChangeListManager">
-    <list default="true" id="2705570e-7e20-4df6-8d36-c92297b76a29" name="Changes" comment="Replace deprecated macos-11 runner with macos-12" />
-    <option name="SHOW_DIALOG" value="false" />
-    <option name="HIGHLIGHT_CONFLICTS" value="true" />
-    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
-    <option name="LAST_RESOLUTION" value="IGNORE" />
-  </component>
-  <component name="Git.Settings">
-    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
-  </component>
-  <component name="ProjectColorInfo">{
-  &quot;customColor&quot;: &quot;&quot;,
-  &quot;associatedIndex&quot;: 3
-}</component>
-  <component name="ProjectId" id="2gBUCZFB7EqaDQifZXXBWSwRrGh" />
-  <component name="ProjectViewState">
-    <option name="hideEmptyMiddlePackages" value="true" />
-    <option name="showLibraryContents" value="true" />
-  </component>
-  <component name="PropertiesComponent">{
-  &quot;keyToString&quot;: {
-    &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
-    &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
-    &quot;git-widget-placeholder&quot;: &quot;main&quot;,
-    &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
-    &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
-    &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
-    &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
-    &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
-    &quot;settings.editor.selected.configurable&quot;: &quot;editor.preferences.fonts.default&quot;,
-    &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
-  }
-}</component>
-  <component name="SharedIndexes">
-    <attachedChunks>
-      <set>
-        <option value="bundled-js-predefined-d6986cc7102b-deb605915726-JavaScript-PY-243.22562.180" />
-        <option value="bundled-python-sdk-85c76a3b01b3-9a18a617cbe4-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-243.22562.180" />
-      </set>
-    </attachedChunks>
-  </component>
-  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
-  <component name="TaskManager">
-    <task active="true" id="Default" summary="Default task">
-      <changelist id="2705570e-7e20-4df6-8d36-c92297b76a29" name="Changes" comment="" />
-      <created>1715170337178</created>
-      <option name="number" value="Default" />
-      <option name="presentableId" value="Default" />
-      <updated>1715170337178</updated>
-      <workItem from="1715170338210" duration="613000" />
-      <workItem from="1715172875954" duration="11000" />
-      <workItem from="1715173506044" duration="8000" />
-      <workItem from="1715173784613" duration="4000" />
-      <workItem from="1719587131749" duration="38000" />
-      <workItem from="1731243605167" duration="2319000" />
-      <workItem from="1731247901341" duration="526000" />
-      <workItem from="1734362051427" duration="624000" />
-      <workItem from="1734375686143" duration="58000" />
-    </task>
-    <task id="LOCAL-00001" summary="Configure whether the background thread is daemon or not">
-      <option name="closed" value="true" />
-      <created>1731245299559</created>
-      <option name="number" value="00001" />
-      <option name="presentableId" value="LOCAL-00001" />
-      <option name="project" value="LOCAL" />
-      <updated>1731245299559</updated>
-    </task>
-    <task id="LOCAL-00002" summary="Non-blocking sounds now return the thread object">
-      <option name="closed" value="true" />
-      <created>1731245841071</created>
-      <option name="number" value="00002" />
-      <option name="presentableId" value="LOCAL-00002" />
-      <option name="project" value="LOCAL" />
-      <updated>1731245841071</updated>
-    </task>
-    <task id="LOCAL-00003" summary="Replace deprecated macos-11 runner with macos-12">
-      <option name="closed" value="true" />
-      <created>1731247941742</created>
-      <option name="number" value="00003" />
-      <option name="presentableId" value="LOCAL-00003" />
-      <option name="project" value="LOCAL" />
-      <updated>1731247941742</updated>
-    </task>
-    <option name="localTasksCounter" value="4" />
-    <servers />
-  </component>
-  <component name="TypeScriptGeneratedFilesManager">
-    <option name="version" value="3" />
-  </component>
-  <component name="VcsManagerConfiguration">
-    <MESSAGE value="Configure whether the background thread is daemon or not" />
-    <MESSAGE value="Non-blocking sounds now return the thread object" />
-    <MESSAGE value="Replace deprecated macos-11 runner with macos-12" />
-    <option name="LAST_COMMIT_MESSAGE" value="Replace deprecated macos-11 runner with macos-12" />
-  </component>
-</project>
\ No newline at end of file
diff -pruN 2.5.2-1/PKG-INFO 3.3.0-1/PKG-INFO
--- 2.5.2-1/PKG-INFO	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/PKG-INFO	2020-02-02 00:00:00.000000000 +0000
@@ -1,10 +1,10 @@
-Metadata-Version: 2.3
+Metadata-Version: 2.4
 Name: playsound3
-Version: 2.5.2
+Version: 3.3.0
 Summary: Cross-platform library to play audio files
-Project-URL: Repository, https://github.com/sjmikler/playsound3
+Project-URL: Home, https://github.com/sjmikler/playsound3
 Project-URL: Issues, https://github.com/sjmikler/playsound3/issues
-Project-URL: Documentation, https://github.com/sjmikler/playsound3?tab=readme-ov-file#documentation
+Project-URL: Documentation, https://github.com/sjmikler/playsound3/blob/main/README.md#quick-start
 Author-email: Szymon Mikler <sjmikler@gmail.com>, Taylor Marks <taylor@marksfam.com>
 Maintainer-email: Szymon Mikler <sjmikler@gmail.com>
 License: MIT License
@@ -15,19 +15,29 @@ Classifier: Intended Audience :: Develop
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: OS Independent
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
 Classifier: Topic :: Multimedia :: Sound/Audio :: Players
 Classifier: Topic :: Multimedia :: Sound/Audio :: Players :: MP3
 Classifier: Typing :: Typed
-Requires-Python: >=3.9
-Requires-Dist: certifi
+Requires-Python: >=3.10
+Requires-Dist: pywin32; sys_platform == 'win32'
+Provides-Extra: dev
+Requires-Dist: pyright; extra == 'dev'
+Requires-Dist: pytest; extra == 'dev'
+Requires-Dist: ruff; extra == 'dev'
 Description-Content-Type: text/markdown
 
+> **Version 3.0.0**
+>
+> New functionalities:
+> * stop sounds by calling `sound.stop()`
+> * check if sound is still playing with `sound.is_alive()`
+
 # playsound3
 
 [![PyPi version](https://img.shields.io/badge/dynamic/json?label=latest&query=info.version&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fplaysound3%2Fjson)](https://pypi.org/project/playsound3)
@@ -45,69 +55,93 @@ pip install playsound3
 
 ## Quick Start
 
-Once installed, you can use the playsound function to play sound files:
+After installation, playing sounds is simple:
 
 ```python
 from playsound3 import playsound
 
+# Play sounds from disk
 playsound("/path/to/sound/file.mp3")
 
-# or use directly on URLs
+# or play sounds from the internet.
 playsound("http://url/to/sound/file.mp3")
+
+# You can play sounds in the background
+sound = playsound("/path/to/sound/file.mp3", block=False)
+
+# and check if they are still playing
+if sound.is_alive():
+    print("Sound is still playing!")
+
+# and stop them whenever you like.
+sound.stop()
 ```
 
-## Documentation
+## Reference
 
-The `playsound3` module contains a single function named `playsound`:
+### playsound
 
 ```python
 def playsound(
-        sound: str | Path,
-        block: bool = True,
-        backend: str | None = None,
-        daemon: bool = True,
-    ) -> Thread | None:
-    """Play a sound file using an audio backend availabile in your system.
-
-    Args:
-        sound: Path or URL to the sound file. Can be a string or pathlib.Path.
-        block: If True, the function will block execution until the sound finishes playing.
-               If False, sound will play in a background thread.
-        backend: Name of the audio backend to use. Use None for automatic selection.
-        daemon: If True, and `block` is True, the background thread will be a daemon thread.
-                This means that the thread will stay alive even after the main program exits.
-
-    Returns:
-        If `block` is True, the function will return None after the sound finishes playing.
-        If `block` is False, the function will return the background thread object.
+    sound: str | Path,
+    block: bool = True,
+    backend: str | None = None,
+) -> Sound
+```
+
+`sound` (required) \
+The audio file you want to play (local or URL).
+
+`block` (optional, default=`True`)\
+Determines whether the sound plays synchronously (blocking) or asynchronously (background).
+
+`backend` (optional, default=`None`) \
+Specify which audio backend to use.
+If `None`, the best backend is determined automatically.
 
-    """
-    ...
+To see a list of backends supported by your system:
+
+```python
+from playsound3 import AVAILABLE_BACKENDS, DEFAULT_BACKEND
+
+print(AVAILABLE_BACKENDS)  # for example: ["gstreamer", "ffmpeg", ...]
+print(DEFAULT_BACKEND)  # for example: "gstreamer"
 ```
 
-It requires one argument: `sound` - the path to the file with the sound you'd like to play.
-This should be a local file or a URL.
-There's an optional second argument: `block` which is set to `True` by default.
-Setting it to `False` makes the function run asynchronously.
-You can manually specify a backend by passing its name as the third argument.
-You can browse available backends by using `playsound3.AVAILABLE_BACKENDS`.
-It is recommended to use the default value of `None` to let the library choose the best backend available.
+### Sound
+
+`playsound` returns a `Sound` object for playback control:
+
+| Method        | Description                               |
+|---------------|-------------------------------------------|
+| `.is_alive()` | Checks if the sound is currently playing. |
+| `.wait()`     | Blocks execution until playback finishes. |
+| `.stop()`     | Immediately stops playback.               |
 
 ## Supported systems
 
-* **Linux** using one of the below backends, whichever is available:
+* **Linux**
     * GStreamer
+    * ALSA (aplay and mpg123)
+* **Windows**
+    * WMPlayer
+    * winmm.dll
+* **macOS**
+    * AppKit
+    * afplay
+* **Multiplatform**
     * FFmpeg
-    * aplay for .wav and mpg123 .mp3
-* **Windows** using winmm.dll (built-in on Windows)
-* **macOS** using afplay utility (built-in on macOS)
+
+## Supported audio formats
+
+The bare minimum supported by every backend are `.mp3` and `.wav` files.
+Using them will keep your program compatible across different systems.
+To see an exhaustive list of extensions supported by a backend, refer to their respective documentation.
 
 ## Fork information
 
-This repository was originally forked from [playsound](https://github.com/TaylorSMarks/playsound) library created by Taylor Marks. The original library is unfortunately not maintained anymore and doesn't accept pull requests. This library: `playsound3` is a major rewrite of the original, including its own set of tests hosted using GitHub Actions.
+This repository was originally forked from [playsound](https://github.com/TaylorSMarks/playsound) library created by Taylor Marks.
+The original library is not maintained anymore and doesn't accept pull requests.
+This library is a major rewrite of its original.
 
-Compared to the original, `playsound3`:
-* Drops support for Python 2
-* Adheres to the latest PEP standards
-* Offers multiple backends with a fallback mechanism if the default backend is not available
-* Accepts contributions
+Feel free to create an issue or contribute to `playsound3`!
diff -pruN 2.5.2-1/README.md 3.3.0-1/README.md
--- 2.5.2-1/README.md	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/README.md	2020-02-02 00:00:00.000000000 +0000
@@ -1,3 +1,9 @@
+> **Version 3.0.0**
+>
+> New functionalities:
+> * stop sounds by calling `sound.stop()`
+> * check if sound is still playing with `sound.is_alive()`
+
 # playsound3
 
 [![PyPi version](https://img.shields.io/badge/dynamic/json?label=latest&query=info.version&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fplaysound3%2Fjson)](https://pypi.org/project/playsound3)
@@ -15,69 +21,93 @@ pip install playsound3
 
 ## Quick Start
 
-Once installed, you can use the playsound function to play sound files:
+After installation, playing sounds is simple:
 
 ```python
 from playsound3 import playsound
 
+# Play sounds from disk
 playsound("/path/to/sound/file.mp3")
 
-# or use directly on URLs
+# or play sounds from the internet.
 playsound("http://url/to/sound/file.mp3")
+
+# You can play sounds in the background
+sound = playsound("/path/to/sound/file.mp3", block=False)
+
+# and check if they are still playing
+if sound.is_alive():
+    print("Sound is still playing!")
+
+# and stop them whenever you like.
+sound.stop()
 ```
 
-## Documentation
+## Reference
 
-The `playsound3` module contains a single function named `playsound`:
+### playsound
 
 ```python
 def playsound(
-        sound: str | Path,
-        block: bool = True,
-        backend: str | None = None,
-        daemon: bool = True,
-    ) -> Thread | None:
-    """Play a sound file using an audio backend availabile in your system.
-
-    Args:
-        sound: Path or URL to the sound file. Can be a string or pathlib.Path.
-        block: If True, the function will block execution until the sound finishes playing.
-               If False, sound will play in a background thread.
-        backend: Name of the audio backend to use. Use None for automatic selection.
-        daemon: If True, and `block` is True, the background thread will be a daemon thread.
-                This means that the thread will stay alive even after the main program exits.
-
-    Returns:
-        If `block` is True, the function will return None after the sound finishes playing.
-        If `block` is False, the function will return the background thread object.
+    sound: str | Path,
+    block: bool = True,
+    backend: str | None = None,
+) -> Sound
+```
+
+`sound` (required) \
+The audio file you want to play (local or URL).
+
+`block` (optional, default=`True`)\
+Determines whether the sound plays synchronously (blocking) or asynchronously (background).
+
+`backend` (optional, default=`None`) \
+Specify which audio backend to use.
+If `None`, the best backend is determined automatically.
 
-    """
-    ...
+To see a list of backends supported by your system:
+
+```python
+from playsound3 import AVAILABLE_BACKENDS, DEFAULT_BACKEND
+
+print(AVAILABLE_BACKENDS)  # for example: ["gstreamer", "ffmpeg", ...]
+print(DEFAULT_BACKEND)  # for example: "gstreamer"
 ```
 
-It requires one argument: `sound` - the path to the file with the sound you'd like to play.
-This should be a local file or a URL.
-There's an optional second argument: `block` which is set to `True` by default.
-Setting it to `False` makes the function run asynchronously.
-You can manually specify a backend by passing its name as the third argument.
-You can browse available backends by using `playsound3.AVAILABLE_BACKENDS`.
-It is recommended to use the default value of `None` to let the library choose the best backend available.
+### Sound
+
+`playsound` returns a `Sound` object for playback control:
+
+| Method        | Description                               |
+|---------------|-------------------------------------------|
+| `.is_alive()` | Checks if the sound is currently playing. |
+| `.wait()`     | Blocks execution until playback finishes. |
+| `.stop()`     | Immediately stops playback.               |
 
 ## Supported systems
 
-* **Linux** using one of the below backends, whichever is available:
+* **Linux**
     * GStreamer
+    * ALSA (aplay and mpg123)
+* **Windows**
+    * WMPlayer
+    * winmm.dll
+* **macOS**
+    * AppKit
+    * afplay
+* **Multiplatform**
     * FFmpeg
-    * aplay for .wav and mpg123 .mp3
-* **Windows** using winmm.dll (built-in on Windows)
-* **macOS** using afplay utility (built-in on macOS)
+
+## Supported audio formats
+
+The bare minimum supported by every backend are `.mp3` and `.wav` files.
+Using them will keep your program compatible across different systems.
+To see an exhaustive list of extensions supported by a backend, refer to their respective documentation.
 
 ## Fork information
 
-This repository was originally forked from [playsound](https://github.com/TaylorSMarks/playsound) library created by Taylor Marks. The original library is unfortunately not maintained anymore and doesn't accept pull requests. This library: `playsound3` is a major rewrite of the original, including its own set of tests hosted using GitHub Actions.
+This repository was originally forked from [playsound](https://github.com/TaylorSMarks/playsound) library created by Taylor Marks.
+The original library is not maintained anymore and doesn't accept pull requests.
+This library is a major rewrite of its original.
 
-Compared to the original, `playsound3`:
-* Drops support for Python 2
-* Adheres to the latest PEP standards
-* Offers multiple backends with a fallback mechanism if the default backend is not available
-* Accepts contributions
+Feel free to create an issue or contribute to `playsound3`!
diff -pruN 2.5.2-1/debian/changelog 3.3.0-1/debian/changelog
--- 2.5.2-1/debian/changelog	2025-02-21 09:25:36.000000000 +0000
+++ 3.3.0-1/debian/changelog	2025-11-10 11:11:33.000000000 +0000
@@ -1,3 +1,10 @@
+python-playsound3 (3.3.0-1) unstable; urgency=medium
+
+  * Merging upstream version 3.3.0.
+  * Add test binaries to debian/source/include-binaries.
+
+ -- Mathias Behrle <mathiasb@m9s.biz>  Mon, 10 Nov 2025 12:11:33 +0100
+
 python-playsound3 (2.5.2-1) unstable; urgency=medium
 
   * Merging upstream version 2.5.2.
diff -pruN 2.5.2-1/debian/source/include-binaries 3.3.0-1/debian/source/include-binaries
--- 2.5.2-1/debian/source/include-binaries	1970-01-01 00:00:00.000000000 +0000
+++ 3.3.0-1/debian/source/include-binaries	2025-11-10 11:11:33.000000000 +0000
@@ -0,0 +1,3 @@
+tests/sounds/sample3s.flac
+tests/sounds/sample3s.mp3
+tests/sounds/звук 音 聲音.wav
diff -pruN 2.5.2-1/playsound3/__init__.py 3.3.0-1/playsound3/__init__.py
--- 2.5.2-1/playsound3/__init__.py	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/playsound3/__init__.py	2020-02-02 00:00:00.000000000 +0000
@@ -1,3 +1,19 @@
-from playsound3.playsound3 import AVAILABLE_BACKENDS, playsound
+__license__ = "MIT"
+__version__ = "3.3.0"
+__author__ = "Szymon Mikler"
 
-__all__ = ["playsound", "AVAILABLE_BACKENDS"]
+from playsound3.playsound3 import (
+    AVAILABLE_BACKENDS,
+    DEFAULT_BACKEND,
+    PlaysoundException,
+    playsound,
+    prefer_backends,
+)
+
+__all__ = [
+    "AVAILABLE_BACKENDS",
+    "DEFAULT_BACKEND",
+    "playsound",
+    "prefer_backends",
+    "PlaysoundException",
+]
diff -pruN 2.5.2-1/playsound3/backends.py 3.3.0-1/playsound3/backends.py
--- 2.5.2-1/playsound3/backends.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.3.0-1/playsound3/backends.py	2020-02-02 00:00:00.000000000 +0000
@@ -0,0 +1,144 @@
+from __future__ import annotations
+
+import time
+import uuid
+from threading import Thread
+from typing import Any
+
+WAIT_TIME: float = 0.02
+
+
+class PlaysoundException(Exception):
+    pass
+
+
+class WmplayerPopen:
+    """Popen-like object for Wmplayer backend."""
+
+    def __init__(self, sound: str):
+        self._playing: bool = True
+        self.thread: Thread = Thread(target=self._play, args=(sound,), daemon=True)
+        self.thread.start()
+
+    def _play(self, sound: str) -> None:
+        try:
+            import pythoncom  # type: ignore
+            import win32com.client  # type: ignore
+        except ImportError as e:
+            raise PlaysoundException("Install 'pywin32' to use the 'wmplayer' backend.") from e
+
+        # Create the Windows Media Player COM object
+        wmp = win32com.client.Dispatch(
+            "WMPlayer.OCX",
+            pythoncom.CoInitialize(),
+        )
+        wmp.settings.autoStart = True  # Ensure playback starts automatically
+
+        # Set the URL to your MP3 file
+        wmp.URL = sound
+        wmp.controls.play()  # Start playback
+
+        while wmp.playState != 1 and self._playing:  # playState 1 indicates stopped
+            pythoncom.PumpWaitingMessages()  # Process COM events
+            time.sleep(WAIT_TIME)
+
+        wmp.controls.stop()
+        self._playing = False
+
+    def terminate(self) -> None:
+        self._playing = False
+
+    def poll(self) -> int | None:
+        """None if sound is playing, integer if not."""
+        return None if self._playing else 0
+
+    def wait(self) -> int:
+        self.thread.join()
+        return 0
+
+
+class WinmmPopen:
+    """Popen-like object for Winmm backend."""
+
+    def __init__(self, sound: str):
+        self._playing: bool = True
+        self.alias: str | None = None
+        self.thread: Thread = Thread(target=self._play, args=(sound,), daemon=True)
+        self.thread.start()
+
+    def _send_winmm_mci_command(self, command: str) -> str:
+        try:
+            import ctypes
+        except ImportError as e:
+            raise PlaysoundException("Install 'ctypes' to use the 'winmm' backend") from e
+
+        winmm = ctypes.WinDLL("winmm.dll")  # type: ignore
+        buffer = ctypes.create_unicode_buffer(255)  # Unicode buffer for wide characters
+        error_code = winmm.mciSendStringW(ctypes.c_wchar_p(command), buffer, 254, 0)
+
+        if error_code:
+            self._playing = False
+            raise RuntimeError(f"winmm was not able to play the file! MCI error code: {error_code}")
+        return buffer.value
+
+    def _play(self, sound: str) -> None:
+        """Play a sound utilizing windll.winmm."""
+        # Select a unique alias for the sound
+        self.alias = str(uuid.uuid4())
+        self._send_winmm_mci_command(f'open "{sound}" type mpegvideo alias {self.alias}')
+        self._send_winmm_mci_command(f"play {self.alias}")
+
+        while self._playing:
+            time.sleep(WAIT_TIME)
+            status = self._send_winmm_mci_command(f"status {self.alias} mode")
+            if status != "playing":
+                break
+
+        self._send_winmm_mci_command(f"stop {self.alias}")
+        self._send_winmm_mci_command(f"close {self.alias}")
+        self._playing = False
+
+    def terminate(self) -> None:
+        self._playing = False
+
+    def poll(self) -> int | None:
+        """None if sound is playing, integer if not."""
+        return None if self._playing else 0
+
+    def wait(self) -> int:
+        self.thread.join()
+        return 0
+
+
+class AppkitPopen:
+    """Popen-like object for AppKit NSSound backend."""
+
+    def __init__(self, sound: str):
+        try:
+            from AppKit import NSSound  # type: ignore
+            from Foundation import NSURL  # type: ignore
+        except ImportError as e:
+            raise PlaysoundException("Install 'PyObjC' to use 'appkit' backend.") from e
+
+        nsurl: Any = NSURL.fileURLWithPath_(sound)
+        self._nssound: Any = NSSound.alloc().initWithContentsOfURL_byReference_(nsurl, True)
+        self._nssound.retain()
+        self._start_time: float = time.time()
+
+        self._nssound.play()
+        self._duration: float = self._nssound.duration()
+
+    def terminate(self) -> None:
+        self._nssound.stop()
+        self._duration = time.time() - self._start_time
+
+    def poll(self) -> int | None:
+        """None if sound is playing, integer if not."""
+        if time.time() - self._start_time >= self._duration:
+            return 0
+        return None
+
+    def wait(self) -> int:
+        while time.time() - self._start_time < self._duration:
+            time.sleep(WAIT_TIME)
+        return 0
diff -pruN 2.5.2-1/playsound3/playsound3.py 3.3.0-1/playsound3/playsound3.py
--- 2.5.2-1/playsound3/playsound3.py	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/playsound3/playsound3.py	2020-02-02 00:00:00.000000000 +0000
@@ -2,76 +2,39 @@ from __future__ import annotations
 
 import atexit
 import logging
-import ssl
+import os
+import shutil
+import signal
 import subprocess
-import sys
 import tempfile
-import urllib.error
 import urllib.request
+from abc import ABC, abstractmethod
+from importlib.util import find_spec
 from pathlib import Path
-from threading import Thread
-from typing import TYPE_CHECKING, Any, Callable
+from typing import Any, Protocol
 
-# Satisfy mypy
-if TYPE_CHECKING or sys.platform == "win32":
-    import ctypes
-    import uuid
-
-import certifi
+from playsound3 import backends
 
 logger = logging.getLogger(__name__)
 
-_DOWNLOAD_CACHE = {}
-
 
 class PlaysoundException(Exception):
     pass
 
 
-def playsound(
-    sound: str | Path,
-    block: bool = True,
-    backend: str | None = None,
-    daemon: bool = True,
-) -> Thread | None:
-    """Play a sound file using an audio backend availabile in your system.
+####################
+## DOWNLOAD TOOLS ##
+####################
 
-    Args:
-        sound: Path or URL to the sound file. Can be a string or pathlib.Path.
-        block: If True, the function will block execution until the sound finishes playing.
-               If False, sound will play in a background thread.
-        backend: Name of the audio backend to use. Use None for automatic selection.
-        daemon: If True, and `block` is True, the background thread will be a daemon thread.
-                This means that the thread will stay alive even after the main program exits.
-
-    Returns:
-        If `block` is True, the function will return None after the sound finishes playing.
-        If `block` is False, the function will return the background thread object.
-
-    """
-    if backend is None:
-        _play = _PLAYSOUND_DEFAULT_BACKEND
-    elif backend in _BACKEND_MAPPING:
-        _play = _BACKEND_MAPPING[backend]
-    else:
-        raise PlaysoundException(f"Unknown backend: {backend}. Available backends: {', '.join(AVAILABLE_BACKENDS)}")
-
-    path = _prepare_path(sound)
-    if block:
-        _play(path)
-    else:
-        thread = Thread(target=_play, args=(path,), daemon=daemon)
-        thread.start()
-        return thread
-    return None
+_DOWNLOAD_CACHE: dict[str, str] = {}
 
 
 def _download_sound_from_web(link: str, destination: Path) -> None:
     # Identifies itself as a browser to avoid HTTP 403 errors
     headers = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64)"}
     request = urllib.request.Request(link, headers=headers)
-    context = ssl.create_default_context(cafile=certifi.where())
-    with urllib.request.urlopen(request, context=context) as response, destination.open("wb") as out_file:
+
+    with urllib.request.urlopen(request) as response, destination.open("wb") as out_file:
         out_file.write(response.read())
 
 
@@ -88,164 +51,297 @@ def _prepare_path(sound: str | Path) ->
     path = Path(sound)
 
     if not path.exists():
-        raise PlaysoundException(f"File not found: {sound}")
+        raise PlaysoundException(f"file not found: {sound}")
     return path.absolute().as_posix()
 
 
-def _select_linux_backend() -> Callable[[str], None]:
-    """Select the best available audio backend for Linux systems."""
-    logger.info("Selecting the best available audio backend for Linux systems.")
+########################
+## BACKEND INTERFACES ##
+########################
 
-    try:
-        subprocess.run(["gst-play-1.0", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
-        logger.info("Using gst-play-1.0 as the audio backend.")
-        return _playsound_gst_play
-    except FileNotFoundError:
-        pass
 
-    try:
-        subprocess.run(["ffplay", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
-        logger.info("Using ffplay as the audio backend.")
-        return _playsound_ffplay
-    except FileNotFoundError:
-        pass
+# Imitating subprocess.Popen
+class PopenLike(Protocol):
+    def poll(self) -> int | None: ...
+
+    def wait(self) -> int: ...
+
+    def terminate(self) -> None: ...
+
 
+class SoundBackend(ABC):
+    """Abstract class for sound backends."""
+
+    @abstractmethod
+    def check(self) -> bool:
+        raise NotImplementedError("check() must be implemented.")
+
+    @abstractmethod
+    def play(self, sound: str) -> PopenLike:
+        raise NotImplementedError("play() must be implemented.")
+
+
+def _set_pdeathsig() -> None:
+    """Set the signal delivered to this process if its parent dies."""
     try:
-        subprocess.run(["aplay", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
-        subprocess.run(["mpg123", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
-        logger.info("Using aplay and mpg123 as the audio backend.")
-        return _playsound_alsa
-    except FileNotFoundError:
+        import ctypes
+
+        libc = ctypes.CDLL("libc.so.6", use_errno=True)
+        PR_SET_PDEATHSIG = 1
+        if libc.prctl(PR_SET_PDEATHSIG, signal.SIGTERM) != 0:
+            err = ctypes.get_errno()
+            raise OSError(err, os.strerror(err))
+    except Exception:  # if unavailable (non-Linux) or fails, do nothing
         pass
 
-    logger.info("No suitable audio backend found.")
-    raise PlaysoundException("No suitable audio backend found. Install gstreamer or ffmpeg!")
+
+def get_platform_specific_kwds() -> dict[str, Any]:
+    """Get platform-specific keyword arguments for subprocess.Popen."""
+    if os.name == "nt":
+        return {}
+    else:
+        # On Unix-like systems, we want to ensure that the child process is terminated if the parent process dies
+        return {"preexec_fn": _set_pdeathsig}
 
 
-def _playsound_gst_play(sound: str) -> None:
-    """Uses gst-play-1.0 utility (built-in Linux)."""
-    logger.debug("gst-play-1.0: starting playing %s", sound)
-    try:
-        subprocess.run(["gst-play-1.0", "--no-interactive", "--quiet", sound], check=True)
-    except subprocess.CalledProcessError as e:
-        raise PlaysoundException(f"gst-play-1.0 failed to play sound: {e}")
-    logger.debug("gst-play-1.0: finishing play %s", sound)
+def run_as_subprocess(commands: list[str], **kwargs: Any) -> subprocess.Popen[str]:
+    """A wrapper around subprocess.Popen to handle platform-specific keyword arguments.
+
+    By default, stdout and stderr are suppressed (set to DEVNULL).
+    Additional keyword arguments can be passed and will override defaults.
+    """
+    popen_kwargs = {
+        "stdout": subprocess.DEVNULL,
+        "stderr": subprocess.DEVNULL,
+        **get_platform_specific_kwds(),
+        **kwargs,
+    }
 
+    return subprocess.Popen(commands, **popen_kwargs)
 
-def _playsound_ffplay(sound: str) -> None:
-    """Uses ffplay utility (built-in Linux)."""
-    logger.debug("ffplay: starting playing %s", sound)
-    try:
-        subprocess.run(
-            ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", sound],
-            check=True,
-            stdout=subprocess.DEVNULL,  # suppress output as ffplay prints an unwanted newline
-        )
-    except subprocess.CalledProcessError as e:
-        raise PlaysoundException(f"ffplay failed to play sound: {e}")
-    logger.debug("ffplay: finishing play %s", sound)
 
+class Gstreamer(SoundBackend):
+    """Gstreamer backend for Linux."""
 
-def _playsound_alsa(sound: str) -> None:
-    """Play a sound using alsa and mpg123 (built-in Linux)."""
-    suffix = Path(sound).suffix
-    if suffix == ".wav":
-        logger.debug("alsa: starting playing %s", sound)
+    def check(self) -> bool:
         try:
-            subprocess.run(["aplay", "--quiet", sound], check=True)
-        except subprocess.CalledProcessError as e:
-            raise PlaysoundException(f"aplay failed to play sound: {e}")
-        logger.debug("alsa: finishing play %s", sound)
-    elif suffix == ".mp3":
-        logger.debug("mpg123: starting playing %s", sound)
+            subprocess.run(
+                ["gst-play-1.0", "--version"],
+                stdout=subprocess.DEVNULL,
+                stderr=subprocess.DEVNULL,
+                check=True,
+            )
+            return True
+        except FileNotFoundError:
+            return False
+
+    def play(self, sound: str) -> subprocess.Popen[str]:
+        return run_as_subprocess(["gst-play-1.0", "--no-interactive", "--quiet", sound])
+
+
+class Alsa(SoundBackend):
+    """ALSA backend for Linux."""
+
+    pty_master: int | None = None
+
+    def check(self) -> bool:
         try:
-            subprocess.run(["mpg123", "-q", sound], check=True)
-        except subprocess.CalledProcessError as e:
-            raise PlaysoundException(f"mpg123 failed to play sound: {e}")
-        logger.debug("mpg123: finishing play %s", sound)
-    else:
-        raise PlaysoundException(f"Backend not supported for {suffix} files")
+            subprocess.run(["aplay", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
+            subprocess.run(["mpg123", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
+            return True
+        except FileNotFoundError:
+            return False
+
+    def play(self, sound: str) -> subprocess.Popen[str]:
+        suffix = Path(sound).suffix
+
+        if self.pty_master is None:
+            self.pty_master, _ = os.openpty()
+
+        if suffix == ".wav":
+            return run_as_subprocess(["aplay", "--quiet", sound])
+        elif suffix == ".mp3":
+            return run_as_subprocess(["mpg123", "-q", sound], stdin=self.pty_master)
+        else:
+            raise PlaysoundException(f"ALSA does not support for {suffix} files.")
 
 
-def _playsound_gst_legacy(sound: str) -> None:
-    """Play a sound using gstreamer (built-in Linux)."""
+class Ffplay(SoundBackend):
+    """FFplay backend for systems with ffmpeg installed."""
 
-    if not sound.startswith("file://"):
-        sound = "file://" + urllib.request.pathname2url(sound)
+    def check(self) -> bool:
+        try:
+            subprocess.run(["ffplay", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
+            return True
+        except (FileNotFoundError, subprocess.CalledProcessError):
+            return False
+
+    def play(self, sound: str) -> subprocess.Popen[str]:
+        return run_as_subprocess(["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", sound])
+
+
+class Wmplayer(SoundBackend):
+    """Windows Media Player backend for Windows."""
+
+    def check(self) -> bool:
+        # The recommended way to check for missing library
+        if find_spec("pythoncom") is None:
+            return False
 
-    try:
-        import gi
-    except ImportError:
-        raise PlaysoundException("PyGObject not found. Run 'pip install pygobject'")
+        try:
+            import win32com.client  # type: ignore
 
-    # Silences gi warning
-    gi.require_version("Gst", "1.0")
+            _ = win32com.client.Dispatch("WMPlayer.OCX")
+            return True
+        except (ImportError, Exception):
+            # pywintypes.com_error can be raised, which inherits directly from Exception
+            return False
+
+    def play(self, sound: str) -> backends.WmplayerPopen:
+        return backends.WmplayerPopen(sound)
 
-    try:
-        # Gst will be available only if GStreamer is installed
-        from gi.repository import Gst
-    except ImportError:
-        raise PlaysoundException("GStreamer not found. Install GStreamer on your system")
-
-    Gst.init(None)
-
-    playbin = Gst.ElementFactory.make("playbin", "playbin")
-    playbin.props.uri = sound
-
-    logger.debug("gstreamer: starting playing %s", sound)
-    set_result = playbin.set_state(Gst.State.PLAYING)
-    if set_result != Gst.StateChangeReturn.ASYNC:
-        raise PlaysoundException("playbin.set_state returned " + repr(set_result))
-    bus = playbin.get_bus()
-    try:
-        bus.poll(Gst.MessageType.EOS, Gst.CLOCK_TIME_NONE)
-    finally:
-        playbin.set_state(Gst.State.NULL)
-    logger.debug("gstreamer: finishing play %s", sound)
-
-
-def _send_winmm_mci_command(command: str) -> Any:
-    if sys.platform != "win32":
-        raise RuntimeError("WinMM is only available on Windows systems.")
-    winmm = ctypes.WinDLL("winmm.dll")
-    buffer = ctypes.create_unicode_buffer(255)  # Unicode buffer for wide characters
-    error_code = winmm.mciSendStringW(ctypes.c_wchar_p(command), buffer, 254, 0)  # Use mciSendStringW
-    if error_code:
-        logger.error("MCI error code: %s", error_code)
-    return buffer.value
-
-
-def _playsound_mci_winmm(sound: str) -> None:
-    """Play a sound utilizing windll.winmm."""
-    if sys.platform != "win32":
-        raise RuntimeError("WinMM is only available on Windows systems.")
-    # Select a unique alias for the sound
-    alias = str(uuid.uuid4())
-    logger.debug("winmm: starting playing %s", sound)
-    _send_winmm_mci_command(f'open "{sound}" type mpegvideo alias {alias}')
-    _send_winmm_mci_command(f"play {alias} wait")
-    _send_winmm_mci_command(f"close {alias}")
-    logger.debug("winmm: finishing play %s", sound)
-
-
-def _playsound_afplay(sound: str) -> None:
-    """Uses afplay utility (built-in macOS)."""
-    logger.debug("afplay: starting playing %s", sound)
-    try:
-        subprocess.run(["afplay", sound], check=True)
-    except subprocess.CalledProcessError as e:
-        raise PlaysoundException(f"afplay failed to play sound: {e}")
-    logger.debug("afplay: finishing play %s", sound)
 
+class Winmm(SoundBackend):
+    """WinMM backend for Windows."""
 
-def _initialize_default_backend() -> Callable[[str], None]:
-    if sys.platform == "win32":
-        return _playsound_mci_winmm
-    if sys.platform == "darwin":
-        return _playsound_afplay
-    # Linux version serves as the fallback
-    # because tools like gstreamer and ffmpeg could be installed on unrecognized systems
-    return _select_linux_backend()
+    def check(self) -> bool:
+        try:
+            import ctypes
+
+            _ = ctypes.WinDLL("winmm.dll")  # type: ignore
+            return True
+        except (ImportError, FileNotFoundError, AttributeError):
+            return False
+
+    def play(self, sound: str) -> backends.WinmmPopen:
+        return backends.WinmmPopen(sound)
+
+
+class Afplay(SoundBackend):
+    """Afplay backend for macOS."""
+
+    def check(self) -> bool:
+        # For some reason successful 'afplay -h' returns non-zero code
+        # So we must use shutil to test if afplay exists
+        return shutil.which("afplay") is not None
+
+    def play(self, sound: str) -> subprocess.Popen[str]:
+        return run_as_subprocess(["afplay", sound])
+
+
+class Appkit(SoundBackend):
+    """Appkit backend for macOS."""
+
+    def check(self) -> bool:
+        try:
+            from AppKit import NSSound  # type: ignore # noqa: F401
+            from Foundation import NSURL  # type: ignore # noqa: F401
+
+            return True
+        except ImportError:
+            return False
+
+    def play(self, sound: str) -> backends.AppkitPopen:
+        return backends.AppkitPopen(sound)
+
+
+################
+## PLAYSOUND  ##
+################
+
+_NO_BACKEND_MESSAGE = "No supported audio backends on this system!"
+
+
+def _auto_select_backend() -> str | None:
+    if "PLAYSOUND3_BACKEND" in os.environ:
+        # Allow users to override the automatic backend choice
+        return os.environ["PLAYSOUND3_BACKEND"]
+
+    for backend in _BACKEND_PREFERENCE:
+        if backend in AVAILABLE_BACKENDS:
+            return backend
+
+    logger.warning(_NO_BACKEND_MESSAGE)
+    return None
+
+
+class Sound:
+    """Subprocess-based sound object.
+
+    Attributes:
+        backend: The name of the backend used to play the sound.
+        subprocess: The subprocess object used to play the sound.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        block: bool,
+        backend: SoundBackend,
+    ) -> None:
+        """Initialize the player and begin playing."""
+        self.backend: str = str(type(backend)).lower()
+        self.subprocess: PopenLike = backend.play(name)
+
+        if block:
+            self.wait()
+
+    def is_alive(self) -> bool:
+        """Check if the sound is still playing.
+
+        Returns:
+            True if the sound is still playing, else False.
+        """
+        return self.subprocess.poll() is None
+
+    def wait(self) -> None:
+        """Block until the sound finishes playing.
+
+        This only makes sense for non-blocking sounds.
+        """
+        self.subprocess.wait()
+
+    def stop(self) -> None:
+        """Stop the sound."""
+        self.subprocess.terminate()
+
+
+def playsound(
+    sound: str | Path,
+    block: bool = True,
+    backend: str | None = None,
+) -> Sound:
+    """Play a sound file using an available audio backend.
+
+    Args:
+        sound: Path or URL of the sound file (string or pathlib.Path).
+        block:
+            - `True` (default): Wait until sound finishes playing.
+            - `False`: Play sound in the background.
+        backend: Specific audio backend to use. Leave None for automatic selection.
+
+    Returns:
+        Sound object for controlling playback.
+    """
+    path = _prepare_path(sound)
+    backend = backend or DEFAULT_BACKEND
+    if backend is None:
+        raise PlaysoundException(_NO_BACKEND_MESSAGE)
+
+    if isinstance(backend, str):
+        if backend in _BACKEND_MAP:
+            backend_obj = _BACKEND_MAP[backend]
+        else:
+            raise PlaysoundException(f"unknown backend '{backend}'")
+
+    # Unofficially, you can pass a SoundBackend object
+    elif isinstance(backend, SoundBackend):
+        backend_obj = backend
+    elif isinstance(backend, type) and issubclass(backend, SoundBackend):
+        backend_obj = backend()
+    else:
+        raise PlaysoundException(f"invalid backend type '{type(backend)}'")
+    return Sound(path, block, backend_obj)
 
 
 def _remove_cached_downloads(cache: dict[str, str]) -> None:
@@ -254,20 +350,52 @@ def _remove_cached_downloads(cache: dict
         Path(path).unlink()
 
 
-# ######################## #
-# PLAYSOUND INITIALIZATION #
-# ######################## #
+####################
+## INITIALIZATION ##
+####################
 
-_PLAYSOUND_DEFAULT_BACKEND = _initialize_default_backend()
 atexit.register(_remove_cached_downloads, _DOWNLOAD_CACHE)
 
-_BACKEND_MAPPING = {
-    "afplay": _playsound_afplay,
-    "alsa_mpg123": _playsound_alsa,
-    "ffplay": _playsound_ffplay,
-    "gst_play": _playsound_gst_play,
-    "gst_legacy": _playsound_gst_legacy,
-    "mci_winmm": _playsound_mci_winmm,
+_BACKEND_PREFERENCE = [
+    "gstreamer",  # Linux; should be installed on every distro
+    "wmplayer",  # Windows; requires pywin32 -- should be working well on Windows
+    "ffplay",  # Multiplatform; requires ffmpeg
+    "appkit",  # macOS; requires PyObjC dependency
+    "afplay",  # macOS; should be installed on every macOS
+    "winmm",  # Windows; should be installed on every Windows, but is quirky with variable bitrate MP3s
+    "alsa",  # Linux; only supports .mp3 and .wav and might not be installed
+]
+
+_BACKEND_MAP: dict[str, SoundBackend] = {
+    name.lower(): obj()
+    for name, obj in globals().items()
+    if isinstance(obj, type) and issubclass(obj, SoundBackend) and obj is not SoundBackend
 }
 
-AVAILABLE_BACKENDS = list(_BACKEND_MAPPING.keys())
+assert sorted(_BACKEND_PREFERENCE) == sorted(_BACKEND_MAP.keys()), "forgot to update _BACKEND_PREFERENCE?"
+AVAILABLE_BACKENDS: list[str] = [name for name in _BACKEND_PREFERENCE if _BACKEND_MAP[name].check()]
+DEFAULT_BACKEND: str | None = _auto_select_backend()
+
+
+# This function is defined here at the bottom because of:
+# SyntaxError: annotated name 'DEFAULT_BACKEND' can't be global
+def prefer_backends(*backends: str) -> str | None:
+    """Add backends to the top of the preference list.
+
+    This function overrides the default backend preference.
+    Backend selected here will be used ONLY if available on the system.
+    This means this function can be used to update the preference for a
+    specific platform without breaking the cross-platform functionality.
+    After updating the preferences, the new default backend is returned.
+
+    Args:
+        backends: Names of the backends to prefer.
+
+    Returns:
+        Name of the newly selected default backend.
+    """
+    global DEFAULT_BACKEND, _BACKEND_PREFERENCE
+
+    _BACKEND_PREFERENCE = list(backends) + _BACKEND_PREFERENCE
+    DEFAULT_BACKEND = _auto_select_backend()
+    return DEFAULT_BACKEND
diff -pruN 2.5.2-1/pyproject.toml 3.3.0-1/pyproject.toml
--- 2.5.2-1/pyproject.toml	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/pyproject.toml	2020-02-02 00:00:00.000000000 +0000
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
 
 [project]
 name = "playsound3"
-version = "2.5.2"
-requires-python = ">=3.9"
+dynamic = ["version"]
+requires-python = ">=3.10"
 authors = [
     { name = "Szymon Mikler", email = "sjmikler@gmail.com" },
     { name = "Taylor Marks", email = "taylor@marksfam.com" },
@@ -22,55 +22,73 @@ classifiers = [
     "Intended Audience :: Developers",
     "License :: OSI Approved :: MIT License",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
     "Topic :: Multimedia :: Sound/Audio :: MIDI",
     "Topic :: Multimedia :: Sound/Audio :: Players",
     "Topic :: Multimedia :: Sound/Audio :: Players :: MP3",
     "Typing :: Typed",
     "Operating System :: OS Independent",
 ]
-dependencies = ["certifi"]
+dependencies = [
+    "pywin32; sys_platform == 'win32'",
+]
 
 [project.urls]
-Repository = "https://github.com/sjmikler/playsound3"
+Home = "https://github.com/sjmikler/playsound3"
 Issues = "https://github.com/sjmikler/playsound3/issues"
-Documentation = "https://github.com/sjmikler/playsound3?tab=readme-ov-file#documentation"
+Documentation = "https://github.com/sjmikler/playsound3/blob/main/README.md#quick-start"
 
-##################################
-## Formatting and testing tools ##
-##################################
+[project.optional-dependencies]
+dev = [
+    "pyright",
+    "pytest",
+    "ruff",
+]
 
-[tool.black]
-line_length = 120
+[tool.hatch.version]
+path = "playsound3/__init__.py"
 
-[tool.flake8]
-max-line-length = 120
+[tool.hatch.build.targets.sdist]
+include = ["playsound3", "README.md", "tests"]
 
-[tool.isort]
-profile = "black"
-line_length = 120
+[tool.hatch.build.targets.wheel]
+packages = ["playsound3"]
 
-[tool.mypy]
-ignore_missing_imports = true
+##################################
+## Formatting and testing tools ##
+##################################
 
 [tool.pyright]
-pythonVersion = "3.9"
+typeCheckingMode = "standard"
+exclude = ["devel", "build", "dist"]
+pythonVersion = "3.10"
 
 [tool.ruff]
 line-length = 120
+target-version = "py310"
 
 [tool.ruff.format]
 quote-style = "double"
 indent-style = "space"
 
 [tool.ruff.lint]
-select = ['ALL']
-ignore = [
-    'FBT001', # boolean-type-hint-positional-argument     - Allow positional booleans in functions, it's not really that much of an issue
-    'FBT002', # boolean-default-value-positional-argument - ^
-    'FBT003', # boolean-positional-value-in-call          - ^
-]
+select = ["E", "F", "I", "B"]
+
+[tool.pytest.ini_options]
+pythonpath = ["."]
+
+# %% Old tools
+
+[tool.black]
+line_length = 120
+
+[tool.flake8]
+max-line-length = 120
+
+[tool.isort]
+profile = "black"
+line_length = 120
Binary files 2.5.2-1/tests/sounds/sample3s.flac and 3.3.0-1/tests/sounds/sample3s.flac differ
Binary files 2.5.2-1/tests/sounds/sample3s.mp3 and 3.3.0-1/tests/sounds/sample3s.mp3 differ
Binary files 2.5.2-1/tests/sounds/звук 音 聲音.wav and 3.3.0-1/tests/sounds/звук 音 聲音.wav differ
diff -pruN 2.5.2-1/tests/test_functionality.py 3.3.0-1/tests/test_functionality.py
--- 2.5.2-1/tests/test_functionality.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.3.0-1/tests/test_functionality.py	2020-02-02 00:00:00.000000000 +0000
@@ -0,0 +1,143 @@
+import os
+import time
+
+from playsound3 import AVAILABLE_BACKENDS, playsound
+from playsound3.playsound3 import _prepare_path
+
+loc_mp3_3s = "tests/sounds/sample3s.mp3"
+loc_flc_3s = "tests/sounds/sample3s.flac"
+web_wav_3s = "https://samplelib.com/lib/preview/wav/sample-3s.wav"
+
+# Download web files to the local cache
+for url in [web_wav_3s]:
+    _prepare_path(url)
+
+
+def get_supported_sounds(backend):
+    not_supporting_flac = ["alsa", "winmm"]
+
+    if backend in not_supporting_flac:
+        return [loc_mp3_3s, web_wav_3s]
+    else:
+        return [loc_mp3_3s, loc_flc_3s, web_wav_3s]
+
+
+CI = os.environ.get("CI", False)
+
+
+def test_blocking_1():
+    for backend in AVAILABLE_BACKENDS:
+        for path in get_supported_sounds(backend):
+            t0 = time.perf_counter()
+            sound = playsound(path, block=True, backend=backend)
+
+            td = time.perf_counter() - t0
+            assert not sound.is_alive(), f"backend={backend}, path={path}"
+            assert td >= 3.0, f"backend={backend}, path={path}"
+            assert CI or td < 5.0, f"backend={backend}, path={path}"
+
+
+def test_waiting_1():
+    for backend in AVAILABLE_BACKENDS:
+        for path in get_supported_sounds(backend):
+            t0 = time.perf_counter()
+            sound = playsound(path, block=False, backend=backend)
+            assert sound.is_alive(), f"backend={backend}, path={path}"
+
+            sound.wait()
+            td = time.perf_counter() - t0
+            assert not sound.is_alive(), f"backend={backend}, path={path}"
+            assert td >= 3.0, f"backend={backend}, path={path}"
+            assert CI or td < 5.0, f"backend={backend}, path={path}"
+
+
+def test_waiting_2():
+    for backend in AVAILABLE_BACKENDS:
+        for path in get_supported_sounds(backend):
+            sound = playsound(path, block=False, backend=backend)
+            assert sound.is_alive(), f"backend={backend}, path={path}"
+
+            time.sleep(5)
+            assert not sound.is_alive(), f"backend={backend}, path={path}"
+
+
+def test_stopping_1():
+    for backend in AVAILABLE_BACKENDS:
+        for path in get_supported_sounds(backend):
+            t0 = time.perf_counter()
+            sound = playsound(path, block=False, backend=backend)
+            assert sound.is_alive(), f"backend={backend}, path={path}"
+
+            time.sleep(1)
+            sound.stop()
+            td = time.perf_counter() - t0
+
+            time.sleep(0.05)
+            assert not sound.is_alive(), f"backend={backend}, path={path}"
+            assert td >= 0.95 and td < 2.0, f"backend={backend}, path={path}"
+
+            # Stopping again should be a no-op
+            sound.stop()
+            assert not sound.is_alive(), f"backend={backend}, path={path}"
+
+
+def test_parallel_1():
+    for backend in AVAILABLE_BACKENDS:
+        for path in get_supported_sounds(backend):
+            t0 = time.perf_counter()
+            sounds = [playsound(path, block=False, backend=backend) for _ in range(3)]
+            time.sleep(0.05)
+            for sound in sounds:
+                assert sound.is_alive(), f"backend={backend}"
+            time.sleep(1)
+
+            sounds[1].stop()
+            time.sleep(0.05)
+            assert sounds[0].is_alive(), f"backend={backend}"
+            assert sounds[2].is_alive(), f"backend={backend}"
+            assert not sounds[1].is_alive(), f"backend={backend}"
+            time.sleep(1)
+
+            assert sounds[0].is_alive(), f"backend={backend}"
+            assert sounds[2].is_alive(), f"backend={backend}"
+            sounds[0].stop()
+            sounds[2].stop()
+            td = time.perf_counter() - t0
+
+            time.sleep(0.05)
+            for sound in sounds:
+                assert not sound.is_alive(), f"backend={backend}"
+            assert td >= 2.0 and td < 3.0, f"backend={backend}"
+
+
+def test_parallel_2():
+    N_PARALLEL = 10  # Careful - this might be loud!
+
+    for backend in AVAILABLE_BACKENDS:
+        for path in get_supported_sounds(backend):
+            sounds = [playsound(path, block=False, backend=backend) for _ in range(N_PARALLEL)]
+
+            time.sleep(1)
+            for sound in sounds:
+                assert sound.is_alive(), f"backend={backend}, path={path}"
+            for sound in sounds:
+                sound.stop()
+
+            time.sleep(0.05)
+            for sound in sounds:
+                assert not sound.is_alive(), f"backend={backend}, path={path}"
+
+
+def test_parallel_3():
+    for backend in AVAILABLE_BACKENDS:
+        sounds = [playsound(path, block=False, backend=backend) for path in get_supported_sounds(backend)]
+
+        time.sleep(1)
+        for sound in sounds:
+            assert sound.is_alive(), f"backend={backend}"
+        for sound in sounds:
+            sound.stop()
+
+        time.sleep(0.05)
+        for sound in sounds:
+            assert not sound.is_alive(), f"backend={backend}"
diff -pruN 2.5.2-1/tests/test_killing.py 3.3.0-1/tests/test_killing.py
--- 2.5.2-1/tests/test_killing.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.3.0-1/tests/test_killing.py	2020-02-02 00:00:00.000000000 +0000
@@ -0,0 +1,88 @@
+import os
+import pathlib
+import subprocess
+import sys
+import time
+import typing as t
+
+import pytest
+
+from playsound3 import AVAILABLE_BACKENDS
+from playsound3.playsound3 import _prepare_path
+
+loc_mp3_3s = "tests/sounds/sample3s.mp3"
+loc_flc_3s = "tests/sounds/sample3s.flac"
+web_wav_3s = "https://samplelib.com/lib/preview/wav/sample-3s.wav"
+
+# Download web files to the local cache
+for url in [web_wav_3s]:
+    _prepare_path(url)
+
+
+def get_supported_sounds(backend: str):
+    not_supporting_flac = ["alsa", "winmm"]
+    if backend in not_supporting_flac:
+        return [loc_mp3_3s, web_wav_3s]
+    else:
+        return [loc_mp3_3s, loc_flc_3s, web_wav_3s]
+
+
+def _iter_pids() -> t.Iterable[int]:
+    proc = pathlib.Path("/proc")
+    for p in proc.iterdir():
+        if p.name.isdigit():
+            yield int(p.name)
+
+
+def _read_file(path: pathlib.Path) -> bytes:
+    try:
+        return path.read_bytes()
+    except Exception:
+        return b""
+
+
+def list_tagged_player_pids(tag: str) -> t.List[int]:
+    """Return PIDs whose environ contains TAG=<tag>"""
+    pids = []
+    for pid in _iter_pids():
+        base = pathlib.Path(f"/proc/{pid}")
+        env = _read_file(base / "environ")
+        if not env:
+            continue
+        # /proc/<pid>/environ is NUL-separated key=val entries
+        if f"PLAYSOUND_TEST_TAG={tag}".encode() not in env.split(b"\x00"):
+            continue
+        cmdline = _read_file(base / "cmdline").replace(b"\x00", b" ").lower()
+        if not cmdline:
+            continue
+        pids.append(pid)
+    return pids
+
+
+HELPER_CODE = """
+import os, sys, time
+from playsound3 import playsound
+
+sound = playsound({path!r}, block=False, backend={backend!r})
+time.sleep(10)
+"""
+
+
+@pytest.mark.skipif(sys.platform != "linux", reason="Linux-only: relies on /proc and PDEATHSIG semantics")
+def test_killing_parent():
+    TAG = "__test_killing_tag"
+
+    for backend in AVAILABLE_BACKENDS:
+        for path in get_supported_sounds(backend):
+            assert len(list_tagged_player_pids(TAG)) == 0
+            code = HELPER_CODE.format(path=path, backend=backend)
+
+            environ = os.environ.copy()
+            environ["PLAYSOUND_TEST_TAG"] = TAG
+            proc = subprocess.Popen(["python", "-c", code], env=environ)
+
+            time.sleep(2.5)
+            assert len(list_tagged_player_pids(TAG)) == 2
+
+            proc.kill()
+            assert len(list_tagged_player_pids(TAG)) == 0
diff -pruN 2.5.2-1/tests/test_playsound.py 3.3.0-1/tests/test_playsound.py
--- 2.5.2-1/tests/test_playsound.py	2020-02-02 00:00:00.000000000 +0000
+++ 3.3.0-1/tests/test_playsound.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,30 +0,0 @@
-import time
-
-from playsound3 import playsound
-
-MP3_3s = "https://samplelib.com/lib/preview/mp3/sample-3s.mp3"
-MP3_6s = "https://samplelib.com/lib/preview/mp3/sample-6s.mp3"
-WAV_3s = "https://samplelib.com/lib/preview/wav/sample-3s.wav"
-WAV_6s = "https://samplelib.com/lib/preview/wav/sample-6s.wav"
-
-
-def test_dummy():
-    pass
-
-
-def test_with_blocking():
-    for sound in [MP3_3s, WAV_3s]:
-        t0 = time.time()
-        playsound(sound, block=True)
-        assert time.time() - t0 >= 3.0
-    for sound in [MP3_6s, WAV_6s]:
-        t0 = time.time()
-        playsound(sound, block=True)
-        assert time.time() - t0 >= 6.0
-
-
-def test_non_blocking():
-    t0 = time.time()
-    for sound in [MP3_3s, WAV_3s, MP3_6s, WAV_6s]:
-        playsound(sound, block=False)
-    assert time.time() - t0 < 1.0
diff -pruN 2.5.2-1/tests/test_raising_errors.py 3.3.0-1/tests/test_raising_errors.py
--- 2.5.2-1/tests/test_raising_errors.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.3.0-1/tests/test_raising_errors.py	2020-02-02 00:00:00.000000000 +0000
@@ -0,0 +1,29 @@
+import urllib.error
+
+import pytest
+
+from playsound3 import playsound
+from playsound3.playsound3 import PlaysoundException
+
+valid = "tests/sounds/sample3s.mp3"
+
+
+def test_invalid_sound_file():
+    with pytest.raises(PlaysoundException):
+        playsound("invalid.mp3")
+
+
+def test_non_existent_file():
+    with pytest.raises(PlaysoundException):
+        playsound("non_existent.mp3")
+
+
+def test_invalid_backend():
+    with pytest.raises(PlaysoundException):
+        playsound(valid, backend="invalid_backend")
+
+
+def test_playsound_from_url():
+    url = "https://wrong-url.com/wrong-audio.mp3"
+    with pytest.raises(urllib.error.URLError):
+        playsound(url)
diff -pruN 2.5.2-1/tests/test_special_characters.py 3.3.0-1/tests/test_special_characters.py
--- 2.5.2-1/tests/test_special_characters.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.3.0-1/tests/test_special_characters.py	2020-02-02 00:00:00.000000000 +0000
@@ -0,0 +1,25 @@
+import time
+
+from playsound3 import AVAILABLE_BACKENDS, playsound
+
+wav = "tests/sounds/звук 音 聲音.wav"
+
+
+def test_with_blocking():
+    for backend in AVAILABLE_BACKENDS:
+        print(f"Testing backend: {backend}")
+
+        sound = playsound(wav, block=True, backend=backend)
+        assert not sound.is_alive()
+
+
+def test_non_blocking():
+    for backend in AVAILABLE_BACKENDS:
+        print(f"Testing backend: {backend}")
+
+        sound = playsound(wav, block=False, backend=backend)
+        time.sleep(0.05)
+        assert sound.is_alive()
+
+        sound.wait()
+        assert not sound.is_alive()
