├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE.md ├── Makefile.toml ├── README.md ├── benches ├── base-mixing.rs └── mixing-task.rs ├── build.rs ├── cliff.toml ├── examples ├── Cargo.toml ├── README.md ├── serenity │ ├── voice │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── voice_cached_audio │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── voice_events_queue │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── voice_receive │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs └── twilight │ ├── Cargo.toml │ └── src │ └── main.rs ├── images ├── arch.afdesign ├── driver.png ├── driver.svg ├── gateway.png ├── gateway.svg ├── scheduler.png └── scheduler.svg ├── resources ├── Cloudkicker - 2011 07.dca1 ├── Cloudkicker - 2011 07.mp3 ├── Cloudkicker - Making Will Mad.opus ├── Cloudkicker - Making Will Mad.webm ├── README.md ├── loop-48.mp3 ├── loop.wav ├── ting-vid.mp4 ├── ting.mp3 └── ting.wav ├── rustfmt.toml ├── songbird-ico.png ├── songbird.png ├── songbird.svg └── src ├── config.rs ├── constants.rs ├── driver ├── bench_internals.rs ├── connection │ ├── error.rs │ └── mod.rs ├── crypto.rs ├── decode_mode.rs ├── mix_mode.rs ├── mod.rs ├── retry │ ├── mod.rs │ └── strategy.rs ├── scheduler │ ├── config.rs │ ├── idle.rs │ ├── live.rs │ ├── mod.rs │ ├── stats.rs │ └── task.rs ├── tasks │ ├── disposal.rs │ ├── error.rs │ ├── events.rs │ ├── message │ │ ├── core.rs │ │ ├── disposal.rs │ │ ├── events.rs │ │ ├── mixer.rs │ │ ├── mod.rs │ │ ├── udp_rx.rs │ │ └── ws.rs │ ├── mixer │ │ ├── mix_logic.rs │ │ ├── mod.rs │ │ ├── pool.rs │ │ ├── result.rs │ │ ├── state.rs │ │ ├── track.rs │ │ └── util.rs │ ├── mod.rs │ ├── udp_rx │ │ ├── decode_sizes.rs │ │ ├── mod.rs │ │ ├── playout_buffer.rs │ │ └── ssrc_state.rs │ └── ws.rs ├── test_config.rs └── test_impls.rs ├── error.rs ├── events ├── context │ ├── data │ │ ├── connect.rs │ │ ├── disconnect.rs │ │ ├── mod.rs │ │ ├── rtcp.rs │ │ ├── rtp.rs │ │ └── voice.rs │ ├── internal_data.rs │ └── mod.rs ├── core.rs ├── data.rs ├── mod.rs ├── store.rs ├── track.rs └── untimed.rs ├── handler.rs ├── id.rs ├── info.rs ├── input ├── adapters │ ├── async_adapter.rs │ ├── cached │ │ ├── compressed.rs │ │ ├── decompressed.rs │ │ ├── error.rs │ │ ├── hint.rs │ │ ├── memory.rs │ │ ├── mod.rs │ │ └── util.rs │ ├── child.rs │ ├── mod.rs │ └── raw_adapter.rs ├── audiostream.rs ├── codecs │ ├── dca │ │ ├── metadata.rs │ │ └── mod.rs │ ├── mod.rs │ ├── opus.rs │ └── raw.rs ├── compose.rs ├── error.rs ├── input_tests.rs ├── live_input.rs ├── metadata │ ├── ffprobe.rs │ ├── mod.rs │ └── ytdl.rs ├── mod.rs ├── parsed.rs ├── sources │ ├── file.rs │ ├── hls.rs │ ├── http.rs │ ├── mod.rs │ └── ytdl.rs └── utils.rs ├── join.rs ├── lib.rs ├── manager.rs ├── serenity.rs ├── shards.rs ├── test_utils.rs ├── tracks ├── action.rs ├── command.rs ├── error.rs ├── handle.rs ├── looping.rs ├── mod.rs ├── mode.rs ├── queue.rs ├── ready.rs ├── state.rs └── view.rs └── ws.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report for any unexpected behaviour 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Songbird version:** (version) 11 | 12 | **Rust version (`rustc -V`):** (version) 13 | 14 | **Serenity/Twilight version:** (version) 15 | 16 | **Output of `ffmpeg -version`, `yt-dlp --version` (if relevant):** 17 | ... 18 | 19 | **Description:** 20 | ... 21 | 22 | **Steps to reproduce:** 23 | ... 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v3 12 | - name: Install toolchain 13 | uses: dtolnay/rust-toolchain@master 14 | with: 15 | toolchain: nightly 16 | components: rustfmt,clippy 17 | - name: Setup cache 18 | uses: Swatinem/rust-cache@v2 19 | - name: Rustfmt 20 | run: cargo +nightly fmt --all -- --check 21 | - name: Clippy 22 | run: cargo clippy --features full-doc 23 | 24 | test: 25 | name: Test 26 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | name: 31 | - stable 32 | - beta 33 | - nightly 34 | - macOS 35 | - Windows 36 | - driver only 37 | - gateway only 38 | include: 39 | - name: beta 40 | toolchain: beta 41 | - name: nightly 42 | toolchain: nightly 43 | - name: macOS 44 | os: macOS-latest 45 | dont-test: true 46 | - name: Windows 47 | os: windows-latest 48 | dont-test: true 49 | - name: driver only 50 | features: driver tungstenite rustls 51 | dont-test: true 52 | - name: gateway only 53 | features: gateway serenity tungstenite rustls 54 | dont-test: true 55 | steps: 56 | - name: Checkout sources 57 | uses: actions/checkout@v3 58 | - name: Install toolchain 59 | uses: dtolnay/rust-toolchain@master 60 | with: 61 | toolchain: ${{ matrix.toolchain || 'stable' }} 62 | - name: Setup cache 63 | uses: Swatinem/rust-cache@v2 64 | - name: Install dependencies 65 | if: runner.os == 'Linux' 66 | run: | 67 | sudo apt-get update 68 | - name: Install yt-dlp (Unix) 69 | if: runner.os != 'Windows' 70 | run: | 71 | sudo wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp 72 | sudo chmod a+rx /usr/local/bin/yt-dlp 73 | - name: Install yt-dlp (Windows) 74 | if: runner.os == 'Windows' 75 | run: choco install yt-dlp 76 | - name: Set RUSTFLAGS 77 | if: runner.os != 'Windows' 78 | run: echo "RUSTFLAGS=${{ matrix.rustflags || '' }}" >> $GITHUB_ENV 79 | - name: Build all features 80 | if: matrix.features == '' 81 | run: cargo build --features full-doc 82 | - name: Test all features 83 | if: ${{ !matrix.dont-test && matrix.features == '' }} 84 | run: cargo test --features full-doc 85 | - name: Build some features 86 | if: matrix.features 87 | run: cargo build --no-default-features --features "${{ matrix.features }}" 88 | - name: Test some features 89 | if: ${{ !matrix.dont-test && matrix.features }} 90 | run: cargo test --no-default-features --features "${{ matrix.features }}" 91 | 92 | doc: 93 | name: Build docs 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Checkout sources 97 | uses: actions/checkout@v3 98 | - name: Install toolchain 99 | uses: dtolnay/rust-toolchain@master 100 | with: 101 | toolchain: nightly 102 | - name: Setup cache 103 | uses: Swatinem/rust-cache@v2 104 | - name: Install dependencies 105 | run: | 106 | sudo apt-get update 107 | sudo apt-get install -y libopus-dev 108 | - name: Build docs 109 | env: 110 | RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links 111 | run: | 112 | cargo doc --no-deps --features full-doc 113 | 114 | examples: 115 | name: Examples 116 | runs-on: ubuntu-latest 117 | steps: 118 | - name: Checkout sources 119 | uses: actions/checkout@v3 120 | - name: Install toolchain 121 | uses: dtolnay/rust-toolchain@master 122 | with: 123 | toolchain: stable 124 | - name: Setup cache 125 | uses: Swatinem/rust-cache@v2 126 | with: 127 | workspaces: examples 128 | - name: Install dependencies 129 | run: | 130 | sudo apt-get update 131 | sudo apt-get install -y libopus-dev 132 | 133 | - name: Build serenity/voice 134 | working-directory: examples 135 | run: cargo build -p voice 136 | - name: Build serenity/voice_events_queue 137 | working-directory: examples 138 | run: cargo build -p voice_events_queue 139 | - name: Build serenity/voice_receive 140 | working-directory: examples 141 | run: cargo build -p voice_receive 142 | - name: Build serenity/voice_cached_audio 143 | working-directory: examples 144 | run: cargo build -p voice_cached_audio 145 | - name: Build twilight 146 | working-directory: examples 147 | run: cargo build -p twilight 148 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - current 7 | - next 8 | 9 | jobs: 10 | docs: 11 | name: Publish docs 12 | runs-on: ubuntu-latest 13 | permissions: 14 | # peaceiris/actions-gh-pages requires write permission 15 | # as it pushes a new commit to the gh-pages branch 16 | contents: write 17 | 18 | steps: 19 | - name: Checkout sources 20 | uses: actions/checkout@v3 21 | 22 | - name: Install toolchain 23 | uses: dtolnay/rust-toolchain@master 24 | with: 25 | toolchain: nightly 26 | 27 | - name: Setup cache 28 | uses: Swatinem/rust-cache@v2 29 | 30 | - name: Install dependencies 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install -y libopus-dev 34 | 35 | - name: Build docs 36 | env: 37 | RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links 38 | run: | 39 | cargo doc --no-deps --features full-doc 40 | 41 | - name: Prepare docs 42 | shell: bash -e -O extglob {0} 43 | run: | 44 | DIR=${GITHUB_REF/refs\/+(heads|tags)\//} 45 | mkdir -p ./docs/$DIR 46 | touch ./docs/.nojekyll 47 | echo '' > ./docs/$DIR/index.html 48 | mv ./target/doc/* ./docs/$DIR/ 49 | 50 | - name: Deploy docs 51 | uses: peaceiris/actions-gh-pages@v3 52 | with: 53 | github_token: ${{ secrets.GITHUB_TOKEN }} 54 | publish_branch: gh-pages 55 | publish_dir: ./docs 56 | allow_empty_commit: false 57 | keep_files: true 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE directories and folders 2 | .vscode/ 3 | .idea/ 4 | 5 | # Target directory 6 | target/ 7 | 8 | # Lockfile 9 | Cargo.lock 10 | 11 | # Misc 12 | rls/ 13 | *.iml 14 | .env 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issues 2 | Thanks for raising a bug or feature request! 3 | If you're raising a bug or issue, please use the following template: 4 | 5 | ```md 6 | **Songbird version:** (version) 7 | 8 | **Rust version (`rustc -V`):** (version) 9 | 10 | **Serenity/Twilight version:** (version) 11 | 12 | **Output of `ffmpeg -version`, `youtube-dl --version` (if relevant):** 13 | ... 14 | 15 | **Description:** 16 | ... 17 | 18 | **Steps to reproduce:** 19 | ... 20 | ``` 21 | 22 | Additionally, tag your issue at the same time you create it. 23 | 24 | If you're requesting a feature, explain how it will be useful to users or improve the library. 25 | If you want to implement it yourself, please include a rough explanation of *how* you'll be going about writing it. 26 | 27 | # Pull Requests 28 | Thanks for considering adding new features or fixing bugs in Songbird! 29 | You might find it helpful to look over [our architecture document] in this repository to understand roughly how components interact at a high level. 30 | Generally, we ask that PRs have a description that answers the following, under headers or in prose: 31 | 32 | * The type of change being made. 33 | * A high-level description of the changes. 34 | * Steps taken to test the new feature/fix. 35 | 36 | Your PR should also readily compile, pass all tests, and undergo automated formatting *before* it is opened. 37 | The simplest way to check that your PR is ready is to install [cargo make], and run the following command: 38 | ```sh 39 | cargo make ready 40 | ``` 41 | 42 | Merged PRs will be squashed into the repository under a single headline: try to tag your PR correctly, and title it with a single short sentence in the imperative mood to make your work easier to merge. 43 | *"Driver: Fix missing track state events"* is a good example: it explains what code was modified, the problem that was solved, and would place the description of *how* the problem was solved in the commit/PR body. 44 | 45 | If you're adding new features or utilities, please open an issue and/or speak with us on Discord to make sure that you aren't duplicating work, and are in line with the overall system architecture. 46 | 47 | At a high level, focus on making new features as clean and usable as possible. 48 | This extends to directing users away from footguns and functions with surprising effects, at the API level or by documentation. 49 | Changes that affect or invalidate large areas of the library API will make a lot of users' lives that much harder when new breaking releases are published, so need deeper justification. 50 | Focus on making sure that new feature additions are general to as many use-cases as possible: for instance, adding some queue-specific state to every audio track forces other users to pay for that additional overhead even when they aren't using this feature. 51 | 52 | ## Breaking changes 53 | Breaking changes (in API or API semantics) must be made to target the `"next"` branch. 54 | Commits here will be released in the next breaking semantic version (i.e., 0.1.7 -> 0.2.0, 1.3.2 -> 2.0.0). 55 | 56 | Bugfixes and new features which do not break semantic versioning should target `"current"`. 57 | Patches will be folded into more incremental patch updates (i.e., 1.3.2 -> 1.3.3) while new features will trigger minor updates (i.e., 1.3.2 -> 1.4.0). 58 | 59 | ## Documentation and naming 60 | Doc-comments, comments, and item names should be written in British English where possible. 61 | All items (`structs`, `enums`, `fn`s, etc.) must be documented in full sentences; these are user-facing, and other developers will naturally rely on them to write correct code and understand what the library can(not) do for them. 62 | Error conditions, reasons to prefer one method over another, and potential use risks should be explained to help library users write the best code they can. 63 | 64 | Code comments should be written similarly – this requirement is not as stringent, but focus on clarity and conciseness. 65 | Try to focus on explaining *why/what* more confusing code exists/does, rather than *how* it performs that task, to try to prevent comments from aging as the codebase evolves. 66 | 67 | ## Testing 68 | Pull requests must not break existing tests, examples, or common feature subsets. 69 | Where possible, new features should include new tests (particularly around event or input handling). 70 | 71 | These steps are included in `cargo make ready`. 72 | 73 | ## Linting 74 | Songbird's linting pipeline requires that you have nightly Rust installed. 75 | Your code must be formatted using `cargo +nightly fmt --all`, and must not add any more Clippy warnings than the base repository already has (as extra lints are added to Clippy over time). 76 | 77 | These commands are included in `cargo make ready`. 78 | 79 | [cargo make]: https://github.com/sagiegurari/cargo-make 80 | [our architecture document]: ARCHITECTURE.md 81 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2020, Songbird Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.format] 2 | toolchain = "nightly" 3 | install_crate = { crate_name = "rustfmt-nightly", rustup_component_name = "rustfmt-preview", binary = "rustfmt", test_arg = "--help" } 4 | command = "cargo" 5 | args = ["fmt", "--all"] 6 | 7 | [tasks.build] 8 | args = ["build", "--features", "full-doc"] 9 | dependencies = ["format"] 10 | 11 | [tasks.build-examples] 12 | args = ["build", "--manifest-path", "./examples/Cargo.toml", "--workspace"] 13 | command = "cargo" 14 | dependencies = ["format"] 15 | 16 | [tasks.build-gateway] 17 | args = ["build", "--no-default-features", "--features", "gateway,serenity,rustls"] 18 | command = "cargo" 19 | dependencies = ["format"] 20 | 21 | [tasks.build-driver] 22 | args = ["build", "--no-default-features", "--features", "driver,rustls,tungstenite"] 23 | command = "cargo" 24 | dependencies = ["format"] 25 | 26 | [tasks.build-variants] 27 | dependencies = ["build", "build-gateway", "build-driver"] 28 | 29 | [tasks.check] 30 | args = ["check", "--features", "full-doc"] 31 | dependencies = ["format"] 32 | 33 | [tasks.clippy] 34 | args = ["clippy", "--features", "full-doc", "--", "-D", "warnings"] 35 | dependencies = ["format"] 36 | 37 | [tasks.test] 38 | args = ["test", "--features", "full-doc", "--", "--include-ignored"] 39 | 40 | [tasks.bench] 41 | description = "Runs performance benchmarks." 42 | category = "Test" 43 | command = "cargo" 44 | args = ["bench", "--features", "internals,full-doc"] 45 | 46 | [tasks.doc] 47 | command = "cargo" 48 | args = ["doc", "--features", "full-doc"] 49 | 50 | [tasks.doc-open] 51 | command = "cargo" 52 | args = ["doc", "--features", "full-doc", "--open"] 53 | 54 | [tasks.ready] 55 | dependencies = ["format", "test", "build-variants", "build-examples", "doc", "clippy"] 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![docs-badge][]][docs] [![next-docs-badge][]][next-docs] [![build badge]][build] [![guild-badge][]][guild] [![crates.io version]][crates.io link] [![rust badge]][rust link] 2 | 3 | # Songbird 4 | 5 | ![](songbird.png) 6 | 7 | Songbird is an async, cross-library compatible voice system for Discord, written in Rust. 8 | The library offers: 9 | * A standalone gateway frontend compatible with [serenity] and [twilight] using the 10 | `"gateway"` and `"[serenity/twilight]"` plus `"[rustls/native]"` features. You can even run 11 | driverless, to help manage your [lavalink] sessions. 12 | * A standalone driver for voice calls, via the `"driver"` feature. If you can create 13 | a `ConnectionInfo` using any other gateway, or language for your bot, then you 14 | can run the songbird voice driver. 15 | * Voice receive and RT(C)P packet handling via the `"receive"` feature. 16 | * And, by default, a fully featured voice system featuring events, queues, 17 | seeking on compatible streams, shared multithreaded audio stream caches, 18 | and direct Opus data passthrough from DCA files. 19 | 20 | ## Intents 21 | Songbird's gateway functionality requires you to specify the `GUILD_VOICE_STATES` intent. 22 | 23 | ## Codec support 24 | Songbird supports all [codecs and formats provided by Symphonia] (pure-Rust), with Opus support 25 | provided by [audiopus] (an FFI wrapper for libopus). 26 | 27 | **By default, *Songbird will not request any codecs from Symphonia*.** To change this, in your own 28 | project you will need to depend on Symphonia as well. 29 | 30 | ```toml 31 | # Including songbird alone gives you support for Opus via the DCA file format. 32 | [dependencies.songbird] 33 | version = "0.5" 34 | features = ["builtin-queue"] 35 | 36 | # To get additional codecs, you *must* add Symphonia yourself. 37 | # This includes the default formats (MKV/WebM, Ogg, Wave) and codecs (FLAC, PCM, Vorbis)... 38 | [dependencies.symphonia] 39 | version = "0.5" 40 | features = ["aac", "mp3", "isomp4", "alac"] # ...as well as any extras you need! 41 | ``` 42 | 43 | ## Dependencies 44 | Songbird needs a few system dependencies before you can use it. 45 | 46 | - Opus - Audio codec that Discord uses. 47 | [audiopus] will use installed libopus binaries if available via pkgconf on Linux/MacOS, otherwise you will need to install cmake to build opus from source. 48 | This is always the case on Windows. 49 | For Unix systems, you can install the library with `apt install libopus-dev` on Ubuntu or `pacman -S opus` on Arch Linux. 50 | If you do not have it installed it will be built for you. However, you will need a C compiler and the GNU autotools installed. 51 | Again, these can be installed with `apt install build-essential autoconf automake libtool m4` on Ubuntu or `pacman -S base-devel` on Arch Linux. 52 | 53 | This is a required dependency. Songbird cannot work without it. 54 | 55 | - yt-dlp / youtube-dl / (similar forks) - Audio/Video download tool. 56 | yt-dlp can be installed [according to the installation instructions on the main repo]. 57 | You can install youtube-dl with Python's package manager, pip, which we recommend for youtube-dl. You can do it with the command `pip install youtube_dl`. 58 | Alternatively, you can install it with your system's package manager, `apt install youtube-dl` on Ubuntu or `pacman -S youtube-dl` on Arch Linux. 59 | 60 | This is an optional dependency for users, but is required as a dev-dependency. It allows Songbird to download audio/video sources from the Internet from a variety of webpages, which it will convert to the Opus audio format Discord uses. 61 | 62 | ## Examples 63 | Full examples showing various types of functionality and integrations can be found in [this crate's examples directory]. 64 | 65 | ## Contributing 66 | If you want to help out or file an issue, please look over [our contributor guidelines]! 67 | 68 | ## Attribution 69 | Songbird's logo is based upon the copyright-free image ["Black-Capped Chickadee"] by George Gorgas White. 70 | 71 | [serenity]: https://github.com/serenity-rs/serenity 72 | [twilight]: https://github.com/twilight-rs/twilight 73 | ["Black-Capped Chickadee"]: https://www.oldbookillustrations.com/illustrations/black-capped-chickadee/ 74 | [lavalink]: https://github.com/freyacodes/Lavalink 75 | [this crate's examples directory]: https://github.com/serenity-rs/songbird/tree/current/examples 76 | [our contributor guidelines]: CONTRIBUTING.md 77 | [codecs and formats provided by Symphonia]: https://github.com/pdeljanov/Symphonia#formats-demuxers 78 | [audiopus]: https://github.com/lakelezz/audiopus 79 | [according to the installation instructions on the main repo]: https://github.com/yt-dlp/yt-dlp#installation 80 | 81 | [build badge]: https://img.shields.io/github/actions/workflow/status/serenity-rs/songbird/ci.yml?branch=current&style=flat-square 82 | [build]: https://github.com/serenity-rs/songbird/actions 83 | 84 | [docs-badge]: https://img.shields.io/badge/docs-current-4d76ae.svg?style=flat-square 85 | [docs]: https://serenity-rs.github.io/songbird/current 86 | 87 | [next-docs-badge]: https://img.shields.io/badge/docs-next-4d76ae.svg?style=flat-square 88 | [next-docs]: https://serenity-rs.github.io/songbird/next 89 | 90 | [guild]: https://discord.gg/9X7vCus 91 | [guild-badge]: https://img.shields.io/discord/381880193251409931.svg?style=flat-square&colorB=7289DA 92 | 93 | [crates.io link]: https://crates.io/crates/songbird 94 | [crates.io version]: https://img.shields.io/crates/v/songbird.svg?style=flat-square 95 | 96 | [rust badge]: https://img.shields.io/badge/rust-1.74+-93450a.svg?style=flat-square 97 | [rust link]: https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html 98 | -------------------------------------------------------------------------------- /benches/base-mixing.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; 2 | use songbird::{ 3 | constants::*, 4 | driver::{ 5 | bench_internals::mixer::{mix_logic, state::DecodeState}, 6 | MixMode, 7 | }, 8 | input::{codecs::*, Input, LiveInput, Parsed}, 9 | test_utils as utils, 10 | }; 11 | use std::io::Cursor; 12 | use symphonia_core::audio::{AudioBuffer, Layout, SampleBuffer, Signal, SignalSpec}; 13 | 14 | pub fn mix_one_frame(c: &mut Criterion) { 15 | let floats = utils::make_sine(1 * STEREO_FRAME_SIZE, true); 16 | 17 | let symph_layout = MixMode::Stereo.into(); 18 | 19 | let mut symph_mix = AudioBuffer::::new( 20 | MONO_FRAME_SIZE as u64, 21 | symphonia_core::audio::SignalSpec::new_with_layout(SAMPLE_RATE_RAW as u32, symph_layout), 22 | ); 23 | let mut resample_scratch = AudioBuffer::::new( 24 | MONO_FRAME_SIZE as u64, 25 | SignalSpec::new_with_layout(SAMPLE_RATE_RAW as u32, Layout::Stereo), 26 | ); 27 | 28 | let mut group = c.benchmark_group("Stereo Target"); 29 | 30 | for (pres, hz) in [("", 48_000), (" (Resample)", 44_100)] { 31 | group.bench_with_input( 32 | BenchmarkId::new(format!("Stereo Source{}", pres), hz), 33 | &hz, 34 | |b, i| { 35 | b.iter_batched_ref( 36 | || black_box(make_src(&floats, 2, *i)), 37 | |(ref mut input, ref mut local_input)| { 38 | symph_mix.clear(); 39 | symph_mix.render_reserved(Some(MONO_FRAME_SIZE)); 40 | resample_scratch.clear(); 41 | 42 | black_box(mix_logic::mix_symph_indiv( 43 | &mut symph_mix, 44 | &mut resample_scratch, 45 | input, 46 | local_input, 47 | black_box(1.0), 48 | None, 49 | )); 50 | }, 51 | BatchSize::SmallInput, 52 | ) 53 | }, 54 | ); 55 | 56 | group.bench_with_input( 57 | BenchmarkId::new(format!("Mono Source{}", pres), hz), 58 | &hz, 59 | |b, i| { 60 | b.iter_batched_ref( 61 | || black_box(make_src(&floats, 1, *i)), 62 | |(ref mut input, ref mut local_input)| { 63 | symph_mix.clear(); 64 | symph_mix.render_reserved(Some(MONO_FRAME_SIZE)); 65 | resample_scratch.clear(); 66 | 67 | black_box(mix_logic::mix_symph_indiv( 68 | &mut symph_mix, 69 | &mut resample_scratch, 70 | input, 71 | local_input, 72 | black_box(1.0), 73 | None, 74 | )); 75 | }, 76 | BatchSize::SmallInput, 77 | ) 78 | }, 79 | ); 80 | } 81 | 82 | group.finish(); 83 | } 84 | 85 | fn make_src(src: &Vec, chans: u32, hz: u32) -> (Parsed, DecodeState) { 86 | let local_input = Default::default(); 87 | 88 | let adapted: Input = 89 | songbird::input::RawAdapter::new(Cursor::new(src.clone()), hz, chans).into(); 90 | let promoted = match adapted { 91 | Input::Live(l, _) => l.promote(get_codec_registry(), get_probe()), 92 | _ => panic!("Failed to create a guaranteed source."), 93 | }; 94 | let parsed = match promoted { 95 | Ok(LiveInput::Parsed(parsed)) => parsed, 96 | Err(e) => panic!("AR {:?}", e), 97 | _ => panic!("Failed to create a guaranteed source."), 98 | }; 99 | 100 | (parsed, local_input) 101 | } 102 | 103 | criterion_group!(benches, mix_one_frame); 104 | criterion_main!(benches); 105 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "driver", not(any(feature = "rustls", feature = "native"))))] 2 | compile_error!( 3 | "You have the `driver` feature enabled: \ 4 | either the `rustls` or `native` feature must be 5 | selected to let Songbird's driver use websockets.\n\ 6 | - `rustls` uses Rustls, a pure Rust TLS-implemenation.\n\ 7 | - `native` uses SChannel on Windows, Secure Transport on macOS, \ 8 | and OpenSSL on other platforms.\n\ 9 | If you are unsure, go with `rustls`." 10 | ); 11 | 12 | #[cfg(any( 13 | all(feature = "driver", feature = "tws", feature = "tungstenite"), 14 | all(feature = "driver", not(feature = "tws"), not(feature = "tungstenite")) 15 | ))] 16 | compile_error!( 17 | "You have the `driver` feature enabled: \ 18 | this requires you specify either: \n\ 19 | - `tungstenite` (recommended with serenity)\n\ 20 | - or `tws` (recommended with twilight).\n\ 21 | You have either specified none, or both - choose exactly one." 22 | ); 23 | 24 | fn main() {} 25 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [changelog] 5 | # template for the changelog body 6 | # https://keats.github.io/tera/docs/#introduction 7 | body = """ 8 | {% if version %}\ 9 | {%- set versionlink = version -%} 10 | {%- set ts = timestamp -%} 11 | {% else %}\ 12 | {%- set versionlink = "unreleased" -%} 13 | {%- set ts = now() -%} 14 | {% endif %}\ 15 | ## [{{ versionlink }}] — {{ ts | date(format="%Y-%m-%d") }} 16 | 17 | Thanks to the following for their contributions: 18 | {% for contributor in github.contributors %} 19 | - [@{{ contributor.username }}] 20 | {%- endfor -%}{% raw %}\n{% endraw %} 21 | 22 | ### Changed 23 | {% for commit in commits %} 24 | - {{ commit.message | split(pat="\n") | first | upper_first | trim }} \ 25 | {% if commit.remote.username %}([@{{ commit.remote.username }}]){% raw %} {% endraw %}{%- endif -%} \ 26 | [c:{{ commit.id | truncate(length=7, end="") }}] 27 | {%- endfor -%} 28 | 29 | {%- macro remote_url() -%} 30 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 31 | {%- endmacro -%} 32 | 33 | {% raw %}\n{% endraw %} 34 | 35 | [{{ versionlink }}]: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ versionlink }} 36 | 37 | \ 38 | {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} 39 | [@{{ contributor.username }}]: https://github.com/{{ contributor.username }} 40 | {%- endfor -%}\ 41 | {% raw %}\n{% endraw %} 42 | 43 | \ 44 | {% for commit in commits %} 45 | [c:{{ commit.id | truncate(length=7, end="") }}]: {{ self::remote_url() }}/commit/{{ commit.id }} 46 | {%- endfor -%} 47 | {% raw %}\n{% endraw %} 48 | 49 | """ 50 | # remove the leading and trailing whitespace from the template 51 | trim = true 52 | # changelog footer 53 | footer = """ 54 | 55 | """ 56 | # postprocessors 57 | postprocessors = [] 58 | 59 | [git] 60 | # parse the commits based on https://www.conventionalcommits.org 61 | conventional_commits = false 62 | # filter out the commits that are not conventional 63 | filter_unconventional = true 64 | # process each line of a commit as an individual commit 65 | split_commits = false 66 | # regex for preprocessing the commit messages 67 | commit_preprocessors = [ 68 | # remove issue numbers from commits 69 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, 70 | ] 71 | # protect breaking changes from being skipped due to matching a skipping commit_parser 72 | protect_breaking_commits = false 73 | # filter out the commits that are not matched by commit parsers 74 | filter_commits = false 75 | # regex for matching git tags 76 | tag_pattern = "v[0-9].*" 77 | # regex for skipping tags 78 | skip_tags = "beta|alpha" 79 | # regex for ignoring tags 80 | ignore_tags = "rc" 81 | # sort the tags topologically 82 | topo_order = false 83 | # sort the commits inside sections by oldest/newest order 84 | sort_commits = "newest" 85 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "serenity/voice", 4 | "serenity/voice_cached_audio", 5 | "serenity/voice_events_queue", 6 | "serenity/voice_receive", 7 | "twilight", 8 | ] 9 | resolver = "2" 10 | 11 | [workspace.dependencies] 12 | reqwest = "0.12" 13 | serenity = { features = ["cache", "framework", "standard_framework", "voice", "http", "rustls_backend"], version = "0.12" } 14 | songbird = { path = "../", version = "0.5", default-features = false } 15 | symphonia = { features = ["aac", "mp3", "isomp4", "alac"], version = "0.5.2" } 16 | tokio = { features = ["macros", "rt-multi-thread", "signal", "sync"], version = "1" } 17 | tracing = "0.1" 18 | tracing-subscriber = "0.3" 19 | tracing-futures = "0.2" 20 | 21 | [profile.release] 22 | debug = true 23 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Songbird examples 2 | 3 | These examples show more advanced use of Songbird, or how to include Songbird in bots built on other libraries, such as twilight or serenity. 4 | -------------------------------------------------------------------------------- /examples/serenity/voice/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "voice" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | reqwest = { workspace = true } 9 | serenity = { workspace = true } 10 | songbird = { workspace = true, default-features = true } 11 | symphonia = { workspace = true } 12 | tokio = { workspace = true } 13 | tracing = { workspace = true } 14 | tracing-subscriber = { workspace = true } 15 | tracing-futures = { workspace = true } 16 | -------------------------------------------------------------------------------- /examples/serenity/voice_cached_audio/.gitignore: -------------------------------------------------------------------------------- 1 | *.dca 2 | -------------------------------------------------------------------------------- /examples/serenity/voice_cached_audio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "voice_cached_audio" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | reqwest = { workspace = true } 9 | serenity = { workspace = true } 10 | songbird = { workspace = true, default-features = true } 11 | symphonia = { workspace = true } 12 | tokio = { workspace = true } 13 | tracing = { workspace = true } 14 | tracing-subscriber = { workspace = true } 15 | tracing-futures = { workspace = true } 16 | -------------------------------------------------------------------------------- /examples/serenity/voice_events_queue/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "voice_events_queue" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | reqwest = { workspace = true } 9 | serenity = { workspace = true } 10 | songbird = { workspace = true, default-features = true, features = ["builtin-queue"] } 11 | symphonia = { workspace = true } 12 | tokio = { workspace = true } 13 | tracing = { workspace = true } 14 | tracing-subscriber = { workspace = true } 15 | tracing-futures = { workspace = true } 16 | -------------------------------------------------------------------------------- /examples/serenity/voice_receive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "voice_receive" 3 | version = "0.1.0" 4 | authors = ["my name "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | dashmap = "6" 9 | serenity = { workspace = true } 10 | songbird = { workspace = true, default-features = true, features = ["receive"] } 11 | symphonia = { workspace = true } 12 | tokio = { workspace = true } 13 | tracing = { workspace = true } 14 | tracing-subscriber = { workspace = true } 15 | tracing-futures = { workspace = true } 16 | -------------------------------------------------------------------------------- /examples/twilight/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "twilight" 3 | version = "0.1.0" 4 | authors = ["Twilight and Serenity Contributors"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | futures = "0.3" 9 | reqwest = { workspace = true } 10 | # In an actual twilight project, use the "tws" feature as below. 11 | # Tungstenite is used here due to workspace feature unification. 12 | # songbird = { workspace = true, features = ["driver", "gateway", "twilight", "rustls", "tws"] } 13 | songbird = { workspace = true, features = ["driver", "gateway", "twilight", "rustls", "tungstenite"] } 14 | symphonia = { workspace = true } 15 | tracing = { workspace = true } 16 | tracing-subscriber = { workspace = true, default-features = true } 17 | tokio = { workspace = true } 18 | twilight-gateway = "0.16.0" 19 | twilight-http = "0.16.0" 20 | twilight-model = "0.16.0" 21 | twilight-standby = "0.16.0" 22 | -------------------------------------------------------------------------------- /images/arch.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/images/arch.afdesign -------------------------------------------------------------------------------- /images/driver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/images/driver.png -------------------------------------------------------------------------------- /images/gateway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/images/gateway.png -------------------------------------------------------------------------------- /images/scheduler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/images/scheduler.png -------------------------------------------------------------------------------- /resources/Cloudkicker - 2011 07.dca1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/Cloudkicker - 2011 07.dca1 -------------------------------------------------------------------------------- /resources/Cloudkicker - 2011 07.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/Cloudkicker - 2011 07.mp3 -------------------------------------------------------------------------------- /resources/Cloudkicker - Making Will Mad.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/Cloudkicker - Making Will Mad.opus -------------------------------------------------------------------------------- /resources/Cloudkicker - Making Will Mad.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/Cloudkicker - Making Will Mad.webm -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | This folder contains various audio files used for testing or within examples. 3 | 4 | Some songs are used under creative commons licenses ([Ben Sharp/Cloudkicker](https://cloudkicker.bandcamp.com/)): 5 | * `Cloudkicker - 2011 07.[mp3,dca1]` – [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/) 6 | * `Cloudkicker - Making Will Mad.[opus,webm]` – [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) 7 | 8 | All sound files are made by contributors. 9 | -------------------------------------------------------------------------------- /resources/loop-48.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/loop-48.mp3 -------------------------------------------------------------------------------- /resources/loop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/loop.wav -------------------------------------------------------------------------------- /resources/ting-vid.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/ting-vid.mp4 -------------------------------------------------------------------------------- /resources/ting.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/ting.mp3 -------------------------------------------------------------------------------- /resources/ting.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/resources/ting.wav -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_layout = "HorizontalVertical" 2 | match_arm_blocks = false 3 | match_block_trailing_comma = true 4 | newline_style = "Unix" 5 | use_field_init_shorthand = true 6 | use_try_shorthand = true 7 | -------------------------------------------------------------------------------- /songbird-ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/songbird-ico.png -------------------------------------------------------------------------------- /songbird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-rs/songbird/c910d7087dc86094677ec3e67a068d9eefe239df/songbird.png -------------------------------------------------------------------------------- /src/driver/bench_internals.rs: -------------------------------------------------------------------------------- 1 | //! Various driver internals which need to be exported for benchmarking. 2 | //! 3 | //! Included if using the `"internals"` feature flag. 4 | //! You should not and/or cannot use these as part of a normal application. 5 | 6 | #![allow(missing_docs)] 7 | 8 | pub use super::tasks::{message as task_message, mixer}; 9 | 10 | pub use super::crypto::{Cipher, CryptoState}; 11 | 12 | use crate::{ 13 | driver::tasks::message::TrackContext, 14 | tracks::{Track, TrackHandle}, 15 | }; 16 | 17 | #[must_use] 18 | pub fn track_context(t: Track) -> (TrackHandle, TrackContext) { 19 | t.into_context() 20 | } 21 | 22 | pub mod scheduler { 23 | pub use crate::driver::scheduler::*; 24 | } 25 | -------------------------------------------------------------------------------- /src/driver/connection/error.rs: -------------------------------------------------------------------------------- 1 | //! Connection errors and convenience types. 2 | 3 | use crate::{ 4 | driver::tasks::{error::Recipient, message::*}, 5 | ws::Error as WsError, 6 | }; 7 | use aes_gcm::Error as CryptoError; 8 | use flume::SendError; 9 | use serde_json::Error as JsonError; 10 | use std::{error::Error as StdError, fmt, io::Error as IoError}; 11 | use tokio::time::error::Elapsed; 12 | 13 | /// Errors encountered while connecting to a Discord voice server over the driver. 14 | #[derive(Debug)] 15 | #[non_exhaustive] 16 | pub enum Error { 17 | /// The driver hung up an internal signaller, either due to another connection attempt 18 | /// or a crash. 19 | AttemptDiscarded, 20 | /// An error occurred during [en/de]cryption of voice packets. 21 | Crypto(CryptoError), 22 | /// The symmetric key supplied by Discord had the wrong size. 23 | CryptoInvalidLength, 24 | /// Server did not return the expected crypto mode during negotiation. 25 | CryptoModeInvalid, 26 | /// Selected crypto mode was not offered by server. 27 | CryptoModeUnavailable, 28 | /// An indicator that an endpoint URL was invalid. 29 | EndpointUrl, 30 | /// Discord failed to correctly respond to IP discovery. 31 | IllegalDiscoveryResponse, 32 | /// Could not parse Discord's view of our IP. 33 | IllegalIp, 34 | /// Miscellaneous I/O error. 35 | Io(IoError), 36 | /// JSON (de)serialization error. 37 | Json(JsonError), 38 | /// Failed to message other background tasks after connection establishment. 39 | InterconnectFailure(Recipient), 40 | /// Error communicating with gateway server over WebSocket. 41 | Ws(WsError), 42 | /// Connection attempt timed out. 43 | TimedOut, 44 | } 45 | 46 | impl From for Error { 47 | fn from(e: CryptoError) -> Self { 48 | Error::Crypto(e) 49 | } 50 | } 51 | 52 | impl From for Error { 53 | fn from(e: IoError) -> Error { 54 | Error::Io(e) 55 | } 56 | } 57 | 58 | impl From for Error { 59 | fn from(e: JsonError) -> Error { 60 | Error::Json(e) 61 | } 62 | } 63 | 64 | impl From> for Error { 65 | fn from(_e: SendError) -> Error { 66 | Error::InterconnectFailure(Recipient::AuxNetwork) 67 | } 68 | } 69 | 70 | impl From> for Error { 71 | fn from(_e: SendError) -> Error { 72 | Error::InterconnectFailure(Recipient::Event) 73 | } 74 | } 75 | 76 | impl From> for Error { 77 | fn from(_e: SendError) -> Error { 78 | Error::InterconnectFailure(Recipient::Mixer) 79 | } 80 | } 81 | 82 | impl From for Error { 83 | fn from(e: WsError) -> Error { 84 | Error::Ws(e) 85 | } 86 | } 87 | 88 | impl From for Error { 89 | fn from(_e: Elapsed) -> Error { 90 | Error::TimedOut 91 | } 92 | } 93 | 94 | impl fmt::Display for Error { 95 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 96 | write!(f, "failed to connect to Discord RTP server: ")?; 97 | match self { 98 | Self::AttemptDiscarded => write!(f, "connection attempt was aborted/discarded"), 99 | Self::Crypto(e) => e.fmt(f), 100 | Self::CryptoInvalidLength => write!(f, "server supplied key of wrong length"), 101 | Self::CryptoModeInvalid => write!(f, "server changed negotiated encryption mode"), 102 | Self::CryptoModeUnavailable => write!(f, "server did not offer chosen encryption mode"), 103 | Self::EndpointUrl => write!(f, "endpoint URL received from gateway was invalid"), 104 | Self::IllegalDiscoveryResponse => { 105 | write!(f, "IP discovery/NAT punching response was invalid") 106 | }, 107 | Self::IllegalIp => write!(f, "IP discovery/NAT punching response had bad IP value"), 108 | Self::Io(e) => e.fmt(f), 109 | Self::Json(e) => e.fmt(f), 110 | Self::InterconnectFailure(e) => write!(f, "failed to contact other task ({e:?})"), 111 | Self::Ws(e) => write!(f, "websocket issue ({e:?})."), 112 | Self::TimedOut => write!(f, "connection attempt timed out"), 113 | } 114 | } 115 | } 116 | 117 | impl StdError for Error { 118 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 119 | match self { 120 | Error::AttemptDiscarded 121 | | Error::Crypto(_) 122 | | Error::CryptoInvalidLength 123 | | Error::CryptoModeInvalid 124 | | Error::CryptoModeUnavailable 125 | | Error::EndpointUrl 126 | | Error::IllegalDiscoveryResponse 127 | | Error::IllegalIp 128 | | Error::InterconnectFailure(_) 129 | | Error::Ws(_) 130 | | Error::TimedOut => None, 131 | Error::Io(e) => e.source(), 132 | Error::Json(e) => e.source(), 133 | } 134 | } 135 | } 136 | 137 | /// Convenience type for Discord voice/driver connection error handling. 138 | pub type Result = std::result::Result; 139 | -------------------------------------------------------------------------------- /src/driver/decode_mode.rs: -------------------------------------------------------------------------------- 1 | use audiopus::{Channels as OpusChannels, SampleRate as OpusRate}; 2 | 3 | /// Decode behaviour for received RTP packets within the driver. 4 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 5 | #[non_exhaustive] 6 | pub enum DecodeMode { 7 | /// Packets received from Discord are handed over to events without any 8 | /// changes applied. 9 | /// 10 | /// No CPU work involved. 11 | Pass, 12 | /// Decrypts the body of each received packet. 13 | /// 14 | /// Small per-packet CPU use. 15 | Decrypt, 16 | /// Decrypts and decodes each received packet, correctly accounting for losses. 17 | /// 18 | /// Larger per-packet CPU use. 19 | Decode, 20 | } 21 | 22 | impl DecodeMode { 23 | /// Returns whether this mode will decrypt received packets. 24 | #[must_use] 25 | pub fn should_decrypt(self) -> bool { 26 | self != DecodeMode::Pass 27 | } 28 | } 29 | 30 | /// The channel layout of output audio when using [`DecodeMode::Decode`]. 31 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] 32 | #[non_exhaustive] 33 | pub enum Channels { 34 | /// Decode received audio packets into a single channel. 35 | Mono, 36 | /// Decode received audio packets into two interleaved channels. 37 | /// 38 | /// Received mono packets' samples will automatically be duplicated across 39 | /// both channels. 40 | /// 41 | /// The default choice. 42 | #[default] 43 | Stereo, 44 | } 45 | 46 | impl Channels { 47 | pub(crate) fn channels(self) -> usize { 48 | match self { 49 | Channels::Mono => 1, 50 | Channels::Stereo => 2, 51 | } 52 | } 53 | } 54 | 55 | impl From for OpusChannels { 56 | fn from(value: Channels) -> Self { 57 | match value { 58 | Channels::Mono => OpusChannels::Mono, 59 | Channels::Stereo => OpusChannels::Stereo, 60 | } 61 | } 62 | } 63 | 64 | /// The sample rate of output audio when using [`DecodeMode::Decode`]. 65 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] 66 | #[non_exhaustive] 67 | pub enum SampleRate { 68 | /// Decode to a sample rate of 8kHz. 69 | Hz8000, 70 | /// Decode to a sample rate of 12kHz. 71 | Hz12000, 72 | /// Decode to a sample rate of 16kHz. 73 | Hz16000, 74 | /// Decode to a sample rate of 24kHz. 75 | Hz24000, 76 | /// Decode to a sample rate of 48kHz. 77 | /// 78 | /// The preferred option for encoding/decoding at or above CD quality. 79 | #[default] 80 | Hz48000, 81 | } 82 | 83 | impl From for OpusRate { 84 | fn from(value: SampleRate) -> Self { 85 | match value { 86 | SampleRate::Hz8000 => OpusRate::Hz8000, 87 | SampleRate::Hz12000 => OpusRate::Hz12000, 88 | SampleRate::Hz16000 => OpusRate::Hz16000, 89 | SampleRate::Hz24000 => OpusRate::Hz24000, 90 | SampleRate::Hz48000 => OpusRate::Hz48000, 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/driver/mix_mode.rs: -------------------------------------------------------------------------------- 1 | use audiopus::Channels; 2 | use symphonia_core::audio::Layout; 3 | 4 | use crate::constants::{MONO_FRAME_SIZE, STEREO_FRAME_SIZE}; 5 | 6 | /// Mixing behaviour for sent audio sources processed within the driver. 7 | /// 8 | /// This has no impact on Opus packet passthrough, which will pass packets 9 | /// irrespective of their channel count. 10 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 11 | pub enum MixMode { 12 | /// Audio sources will be downmixed into a mono buffer. 13 | Mono, 14 | /// Audio sources will be mixed into into a stereo buffer, where mono sources 15 | /// will be duplicated into both channels. 16 | Stereo, 17 | } 18 | 19 | impl MixMode { 20 | pub(crate) const fn to_opus(self) -> Channels { 21 | match self { 22 | Self::Mono => Channels::Mono, 23 | Self::Stereo => Channels::Stereo, 24 | } 25 | } 26 | 27 | pub(crate) const fn sample_count_in_frame(self) -> usize { 28 | match self { 29 | Self::Mono => MONO_FRAME_SIZE, 30 | Self::Stereo => STEREO_FRAME_SIZE, 31 | } 32 | } 33 | 34 | pub(crate) const fn channels(self) -> usize { 35 | match self { 36 | Self::Mono => 1, 37 | Self::Stereo => 2, 38 | } 39 | } 40 | 41 | pub(crate) const fn symph_layout(self) -> Layout { 42 | match self { 43 | Self::Mono => Layout::Mono, 44 | Self::Stereo => Layout::Stereo, 45 | } 46 | } 47 | } 48 | 49 | impl From for Layout { 50 | fn from(val: MixMode) -> Self { 51 | val.symph_layout() 52 | } 53 | } 54 | 55 | impl From for Channels { 56 | fn from(val: MixMode) -> Self { 57 | val.to_opus() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/driver/retry/mod.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for connection retries. 2 | 3 | mod strategy; 4 | 5 | pub use self::strategy::*; 6 | 7 | use std::time::Duration; 8 | 9 | /// Configuration to be used for retrying driver connection attempts. 10 | #[derive(Clone, Copy, Debug, PartialEq)] 11 | pub struct Retry { 12 | /// Strategy used to determine how long to wait between retry attempts. 13 | /// 14 | /// *Defaults to an [`ExponentialBackoff`] from 0.25s 15 | /// to 10s, with a jitter of `0.1`.* 16 | /// 17 | /// [`ExponentialBackoff`]: Strategy::Backoff 18 | pub strategy: Strategy, 19 | /// The maximum number of retries to attempt. 20 | /// 21 | /// `None` will attempt an infinite number of retries, 22 | /// while `Some(0)` will attempt to connect *once* (no retries). 23 | /// 24 | /// *Defaults to `Some(5)`.* 25 | pub retry_limit: Option, 26 | } 27 | 28 | impl Default for Retry { 29 | fn default() -> Self { 30 | Self { 31 | strategy: Strategy::Backoff(ExponentialBackoff::default()), 32 | retry_limit: Some(5), 33 | } 34 | } 35 | } 36 | 37 | impl Retry { 38 | pub(crate) fn retry_in( 39 | &self, 40 | last_wait: Option, 41 | attempts: usize, 42 | ) -> Option { 43 | if self.retry_limit.map_or(true, |a| attempts < a) { 44 | Some(self.strategy.retry_in(last_wait)) 45 | } else { 46 | None 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/driver/retry/strategy.rs: -------------------------------------------------------------------------------- 1 | use rand::random; 2 | use std::time::Duration; 3 | 4 | /// Logic used to determine how long to wait between retry attempts. 5 | #[derive(Clone, Copy, Debug, PartialEq)] 6 | #[non_exhaustive] 7 | pub enum Strategy { 8 | /// The driver will wait for the same amount of time between each retry. 9 | Every(Duration), 10 | /// Exponential backoff waiting strategy, where the duration between 11 | /// attempts (approximately) doubles each time. 12 | Backoff(ExponentialBackoff), 13 | } 14 | 15 | impl Strategy { 16 | pub(crate) fn retry_in(&self, last_wait: Option) -> Duration { 17 | match self { 18 | Self::Every(t) => *t, 19 | Self::Backoff(exp) => exp.retry_in(last_wait), 20 | } 21 | } 22 | } 23 | 24 | /// Exponential backoff waiting strategy. 25 | /// 26 | /// Each attempt waits for twice the last delay plus/minus a 27 | /// random jitter, clamped to a min and max value. 28 | #[derive(Clone, Copy, Debug, PartialEq)] 29 | pub struct ExponentialBackoff { 30 | /// Minimum amount of time to wait between retries. 31 | /// 32 | /// *Defaults to 0.25s.* 33 | pub min: Duration, 34 | /// Maximum amount of time to wait between retries. 35 | /// 36 | /// This will be clamped to `>=` min. 37 | /// 38 | /// *Defaults to 10s.* 39 | pub max: Duration, 40 | /// Amount of uniform random jitter to apply to generated wait times. 41 | /// I.e., 0.1 will add +/-10% to generated intervals. 42 | /// 43 | /// This is restricted to within +/-100%. 44 | /// 45 | /// *Defaults to `0.1`.* 46 | pub jitter: f32, 47 | } 48 | 49 | impl Default for ExponentialBackoff { 50 | fn default() -> Self { 51 | Self { 52 | min: Duration::from_millis(250), 53 | max: Duration::from_secs(10), 54 | jitter: 0.1, 55 | } 56 | } 57 | } 58 | 59 | impl ExponentialBackoff { 60 | pub(crate) fn retry_in(&self, last_wait: Option) -> Duration { 61 | let attempt = last_wait.map_or(self.min, |t| 2 * t); 62 | let perturb = (1.0 - (self.jitter * 2.0 * (random::() - 1.0))).clamp(0.0, 2.0); 63 | let mut target_time = attempt.mul_f32(perturb); 64 | 65 | // Now clamp target time into given range. 66 | let safe_max = if self.max < self.min { 67 | self.min 68 | } else { 69 | self.max 70 | }; 71 | 72 | target_time = target_time.clamp(self.min, safe_max); 73 | 74 | target_time 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/driver/scheduler/config.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Configuration for how a [`Scheduler`] handles tasks. 4 | /// 5 | /// [`Scheduler`]: super::Scheduler 6 | #[derive(Clone, Debug)] 7 | #[non_exhaustive] 8 | pub struct Config { 9 | /// How Live mixer tasks will be mapped to individual threads. 10 | /// 11 | /// Defaults to `Mode::MaxPerThread(16)`. 12 | pub strategy: Mode, 13 | /// Move costly mixers to another thread if their parent worker is at 14 | /// risk of missing its deadlines. 15 | /// 16 | /// Defaults to `true`. 17 | pub move_expensive_tasks: bool, 18 | } 19 | 20 | impl Default for Config { 21 | fn default() -> Self { 22 | Self { 23 | strategy: Mode::default(), 24 | move_expensive_tasks: true, 25 | } 26 | } 27 | } 28 | 29 | /// Strategies for mapping live mixer tasks to individual threads. 30 | /// 31 | /// Defaults to `MaxPerThread(16)`. 32 | #[derive(Clone, Debug)] 33 | #[non_exhaustive] 34 | pub enum Mode { 35 | /// Allows at most `n` tasks to run per thread. 36 | MaxPerThread(NonZeroUsize), 37 | } 38 | 39 | impl Mode { 40 | /// Returns the number of `Mixer`s that a scheduler should preallocate 41 | /// resources for. 42 | pub(crate) fn prealloc_size(&self) -> usize { 43 | match self { 44 | Self::MaxPerThread(n) => n.get(), 45 | } 46 | } 47 | 48 | /// Returns the maximum number of concurrent mixers that a scheduler is 49 | /// allowed to place on a single thread. 50 | /// 51 | /// Future scheduling modes may choose to limit *only* on execution cost. 52 | #[allow(clippy::unnecessary_wraps)] 53 | pub(crate) fn task_limit(&self) -> Option { 54 | match self { 55 | Self::MaxPerThread(n) => Some(n.get()), 56 | } 57 | } 58 | } 59 | 60 | impl Default for Mode { 61 | fn default() -> Self { 62 | Self::MaxPerThread(DEFAULT_MIXERS_PER_THREAD) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/driver/scheduler/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error as StdError, 3 | fmt::Display, 4 | num::NonZeroUsize, 5 | sync::{Arc, OnceLock}, 6 | }; 7 | 8 | use flume::{Receiver, RecvError, Sender}; 9 | 10 | use super::tasks::message::{Interconnect, MixerMessage}; 11 | use crate::{constants::TIMESTEP_LENGTH, Config as DriverConfig}; 12 | 13 | mod config; 14 | mod idle; 15 | mod live; 16 | mod stats; 17 | mod task; 18 | 19 | pub use config::*; 20 | use idle::*; 21 | pub use live::*; 22 | pub use stats::*; 23 | pub use task::*; 24 | 25 | /// A soft maximum of 90% of the 20ms budget to account for variance in execution time. 26 | const RESCHEDULE_THRESHOLD: u64 = ((TIMESTEP_LENGTH.subsec_nanos() as u64) * 9) / 10; 27 | 28 | const DEFAULT_MIXERS_PER_THREAD: NonZeroUsize = match NonZeroUsize::new(16) { 29 | Some(v) => v, 30 | None => unreachable!(), 31 | }; 32 | 33 | /// The default shared scheduler instance. 34 | /// 35 | /// This is built using the default value of [`ScheduleMode`]. Users desiring 36 | /// a custom strategy should avoid calling [`Config::default`]. 37 | /// 38 | /// [`Config::default`]: crate::Config::default 39 | /// [`ScheduleMode`]: Mode 40 | pub fn get_default_scheduler() -> &'static Scheduler { 41 | static DEFAULT_SCHEDULER: OnceLock = OnceLock::new(); 42 | DEFAULT_SCHEDULER.get_or_init(Scheduler::default) 43 | } 44 | 45 | /// A reference to a shared group of threads used for running idle and active 46 | /// audio threads. 47 | #[derive(Clone, Debug)] 48 | pub struct Scheduler { 49 | inner: Arc, 50 | } 51 | 52 | /// Inner contents of a [`Scheduler`] instance. 53 | /// 54 | /// This is an `Arc` around `Arc`'d contents so that we can make use of the 55 | /// drop check on `Scheduler` to clean up resources. 56 | #[derive(Clone, Debug)] 57 | struct InnerScheduler { 58 | tx: Sender, 59 | stats: Arc, 60 | } 61 | 62 | impl Scheduler { 63 | /// Create a new mixer scheduler from the allocation strategy in `config`. 64 | #[must_use] 65 | pub fn new(config: Config) -> Self { 66 | let (core, tx) = Idle::new(config); 67 | 68 | let stats = core.stats.clone(); 69 | core.spawn(); 70 | 71 | let inner = Arc::new(InnerScheduler { tx, stats }); 72 | 73 | Self { inner } 74 | } 75 | 76 | pub(crate) fn new_mixer( 77 | &self, 78 | config: &DriverConfig, 79 | ic: Interconnect, 80 | rx: Receiver, 81 | ) { 82 | self.inner 83 | .tx 84 | .send(SchedulerMessage::NewMixer(rx, ic, config.clone())) 85 | .unwrap(); 86 | } 87 | 88 | /// Returns the total number of calls (idle and active) scheduled. 89 | #[must_use] 90 | pub fn total_tasks(&self) -> u64 { 91 | self.inner.stats.total_mixers() 92 | } 93 | 94 | /// Returns the total number of *active* calls scheduled and processing 95 | /// audio. 96 | #[must_use] 97 | pub fn live_tasks(&self) -> u64 { 98 | self.inner.stats.live_mixers() 99 | } 100 | 101 | /// Returns the total number of threads spawned to process live audio sessions. 102 | #[must_use] 103 | pub fn worker_threads(&self) -> u64 { 104 | self.inner.stats.worker_threads() 105 | } 106 | 107 | /// Request a list of handles to statistics for currently live workers. 108 | pub async fn worker_thread_stats(&self) -> Result>, Error> { 109 | let (tx, rx) = flume::bounded(1); 110 | _ = self.inner.tx.send(SchedulerMessage::GetStats(tx)); 111 | 112 | rx.recv_async().await.map_err(Error::from) 113 | } 114 | 115 | /// Request a list of handles to statistics for currently live workers with a blocking call. 116 | pub fn worker_thread_stats_blocking(&self) -> Result>, Error> { 117 | let (tx, rx) = flume::bounded(1); 118 | _ = self.inner.tx.send(SchedulerMessage::GetStats(tx)); 119 | 120 | rx.recv().map_err(Error::from) 121 | } 122 | } 123 | 124 | impl Drop for InnerScheduler { 125 | fn drop(&mut self) { 126 | _ = self.tx.send(SchedulerMessage::Kill); 127 | } 128 | } 129 | 130 | impl Default for Scheduler { 131 | fn default() -> Self { 132 | Scheduler::new(Config::default()) 133 | } 134 | } 135 | 136 | /// Control messages for a scheduler. 137 | pub enum SchedulerMessage { 138 | /// Build a new `Mixer` as part of the initialisation of a `Driver`. 139 | NewMixer(Receiver, Interconnect, DriverConfig), 140 | /// Forward a command for 141 | Do(TaskId, MixerMessage), 142 | /// Return a `Mixer` from a worker back to the idle pool. 143 | Demote(TaskId, ParkedMixer), 144 | /// Move an expensive `Mixer` to another thread in the worker pool. 145 | Overspill(WorkerId, TaskId, ParkedMixer), 146 | /// Request a copy of all per-worker statistics. 147 | GetStats(Sender>>), 148 | /// Cleanup once all `Scheduler` handles are dropped. 149 | Kill, 150 | } 151 | 152 | /// Errors encountered when communicating with the internals of a [`Scheduler`]. 153 | /// 154 | /// [`Scheduler`]: crate::driver::Scheduler 155 | #[non_exhaustive] 156 | #[derive(Debug)] 157 | pub enum Error { 158 | /// The scheduler exited or crashed while awating the request. 159 | Disconnected, 160 | } 161 | 162 | impl Display for Error { 163 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 164 | match self { 165 | Self::Disconnected => f.write_str("the scheduler terminated mid-request"), 166 | } 167 | } 168 | } 169 | 170 | impl StdError for Error { 171 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 172 | None 173 | } 174 | } 175 | 176 | impl From for Error { 177 | fn from(_: RecvError) -> Self { 178 | Self::Disconnected 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/driver/scheduler/stats.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::atomic::{AtomicU64, Ordering}, 3 | time::Duration, 4 | }; 5 | 6 | use super::{Mode, ParkedMixer, RESCHEDULE_THRESHOLD}; 7 | 8 | /// Statistics shared by an entire `Scheduler`. 9 | #[derive(Debug, Default)] 10 | pub struct StatBlock { 11 | total: AtomicU64, 12 | live: AtomicU64, 13 | threads: AtomicU64, 14 | } 15 | 16 | #[allow(missing_docs)] 17 | impl StatBlock { 18 | #[inline] 19 | pub fn total_mixers(&self) -> u64 { 20 | self.total.load(Ordering::Relaxed) 21 | } 22 | 23 | #[inline] 24 | pub fn live_mixers(&self) -> u64 { 25 | self.live.load(Ordering::Relaxed) 26 | } 27 | 28 | #[inline] 29 | pub fn worker_threads(&self) -> u64 { 30 | self.threads.load(Ordering::Relaxed) 31 | } 32 | 33 | #[inline] 34 | pub fn add_idle_mixer(&self) { 35 | self.total.fetch_add(1, Ordering::Relaxed); 36 | } 37 | 38 | #[inline] 39 | pub fn remove_idle_mixers(&self, n: u64) { 40 | self.total.fetch_sub(n, Ordering::Relaxed); 41 | } 42 | 43 | #[inline] 44 | pub fn move_mixer_to_live(&self) { 45 | self.live.fetch_add(1, Ordering::Relaxed); 46 | } 47 | 48 | #[inline] 49 | pub fn move_mixer_to_idle(&self) { 50 | self.move_mixers_to_idle(1); 51 | } 52 | 53 | #[inline] 54 | pub fn move_mixers_to_idle(&self, n: u64) { 55 | self.live.fetch_sub(n, Ordering::Relaxed); 56 | } 57 | 58 | #[inline] 59 | pub fn remove_live_mixer(&self) { 60 | self.remove_live_mixers(1); 61 | } 62 | 63 | #[inline] 64 | pub fn remove_live_mixers(&self, n: u64) { 65 | self.move_mixers_to_idle(n); 66 | self.remove_idle_mixers(n); 67 | } 68 | 69 | #[inline] 70 | pub fn add_worker(&self) { 71 | self.threads.fetch_add(1, Ordering::Relaxed); 72 | } 73 | 74 | #[inline] 75 | pub fn remove_worker(&self) { 76 | self.threads.fetch_sub(1, Ordering::Relaxed); 77 | } 78 | } 79 | 80 | /// Runtime statistics for an individual worker. 81 | /// 82 | /// Individual statistics are measured atomically -- the worker thread 83 | /// may have been cleaned up, or its mixer count may not match the 84 | /// count when [`Self::last_compute_cost_ns`] was set. 85 | #[derive(Debug, Default)] 86 | pub struct LiveStatBlock { 87 | live: AtomicU64, 88 | last_ns: AtomicU64, 89 | } 90 | 91 | impl LiveStatBlock { 92 | /// Returns the number of mixer tasks scheduled on this worker thread. 93 | #[inline] 94 | pub fn live_mixers(&self) -> u64 { 95 | self.live.load(Ordering::Relaxed) 96 | } 97 | 98 | #[inline] 99 | pub(crate) fn add_mixer(&self) { 100 | self.live.fetch_add(1, Ordering::Relaxed); 101 | } 102 | 103 | #[inline] 104 | pub(crate) fn remove_mixer(&self) { 105 | self.live.fetch_sub(1, Ordering::Relaxed); 106 | } 107 | 108 | #[inline] 109 | pub(crate) fn store_compute_cost(&self, work: Duration) -> u64 { 110 | let cost = work.as_nanos() as u64; 111 | self.last_ns.store(cost, Ordering::Relaxed); 112 | cost 113 | } 114 | 115 | /// Returns the number of nanoseconds required to process all worker threads' 116 | /// packet transmission, mixing, encoding, and encryption in the last tick. 117 | #[inline] 118 | pub fn last_compute_cost_ns(&self) -> u64 { 119 | self.last_ns.load(Ordering::Relaxed) 120 | } 121 | 122 | #[inline] 123 | pub(crate) fn has_room(&self, strategy: &Mode, task: &ParkedMixer) -> bool { 124 | let task_room = strategy 125 | .task_limit() 126 | .map_or(true, |limit| self.live_mixers() < limit as u64); 127 | 128 | let exec_room = task.last_cost.map_or(true, |cost| { 129 | cost.as_nanos() as u64 + self.last_compute_cost_ns() < RESCHEDULE_THRESHOLD 130 | }); 131 | 132 | task_room && exec_room 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/driver/tasks/disposal.rs: -------------------------------------------------------------------------------- 1 | use super::message::*; 2 | use flume::{Receiver, Sender}; 3 | use tracing::{instrument, trace}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct DisposalThread(Sender); 7 | 8 | impl Default for DisposalThread { 9 | fn default() -> Self { 10 | Self::run() 11 | } 12 | } 13 | 14 | impl DisposalThread { 15 | #[must_use] 16 | pub fn run() -> Self { 17 | let (mix_tx, mix_rx) = flume::unbounded(); 18 | std::thread::spawn(move || { 19 | trace!("Disposal thread started."); 20 | runner(mix_rx); 21 | trace!("Disposal thread finished."); 22 | }); 23 | 24 | Self(mix_tx) 25 | } 26 | 27 | pub(super) fn dispose(&self, message: DisposalMessage) { 28 | drop(self.0.send(message)); 29 | } 30 | } 31 | 32 | /// The mixer's disposal thread is also synchronous, due to tracks, 33 | /// inputs, etc. being based on synchronous I/O. 34 | /// 35 | /// The mixer uses this to offload heavy and expensive drop operations 36 | /// to prevent deadline misses. 37 | #[instrument(skip(mix_rx))] 38 | fn runner(mix_rx: Receiver) { 39 | while mix_rx.recv().is_ok() {} 40 | } 41 | -------------------------------------------------------------------------------- /src/driver/tasks/error.rs: -------------------------------------------------------------------------------- 1 | use super::message::*; 2 | use crate::ws::Error as WsError; 3 | use aes_gcm::Error as CryptoError; 4 | use audiopus::Error as OpusError; 5 | use flume::SendError; 6 | use std::io::{Error as IoError, ErrorKind as IoErrorKind}; 7 | 8 | #[derive(Debug)] 9 | pub enum Recipient { 10 | AuxNetwork, 11 | Event, 12 | Mixer, 13 | #[cfg(feature = "receive")] 14 | UdpRx, 15 | } 16 | 17 | pub type Result = std::result::Result; 18 | 19 | #[derive(Debug)] 20 | #[non_exhaustive] 21 | pub enum Error { 22 | Crypto(CryptoError), 23 | #[cfg(any(feature = "receive", test))] 24 | /// Received an illegal voice packet on the voice UDP socket. 25 | IllegalVoicePacket, 26 | InterconnectFailure(Recipient), 27 | Io(IoError), 28 | Other, 29 | } 30 | 31 | impl Error { 32 | pub(crate) fn should_trigger_connect(&self) -> bool { 33 | match self { 34 | Error::InterconnectFailure(Recipient::AuxNetwork) => true, 35 | #[cfg(feature = "receive")] 36 | Error::InterconnectFailure(Recipient::UdpRx) => true, 37 | _ => false, 38 | } 39 | } 40 | 41 | pub(crate) fn should_trigger_interconnect_rebuild(&self) -> bool { 42 | matches!(self, Error::InterconnectFailure(Recipient::Event)) 43 | } 44 | 45 | // This prevents a `WouldBlock` from triggering a full reconnect, 46 | // instead simply dropping the packet. 47 | pub(crate) fn disarm_would_block(self) -> Result<()> { 48 | match self { 49 | Self::Io(i) if i.kind() == IoErrorKind::WouldBlock => Ok(()), 50 | e => Err(e), 51 | } 52 | } 53 | } 54 | 55 | impl From for Error { 56 | fn from(e: CryptoError) -> Self { 57 | Error::Crypto(e) 58 | } 59 | } 60 | 61 | impl From for Error { 62 | fn from(e: IoError) -> Error { 63 | Error::Io(e) 64 | } 65 | } 66 | 67 | impl From for Error { 68 | fn from(_: OpusError) -> Error { 69 | Error::Other 70 | } 71 | } 72 | 73 | impl From> for Error { 74 | fn from(_e: SendError) -> Error { 75 | Error::InterconnectFailure(Recipient::AuxNetwork) 76 | } 77 | } 78 | 79 | impl From> for Error { 80 | fn from(_e: SendError) -> Error { 81 | Error::InterconnectFailure(Recipient::Event) 82 | } 83 | } 84 | 85 | impl From> for Error { 86 | fn from(_e: SendError) -> Error { 87 | Error::InterconnectFailure(Recipient::Mixer) 88 | } 89 | } 90 | 91 | #[cfg(feature = "receive")] 92 | impl From> for Error { 93 | fn from(_e: SendError) -> Error { 94 | Error::InterconnectFailure(Recipient::UdpRx) 95 | } 96 | } 97 | 98 | impl From for Error { 99 | fn from(_: WsError) -> Error { 100 | Error::Other 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/driver/tasks/message/core.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use crate::{ 4 | driver::{connection::error::Error, Bitrate, Config}, 5 | events::{context_data::DisconnectReason, EventData}, 6 | tracks::{Track, TrackCommand, TrackHandle}, 7 | ConnectionInfo, 8 | }; 9 | use flume::{Receiver, Sender}; 10 | 11 | pub enum CoreMessage { 12 | ConnectWithResult(ConnectionInfo, Sender>), 13 | RetryConnect(usize), 14 | SignalWsClosure(usize, ConnectionInfo, Option), 15 | Disconnect, 16 | SetTrack(Option), 17 | AddTrack(TrackContext), 18 | SetBitrate(Bitrate), 19 | AddEvent(EventData), 20 | RemoveGlobalEvents, 21 | SetConfig(Config), 22 | Mute(bool), 23 | Reconnect, 24 | FullReconnect, 25 | RebuildInterconnect, 26 | Poison, 27 | } 28 | 29 | pub struct TrackContext { 30 | pub track: Track, 31 | pub handle: TrackHandle, 32 | pub receiver: Receiver, 33 | } 34 | -------------------------------------------------------------------------------- /src/driver/tasks/message/disposal.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use crate::{driver::tasks::mixer::InternalTrack, tracks::TrackHandle}; 4 | 5 | #[allow(dead_code)] // We don't read because all we are doing is dropping. 6 | pub enum DisposalMessage { 7 | Track(Box), 8 | Handle(TrackHandle), 9 | } 10 | -------------------------------------------------------------------------------- /src/driver/tasks/message/events.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use crate::{ 4 | events::{CoreContext, EventData, EventStore}, 5 | tracks::{LoopState, PlayMode, ReadyState, TrackHandle, TrackState}, 6 | }; 7 | use std::time::Duration; 8 | 9 | pub enum EventMessage { 10 | // Event related. 11 | // Track events should fire off the back of state changes. 12 | AddGlobalEvent(EventData), 13 | AddTrackEvent(usize, EventData), 14 | FireCoreEvent(CoreContext), 15 | RemoveGlobalEvents, 16 | 17 | AddTrack(EventStore, TrackState, TrackHandle), 18 | ChangeState(usize, TrackStateChange), 19 | RemoveAllTracks, 20 | Tick, 21 | 22 | Poison, 23 | } 24 | 25 | #[derive(Debug)] 26 | pub enum TrackStateChange { 27 | Mode(PlayMode), 28 | Volume(f32), 29 | Position(Duration), 30 | // Bool indicates user-set. 31 | Loops(LoopState, bool), 32 | Total(TrackState), 33 | Ready(ReadyState), 34 | } 35 | -------------------------------------------------------------------------------- /src/driver/tasks/message/mixer.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | #[cfg(feature = "receive")] 4 | use super::UdpRxMessage; 5 | use super::{Interconnect, TrackContext, WsMessage}; 6 | 7 | use crate::{ 8 | driver::{crypto::Cipher, Bitrate, Config, CryptoState}, 9 | input::{AudioStreamError, Compose, Parsed}, 10 | }; 11 | use flume::Sender; 12 | use std::{net::UdpSocket, sync::Arc}; 13 | use symphonia_core::{errors::Error as SymphoniaError, formats::SeekedTo}; 14 | 15 | pub struct MixerConnection { 16 | pub cipher: Cipher, 17 | pub crypto_state: CryptoState, 18 | #[cfg(feature = "receive")] 19 | pub udp_rx: Sender, 20 | pub udp_tx: UdpSocket, 21 | } 22 | 23 | pub enum MixerMessage { 24 | AddTrack(TrackContext), 25 | SetTrack(Option), 26 | 27 | SetBitrate(Bitrate), 28 | SetConfig(Config), 29 | SetMute(bool), 30 | 31 | SetConn(MixerConnection, u32), 32 | Ws(Option>), 33 | DropConn, 34 | 35 | ReplaceInterconnect(Interconnect), 36 | RebuildEncoder, 37 | 38 | Poison, 39 | } 40 | 41 | impl MixerMessage { 42 | #[must_use] 43 | pub fn is_mixer_maybe_live(&self) -> bool { 44 | matches!( 45 | self, 46 | Self::AddTrack(_) | Self::SetTrack(Some(_)) | Self::SetConn(..) 47 | ) 48 | } 49 | } 50 | 51 | pub enum MixerInputResultMessage { 52 | CreateErr(Arc), 53 | ParseErr(Arc), 54 | Seek( 55 | Parsed, 56 | Option>, 57 | Result>, 58 | ), 59 | Built(Parsed, Option>), 60 | } 61 | -------------------------------------------------------------------------------- /src/driver/tasks/message/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | mod core; 4 | mod disposal; 5 | mod events; 6 | mod mixer; 7 | #[cfg(feature = "receive")] 8 | mod udp_rx; 9 | mod ws; 10 | 11 | #[cfg(feature = "receive")] 12 | pub use self::udp_rx::*; 13 | pub use self::{core::*, disposal::*, events::*, mixer::*, ws::*}; 14 | 15 | use flume::Sender; 16 | use tokio::spawn; 17 | use tracing::trace; 18 | 19 | #[derive(Clone, Debug)] 20 | pub struct Interconnect { 21 | pub core: Sender, 22 | pub events: Sender, 23 | pub mixer: Sender, 24 | } 25 | 26 | impl Interconnect { 27 | pub fn poison(&self) { 28 | drop(self.events.send(EventMessage::Poison)); 29 | } 30 | 31 | pub fn poison_all(&self) { 32 | drop(self.mixer.send(MixerMessage::Poison)); 33 | self.poison(); 34 | } 35 | 36 | pub fn restart_volatile_internals(&mut self) { 37 | self.poison(); 38 | 39 | let (evt_tx, evt_rx) = flume::unbounded(); 40 | 41 | self.events = evt_tx; 42 | 43 | spawn(async move { 44 | trace!("Event processor restarted."); 45 | super::events::runner(evt_rx).await; 46 | trace!("Event processor finished."); 47 | }); 48 | 49 | // Make mixer aware of new targets... 50 | drop( 51 | self.mixer 52 | .send(MixerMessage::ReplaceInterconnect(self.clone())), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/driver/tasks/message/udp_rx.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use super::Interconnect; 4 | use crate::driver::Config; 5 | use dashmap::{DashMap, DashSet}; 6 | use serenity_voice_model::id::UserId; 7 | 8 | pub enum UdpRxMessage { 9 | SetConfig(Config), 10 | ReplaceInterconnect(Interconnect), 11 | } 12 | 13 | #[derive(Debug, Default)] 14 | pub struct SsrcTracker { 15 | pub disconnected_users: DashSet, 16 | pub user_ssrc_map: DashMap, 17 | } 18 | -------------------------------------------------------------------------------- /src/driver/tasks/message/ws.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use super::Interconnect; 4 | use crate::{model::Event as GatewayEvent, ws::WsStream}; 5 | 6 | pub enum WsMessage { 7 | Ws(Box), 8 | ReplaceInterconnect(Interconnect), 9 | SetKeepalive(f64), 10 | Speaking(bool), 11 | Deliver(GatewayEvent), 12 | } 13 | -------------------------------------------------------------------------------- /src/driver/tasks/mixer/pool.rs: -------------------------------------------------------------------------------- 1 | use super::util::copy_seek_to; 2 | 3 | use crate::{ 4 | driver::tasks::message::MixerInputResultMessage, 5 | input::{AudioStream, AudioStreamError, Compose, Input, LiveInput, Parsed}, 6 | Config, 7 | }; 8 | use flume::Sender; 9 | use rusty_pool::ThreadPool; 10 | use std::{result::Result as StdResult, sync::Arc, time::Duration}; 11 | use symphonia_core::{ 12 | formats::{SeekMode, SeekTo}, 13 | io::MediaSource, 14 | }; 15 | use tokio::runtime::Handle; 16 | 17 | #[derive(Clone)] 18 | pub struct BlockyTaskPool { 19 | pool: ThreadPool, 20 | handle: Handle, 21 | } 22 | 23 | impl BlockyTaskPool { 24 | pub fn new(handle: Handle) -> Self { 25 | Self { 26 | pool: ThreadPool::new(0, 64, Duration::from_secs(5)), 27 | handle, 28 | } 29 | } 30 | 31 | pub fn create( 32 | &self, 33 | callback: Sender, 34 | input: Input, 35 | seek_time: Option, 36 | config: Arc, 37 | ) { 38 | // Moves an Input from Lazy -> Live. 39 | // We either do this on this pool, or move it to the tokio executor as the source requires. 40 | // This takes a seek_time to pass on and execute *after* parsing (i.e., back-seek on 41 | // read-only stream). 42 | match input { 43 | Input::Lazy(mut lazy) => { 44 | let far_pool = self.clone(); 45 | if lazy.should_create_async() { 46 | self.handle.spawn(async move { 47 | let out = lazy.create_async().await; 48 | far_pool.send_to_parse(out, lazy, callback, seek_time, config); 49 | }); 50 | } else { 51 | self.pool.execute(move || { 52 | let out = lazy.create(); 53 | far_pool.send_to_parse(out, lazy, callback, seek_time, config); 54 | }); 55 | } 56 | }, 57 | Input::Live(live, maybe_create) => 58 | self.parse(config, callback, live, maybe_create, seek_time), 59 | } 60 | } 61 | 62 | pub fn send_to_parse( 63 | &self, 64 | create_res: StdResult>, AudioStreamError>, 65 | rec: Box, 66 | callback: Sender, 67 | seek_time: Option, 68 | config: Arc, 69 | ) { 70 | match create_res { 71 | Ok(o) => { 72 | self.parse(config, callback, LiveInput::Raw(o), Some(rec), seek_time); 73 | }, 74 | Err(e) => { 75 | drop(callback.send(MixerInputResultMessage::CreateErr(e.into()))); 76 | }, 77 | } 78 | } 79 | 80 | pub fn parse( 81 | &self, 82 | config: Arc, 83 | callback: Sender, 84 | input: LiveInput, 85 | rec: Option>, 86 | seek_time: Option, 87 | ) { 88 | let pool_clone = self.clone(); 89 | 90 | self.pool.execute(move || { 91 | match input.promote(config.codec_registry, config.format_registry) { 92 | Ok(LiveInput::Parsed(parsed)) => match seek_time { 93 | // If seek time is zero, then wipe it out. 94 | // Some formats (MKV) make SeekTo(0) require a backseek to realign with the 95 | // current page. 96 | Some(seek_time) if !super::util::seek_to_is_zero(&seek_time) => { 97 | pool_clone.seek(callback, parsed, rec, seek_time, false, config); 98 | }, 99 | _ => { 100 | drop(callback.send(MixerInputResultMessage::Built(parsed, rec))); 101 | }, 102 | }, 103 | Ok(_) => unreachable!(), 104 | Err(e) => { 105 | drop(callback.send(MixerInputResultMessage::ParseErr(e.into()))); 106 | }, 107 | } 108 | }); 109 | } 110 | 111 | pub fn seek( 112 | &self, 113 | callback: Sender, 114 | mut input: Parsed, 115 | rec: Option>, 116 | seek_time: SeekTo, 117 | // Not all of symphonia's formats bother to return SeekErrorKind::ForwardOnly. 118 | // So, we need *this* flag. 119 | backseek_needed: bool, 120 | config: Arc, 121 | ) { 122 | let pool_clone = self.clone(); 123 | 124 | self.pool.execute(move || match rec { 125 | Some(rec) if (!input.supports_backseek) && backseek_needed => { 126 | pool_clone.create(callback, Input::Lazy(rec), Some(seek_time), config); 127 | }, 128 | _ => { 129 | let seek_result = input 130 | .format 131 | .seek(SeekMode::Accurate, copy_seek_to(&seek_time)); 132 | input.decoder.reset(); 133 | drop(callback.send(MixerInputResultMessage::Seek( 134 | input, 135 | rec, 136 | seek_result.map_err(Arc::new), 137 | ))); 138 | }, 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/driver/tasks/mixer/result.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | input::AudioStreamError, 3 | tracks::{PlayError, SeekRequest}, 4 | }; 5 | use std::sync::Arc; 6 | use symphonia_core::errors::Error as SymphoniaError; 7 | 8 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 9 | pub enum MixType { 10 | Passthrough(usize), 11 | MixedPcm(usize), 12 | } 13 | 14 | pub enum MixStatus { 15 | Live, 16 | Ended, 17 | Errored(SymphoniaError), 18 | } 19 | 20 | impl From for MixStatus { 21 | fn from(e: SymphoniaError) -> Self { 22 | Self::Errored(e) 23 | } 24 | } 25 | 26 | // The Symph errors are Arc'd here since if they come up, they will always 27 | // be Arc'd anyway via into_user. 28 | #[derive(Clone, Debug)] 29 | pub enum InputReadyingError { 30 | Parsing(Arc), 31 | Creation(Arc), 32 | Seeking(Arc), 33 | Dropped, 34 | Waiting, 35 | NeedsSeek(SeekRequest), 36 | } 37 | 38 | impl InputReadyingError { 39 | pub fn as_user(&self) -> Option { 40 | match self { 41 | Self::Parsing(e) => Some(PlayError::Parse(e.clone())), 42 | Self::Creation(e) => Some(PlayError::Create(e.clone())), 43 | Self::Seeking(e) => Some(PlayError::Seek(e.clone())), 44 | _ => None, 45 | } 46 | } 47 | 48 | pub fn into_seek_request(self) -> Option { 49 | if let Self::NeedsSeek(a) = self { 50 | Some(a) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/driver/tasks/mixer/state.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::OPUS_PASSTHROUGH_STRIKE_LIMIT, 3 | driver::tasks::message::*, 4 | input::{Compose, Input, LiveInput, Metadata, Parsed}, 5 | tracks::{ReadyState, SeekRequest}, 6 | }; 7 | use flume::Receiver; 8 | use rubato::FftFixedOut; 9 | use std::time::Instant; 10 | 11 | pub enum InputState { 12 | NotReady(Input), 13 | Preparing(PreparingInfo), 14 | Ready(Parsed, Option>), 15 | } 16 | 17 | impl InputState { 18 | pub fn metadata(&mut self) -> Option> { 19 | if let Self::Ready(parsed, _) = self { 20 | Some(parsed.into()) 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | #[must_use] 27 | pub fn ready_state(&self) -> ReadyState { 28 | match self { 29 | Self::NotReady(_) => ReadyState::Uninitialised, 30 | Self::Preparing(_) => ReadyState::Preparing, 31 | Self::Ready(_, _) => ReadyState::Playable, 32 | } 33 | } 34 | } 35 | 36 | impl From for InputState { 37 | fn from(val: Input) -> Self { 38 | match val { 39 | a @ Input::Lazy(_) => Self::NotReady(a), 40 | Input::Live(live, rec) => match live { 41 | LiveInput::Parsed(p) => Self::Ready(p, rec), 42 | other => Self::NotReady(Input::Live(other, rec)), 43 | }, 44 | } 45 | } 46 | } 47 | 48 | pub struct PreparingInfo { 49 | #[allow(dead_code)] 50 | /// Time this request was fired. 51 | pub time: Instant, 52 | /// Used to handle seek requests fired while a track was being created (or a seek was in progress). 53 | pub queued_seek: Option, 54 | /// Callback from the thread pool to indicate the result of creating/parsing this track. 55 | pub callback: Receiver, 56 | } 57 | 58 | pub struct DecodeState { 59 | pub inner_pos: usize, 60 | pub resampler: Option<(usize, FftFixedOut, Vec>)>, 61 | pub passthrough: Passthrough, 62 | pub passthrough_violations: u8, 63 | } 64 | 65 | impl DecodeState { 66 | pub fn reset(&mut self) { 67 | self.inner_pos = 0; 68 | self.resampler = None; 69 | } 70 | 71 | pub fn record_and_check_passthrough_strike_final(&mut self, fatal: bool) -> bool { 72 | self.passthrough_violations = self.passthrough_violations.saturating_add(1); 73 | let blocked = fatal || self.passthrough_violations > OPUS_PASSTHROUGH_STRIKE_LIMIT; 74 | if blocked { 75 | self.passthrough = Passthrough::Block; 76 | } 77 | blocked 78 | } 79 | } 80 | 81 | impl Default for DecodeState { 82 | fn default() -> Self { 83 | Self { 84 | inner_pos: 0, 85 | resampler: None, 86 | passthrough: Passthrough::Inactive, 87 | passthrough_violations: 0, 88 | } 89 | } 90 | } 91 | 92 | /// Simple state to manage decoder resets etc. 93 | /// 94 | /// Inactive->Active transitions should trigger a reset. 95 | /// 96 | /// Block should be used if a source contains known-bad packets: 97 | /// it's unlikely that packet sizes will vary, but if they do then 98 | /// we can't passthrough (and every attempt will trigger a codec reset, 99 | /// which probably won't sound too smooth). 100 | #[derive(Clone, Copy, Eq, PartialEq)] 101 | pub enum Passthrough { 102 | Active, 103 | Inactive, 104 | Block, 105 | } 106 | -------------------------------------------------------------------------------- /src/driver/tasks/mixer/util.rs: -------------------------------------------------------------------------------- 1 | use symphonia_core::{formats::SeekTo, units::Time}; 2 | 3 | // SeekTo lacks Copy and Clone... somehow. 4 | pub fn copy_seek_to(pos: &SeekTo) -> SeekTo { 5 | match *pos { 6 | SeekTo::Time { time, track_id } => SeekTo::Time { time, track_id }, 7 | SeekTo::TimeStamp { ts, track_id } => SeekTo::TimeStamp { ts, track_id }, 8 | } 9 | } 10 | 11 | pub fn seek_to_is_zero(pos: &SeekTo) -> bool { 12 | match *pos { 13 | SeekTo::Time { time, .. } => 14 | time == Time { 15 | seconds: 0, 16 | frac: 0.0, 17 | }, 18 | SeekTo::TimeStamp { ts, .. } => ts == 0, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/driver/tasks/udp_rx/decode_sizes.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::STEREO_FRAME_SIZE; 2 | 3 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 4 | pub enum PacketDecodeSize { 5 | /// Minimum frame size on Discord. 6 | TwentyMillis, 7 | /// Hybrid packet, sent by Firefox web client. 8 | /// 9 | /// Likely 20ms frame + 10ms frame. 10 | ThirtyMillis, 11 | /// Next largest frame size. 12 | FortyMillis, 13 | /// Maximum Opus frame size. 14 | SixtyMillis, 15 | /// Maximum Opus packet size: 120ms. 16 | Max, 17 | } 18 | 19 | impl PacketDecodeSize { 20 | pub fn bump_up(self) -> Self { 21 | match self { 22 | Self::TwentyMillis => Self::ThirtyMillis, 23 | Self::ThirtyMillis => Self::FortyMillis, 24 | Self::FortyMillis => Self::SixtyMillis, 25 | Self::SixtyMillis | Self::Max => Self::Max, 26 | } 27 | } 28 | 29 | pub fn can_bump_up(self) -> bool { 30 | self != Self::Max 31 | } 32 | 33 | pub fn len(self) -> usize { 34 | match self { 35 | Self::TwentyMillis => STEREO_FRAME_SIZE, 36 | Self::ThirtyMillis => (STEREO_FRAME_SIZE / 2) * 3, 37 | Self::FortyMillis => 2 * STEREO_FRAME_SIZE, 38 | Self::SixtyMillis => 3 * STEREO_FRAME_SIZE, 39 | Self::Max => 6 * STEREO_FRAME_SIZE, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Driver and gateway error handling. 2 | 3 | #[cfg(feature = "serenity")] 4 | use futures::channel::mpsc::TrySendError; 5 | pub use serde_json::Error as JsonError; 6 | #[cfg(feature = "serenity")] 7 | use serenity::gateway::ShardRunnerMessage; 8 | #[cfg(feature = "gateway")] 9 | use std::{error::Error, fmt}; 10 | #[cfg(feature = "twilight")] 11 | use twilight_gateway::error::ChannelError; 12 | 13 | #[cfg(feature = "gateway")] 14 | #[derive(Debug)] 15 | #[non_exhaustive] 16 | /// Error returned when a manager or call handler is 17 | /// unable to send messages over Discord's gateway. 18 | pub enum JoinError { 19 | /// Request to join was dropped, cancelled, or replaced. 20 | Dropped, 21 | /// No available gateway connection was provided to send 22 | /// voice state update messages. 23 | NoSender, 24 | /// Tried to leave a [`Call`] which was not found. 25 | /// 26 | /// [`Call`]: crate::Call 27 | NoCall, 28 | /// Connection details were not received from Discord in the 29 | /// time given in [the `Call`'s configuration]. 30 | /// 31 | /// This can occur if a message is lost by the Discord client 32 | /// between restarts, or if Discord's gateway believes that 33 | /// this bot is still in the channel it attempts to join. 34 | /// 35 | /// *Users should `leave` the server on the gateway before 36 | /// re-attempting connection.* 37 | /// 38 | /// [the `Call`'s configuration]: crate::Config 39 | TimedOut, 40 | #[cfg(feature = "driver")] 41 | /// The driver failed to establish a voice connection. 42 | /// 43 | /// *Users should `leave` the server on the gateway before 44 | /// re-attempting connection.* 45 | Driver(ConnectionError), 46 | #[cfg(feature = "serenity")] 47 | /// Serenity-specific WebSocket send error. 48 | Serenity(Box>), 49 | #[cfg(feature = "twilight")] 50 | /// Twilight-specific WebSocket send error when a message fails to send over websocket. 51 | Twilight(ChannelError), 52 | } 53 | 54 | #[cfg(feature = "gateway")] 55 | impl JoinError { 56 | /// Indicates whether this failure may have left (or been 57 | /// caused by) Discord's gateway state being in an 58 | /// inconsistent state. 59 | /// 60 | /// Failure to `leave` before rejoining may cause further 61 | /// timeouts. 62 | pub fn should_leave_server(&self) -> bool { 63 | matches!(self, JoinError::TimedOut) 64 | } 65 | 66 | #[cfg(feature = "driver")] 67 | /// Indicates whether this failure can be reattempted via 68 | /// [`Driver::connect`] with retreived connection info. 69 | /// 70 | /// Failure to `leave` before rejoining may cause further 71 | /// timeouts. 72 | /// 73 | /// [`Driver::connect`]: crate::driver::Driver 74 | pub fn should_reconnect_driver(&self) -> bool { 75 | matches!(self, JoinError::Driver(_)) 76 | } 77 | } 78 | 79 | #[cfg(feature = "gateway")] 80 | impl fmt::Display for JoinError { 81 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | write!(f, "failed to join voice channel: ")?; 83 | match self { 84 | JoinError::Dropped => write!(f, "request was cancelled/dropped"), 85 | JoinError::NoSender => write!(f, "no gateway destination"), 86 | JoinError::NoCall => write!(f, "tried to leave a non-existent call"), 87 | JoinError::TimedOut => write!(f, "gateway response from Discord timed out"), 88 | #[cfg(feature = "driver")] 89 | JoinError::Driver(_) => write!(f, "establishing connection failed"), 90 | #[cfg(feature = "serenity")] 91 | JoinError::Serenity(e) => e.fmt(f), 92 | #[cfg(feature = "twilight")] 93 | JoinError::Twilight(e) => e.fmt(f), 94 | } 95 | } 96 | } 97 | 98 | #[cfg(feature = "gateway")] 99 | impl Error for JoinError { 100 | fn source(&self) -> Option<&(dyn Error + 'static)> { 101 | match self { 102 | JoinError::Dropped => None, 103 | JoinError::NoSender => None, 104 | JoinError::NoCall => None, 105 | JoinError::TimedOut => None, 106 | #[cfg(feature = "driver")] 107 | JoinError::Driver(e) => Some(e), 108 | #[cfg(feature = "serenity")] 109 | JoinError::Serenity(e) => e.source(), 110 | #[cfg(feature = "twilight")] 111 | JoinError::Twilight(e) => e.source(), 112 | } 113 | } 114 | } 115 | 116 | #[cfg(all(feature = "serenity", feature = "gateway"))] 117 | impl From>> for JoinError { 118 | fn from(e: Box>) -> Self { 119 | JoinError::Serenity(e) 120 | } 121 | } 122 | 123 | #[cfg(all(feature = "twilight", feature = "gateway"))] 124 | impl From for JoinError { 125 | fn from(e: ChannelError) -> Self { 126 | JoinError::Twilight(e) 127 | } 128 | } 129 | 130 | #[cfg(all(feature = "driver", feature = "gateway"))] 131 | impl From for JoinError { 132 | fn from(e: ConnectionError) -> Self { 133 | JoinError::Driver(e) 134 | } 135 | } 136 | 137 | #[cfg(feature = "gateway")] 138 | /// Convenience type for Discord gateway error handling. 139 | pub type JoinResult = Result; 140 | 141 | #[cfg(feature = "driver")] 142 | pub use crate::{ 143 | driver::{ 144 | connection::error::{Error as ConnectionError, Result as ConnectionResult}, 145 | SchedulerError, 146 | }, 147 | tracks::{ControlError, PlayError, TrackResult}, 148 | }; 149 | -------------------------------------------------------------------------------- /src/events/context/data/connect.rs: -------------------------------------------------------------------------------- 1 | use crate::id::*; 2 | 3 | /// Voice connection details gathered at setup/reinstantiation. 4 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 5 | #[non_exhaustive] 6 | pub struct ConnectData<'a> { 7 | /// ID of the voice channel being joined, if it is known. 8 | /// 9 | /// If this is available, then this can be used to reconnect/renew 10 | /// a voice session via thew gateway. 11 | pub channel_id: Option, 12 | /// ID of the target voice channel's parent guild. 13 | pub guild_id: GuildId, 14 | /// Unique string describing this session for validation/authentication purposes. 15 | pub session_id: &'a str, 16 | /// The domain name of Discord's voice/TURN server. 17 | /// 18 | /// With the introduction of Discord's automatic voice server selection, 19 | /// this is no longer guaranteed to match a server's settings. This field 20 | /// may be useful if you need/wish to move your voice connection to a node/shard 21 | /// closer to Discord. 22 | pub server: &'a str, 23 | /// The [RTP SSRC] *("Synchronisation source")* assigned by the voice server 24 | /// for the duration of this call. 25 | /// 26 | /// All packets sent will use this SSRC, which is not related to the sender's User 27 | /// ID. These are usually allocated sequentially by Discord, following on from 28 | /// a random starting SSRC. 29 | /// 30 | /// [RTP SSRC]: https://tools.ietf.org/html/rfc3550#section-3 31 | pub ssrc: u32, 32 | } 33 | -------------------------------------------------------------------------------- /src/events/context/data/disconnect.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ConnectionError, 3 | id::*, 4 | model::{CloseCode as VoiceCloseCode, FromPrimitive}, 5 | ws::Error as WsError, 6 | }; 7 | #[cfg(feature = "tungstenite")] 8 | use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; 9 | 10 | /// Voice connection details gathered at termination or failure. 11 | /// 12 | /// In the event of a failure, this event data is gathered after 13 | /// a reconnection strategy has exhausted all of its attempts. 14 | #[derive(Debug)] 15 | #[non_exhaustive] 16 | pub struct DisconnectData<'a> { 17 | /// The location that a voice connection was terminated. 18 | pub kind: DisconnectKind, 19 | /// The cause of any connection failure. 20 | /// 21 | /// If `None`, then this disconnect was requested by the user in some way 22 | /// (i.e., leaving or changing voice channels). 23 | pub reason: Option, 24 | /// ID of the voice channel being joined, if it is known. 25 | /// 26 | /// If this is available, then this can be used to reconnect/renew 27 | /// a voice session via thew gateway. 28 | pub channel_id: Option, 29 | /// ID of the target voice channel's parent guild. 30 | pub guild_id: GuildId, 31 | /// Unique string describing this session for validation/authentication purposes. 32 | pub session_id: &'a str, 33 | } 34 | 35 | /// The location that a voice connection was terminated. 36 | #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] 37 | #[non_exhaustive] 38 | pub enum DisconnectKind { 39 | /// The voice driver failed to connect to the server. 40 | /// 41 | /// This requires explicit handling at the gateway level 42 | /// to either reconnect or fully disconnect. 43 | Connect, 44 | /// The voice driver failed to reconnect to the server. 45 | /// 46 | /// This requires explicit handling at the gateway level 47 | /// to either reconnect or fully disconnect. 48 | Reconnect, 49 | /// The voice connection was terminated mid-session by either 50 | /// the user or Discord. 51 | /// 52 | /// If `reason == None`, then this disconnection is either 53 | /// a full disconnect or a user-requested channel change. 54 | /// Otherwise, this is likely a session expiry (requiring user 55 | /// handling to fully disconnect/reconnect). 56 | Runtime, 57 | } 58 | 59 | /// The reason that a voice connection failed. 60 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 61 | #[non_exhaustive] 62 | pub enum DisconnectReason { 63 | /// This (re)connection attempt was dropped due to another request. 64 | AttemptDiscarded, 65 | /// Songbird had an internal error. 66 | /// 67 | /// This should never happen; if this is ever seen, raise an issue with logs. 68 | Internal, 69 | /// A host-specific I/O error caused the fault; this is likely transient, and 70 | /// should be retried some time later. 71 | Io, 72 | /// Songbird and Discord disagreed on the protocol used to establish a 73 | /// voice connection. 74 | /// 75 | /// This should never happen; if this is ever seen, raise an issue with logs. 76 | ProtocolViolation, 77 | /// A voice connection was not established in the specified time. 78 | TimedOut, 79 | /// The call was manually disconnected by a user command, e.g. [`Driver::leave`]. 80 | /// 81 | /// [`Driver::leave`]: crate::driver::Driver::leave 82 | Requested, 83 | /// The Websocket connection was closed by Discord. 84 | /// 85 | /// This typically indicates that the voice session has expired, 86 | /// and a new one needs to be requested via the gateway. 87 | WsClosed(Option), 88 | } 89 | 90 | impl From<&ConnectionError> for DisconnectReason { 91 | fn from(e: &ConnectionError) -> Self { 92 | match e { 93 | ConnectionError::AttemptDiscarded => Self::AttemptDiscarded, 94 | ConnectionError::CryptoInvalidLength 95 | | ConnectionError::CryptoModeInvalid 96 | | ConnectionError::CryptoModeUnavailable 97 | | ConnectionError::EndpointUrl 98 | | ConnectionError::IllegalDiscoveryResponse 99 | | ConnectionError::IllegalIp 100 | | ConnectionError::Json(_) => Self::ProtocolViolation, 101 | ConnectionError::Io(_) => Self::Io, 102 | ConnectionError::Crypto(_) | ConnectionError::InterconnectFailure(_) => Self::Internal, 103 | ConnectionError::Ws(ws) => ws.into(), 104 | ConnectionError::TimedOut => Self::TimedOut, 105 | } 106 | } 107 | } 108 | 109 | impl From<&WsError> for DisconnectReason { 110 | fn from(e: &WsError) -> Self { 111 | Self::WsClosed(match e { 112 | #[cfg(feature = "tungstenite")] 113 | WsError::WsClosed(Some(frame)) => match frame.code { 114 | CloseCode::Library(l) => VoiceCloseCode::from_u16(l), 115 | _ => None, 116 | }, 117 | #[cfg(feature = "tws")] 118 | WsError::WsClosed(Some(code)) => match (*code).into() { 119 | code @ 4000..=4999_u16 => VoiceCloseCode::from_u16(code), 120 | _ => None, 121 | }, 122 | _ => None, 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/events/context/data/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types containing the main body of an [`EventContext`]. 2 | //! 3 | //! [`EventContext`]: super::EventContext 4 | mod connect; 5 | mod disconnect; 6 | #[cfg(feature = "receive")] 7 | mod rtcp; 8 | #[cfg(feature = "receive")] 9 | mod rtp; 10 | #[cfg(feature = "receive")] 11 | mod voice; 12 | 13 | #[cfg(feature = "receive")] 14 | use bytes::Bytes; 15 | 16 | pub use self::{connect::*, disconnect::*}; 17 | #[cfg(feature = "receive")] 18 | pub use self::{rtcp::*, rtp::*, voice::*}; 19 | -------------------------------------------------------------------------------- /src/events/context/data/rtcp.rs: -------------------------------------------------------------------------------- 1 | use discortp::rtcp::RtcpPacket; 2 | 3 | use super::*; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq)] 6 | #[non_exhaustive] 7 | /// Telemetry/statistics packet, received from another stream 8 | /// 9 | /// `payload_offset` contains the true payload location within the raw packet's `payload()`, 10 | /// to allow manual decoding of `Rtcp` packet bodies. 11 | pub struct RtcpData { 12 | /// Raw RTCP packet data. 13 | pub packet: Bytes, 14 | /// Byte index into the packet body (after headers) for where the payload begins. 15 | pub payload_offset: usize, 16 | /// Number of bytes at the end of the packet to discard. 17 | pub payload_end_pad: usize, 18 | } 19 | 20 | impl RtcpData { 21 | /// Create a zero-copy view of the inner RTCP packet. 22 | /// 23 | /// This allows easy access to packet header fields, taking them from the underlying 24 | /// `Bytes` as needed while handling endianness etc. 25 | pub fn rtcp(&'_ self) -> RtcpPacket<'_> { 26 | RtcpPacket::new(&self.packet) 27 | .expect("FATAL: leaked illegally small RTP packet from UDP Rx task.") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/events/context/data/rtp.rs: -------------------------------------------------------------------------------- 1 | use discortp::rtp::RtpPacket; 2 | 3 | use super::*; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq)] 6 | #[non_exhaustive] 7 | /// Opus audio packet, received from another stream 8 | /// 9 | /// `payload_offset` contains the true payload location within the raw packet's `payload()`, 10 | /// if extensions or raw packet data are required. 11 | pub struct RtpData { 12 | /// Raw RTP packet data. 13 | /// 14 | /// Includes the SSRC (i.e., sender) of this packet. 15 | pub packet: Bytes, 16 | /// Byte index into the packet body (after headers) for where the payload begins. 17 | pub payload_offset: usize, 18 | /// Number of bytes at the end of the packet to discard. 19 | pub payload_end_pad: usize, 20 | } 21 | 22 | impl RtpData { 23 | /// Create a zero-copy view of the inner RTP packet. 24 | /// 25 | /// This allows easy access to packet header fields, taking them from the underlying 26 | /// `Bytes` as needed while handling endianness etc. 27 | pub fn rtp(&'_ self) -> RtpPacket<'_> { 28 | RtpPacket::new(&self.packet) 29 | .expect("FATAL: leaked illegally small RTP packet from UDP Rx task.") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/events/context/data/voice.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use super::*; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq)] 6 | #[non_exhaustive] 7 | /// Audio data from all users in a voice channel, fired every 20ms. 8 | /// 9 | /// Songbird implements a jitter buffer to sycnhronise user packets, smooth out network latency, and 10 | /// handle packet reordering by the network. Packet playout via this event is delayed by approximately 11 | /// [`Config::playout_buffer_length`]` * 20ms` from its original arrival. 12 | /// 13 | /// [`Config::playout_buffer_length`]: crate::Config::playout_buffer_length 14 | pub struct VoiceTick { 15 | /// Decoded voice data and source packets sent by each user. 16 | pub speaking: HashMap, 17 | 18 | /// Set of all SSRCs currently known in the call who aren't included in [`Self::speaking`]. 19 | pub silent: HashSet, 20 | } 21 | 22 | #[derive(Clone, Debug, Eq, PartialEq)] 23 | #[non_exhaustive] 24 | /// Voice packet and audio data for a single user, from a single tick. 25 | pub struct VoiceData { 26 | /// RTP packet clocked out for this tick. 27 | /// 28 | /// If `None`, then the packet was lost, and [`Self::decoded_voice`] may include 29 | /// around one codec delay's worth of audio. 30 | pub packet: Option, 31 | /// PCM audio obtained from a user. 32 | /// 33 | /// Valid audio data (`Some(audio)` where `audio.len >= 0`) typically contains 20ms of 16-bit PCM audio 34 | /// using native endianness. This defaults to stereo audio at 48kHz, and can be configured via 35 | /// [`Config::decode_channels`] and [`Config::decode_sample_rate`] -- channels are interleaved 36 | /// (i.e., `L, R, L, R, ...`) if stereo. 37 | /// 38 | /// This value will be `None` if Songbird is not configured to decode audio. 39 | /// 40 | /// [`Config::decode_channels`]: crate::Config::decode_channels 41 | /// [`Config::decode_sample_rate`]: crate::Config::decode_sample_rate 42 | pub decoded_voice: Option>, 43 | } 44 | -------------------------------------------------------------------------------- /src/events/context/internal_data.rs: -------------------------------------------------------------------------------- 1 | use super::context_data::*; 2 | use crate::ConnectionInfo; 3 | 4 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 5 | pub struct InternalConnect { 6 | pub info: ConnectionInfo, 7 | pub ssrc: u32, 8 | } 9 | 10 | #[derive(Debug)] 11 | pub struct InternalDisconnect { 12 | pub kind: DisconnectKind, 13 | pub reason: Option, 14 | pub info: ConnectionInfo, 15 | } 16 | 17 | impl<'a> From<&'a InternalConnect> for ConnectData<'a> { 18 | fn from(val: &'a InternalConnect) -> Self { 19 | Self { 20 | channel_id: val.info.channel_id, 21 | guild_id: val.info.guild_id, 22 | session_id: &val.info.session_id, 23 | server: &val.info.endpoint, 24 | ssrc: val.ssrc, 25 | } 26 | } 27 | } 28 | 29 | impl<'a> From<&'a InternalDisconnect> for DisconnectData<'a> { 30 | fn from(val: &'a InternalDisconnect) -> Self { 31 | Self { 32 | kind: val.kind, 33 | reason: val.reason, 34 | channel_id: val.info.channel_id, 35 | guild_id: val.info.guild_id, 36 | session_id: &val.info.session_id, 37 | } 38 | } 39 | } 40 | 41 | #[cfg(feature = "receive")] 42 | mod receive { 43 | use super::*; 44 | use bytes::Bytes; 45 | 46 | #[derive(Clone, Debug, Eq, PartialEq)] 47 | pub struct InternalRtpPacket { 48 | pub packet: Bytes, 49 | pub payload_offset: usize, 50 | pub payload_end_pad: usize, 51 | } 52 | 53 | #[derive(Clone, Debug, Eq, PartialEq)] 54 | pub struct InternalRtcpPacket { 55 | pub packet: Bytes, 56 | pub payload_offset: usize, 57 | pub payload_end_pad: usize, 58 | } 59 | 60 | impl<'a> From<&'a InternalRtpPacket> for RtpData { 61 | fn from(val: &'a InternalRtpPacket) -> Self { 62 | Self { 63 | packet: val.packet.clone(), 64 | payload_offset: val.payload_offset, 65 | payload_end_pad: val.payload_end_pad, 66 | } 67 | } 68 | } 69 | 70 | impl<'a> From<&'a InternalRtcpPacket> for RtcpData { 71 | fn from(val: &'a InternalRtcpPacket) -> Self { 72 | Self { 73 | packet: val.packet.clone(), 74 | payload_offset: val.payload_offset, 75 | payload_end_pad: val.payload_end_pad, 76 | } 77 | } 78 | } 79 | } 80 | 81 | #[cfg(feature = "receive")] 82 | pub use receive::*; 83 | -------------------------------------------------------------------------------- /src/events/context/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub(crate) mod internal_data; 3 | 4 | use super::*; 5 | use crate::{ 6 | model::payload::{ClientDisconnect, Speaking}, 7 | tracks::{TrackHandle, TrackState}, 8 | }; 9 | pub use data as context_data; 10 | use data::*; 11 | use internal_data::*; 12 | 13 | /// Information about which tracks or data fired an event. 14 | /// 15 | /// [`Track`] events may be local or global, and have no tracks 16 | /// if fired on the global context via [`Driver::add_global_event`]. 17 | /// 18 | /// [`Track`]: crate::tracks::Track 19 | /// [`Driver::add_global_event`]: crate::driver::Driver::add_global_event 20 | #[derive(Debug)] 21 | #[non_exhaustive] 22 | pub enum EventContext<'a> { 23 | /// Track event context, passed to events created via [`TrackHandle::add_event`], 24 | /// [`EventStore::add_event`], or relevant global events. 25 | /// 26 | /// [`EventStore::add_event`]: EventStore::add_event 27 | /// [`TrackHandle::add_event`]: TrackHandle::add_event 28 | Track(&'a [(&'a TrackState, &'a TrackHandle)]), 29 | 30 | /// Speaking state update, typically describing how another voice 31 | /// user is transmitting audio data. Clients must send at least one such 32 | /// packet to allow SSRC/UserID matching. 33 | SpeakingStateUpdate(Speaking), 34 | 35 | #[cfg(feature = "receive")] 36 | /// Reordered and decoded audio packets, received every 20ms. 37 | VoiceTick(VoiceTick), 38 | 39 | #[cfg(feature = "receive")] 40 | /// Opus audio packet, received from another stream. 41 | RtpPacket(RtpData), 42 | 43 | #[cfg(feature = "receive")] 44 | /// Telemetry/statistics packet, received from another stream. 45 | RtcpPacket(RtcpData), 46 | 47 | /// Fired whenever a client disconnects. 48 | ClientDisconnect(ClientDisconnect), 49 | 50 | /// Fires when this driver successfully connects to a voice channel. 51 | DriverConnect(ConnectData<'a>), 52 | 53 | /// Fires when this driver successfully reconnects after a network error. 54 | DriverReconnect(ConnectData<'a>), 55 | 56 | /// Fires when this driver fails to connect to, or drops from, a voice channel. 57 | DriverDisconnect(DisconnectData<'a>), 58 | } 59 | 60 | #[derive(Debug)] 61 | pub enum CoreContext { 62 | SpeakingStateUpdate(Speaking), 63 | #[cfg(feature = "receive")] 64 | VoiceTick(VoiceTick), 65 | #[cfg(feature = "receive")] 66 | RtpPacket(InternalRtpPacket), 67 | #[cfg(feature = "receive")] 68 | RtcpPacket(InternalRtcpPacket), 69 | ClientDisconnect(ClientDisconnect), 70 | DriverConnect(InternalConnect), 71 | DriverReconnect(InternalConnect), 72 | DriverDisconnect(InternalDisconnect), 73 | } 74 | 75 | impl<'a> CoreContext { 76 | pub(crate) fn to_user_context(&'a self) -> EventContext<'a> { 77 | match self { 78 | Self::SpeakingStateUpdate(evt) => EventContext::SpeakingStateUpdate(*evt), 79 | #[cfg(feature = "receive")] 80 | Self::VoiceTick(evt) => EventContext::VoiceTick(evt.clone()), 81 | #[cfg(feature = "receive")] 82 | Self::RtpPacket(evt) => EventContext::RtpPacket(RtpData::from(evt)), 83 | #[cfg(feature = "receive")] 84 | Self::RtcpPacket(evt) => EventContext::RtcpPacket(RtcpData::from(evt)), 85 | Self::ClientDisconnect(evt) => EventContext::ClientDisconnect(*evt), 86 | Self::DriverConnect(evt) => EventContext::DriverConnect(ConnectData::from(evt)), 87 | Self::DriverReconnect(evt) => EventContext::DriverReconnect(ConnectData::from(evt)), 88 | Self::DriverDisconnect(evt) => 89 | EventContext::DriverDisconnect(DisconnectData::from(evt)), 90 | } 91 | } 92 | } 93 | 94 | impl EventContext<'_> { 95 | /// Retreive the event class for an event (i.e., when matching) 96 | /// an event against the registered listeners. 97 | #[must_use] 98 | pub fn to_core_event(&self) -> Option { 99 | match self { 100 | Self::SpeakingStateUpdate(_) => Some(CoreEvent::SpeakingStateUpdate), 101 | #[cfg(feature = "receive")] 102 | Self::VoiceTick(_) => Some(CoreEvent::VoiceTick), 103 | #[cfg(feature = "receive")] 104 | Self::RtpPacket(_) => Some(CoreEvent::RtpPacket), 105 | #[cfg(feature = "receive")] 106 | Self::RtcpPacket(_) => Some(CoreEvent::RtcpPacket), 107 | Self::ClientDisconnect(_) => Some(CoreEvent::ClientDisconnect), 108 | Self::DriverConnect(_) => Some(CoreEvent::DriverConnect), 109 | Self::DriverReconnect(_) => Some(CoreEvent::DriverReconnect), 110 | Self::DriverDisconnect(_) => Some(CoreEvent::DriverDisconnect), 111 | _ => None, 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/events/core.rs: -------------------------------------------------------------------------------- 1 | /// Voice core events occur on receipt of 2 | /// voice packets and telemetry. 3 | /// 4 | /// Core events persist while the `action` in [`EventData`] 5 | /// returns `None`. 6 | /// 7 | /// ## Events from other users 8 | /// Songbird can observe when a user *speaks for the first time* ([`SpeakingStateUpdate`]), 9 | /// when a client leaves the session ([`ClientDisconnect`]). 10 | /// 11 | /// When the `"receive"` feature is enabled, songbird can also handle voice packets 12 | #[cfg_attr(feature = "receive", doc = "([`RtpPacket`](Self::RtpPacket)),")] 13 | #[cfg_attr(not(feature = "receive"), doc = "(`RtpPacket`),")] 14 | /// decode and track speaking users 15 | #[cfg_attr(feature = "receive", doc = "([`VoiceTick`](Self::VoiceTick)),")] 16 | #[cfg_attr(not(feature = "receive"), doc = "(`VoiceTick`),")] 17 | /// and handle telemetry data 18 | #[cfg_attr(feature = "receive", doc = "([`RtcpPacket`](Self::RtcpPacket)).")] 19 | #[cfg_attr(not(feature = "receive"), doc = "(`RtcpPacket`).")] 20 | /// The format of voice packets is described by 21 | #[cfg_attr( 22 | feature = "receive", 23 | doc = "[`VoiceData`](super::context::data::VoiceData)." 24 | )] 25 | #[cfg_attr(not(feature = "receive"), doc = "`VoiceData`.")] 26 | /// 27 | /// To detect when a user connects, you must correlate gateway (e.g., `VoiceStateUpdate`) events 28 | /// from the main part of your bot. 29 | /// 30 | /// To obtain a user's SSRC, you must use [`SpeakingStateUpdate`] events. 31 | /// 32 | /// [`EventData`]: super::EventData 33 | /// [`SpeakingStateUpdate`]: Self::SpeakingStateUpdate 34 | /// [`ClientDisconnect`]: Self::ClientDisconnect 35 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 36 | #[non_exhaustive] 37 | pub enum CoreEvent { 38 | /// Speaking state update from the WS gateway, typically describing how another voice 39 | /// user is transmitting audio data. Clients must send at least one such 40 | /// packet to allow SSRC/UserID matching. 41 | /// 42 | /// Fired on receipt of a speaking state update from another host. 43 | /// 44 | /// Note: this will fire when a user starts speaking for the first time, 45 | /// or changes their capabilities. 46 | SpeakingStateUpdate, 47 | 48 | #[cfg(feature = "receive")] 49 | /// Fires every 20ms, containing the scheduled voice packet and decoded audio 50 | /// data for each live user. 51 | VoiceTick, 52 | 53 | #[cfg(feature = "receive")] 54 | /// Fires on receipt of a voice packet from another stream in the voice call. 55 | /// 56 | /// As RTP packets do not map to Discord's notion of users, SSRCs must be mapped 57 | /// back using the user IDs seen through client connection, disconnection, 58 | /// or speaking state update. 59 | RtpPacket, 60 | 61 | #[cfg(feature = "receive")] 62 | /// Fires on receipt of an RTCP packet, containing various call stats 63 | /// such as latency reports. 64 | RtcpPacket, 65 | 66 | /// Fires whenever a user disconnects from the same stream as the bot. 67 | ClientDisconnect, 68 | 69 | /// Fires when this driver successfully connects to a voice channel. 70 | DriverConnect, 71 | 72 | /// Fires when this driver successfully reconnects after a network error. 73 | DriverReconnect, 74 | 75 | /// Fires when this driver fails to connect to, or drops from, a voice channel. 76 | DriverDisconnect, 77 | } 78 | -------------------------------------------------------------------------------- /src/events/data.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::cmp::Ordering; 3 | 4 | /// Internal representation of an event, as handled by the audio context. 5 | pub struct EventData { 6 | pub(crate) event: Event, 7 | pub(crate) fire_time: Option, 8 | pub(crate) action: Box, 9 | } 10 | 11 | impl EventData { 12 | /// Create a representation of an event and its associated handler. 13 | /// 14 | /// An event handler, `action`, receives an [`EventContext`] and optionally 15 | /// produces a new [`Event`] type for itself. Returning `None` will 16 | /// maintain the same event type, while removing any [`Delayed`] entries. 17 | /// Event handlers will be re-added with their new trigger condition, 18 | /// or removed if [`Cancel`]led 19 | /// 20 | /// [`EventContext`]: EventContext 21 | /// [`Event`]: Event 22 | /// [`Delayed`]: Event::Delayed 23 | /// [`Cancel`]: Event::Cancel 24 | pub fn new(event: Event, action: F) -> Self { 25 | Self { 26 | event, 27 | fire_time: None, 28 | action: Box::new(action), 29 | } 30 | } 31 | 32 | /// Computes the next firing time for a timer event. 33 | pub fn compute_activation(&mut self, now: Duration) { 34 | match self.event { 35 | Event::Periodic(period, phase) => { 36 | self.fire_time = Some(now + phase.unwrap_or(period)); 37 | }, 38 | Event::Delayed(offset) => { 39 | self.fire_time = Some(now + offset); 40 | }, 41 | _ => {}, 42 | } 43 | } 44 | } 45 | 46 | impl std::fmt::Debug for EventData { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 48 | write!( 49 | f, 50 | "Event {{ event: {:?}, fire_time: {:?}, action: }}", 51 | self.event, self.fire_time 52 | ) 53 | } 54 | } 55 | 56 | /// Events are ordered/compared based on their firing time. 57 | impl Ord for EventData { 58 | fn cmp(&self, other: &Self) -> Ordering { 59 | if self.fire_time.is_some() && other.fire_time.is_some() { 60 | let t1 = self 61 | .fire_time 62 | .as_ref() 63 | .expect("T1 known to be well-defined by above."); 64 | let t2 = other 65 | .fire_time 66 | .as_ref() 67 | .expect("T2 known to be well-defined by above."); 68 | 69 | t2.cmp(t1) 70 | } else { 71 | Ordering::Equal 72 | } 73 | } 74 | } 75 | 76 | impl PartialOrd for EventData { 77 | fn partial_cmp(&self, other: &Self) -> Option { 78 | Some(self.cmp(other)) 79 | } 80 | } 81 | 82 | impl PartialEq for EventData { 83 | fn eq(&self, other: &Self) -> bool { 84 | self.fire_time == other.fire_time 85 | } 86 | } 87 | 88 | impl Eq for EventData {} 89 | -------------------------------------------------------------------------------- /src/events/mod.rs: -------------------------------------------------------------------------------- 1 | //! Events relating to tracks, timing, and other callers. 2 | //! 3 | //! ## Listening for events 4 | //! Driver events in Songbird are composed of two parts: 5 | //! * An [`Event`] to listen out for. These may be discrete events, 6 | //! or generated by timers. 7 | //! * An [`EventHandler`] to be called on receipt of an event. As event 8 | //! handlers may be shared between several events, the handler is called 9 | //! with an [`EventContext`] describing which event was fired. 10 | //! 11 | //! Event handlers are registered using functions such as [`Driver::add_global_event`], 12 | //! or [`TrackHandle::add_event`], or. Internally, these pairs are stored 13 | //! as [`EventData`]. 14 | //! 15 | //! ## `EventHandler` lifecycle 16 | //! An event handler is essentially just an async function which may return 17 | //! another type of event to listen out for (an `Option`). For instance, 18 | //! [`Some(Event::Cancel)`] will remove that event listener, while `None` won't 19 | //! change it at all. 20 | //! 21 | //! The exception is one-off events like [`Event::Delayed`], which remove themselves 22 | //! after one call *unless* an [`Event`] override is returned. 23 | //! 24 | //! ## Global and local listeners 25 | //! *Global* event listeners are those which are placed onto the [`Driver`], 26 | //! while *local* event listeners are those which are placed on a 27 | //! [`Track`]/[`Handle`]. 28 | //! 29 | //! Track or timed events, when local, return a reference to the parent track. 30 | //! When registered globally, they fire on a per-tick basis, returning references to 31 | //! all relevant tracks in that 20ms window. Global/local timed events use a global 32 | //! timer or a [track's playback time], respectively. 33 | //! 34 | //! [`CoreEvent`]s may only be registered globally. 35 | //! 36 | //! [`Event`]: Event 37 | //! [`EventHandler`]: EventHandler 38 | //! [`EventContext`]: EventContext 39 | //! [`Driver::add_global_event`]: crate::driver::Driver::add_global_event 40 | //! [`Driver`]: crate::driver::Driver::add_global_event 41 | //! [`TrackHandle::add_event`]: crate::tracks::TrackHandle::add_event 42 | //! [`Track`]: crate::tracks::Track::events 43 | //! [`Handle`]: crate::tracks::TrackHandle::add_event 44 | //! [`EventData`]: EventData 45 | //! [`Some(Event::Cancel)`]: Event::Cancel 46 | //! [`Event::Delayed`]: Event::Delayed 47 | //! [track's playback time]: crate::tracks::TrackState::play_time 48 | //! [`CoreEvent`]: CoreEvent 49 | 50 | mod context; 51 | mod core; 52 | mod data; 53 | mod store; 54 | mod track; 55 | mod untimed; 56 | 57 | pub use self::{ 58 | context::{context_data, EventContext}, 59 | core::*, 60 | data::*, 61 | store::*, 62 | track::*, 63 | untimed::*, 64 | }; 65 | pub(crate) use context::{internal_data, CoreContext}; 66 | 67 | use async_trait::async_trait; 68 | use std::time::Duration; 69 | 70 | /// Trait to handle an event which can be fired per-track, or globally. 71 | /// 72 | /// These may be feasibly reused between several event sources. 73 | #[async_trait] 74 | pub trait EventHandler: Send + Sync { 75 | /// Respond to one received event. 76 | async fn act(&self, ctx: &EventContext<'_>) -> Option; 77 | } 78 | 79 | /// Classes of event which may occur, triggering a handler 80 | /// at the local (track-specific) or global level. 81 | /// 82 | /// Local time-based events rely upon the current playback 83 | /// time of a track, and so will not fire if a track becomes paused 84 | /// or stops. In case this is required, global events are a better 85 | /// fit. 86 | /// 87 | /// Event handlers themselves are described in [`EventData::new`]. 88 | /// 89 | /// [`EventData::new`]: EventData::new 90 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 91 | #[non_exhaustive] 92 | pub enum Event { 93 | /// Periodic events rely upon two parameters: a *period* 94 | /// and an optional *phase*. 95 | /// 96 | /// If the *phase* is `None`, then the event will first fire 97 | /// in one *period*. Periodic events repeat automatically 98 | /// so long as the `action` in [`EventData`] returns `None`. 99 | /// 100 | /// [`EventData`]: EventData 101 | Periodic(Duration, Option), 102 | /// Delayed events rely upon a *delay* parameter, and 103 | /// fire one *delay* after the audio context processes them. 104 | /// 105 | /// Delayed events are automatically removed once fired, 106 | /// so long as the `action` in [`EventData`] returns `None`. 107 | /// 108 | /// [`EventData`]: EventData 109 | Delayed(Duration), 110 | /// Track events correspond to certain actions or changes 111 | /// of state, such as a track finishing, looping, or being 112 | /// manually stopped. 113 | /// 114 | /// Track events persist while the `action` in [`EventData`] 115 | /// returns `None`. 116 | /// 117 | /// [`EventData`]: EventData 118 | Track(TrackEvent), 119 | /// Core events 120 | /// 121 | /// Track events persist while the `action` in [`EventData`] 122 | /// returns `None`. Core events **must** be applied globally, 123 | /// as attaching them to a track is a no-op. 124 | /// 125 | /// [`EventData`]: EventData 126 | Core(CoreEvent), 127 | /// Cancels the event, if it was intended to persist. 128 | Cancel, 129 | } 130 | 131 | impl Event { 132 | pub(crate) fn is_global_only(&self) -> bool { 133 | matches!(self, Self::Core(_)) 134 | } 135 | } 136 | 137 | impl From for Event { 138 | fn from(evt: TrackEvent) -> Self { 139 | Event::Track(evt) 140 | } 141 | } 142 | 143 | impl From for Event { 144 | fn from(evt: CoreEvent) -> Self { 145 | Event::Core(evt) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/events/track.rs: -------------------------------------------------------------------------------- 1 | // TODO: Could this be a bitset? Could accelerate lookups, 2 | // allow easy joint subscription & remove Vecs for related evt handling? 3 | 4 | /// Track events correspond to certain actions or changes 5 | /// of state, such as a track finishing, looping, or being 6 | /// manually stopped. Voice core events occur on receipt of 7 | /// voice packets and telemetry. 8 | /// 9 | /// Track events persist while the `action` in [`EventData`] 10 | /// returns `None`. 11 | /// 12 | /// [`EventData`]: super::EventData 13 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 14 | #[non_exhaustive] 15 | pub enum TrackEvent { 16 | /// The attached track has resumed playing. 17 | /// 18 | /// This event will not fire when a track first starts, 19 | /// but will fire when a track changes from, e.g., paused to playing. 20 | /// This is most relevant for queue users: queued tracks placed into a 21 | /// non-empty queue are initlally paused, and are later moved to `Play`. 22 | Play, 23 | /// The attached track has been paused. 24 | Pause, 25 | /// The attached track has ended. 26 | End, 27 | /// The attached track has looped. 28 | Loop, 29 | /// The attached track is being readied or recreated. 30 | Preparing, 31 | /// The attached track has become playable. 32 | Playable, 33 | /// The attached track has encountered a runtime or initialisation error. 34 | Error, 35 | } 36 | -------------------------------------------------------------------------------- /src/events/untimed.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Track and voice core events. 4 | /// 5 | /// Untimed events persist while the `action` in [`EventData`] 6 | /// returns `None`. 7 | /// 8 | /// [`EventData`]: EventData 9 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 10 | #[non_exhaustive] 11 | pub enum UntimedEvent { 12 | /// Untimed events belonging to a track, such as state changes, end, or loops. 13 | Track(TrackEvent), 14 | /// Untimed events belonging to the global context, such as finished tracks, 15 | /// client speaking updates, or RT(C)P voice and telemetry data. 16 | Core(CoreEvent), 17 | } 18 | 19 | impl From for UntimedEvent { 20 | fn from(evt: TrackEvent) -> Self { 21 | UntimedEvent::Track(evt) 22 | } 23 | } 24 | 25 | impl From for UntimedEvent { 26 | fn from(evt: CoreEvent) -> Self { 27 | UntimedEvent::Core(evt) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/id.rs: -------------------------------------------------------------------------------- 1 | //! Newtypes around Discord IDs for library cross-compatibility. 2 | 3 | #[cfg(feature = "driver")] 4 | use crate::model::id::{GuildId as DriverGuild, UserId as DriverUser}; 5 | #[cfg(feature = "serenity")] 6 | use serenity::model::id::{ 7 | ChannelId as SerenityChannel, 8 | GuildId as SerenityGuild, 9 | UserId as SerenityUser, 10 | }; 11 | use std::{ 12 | fmt::{Display, Formatter, Result as FmtResult}, 13 | num::NonZeroU64, 14 | }; 15 | #[cfg(feature = "twilight")] 16 | use twilight_model::id::{ 17 | marker::{ChannelMarker, GuildMarker, UserMarker}, 18 | Id as TwilightId, 19 | }; 20 | 21 | /// ID of a Discord voice/text channel. 22 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 23 | pub struct ChannelId(pub NonZeroU64); 24 | 25 | /// ID of a Discord guild (colloquially, "server"). 26 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 27 | pub struct GuildId(pub NonZeroU64); 28 | 29 | /// ID of a Discord user. 30 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 31 | pub struct UserId(pub NonZeroU64); 32 | 33 | impl Display for ChannelId { 34 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 35 | Display::fmt(&self.0, f) 36 | } 37 | } 38 | 39 | impl From for ChannelId { 40 | fn from(id: NonZeroU64) -> Self { 41 | Self(id) 42 | } 43 | } 44 | 45 | #[cfg(feature = "serenity")] 46 | impl From for ChannelId { 47 | fn from(id: SerenityChannel) -> Self { 48 | Self(NonZeroU64::new(id.get()).unwrap()) 49 | } 50 | } 51 | 52 | #[cfg(feature = "twilight")] 53 | impl From> for ChannelId { 54 | fn from(id: TwilightId) -> Self { 55 | Self(id.into_nonzero()) 56 | } 57 | } 58 | 59 | impl Display for GuildId { 60 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 61 | Display::fmt(&self.0, f) 62 | } 63 | } 64 | 65 | impl From for GuildId { 66 | fn from(id: NonZeroU64) -> Self { 67 | Self(id) 68 | } 69 | } 70 | 71 | #[cfg(feature = "serenity")] 72 | impl From for GuildId { 73 | fn from(id: SerenityGuild) -> Self { 74 | Self(NonZeroU64::new(id.get()).unwrap()) 75 | } 76 | } 77 | 78 | #[cfg(feature = "driver")] 79 | impl From for DriverGuild { 80 | fn from(id: GuildId) -> Self { 81 | Self(id.0.get()) 82 | } 83 | } 84 | 85 | #[cfg(feature = "twilight")] 86 | impl From> for GuildId { 87 | fn from(id: TwilightId) -> Self { 88 | Self(id.into_nonzero()) 89 | } 90 | } 91 | 92 | impl Display for UserId { 93 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 94 | Display::fmt(&self.0, f) 95 | } 96 | } 97 | 98 | impl From for UserId { 99 | fn from(id: NonZeroU64) -> Self { 100 | Self(id) 101 | } 102 | } 103 | 104 | #[cfg(feature = "serenity")] 105 | impl From for UserId { 106 | fn from(id: SerenityUser) -> Self { 107 | Self(NonZeroU64::new(id.get()).unwrap()) 108 | } 109 | } 110 | 111 | #[cfg(feature = "driver")] 112 | impl From for DriverUser { 113 | fn from(id: UserId) -> Self { 114 | Self(id.0.get()) 115 | } 116 | } 117 | 118 | #[cfg(feature = "twilight")] 119 | impl From> for UserId { 120 | fn from(id: TwilightId) -> Self { 121 | Self(id.into_nonzero()) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/input/adapters/cached/decompressed.rs: -------------------------------------------------------------------------------- 1 | use super::{compressed::Config, CodecCacheError, ToAudioBytes}; 2 | use crate::{ 3 | constants::SAMPLE_RATE_RAW, 4 | input::{AudioStream, Input, LiveInput, RawAdapter}, 5 | }; 6 | use std::io::{Read, Result as IoResult, Seek, SeekFrom}; 7 | use streamcatcher::Catcher; 8 | use symphonia_core::{audio::Channels, io::MediaSource}; 9 | 10 | /// A wrapper around an existing [`Input`] which caches 11 | /// the decoded and converted audio data locally in memory 12 | /// as `f32`-format PCM data. 13 | /// 14 | /// The main purpose of this wrapper is to enable seeking on 15 | /// incompatible sources (i.e., ffmpeg output) and to ease resource 16 | /// consumption for commonly reused/shared tracks. [`Compressed`] 17 | /// offers similar functionality with different 18 | /// tradeoffs. 19 | /// 20 | /// This is intended for use with small, repeatedly used audio 21 | /// tracks shared between sources, and stores the sound data 22 | /// retrieved in **uncompressed floating point** form to minimise the 23 | /// cost of audio processing when mixing several tracks together. 24 | /// This must be used sparingly: these cost a significant 25 | /// *3 Mbps (375 kiB/s)*, or 131 MiB of RAM for a 6 minute song. 26 | /// 27 | /// [`Input`]: crate::input::Input 28 | /// [`Compressed`]: super::Compressed 29 | #[derive(Clone)] 30 | pub struct Decompressed { 31 | /// Inner shared bytestore. 32 | pub raw: Catcher>, 33 | } 34 | 35 | impl Decompressed { 36 | /// Wrap an existing [`Input`] with an in-memory store, decompressed into `f32` PCM audio. 37 | /// 38 | /// [`Input`]: Input 39 | pub async fn new(source: Input) -> Result { 40 | Self::with_config(source, None).await 41 | } 42 | 43 | /// Wrap an existing [`Input`] with an in-memory store, decompressed into `f32` PCM audio, 44 | /// with custom configuration for both Symphonia and the backing store. 45 | /// 46 | /// [`Input`]: Input 47 | pub async fn with_config( 48 | source: Input, 49 | config: Option, 50 | ) -> Result { 51 | let input = match source { 52 | Input::Lazy(mut r) => { 53 | let created = if r.should_create_async() { 54 | r.create_async().await.map_err(CodecCacheError::from) 55 | } else { 56 | tokio::task::spawn_blocking(move || r.create().map_err(CodecCacheError::from)) 57 | .await 58 | .map_err(CodecCacheError::from) 59 | .and_then(|v| v) 60 | }; 61 | 62 | created.map(LiveInput::Raw) 63 | }, 64 | Input::Live(LiveInput::Parsed(_), _) => Err(CodecCacheError::StreamNotAtStart), 65 | Input::Live(a, _rec) => Ok(a), 66 | }?; 67 | 68 | let cost_per_sec = super::raw_cost_per_sec(true); 69 | let config = config.unwrap_or_else(|| Config::default_from_cost(cost_per_sec)); 70 | 71 | let promoted = tokio::task::spawn_blocking(move || { 72 | input.promote(config.codec_registry, config.format_registry) 73 | }) 74 | .await??; 75 | 76 | // If success, guaranteed to be Parsed 77 | let LiveInput::Parsed(parsed) = promoted else { 78 | unreachable!() 79 | }; 80 | 81 | let track_info = parsed.decoder.codec_params(); 82 | let chan_count = track_info 83 | .channels 84 | .map(Channels::count) 85 | .ok_or(CodecCacheError::UnknownChannelCount)?; 86 | let sample_rate = SAMPLE_RATE_RAW as u32; 87 | 88 | let source = RawAdapter::new( 89 | ToAudioBytes::new(parsed, Some(chan_count)), 90 | sample_rate, 91 | chan_count as u32, 92 | ); 93 | 94 | let raw = config.streamcatcher.build(source)?; 95 | 96 | Ok(Self { raw }) 97 | } 98 | 99 | /// Acquire a new handle to this object, creating a new 100 | /// view of the existing cached data from the beginning. 101 | #[must_use] 102 | pub fn new_handle(&self) -> Self { 103 | Self { 104 | raw: self.raw.new_handle(), 105 | } 106 | } 107 | } 108 | 109 | impl Read for Decompressed { 110 | fn read(&mut self, buf: &mut [u8]) -> IoResult { 111 | self.raw.read(buf) 112 | } 113 | } 114 | 115 | impl Seek for Decompressed { 116 | fn seek(&mut self, pos: SeekFrom) -> IoResult { 117 | self.raw.seek(pos) 118 | } 119 | } 120 | 121 | impl MediaSource for Decompressed { 122 | fn is_seekable(&self) -> bool { 123 | true 124 | } 125 | 126 | fn byte_len(&self) -> Option { 127 | if self.raw.is_finished() { 128 | Some(self.raw.len() as u64) 129 | } else { 130 | None 131 | } 132 | } 133 | } 134 | 135 | impl From for Input { 136 | fn from(val: Decompressed) -> Input { 137 | let input = Box::new(val); 138 | Input::Live(LiveInput::Raw(AudioStream { input, hint: None }), None) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/input/adapters/cached/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::JsonError, input::AudioStreamError}; 2 | use audiopus::error::Error as OpusError; 3 | use std::{ 4 | error::Error as StdError, 5 | fmt::{Display, Formatter, Result as FmtResult}, 6 | }; 7 | use streamcatcher::CatcherError; 8 | use symphonia_core::errors::Error as SymphError; 9 | use tokio::task::JoinError; 10 | 11 | /// Errors encountered using a [`Memory`] cached source. 12 | /// 13 | /// [`Memory`]: super::Memory 14 | #[derive(Debug)] 15 | pub enum Error { 16 | /// The audio stream could not be created. 17 | Create(AudioStreamError), 18 | /// The audio stream failed to be created due to a panic in `spawn_blocking`. 19 | CreatePanicked, 20 | /// Streamcatcher's configuration was illegal, and the cache could not be created. 21 | Streamcatcher(CatcherError), 22 | /// The input stream had already been read (i.e., `Parsed`) and so the whole stream 23 | /// could not be used. 24 | StreamNotAtStart, 25 | } 26 | 27 | impl Display for Error { 28 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 29 | match self { 30 | Self::Create(c) => f.write_fmt(format_args!("failed to create audio stream: {c}")), 31 | Self::CreatePanicked => f.write_str("sync thread panicked while creating stream"), 32 | Self::Streamcatcher(s) => 33 | f.write_fmt(format_args!("illegal streamcatcher config: {s}")), 34 | Self::StreamNotAtStart => 35 | f.write_str("stream cannot have been pre-read/parsed, missing headers"), 36 | } 37 | } 38 | } 39 | 40 | impl StdError for Error {} 41 | 42 | impl From for Error { 43 | fn from(val: AudioStreamError) -> Self { 44 | Self::Create(val) 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(val: CatcherError) -> Self { 50 | Self::Streamcatcher(val) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(_val: JoinError) -> Self { 56 | Self::CreatePanicked 57 | } 58 | } 59 | 60 | /// Errors encountered using a [`Compressed`] or [`Decompressed`] cached source. 61 | /// 62 | /// [`Compressed`]: super::Compressed 63 | /// [`Decompressed`]: super::Decompressed 64 | #[derive(Debug)] 65 | pub enum CodecCacheError { 66 | /// The audio stream could not be created. 67 | Create(AudioStreamError), 68 | /// Symphonia failed to parse the container or decode the default stream. 69 | Parse(SymphError), 70 | /// The Opus encoder could not be created. 71 | Opus(OpusError), 72 | /// The file's metadata could not be converted to JSON. 73 | MetadataEncoding(JsonError), 74 | /// The input's metadata was too large after conversion to JSON to fit in a DCA file. 75 | MetadataTooLarge, 76 | /// The audio stream failed to be created due to a panic in `spawn_blocking`. 77 | CreatePanicked, 78 | /// The audio stream's channel count could not be determined. 79 | UnknownChannelCount, 80 | /// Streamcatcher's configuration was illegal, and the cache could not be created. 81 | Streamcatcher(CatcherError), 82 | /// The input stream had already been read (i.e., `Parsed`) and so the whole stream 83 | /// could not be used. 84 | StreamNotAtStart, 85 | } 86 | 87 | impl Display for CodecCacheError { 88 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 89 | match self { 90 | Self::Create(c) => f.write_fmt(format_args!("failed to create audio stream: {c}")), 91 | Self::Parse(p) => f.write_fmt(format_args!("failed to parse audio format: {p}")), 92 | Self::Opus(o) => f.write_fmt(format_args!("failed to create Opus encoder: {o}")), 93 | Self::MetadataEncoding(m) => f.write_fmt(format_args!( 94 | "failed to convert track metadata to JSON: {m}" 95 | )), 96 | Self::MetadataTooLarge => f.write_str("track metadata was too large, >= 32kiB"), 97 | Self::CreatePanicked => f.write_str("sync thread panicked while creating stream"), 98 | Self::UnknownChannelCount => 99 | f.write_str("audio stream's channel count could not be determined"), 100 | Self::Streamcatcher(s) => 101 | f.write_fmt(format_args!("illegal streamcatcher config: {s}")), 102 | Self::StreamNotAtStart => 103 | f.write_str("stream cannot have been pre-read/parsed, missing headers"), 104 | } 105 | } 106 | } 107 | 108 | impl StdError for CodecCacheError {} 109 | 110 | impl From for CodecCacheError { 111 | fn from(val: AudioStreamError) -> Self { 112 | Self::Create(val) 113 | } 114 | } 115 | 116 | impl From for CodecCacheError { 117 | fn from(val: CatcherError) -> Self { 118 | Self::Streamcatcher(val) 119 | } 120 | } 121 | 122 | impl From for CodecCacheError { 123 | fn from(_val: JoinError) -> Self { 124 | Self::CreatePanicked 125 | } 126 | } 127 | 128 | impl From for CodecCacheError { 129 | fn from(val: JsonError) -> Self { 130 | Self::MetadataEncoding(val) 131 | } 132 | } 133 | 134 | impl From for CodecCacheError { 135 | fn from(val: OpusError) -> Self { 136 | Self::Opus(val) 137 | } 138 | } 139 | 140 | impl From for CodecCacheError { 141 | fn from(val: SymphError) -> Self { 142 | Self::Parse(val) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/input/adapters/cached/hint.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use streamcatcher::Config; 3 | 4 | /// Expected amount of time that an input should last. 5 | #[derive(Copy, Clone, Debug)] 6 | pub enum LengthHint { 7 | /// Estimate of a source's length in bytes. 8 | Bytes(usize), 9 | /// Estimate of a source's length in time. 10 | /// 11 | /// This will be converted to a bytecount at setup. 12 | Time(Duration), 13 | } 14 | 15 | impl From for LengthHint { 16 | fn from(size: usize) -> Self { 17 | LengthHint::Bytes(size) 18 | } 19 | } 20 | 21 | impl From for LengthHint { 22 | fn from(size: Duration) -> Self { 23 | LengthHint::Time(size) 24 | } 25 | } 26 | 27 | /// Modify the given cache configuration to initially allocate 28 | /// enough bytes to store a length of audio at the given bitrate. 29 | pub fn apply_length_hint(config: &mut Config, hint: H, cost_per_sec: usize) 30 | where 31 | H: Into, 32 | { 33 | config.length_hint = Some(match hint.into() { 34 | LengthHint::Bytes(a) => a, 35 | LengthHint::Time(t) => { 36 | let s = t.as_secs() + u64::from(t.subsec_millis() > 0); 37 | (s as usize) * cost_per_sec 38 | }, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/input/adapters/cached/memory.rs: -------------------------------------------------------------------------------- 1 | use super::{default_config, raw_cost_per_sec, Error}; 2 | use crate::input::{AudioStream, Input, LiveInput}; 3 | use std::io::{Read, Result as IoResult, Seek}; 4 | use streamcatcher::{Catcher, Config}; 5 | use symphonia_core::io::MediaSource; 6 | 7 | /// A wrapper around an existing [`Input`] which caches its data 8 | /// in memory. 9 | /// 10 | /// The main purpose of this wrapper is to enable fast seeking on 11 | /// incompatible sources (i.e., HTTP streams) and to ease resource 12 | /// consumption for commonly reused/shared tracks. 13 | /// 14 | /// This consumes exactly as many bytes of memory as the input stream contains. 15 | /// 16 | /// [`Input`]: Input 17 | #[derive(Clone)] 18 | pub struct Memory { 19 | /// Inner shared bytestore. 20 | pub raw: Catcher>, 21 | } 22 | 23 | impl Memory { 24 | /// Wrap an existing [`Input`] with an in-memory store with the same codec and framing. 25 | /// 26 | /// [`Input`]: Input 27 | pub async fn new(source: Input) -> Result { 28 | Self::with_config(source, None).await 29 | } 30 | 31 | /// Wrap an existing [`Input`] with an in-memory store with the same codec and framing. 32 | /// 33 | /// `length_hint` may be used to control the size of the initial chunk, preventing 34 | /// needless allocations and copies. 35 | /// 36 | /// [`Input`]: Input 37 | pub async fn with_config(source: Input, config: Option) -> Result { 38 | let input = match source { 39 | Input::Lazy(mut r) => { 40 | let created = if r.should_create_async() { 41 | r.create_async().await 42 | } else { 43 | tokio::task::spawn_blocking(move || r.create()).await? 44 | }; 45 | 46 | created.map(|v| v.input).map_err(Error::from) 47 | }, 48 | Input::Live(LiveInput::Raw(a), _rec) => Ok(a.input), 49 | Input::Live(LiveInput::Wrapped(a), _rec) => 50 | Ok(Box::new(a.input) as Box), 51 | Input::Live(LiveInput::Parsed(_), _) => Err(Error::StreamNotAtStart), 52 | }?; 53 | 54 | let cost_per_sec = raw_cost_per_sec(true); 55 | 56 | let config = config.unwrap_or_else(|| default_config(cost_per_sec)); 57 | 58 | // TODO: apply length hint. 59 | // if config.length_hint.is_none() { 60 | // if let Some(dur) = metadata.duration { 61 | // apply_length_hint(&mut config, dur, cost_per_sec); 62 | // } 63 | // } 64 | 65 | let raw = config.build(input)?; 66 | 67 | Ok(Self { raw }) 68 | } 69 | 70 | /// Acquire a new handle to this object, creating a new 71 | /// view of the existing cached data from the beginning. 72 | #[must_use] 73 | pub fn new_handle(&self) -> Self { 74 | Self { 75 | raw: self.raw.new_handle(), 76 | } 77 | } 78 | } 79 | 80 | impl Read for Memory { 81 | fn read(&mut self, buf: &mut [u8]) -> IoResult { 82 | self.raw.read(buf) 83 | } 84 | } 85 | 86 | impl Seek for Memory { 87 | fn seek(&mut self, pos: std::io::SeekFrom) -> IoResult { 88 | self.raw.seek(pos) 89 | } 90 | } 91 | 92 | impl MediaSource for Memory { 93 | fn is_seekable(&self) -> bool { 94 | true 95 | } 96 | 97 | fn byte_len(&self) -> Option { 98 | if self.raw.is_finished() { 99 | Some(self.raw.len() as u64) 100 | } else { 101 | None 102 | } 103 | } 104 | } 105 | 106 | impl From for Input { 107 | fn from(val: Memory) -> Input { 108 | let input = Box::new(val); 109 | Input::Live(LiveInput::Raw(AudioStream { input, hint: None }), None) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/input/adapters/cached/mod.rs: -------------------------------------------------------------------------------- 1 | //! In-memory, shared input sources for reuse between calls, fast seeking, and 2 | //! direct Opus frame passthrough. 3 | 4 | mod compressed; 5 | mod decompressed; 6 | mod error; 7 | mod hint; 8 | mod memory; 9 | mod util; 10 | 11 | pub(crate) use self::util::*; 12 | pub use self::{compressed::*, decompressed::*, error::*, hint::*, memory::*}; 13 | 14 | use crate::constants::*; 15 | use crate::input::utils; 16 | use audiopus::Bitrate; 17 | use std::{mem, time::Duration}; 18 | use streamcatcher::{Config as ScConfig, GrowthStrategy}; 19 | 20 | /// Estimates the cost, in B/s, of audio data compressed at the given bitrate. 21 | #[must_use] 22 | pub fn compressed_cost_per_sec(bitrate: Bitrate) -> usize { 23 | let framing_cost_per_sec = AUDIO_FRAME_RATE * mem::size_of::(); 24 | 25 | let bitrate_raw = match bitrate { 26 | Bitrate::BitsPerSecond(i) => i, 27 | Bitrate::Auto => 64_000, 28 | Bitrate::Max => 512_000, 29 | } as usize; 30 | 31 | (bitrate_raw / 8) + framing_cost_per_sec 32 | } 33 | 34 | /// Calculates the cost, in B/s, of raw floating-point audio data. 35 | #[must_use] 36 | pub fn raw_cost_per_sec(stereo: bool) -> usize { 37 | utils::timestamp_to_byte_count(Duration::from_secs(1), stereo) 38 | } 39 | 40 | /// Provides the default config used by a cached source. 41 | /// 42 | /// This maps to the default configuration in [`streamcatcher`], using 43 | /// a constant chunk size of 5s worth of audio at the given bitrate estimate. 44 | /// 45 | /// [`streamcatcher`]: https://docs.rs/streamcatcher/0.1.0/streamcatcher/struct.Config.html 46 | #[must_use] 47 | pub fn default_config(cost_per_sec: usize) -> ScConfig { 48 | ScConfig::new().chunk_size(GrowthStrategy::Constant(5 * cost_per_sec)) 49 | } 50 | -------------------------------------------------------------------------------- /src/input/adapters/child.rs: -------------------------------------------------------------------------------- 1 | use crate::input::{AudioStream, Input, LiveInput}; 2 | use std::{ 3 | io::{Read, Result as IoResult}, 4 | mem, 5 | process::Child, 6 | }; 7 | use symphonia_core::io::{MediaSource, ReadOnlySource}; 8 | use tokio::runtime::Handle; 9 | use tracing::debug; 10 | 11 | /// Handle for a child process which ensures that any subprocesses are properly closed 12 | /// on drop. 13 | /// 14 | /// # Warning 15 | /// To allow proper cleanup of child processes, if you create a process chain you must 16 | /// make sure to use `From>`. Here, the *last* process in the `Vec` will be 17 | /// used as the audio byte source. 18 | #[derive(Debug)] 19 | pub struct ChildContainer(pub Vec); 20 | 21 | impl Read for ChildContainer { 22 | fn read(&mut self, buffer: &mut [u8]) -> IoResult { 23 | match self.0.last_mut() { 24 | Some(ref mut child) => child.stdout.as_mut().unwrap().read(buffer), 25 | None => Ok(0), 26 | } 27 | } 28 | } 29 | 30 | impl ChildContainer { 31 | /// Create a new [`ChildContainer`] from a child process 32 | #[must_use] 33 | pub fn new(children: Vec) -> Self { 34 | Self(children) 35 | } 36 | } 37 | 38 | impl From for ChildContainer { 39 | fn from(container: Child) -> Self { 40 | Self(vec![container]) 41 | } 42 | } 43 | 44 | impl From> for ChildContainer { 45 | fn from(container: Vec) -> Self { 46 | Self(container) 47 | } 48 | } 49 | 50 | impl From for Input { 51 | fn from(val: ChildContainer) -> Self { 52 | let audio_stream = AudioStream { 53 | input: Box::new(ReadOnlySource::new(val)) as Box, 54 | hint: None, 55 | }; 56 | Input::Live(LiveInput::Raw(audio_stream), None) 57 | } 58 | } 59 | 60 | impl Drop for ChildContainer { 61 | fn drop(&mut self) { 62 | let children = mem::take(&mut self.0); 63 | 64 | if let Ok(handle) = Handle::try_current() { 65 | handle.spawn_blocking(move || { 66 | cleanup_child_processes(children); 67 | }); 68 | } else { 69 | cleanup_child_processes(children); 70 | } 71 | } 72 | } 73 | 74 | fn cleanup_child_processes(mut children: Vec) { 75 | let attempt = if let Some(child) = children.last_mut() { 76 | child.kill() 77 | } else { 78 | return; 79 | }; 80 | 81 | let attempt = attempt.and_then(|()| { 82 | children 83 | .iter_mut() 84 | .rev() 85 | .try_for_each(|child| child.wait().map(|_| ())) 86 | }); 87 | 88 | if let Err(e) = attempt { 89 | debug!("Error awaiting child process: {:?}", e); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/input/adapters/mod.rs: -------------------------------------------------------------------------------- 1 | mod async_adapter; 2 | pub mod cached; 3 | mod child; 4 | mod raw_adapter; 5 | 6 | pub use self::{async_adapter::*, child::*, raw_adapter::*}; 7 | -------------------------------------------------------------------------------- /src/input/adapters/raw_adapter.rs: -------------------------------------------------------------------------------- 1 | use crate::input::{AudioStream, Input, LiveInput}; 2 | use byteorder::{LittleEndian, WriteBytesExt}; 3 | use std::io::{ErrorKind as IoErrorKind, Read, Result as IoResult, Seek, SeekFrom, Write}; 4 | use symphonia::core::io::MediaSource; 5 | 6 | // format header is a magic string, followed by two LE u32s (sample rate, channel count) 7 | const FMT_HEADER: &[u8; 16] = b"SbirdRaw\0\0\0\0\0\0\0\0"; 8 | 9 | /// Adapter around a raw, interleaved, `f32` PCM byte stream. 10 | /// 11 | /// This may be used to port legacy songbird audio sources to be compatible with 12 | /// the symphonia backend, particularly those with unknown length (making WAV 13 | /// unsuitable). 14 | /// 15 | /// The format is described in [`RawReader`]. 16 | /// 17 | /// [`RawReader`]: crate::input::codecs::RawReader 18 | pub struct RawAdapter { 19 | prepend: [u8; 16], 20 | inner: A, 21 | pos: u64, 22 | } 23 | 24 | impl RawAdapter { 25 | /// Wrap an input PCM byte source to be readable by symphonia. 26 | pub fn new(audio_source: A, sample_rate: u32, channel_count: u32) -> Self { 27 | let mut prepend: [u8; 16] = *FMT_HEADER; 28 | let mut write_space = &mut prepend[8..]; 29 | 30 | write_space 31 | .write_u32::(sample_rate) 32 | .expect("Prepend buffer is sized to include enough space for sample rate."); 33 | write_space 34 | .write_u32::(channel_count) 35 | .expect("Prepend buffer is sized to include enough space for number of channels."); 36 | 37 | Self { 38 | prepend, 39 | inner: audio_source, 40 | pos: 0, 41 | } 42 | } 43 | } 44 | 45 | impl Read for RawAdapter { 46 | fn read(&mut self, mut buf: &mut [u8]) -> IoResult { 47 | let out = if self.pos < self.prepend.len() as u64 { 48 | let upos = self.pos as usize; 49 | let remaining = self.prepend.len() - upos; 50 | let to_write = buf.len().min(remaining); 51 | 52 | buf.write(&self.prepend[upos..][..to_write]) 53 | } else { 54 | self.inner.read(buf) 55 | }; 56 | 57 | if let Ok(n) = out { 58 | self.pos += n as u64; 59 | } 60 | 61 | out 62 | } 63 | } 64 | 65 | impl Seek for RawAdapter { 66 | fn seek(&mut self, pos: SeekFrom) -> IoResult { 67 | if self.is_seekable() { 68 | let target_pos = match pos { 69 | SeekFrom::Start(p) => p, 70 | SeekFrom::End(_) => return Err(IoErrorKind::Unsupported.into()), 71 | SeekFrom::Current(p) if p.unsigned_abs() > self.pos => 72 | return Err(IoErrorKind::InvalidInput.into()), 73 | SeekFrom::Current(p) => (self.pos as i64 + p) as u64, 74 | }; 75 | 76 | let out = if target_pos as usize <= self.prepend.len() { 77 | self.inner.rewind().map(|()| 0) 78 | } else { 79 | self.inner.seek(SeekFrom::Start(target_pos)) 80 | }; 81 | 82 | match out { 83 | Ok(0) => self.pos = target_pos, 84 | Ok(a) => self.pos = a + self.prepend.len() as u64, 85 | _ => {}, 86 | } 87 | 88 | out.map(|_| self.pos) 89 | } else { 90 | Err(IoErrorKind::Unsupported.into()) 91 | } 92 | } 93 | } 94 | 95 | impl MediaSource for RawAdapter { 96 | fn is_seekable(&self) -> bool { 97 | self.inner.is_seekable() 98 | } 99 | 100 | fn byte_len(&self) -> Option { 101 | self.inner.byte_len().map(|m| m + self.prepend.len() as u64) 102 | } 103 | } 104 | 105 | impl From> for Input { 106 | fn from(val: RawAdapter) -> Self { 107 | let live = LiveInput::Raw(AudioStream { 108 | input: Box::new(val), 109 | hint: None, 110 | }); 111 | 112 | Input::Live(live, None) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/input/audiostream.rs: -------------------------------------------------------------------------------- 1 | use symphonia_core::probe::Hint; 2 | 3 | /// An unread byte stream for an audio file. 4 | pub struct AudioStream { 5 | /// The wrapped file stream. 6 | /// 7 | /// An input stream *must not* have been read into past the start of the 8 | /// audio container's header. 9 | pub input: T, 10 | /// Extension and MIME type information which may help guide format selection. 11 | pub hint: Option, 12 | } 13 | -------------------------------------------------------------------------------- /src/input/codecs/dca/metadata.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Deserialize, Serialize)] 4 | pub struct DcaMetadata { 5 | pub dca: DcaInfo, 6 | pub opus: Opus, 7 | pub info: Option, 8 | pub origin: Option, 9 | pub extra: Option, 10 | } 11 | 12 | #[derive(Debug, Deserialize, Serialize)] 13 | pub struct DcaInfo { 14 | pub version: u64, 15 | pub tool: Tool, 16 | } 17 | 18 | #[derive(Debug, Deserialize, Serialize)] 19 | pub struct Tool { 20 | pub name: String, 21 | pub version: String, 22 | pub url: Option, 23 | pub author: Option, 24 | } 25 | 26 | #[derive(Debug, Deserialize, Serialize)] 27 | pub struct Opus { 28 | pub mode: String, 29 | pub sample_rate: u32, 30 | pub frame_size: u64, 31 | pub abr: Option, 32 | pub vbr: bool, 33 | pub channels: u8, 34 | } 35 | 36 | #[derive(Debug, Deserialize, Serialize)] 37 | pub struct Info { 38 | pub title: Option, 39 | pub artist: Option, 40 | pub album: Option, 41 | pub genre: Option, 42 | pub cover: Option, 43 | pub comments: Option, 44 | } 45 | 46 | #[derive(Debug, Deserialize, Serialize)] 47 | pub struct Origin { 48 | pub source: Option, 49 | pub abr: Option, 50 | pub channels: Option, 51 | pub encoding: Option, 52 | pub url: Option, 53 | } 54 | -------------------------------------------------------------------------------- /src/input/codecs/mod.rs: -------------------------------------------------------------------------------- 1 | //! Codec registries extending Symphonia's probe and registry formats with Opus and DCA support. 2 | 3 | pub(crate) mod dca; 4 | mod opus; 5 | mod raw; 6 | 7 | use std::sync::OnceLock; 8 | 9 | pub use self::{dca::DcaReader, opus::OpusDecoder, raw::*}; 10 | use symphonia::{ 11 | core::{codecs::CodecRegistry, probe::Probe}, 12 | default::*, 13 | }; 14 | 15 | /// Default Symphonia [`CodecRegistry`], including the (audiopus-backed) Opus codec. 16 | pub fn get_codec_registry() -> &'static CodecRegistry { 17 | static CODEC_REGISTRY: OnceLock = OnceLock::new(); 18 | CODEC_REGISTRY.get_or_init(|| { 19 | let mut registry = CodecRegistry::new(); 20 | register_enabled_codecs(&mut registry); 21 | registry.register_all::(); 22 | registry 23 | }) 24 | } 25 | 26 | /// Default Symphonia Probe, including DCA format support. 27 | pub fn get_probe() -> &'static Probe { 28 | static PROBE: OnceLock = OnceLock::new(); 29 | PROBE.get_or_init(|| { 30 | let mut probe = Probe::default(); 31 | probe.register_all::(); 32 | probe.register_all::(); 33 | register_enabled_formats(&mut probe); 34 | probe 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/input/compose.rs: -------------------------------------------------------------------------------- 1 | use super::{AudioStream, AudioStreamError, AuxMetadata}; 2 | 3 | use symphonia_core::io::MediaSource; 4 | 5 | /// Data and behaviour required to instantiate a lazy audio source. 6 | #[async_trait::async_trait] 7 | pub trait Compose: Send { 8 | /// Create a source synchronously. 9 | /// 10 | /// If [`should_create_async`] returns `false`, this method will chosen at runtime. 11 | /// 12 | /// [`should_create_async`]: Self::should_create_async 13 | fn create(&mut self) -> Result>, AudioStreamError>; 14 | 15 | /// Create a source asynchronously. 16 | /// 17 | /// If [`should_create_async`] returns `true`, this method will chosen at runtime. 18 | /// 19 | /// [`should_create_async`]: Self::should_create_async 20 | async fn create_async(&mut self) 21 | -> Result>, AudioStreamError>; 22 | 23 | /// Determines whether this source will be instantiated using [`create`] or [`create_async`]. 24 | /// 25 | /// Songbird will create the audio stream using either a dynamically sized thread pool, 26 | /// or a task on the async runtime it was spawned in respectively. Users do not need to 27 | /// support both these methods. 28 | /// 29 | /// [`create_async`]: Self::create_async 30 | /// [`create`]: Self::create 31 | fn should_create_async(&self) -> bool; 32 | 33 | /// Requests auxiliary metadata which can be accessed without parsing the file. 34 | /// 35 | /// This method will never be called by songbird but allows, for instance, access to metadata 36 | /// which might only be visible to a web crawler e.g., uploader or source URL. 37 | async fn aux_metadata(&mut self) -> Result { 38 | Err(AudioStreamError::Unsupported) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/input/input_tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use crate::{ 4 | driver::Driver, 5 | tracks::{PlayMode, ReadyState, Track}, 6 | Config, 7 | }; 8 | 9 | use std::time::Duration; 10 | 11 | pub async fn track_plays_passthrough(make_track: F) 12 | where 13 | T: Into, 14 | F: FnOnce() -> T, 15 | { 16 | track_plays_base(make_track, true, None).await; 17 | } 18 | 19 | pub async fn track_plays_passthrough_when_is_only_active(make_track: F) 20 | where 21 | T: Into, 22 | F: FnOnce() -> T, 23 | { 24 | track_plays_base( 25 | make_track, 26 | true, 27 | Some(include_bytes!("../../resources/loop.wav")), 28 | ) 29 | .await; 30 | } 31 | 32 | pub async fn track_plays_mixed(make_track: F) 33 | where 34 | T: Into, 35 | F: FnOnce() -> T, 36 | { 37 | track_plays_base(make_track, false, None).await; 38 | } 39 | 40 | pub async fn track_plays_base( 41 | make_track: F, 42 | passthrough: bool, 43 | dummy_track: Option<&'static [u8]>, 44 | ) where 45 | T: Into, 46 | F: FnOnce() -> T, 47 | { 48 | let (t_handle, config) = Config::test_cfg(true); 49 | let mut driver = Driver::new(config.clone()); 50 | 51 | // Used to ensure that paused tracks won't prevent passthrough from happening 52 | // i.e., most queue users :) 53 | if let Some(audio_data) = dummy_track { 54 | driver.play(Track::from(audio_data).pause()); 55 | } 56 | 57 | let file = make_track(); 58 | 59 | // Get input in place, playing. Wait for IO to ready. 60 | t_handle.ready_track(&driver.play(file.into()), None).await; 61 | t_handle.tick(1); 62 | 63 | // post-conditions: 64 | // 1) track produces a packet. 65 | // 2) that packet is passthrough/mixed when we expect them to. 66 | let pkt = t_handle.recv_async().await; 67 | let pkt = pkt.raw().unwrap(); 68 | 69 | if passthrough { 70 | assert!(pkt.is_passthrough()); 71 | } else { 72 | assert!(pkt.is_mixed_with_nonzero_signal()); 73 | } 74 | } 75 | 76 | pub async fn forward_seek_correct(make_track: F) 77 | where 78 | T: Into, 79 | F: FnOnce() -> T, 80 | { 81 | let (t_handle, config) = Config::test_cfg(true); 82 | let mut driver = Driver::new(config.clone()); 83 | 84 | let file = make_track(); 85 | let handle = driver.play(file.into()); 86 | 87 | // Get input in place, playing. Wait for IO to ready. 88 | t_handle.ready_track(&handle, None).await; 89 | 90 | let target_time = Duration::from_secs(30); 91 | assert!(!handle.seek(target_time).is_hung_up()); 92 | t_handle.ready_track(&handle, None).await; 93 | 94 | // post-conditions: 95 | // 1) track is readied 96 | // 2) track's position is approx 30s 97 | // 3) track's play time is considerably less (O(5s)) 98 | let state = handle.get_info(); 99 | t_handle.spawn_ticker(); 100 | let state = state.await.expect("Should have received valid state."); 101 | 102 | assert_eq!(state.ready, ReadyState::Playable); 103 | assert_eq!(state.playing, PlayMode::Play); 104 | assert!(state.play_time < Duration::from_secs(5)); 105 | assert!( 106 | state.position < target_time + Duration::from_millis(100) 107 | && state.position > target_time - Duration::from_millis(100) 108 | ); 109 | } 110 | 111 | pub async fn backward_seek_correct(make_track: F) 112 | where 113 | T: Into, 114 | F: FnOnce() -> T, 115 | { 116 | let (t_handle, config) = Config::test_cfg(true); 117 | let mut driver = Driver::new(config.clone()); 118 | 119 | let file = make_track(); 120 | let handle = driver.play(file.into()); 121 | 122 | // Get input in place, playing. Wait for IO to ready. 123 | t_handle.ready_track(&handle, None).await; 124 | 125 | // Accelerated playout -- 4 seconds worth. 126 | let n_secs = 4; 127 | let n_ticks = 50 * n_secs; 128 | t_handle.skip(n_ticks).await; 129 | 130 | let target_time = Duration::from_secs(1); 131 | assert!(!handle.seek(target_time).is_hung_up()); 132 | t_handle.ready_track(&handle, None).await; 133 | 134 | // post-conditions: 135 | // 1) track is readied 136 | // 2) track's position is approx 1s 137 | // 3) track's play time is preserved (About 4s) 138 | let state = handle.get_info(); 139 | t_handle.spawn_ticker(); 140 | let state = state.await.expect("Should have received valid state."); 141 | 142 | assert_eq!(state.ready, ReadyState::Playable); 143 | assert_eq!(state.playing, PlayMode::Play); 144 | assert!(state.play_time >= Duration::from_secs(n_secs)); 145 | assert!( 146 | state.position < target_time + Duration::from_millis(100) 147 | && state.position > target_time - Duration::from_millis(100) 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/input/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | //! Metadata formats specific to [`crate::input::Compose`] types. 2 | 3 | use crate::error::JsonError; 4 | use std::time::Duration; 5 | use symphonia_core::{meta::Metadata as ContainerMetadata, probe::ProbedMetadata}; 6 | 7 | pub(crate) mod ffprobe; 8 | mod ytdl; 9 | pub use ytdl::Output as YoutubeDlOutput; 10 | 11 | use super::Parsed; 12 | 13 | /// Extra information about an [`Input`] which is acquired without 14 | /// parsing the file itself (e.g., from a webpage). 15 | /// 16 | /// You can access this via [`Input::aux_metadata`] and [`Compose::aux_metadata`]. 17 | /// 18 | /// [`Input`]: crate::input::Input 19 | /// [`Input::aux_metadata`]: crate::input::Input::aux_metadata 20 | /// [`Compose::aux_metadata`]: crate::input::Compose::aux_metadata 21 | #[derive(Clone, Debug, Default, Eq, PartialEq)] 22 | pub struct AuxMetadata { 23 | /// The track name of this stream. 24 | pub track: Option, 25 | /// The main artist of this stream. 26 | pub artist: Option, 27 | /// The album name of this stream. 28 | pub album: Option, 29 | /// The date of creation of this stream. 30 | pub date: Option, 31 | 32 | /// The number of audio channels in this stream. 33 | pub channels: Option, 34 | /// The YouTube channel of this stream. 35 | pub channel: Option, 36 | /// The time at which the first true sample is played back. 37 | /// 38 | /// This occurs as an artefact of coder delay. 39 | pub start_time: Option, 40 | /// The reported duration of this stream. 41 | pub duration: Option, 42 | /// The sample rate of this stream. 43 | pub sample_rate: Option, 44 | /// The source url of this stream. 45 | pub source_url: Option, 46 | /// The YouTube title of this stream. 47 | pub title: Option, 48 | /// The thumbnail url of this stream. 49 | pub thumbnail: Option, 50 | } 51 | 52 | impl AuxMetadata { 53 | /// Extract metadata and details from the output of `ffprobe -of json`. 54 | pub fn from_ffprobe_json(value: &mut [u8]) -> Result { 55 | let output: ffprobe::Output = serde_json::from_slice(value)?; 56 | 57 | Ok(output.into_aux_metadata()) 58 | } 59 | 60 | /// Move all fields from an [`AuxMetadata`] object into a new one. 61 | #[must_use] 62 | pub fn take(&mut self) -> Self { 63 | Self { 64 | track: self.track.take(), 65 | artist: self.artist.take(), 66 | album: self.album.take(), 67 | date: self.date.take(), 68 | channels: self.channels.take(), 69 | channel: self.channel.take(), 70 | start_time: self.start_time.take(), 71 | duration: self.duration.take(), 72 | sample_rate: self.sample_rate.take(), 73 | source_url: self.source_url.take(), 74 | title: self.title.take(), 75 | thumbnail: self.thumbnail.take(), 76 | } 77 | } 78 | } 79 | 80 | /// In-stream information about an [`Input`] acquired by parsing an audio file. 81 | /// 82 | /// To access this, the [`Input`] must be made live and parsed by symphonia. To do 83 | /// this, you can: 84 | /// * Pre-process the track in your own code using [`Input::make_playable`], and 85 | /// then [`Input::metadata`]. 86 | /// * Use [`TrackHandle::action`] to access the track's metadata via [`View`], 87 | /// *if the track has started or been made playable*. 88 | /// 89 | /// You probably want to use [`AuxMetadata`] instead; this requires a live track, 90 | /// which has higher memory use for buffers etc. 91 | /// 92 | /// [`Input`]: crate::input::Input 93 | /// [`Input::make_playable`]: super::Input::make_playable 94 | /// [`Input::metadata`]: super::Input::metadata 95 | /// [`TrackHandle::action`]: crate::tracks::TrackHandle::action 96 | /// [`View`]: crate::tracks::View 97 | pub struct Metadata<'a> { 98 | /// Metadata found while probing for the format of an [`Input`] (e.g., ID3 tags). 99 | /// 100 | /// [`Input`]: crate::input::Input 101 | pub probe: &'a mut ProbedMetadata, 102 | /// Metadata found inside the format/container of an audio stream. 103 | pub format: ContainerMetadata<'a>, 104 | } 105 | 106 | impl<'a> From<&'a mut Parsed> for Metadata<'a> { 107 | fn from(val: &'a mut Parsed) -> Self { 108 | Metadata { 109 | probe: &mut val.meta, 110 | format: val.format.metadata(), 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/input/metadata/ytdl.rs: -------------------------------------------------------------------------------- 1 | //! `YoutubeDl` track metadata. 2 | 3 | use super::AuxMetadata; 4 | use crate::constants::SAMPLE_RATE_RAW; 5 | use serde::{Deserialize, Serialize}; 6 | use std::{collections::HashMap, time::Duration}; 7 | 8 | /// Information returned by yt-dlp about a URL. 9 | /// 10 | /// Returned by [`crate::input::YoutubeDl::query`]. 11 | #[derive(Deserialize, Serialize, Debug)] 12 | pub struct Output { 13 | /// The main artist. 14 | pub artist: Option, 15 | /// The album name. 16 | pub album: Option, 17 | /// The channel name. 18 | pub channel: Option, 19 | /// The duration of the stream in seconds. 20 | pub duration: Option, 21 | /// The size of the stream. 22 | pub filesize: Option, 23 | /// Required HTTP headers to fetch the track stream. 24 | pub http_headers: Option>, 25 | /// Release date of this track. 26 | pub release_date: Option, 27 | /// The thumbnail URL for this track. 28 | pub thumbnail: Option, 29 | /// The title of this track. 30 | pub title: Option, 31 | /// The track name. 32 | pub track: Option, 33 | /// The date this track was uploaded on. 34 | pub upload_date: Option, 35 | /// The name of the uploader. 36 | pub uploader: Option, 37 | /// The stream URL. 38 | pub url: String, 39 | /// The URL of the public-facing webpage for this track. 40 | pub webpage_url: Option, 41 | /// The stream protocol. 42 | pub protocol: Option, 43 | } 44 | 45 | impl Output { 46 | /// Requests auxiliary metadata which can be accessed without parsing the file. 47 | pub fn as_aux_metadata(&self) -> AuxMetadata { 48 | let album = self.album.clone(); 49 | let track = self.track.clone(); 50 | let true_artist = self.artist.as_ref(); 51 | let artist = true_artist.or(self.uploader.as_ref()).cloned(); 52 | let r_date = self.release_date.as_ref(); 53 | let date = r_date.or(self.upload_date.as_ref()).cloned(); 54 | let channel = self.channel.clone(); 55 | let duration = self.duration.map(Duration::from_secs_f64); 56 | let source_url = self.webpage_url.clone(); 57 | let title = self.title.clone(); 58 | let thumbnail = self.thumbnail.clone(); 59 | 60 | AuxMetadata { 61 | track, 62 | artist, 63 | album, 64 | date, 65 | 66 | channels: Some(2), 67 | channel, 68 | duration, 69 | sample_rate: Some(SAMPLE_RATE_RAW as u32), 70 | source_url, 71 | title, 72 | thumbnail, 73 | 74 | ..AuxMetadata::default() 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/input/parsed.rs: -------------------------------------------------------------------------------- 1 | use symphonia_core::{codecs::Decoder, formats::FormatReader, probe::ProbedMetadata}; 2 | 3 | /// An audio file which has had its headers parsed and decoder state built. 4 | pub struct Parsed { 5 | /// Audio packet, seeking, and state access for all tracks in a file. 6 | /// 7 | /// This may be used to access packets one at a time from the input file. 8 | /// Additionally, this exposes container-level and per track metadata which 9 | /// have been extracted. 10 | pub format: Box, 11 | 12 | /// Decoder state for the chosen track. 13 | pub decoder: Box, 14 | 15 | /// The chosen track's ID. 16 | /// 17 | /// This is required to identify the correct packet stream inside the container. 18 | pub track_id: u32, 19 | 20 | /// Metadata extracted by symphonia while detecting a file's format. 21 | /// 22 | /// Typically, this detects metadata *outside* the file's core format (i.e., 23 | /// ID3 tags in MP3 and WAV files). 24 | pub meta: ProbedMetadata, 25 | 26 | /// Whether the contained format supports arbitrary seeking. 27 | /// 28 | /// If set to false, Songbird will attempt to recreate the input if 29 | /// it must seek backwards. 30 | pub supports_backseek: bool, 31 | } 32 | -------------------------------------------------------------------------------- /src/input/sources/file.rs: -------------------------------------------------------------------------------- 1 | use crate::input::{AudioStream, AudioStreamError, Compose, Input}; 2 | use std::{error::Error, ffi::OsStr, path::Path}; 3 | use symphonia_core::{io::MediaSource, probe::Hint}; 4 | 5 | /// A lazily instantiated local file. 6 | #[derive(Clone, Debug)] 7 | pub struct File> { 8 | path: P, 9 | } 10 | 11 | impl> File

{ 12 | /// Creates a lazy file object, which will open the target path. 13 | /// 14 | /// This is infallible as the path is only checked during creation. 15 | pub fn new(path: P) -> Self { 16 | Self { path } 17 | } 18 | } 19 | 20 | impl + Send + Sync + 'static> From> for Input { 21 | fn from(val: File

) -> Self { 22 | Input::Lazy(Box::new(val)) 23 | } 24 | } 25 | 26 | #[async_trait::async_trait] 27 | impl + Send + Sync> Compose for File

{ 28 | fn create(&mut self) -> Result>, AudioStreamError> { 29 | let err: Box = 30 | "Files should be created asynchronously.".to_string().into(); 31 | Err(AudioStreamError::Fail(err)) 32 | } 33 | 34 | async fn create_async( 35 | &mut self, 36 | ) -> Result>, AudioStreamError> { 37 | let file = tokio::fs::File::open(&self.path) 38 | .await 39 | .map_err(|io| AudioStreamError::Fail(Box::new(io)))?; 40 | 41 | let input = Box::new(file.into_std().await); 42 | 43 | let mut hint = Hint::default(); 44 | if let Some(ext) = self.path.as_ref().extension().and_then(OsStr::to_str) { 45 | hint.with_extension(ext); 46 | } 47 | 48 | Ok(AudioStream { 49 | input, 50 | hint: Some(hint), 51 | }) 52 | } 53 | 54 | fn should_create_async(&self) -> bool { 55 | true 56 | } 57 | 58 | // SEE: issue #186 59 | // Below is removed due to issues with: 60 | // 1) deadlocks on small files. 61 | // 2) serde_aux poorly handles missing field names. 62 | // 63 | 64 | // Probes for metadata about this audio file using `ffprobe`. 65 | // async fn aux_metadata(&mut self) -> Result { 66 | // let args = [ 67 | // "-v", 68 | // "quiet", 69 | // "-of", 70 | // "json", 71 | // "-show_format", 72 | // "-show_streams", 73 | // "-i", 74 | // ]; 75 | 76 | // let mut output = Command::new("ffprobe") 77 | // .args(args) 78 | // .arg(self.path.as_ref().as_os_str()) 79 | // .output() 80 | // .await 81 | // .map_err(|e| AudioStreamError::Fail(Box::new(e)))?; 82 | 83 | // AuxMetadata::from_ffprobe_json(&mut output.stdout[..]) 84 | // .map_err(|e| AudioStreamError::Fail(Box::new(e))) 85 | // } 86 | } 87 | -------------------------------------------------------------------------------- /src/input/sources/hls.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use bytes::Bytes; 3 | use futures::StreamExt; 4 | use reqwest::{header::HeaderMap, Client}; 5 | use stream_lib::Event; 6 | use symphonia_core::io::MediaSource; 7 | use tokio_util::io::StreamReader; 8 | 9 | use crate::input::{ 10 | AsyncAdapterStream, 11 | AsyncReadOnlySource, 12 | AudioStream, 13 | AudioStreamError, 14 | Compose, 15 | Input, 16 | }; 17 | 18 | /// Lazy HLS stream 19 | /// 20 | /// # Note: 21 | /// 22 | /// `Compose::create` for this struct panics if called outside of a 23 | /// tokio executor since it uses background tasks. 24 | #[derive(Debug)] 25 | pub struct HlsRequest { 26 | /// HTTP client 27 | client: Client, 28 | /// URL of hls playlist 29 | request: String, 30 | /// Headers of the request 31 | headers: HeaderMap, 32 | } 33 | 34 | impl HlsRequest { 35 | #[must_use] 36 | /// Create a lazy HLS request. 37 | pub fn new(client: Client, request: String) -> Self { 38 | Self::new_with_headers(client, request, HeaderMap::default()) 39 | } 40 | 41 | #[must_use] 42 | /// Create a lazy HTTP request. 43 | pub fn new_with_headers(client: Client, request: String, headers: HeaderMap) -> Self { 44 | HlsRequest { 45 | client, 46 | request, 47 | headers, 48 | } 49 | } 50 | 51 | fn create_stream(&mut self) -> Result { 52 | let request = self 53 | .client 54 | .get(&self.request) 55 | .headers(self.headers.clone()) 56 | .build() 57 | .map_err(|why| AudioStreamError::Fail(why.into()))?; 58 | 59 | let hls = stream_lib::download_hls(self.client.clone(), request, None); 60 | 61 | let stream = Box::new(StreamReader::new(hls.map(|ev| match ev { 62 | Event::Bytes { bytes } => Ok(bytes), 63 | Event::End => Ok(Bytes::new()), 64 | Event::Error { error } => Err(std::io::Error::new( 65 | std::io::ErrorKind::UnexpectedEof, 66 | error, 67 | )), 68 | }))); 69 | 70 | Ok(AsyncReadOnlySource { stream }) 71 | } 72 | } 73 | 74 | #[async_trait] 75 | impl Compose for HlsRequest { 76 | fn create(&mut self) -> Result>, AudioStreamError> { 77 | self.create_stream().map(|input| { 78 | let stream = AsyncAdapterStream::new(Box::new(input), 64 * 1024); 79 | 80 | AudioStream { 81 | input: Box::new(stream) as Box, 82 | hint: None, 83 | } 84 | }) 85 | } 86 | 87 | async fn create_async( 88 | &mut self, 89 | ) -> Result>, AudioStreamError> { 90 | self.create() 91 | } 92 | 93 | fn should_create_async(&self) -> bool { 94 | true 95 | } 96 | } 97 | 98 | impl From for Input { 99 | fn from(val: HlsRequest) -> Self { 100 | Input::Lazy(Box::new(val)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/input/sources/mod.rs: -------------------------------------------------------------------------------- 1 | mod file; 2 | mod hls; 3 | mod http; 4 | mod ytdl; 5 | 6 | pub use self::{file::*, hls::*, http::*, ytdl::*}; 7 | 8 | use std::{ 9 | io::{ErrorKind as IoErrorKind, Result as IoResult, SeekFrom}, 10 | pin::Pin, 11 | task::{Context, Poll}, 12 | }; 13 | 14 | use async_trait::async_trait; 15 | use pin_project::pin_project; 16 | use tokio::io::{AsyncRead, AsyncSeek, ReadBuf}; 17 | 18 | use crate::input::{AsyncMediaSource, AudioStreamError}; 19 | 20 | /// `AsyncReadOnlySource` wraps any source implementing [`tokio::io::AsyncRead`] in an unseekable 21 | /// [`symphonia_core::io::MediaSource`], similar to [`symphonia_core::io::ReadOnlySource`] 22 | #[pin_project] 23 | pub struct AsyncReadOnlySource { 24 | #[pin] 25 | stream: Box, 26 | } 27 | 28 | impl AsyncReadOnlySource { 29 | /// Instantiates a new `AsyncReadOnlySource` by taking ownership and wrapping the provided 30 | /// `Read`er. 31 | pub fn new(inner: R) -> Self 32 | where 33 | R: AsyncRead + Send + Sync + Unpin + 'static, 34 | { 35 | AsyncReadOnlySource { 36 | stream: Box::new(inner), 37 | } 38 | } 39 | 40 | /// Gets a reference to the underlying reader. 41 | #[allow(clippy::borrowed_box)] // FIXME: Changing this may break compatibility. remedy in v0.5 42 | #[must_use] 43 | pub fn get_ref(&self) -> &Box { 44 | &self.stream 45 | } 46 | 47 | /// Unwraps this `AsyncReadOnlySource`, returning the underlying reader. 48 | #[must_use] 49 | pub fn into_inner(self) -> Box { 50 | self.stream 51 | } 52 | } 53 | 54 | impl AsyncRead for AsyncReadOnlySource { 55 | fn poll_read( 56 | self: Pin<&mut Self>, 57 | cx: &mut Context<'_>, 58 | buf: &mut ReadBuf<'_>, 59 | ) -> Poll> { 60 | AsyncRead::poll_read(self.project().stream, cx, buf) 61 | } 62 | } 63 | 64 | impl AsyncSeek for AsyncReadOnlySource { 65 | fn start_seek(self: Pin<&mut Self>, _position: SeekFrom) -> IoResult<()> { 66 | Err(IoErrorKind::Unsupported.into()) 67 | } 68 | 69 | fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 70 | unreachable!() 71 | } 72 | } 73 | 74 | #[async_trait] 75 | impl AsyncMediaSource for AsyncReadOnlySource { 76 | fn is_seekable(&self) -> bool { 77 | false 78 | } 79 | 80 | async fn byte_len(&self) -> Option { 81 | None 82 | } 83 | 84 | async fn try_resume( 85 | &mut self, 86 | _offset: u64, 87 | ) -> Result, AudioStreamError> { 88 | Err(AudioStreamError::Unsupported) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/input/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility methods for seeking or decoding. 2 | 3 | use crate::constants::*; 4 | use audiopus::{coder::Decoder, Channels, Result as OpusResult, SampleRate}; 5 | use std::{mem, time::Duration}; 6 | 7 | /// Calculates the sample position in a `FloatPCM` stream from a timestamp. 8 | #[must_use] 9 | pub fn timestamp_to_sample_count(timestamp: Duration, stereo: bool) -> usize { 10 | ((timestamp.as_millis() as usize) * (MONO_FRAME_SIZE / FRAME_LEN_MS)) << stereo as usize 11 | } 12 | 13 | /// Calculates the time position in a `FloatPCM` stream from a sample index. 14 | #[must_use] 15 | pub fn sample_count_to_timestamp(amt: usize, stereo: bool) -> Duration { 16 | Duration::from_millis((((amt * FRAME_LEN_MS) / MONO_FRAME_SIZE) as u64) >> stereo as u64) 17 | } 18 | 19 | /// Calculates the byte position in a `FloatPCM` stream from a timestamp. 20 | /// 21 | /// Each sample is sized by `mem::size_of::() == 4usize`. 22 | #[must_use] 23 | pub fn timestamp_to_byte_count(timestamp: Duration, stereo: bool) -> usize { 24 | timestamp_to_sample_count(timestamp, stereo) * mem::size_of::() 25 | } 26 | 27 | /// Calculates the time position in a `FloatPCM` stream from a byte index. 28 | /// 29 | /// Each sample is sized by `mem::size_of::() == 4usize`. 30 | #[must_use] 31 | pub fn byte_count_to_timestamp(amt: usize, stereo: bool) -> Duration { 32 | sample_count_to_timestamp(amt / mem::size_of::(), stereo) 33 | } 34 | 35 | /// Create an Opus decoder outputting at a sample rate of 48kHz. 36 | pub fn decoder(stereo: bool) -> OpusResult { 37 | Decoder::new( 38 | SampleRate::Hz48000, 39 | if stereo { 40 | Channels::Stereo 41 | } else { 42 | Channels::Mono 43 | }, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/join.rs: -------------------------------------------------------------------------------- 1 | //! Future types for gateway interactions. 2 | 3 | #[cfg(feature = "driver")] 4 | use crate::error::ConnectionResult; 5 | use crate::{ 6 | error::{JoinError, JoinResult}, 7 | ConnectionInfo, 8 | }; 9 | use core::{ 10 | convert, 11 | future::Future, 12 | pin::Pin, 13 | task::{Context, Poll}, 14 | time::Duration, 15 | }; 16 | use flume::r#async::RecvFut; 17 | use pin_project::pin_project; 18 | use tokio::time::{self, Timeout}; 19 | 20 | #[cfg(feature = "driver")] 21 | /// Future for a call to [`Call::join`]. 22 | /// 23 | /// This future `await`s Discord's response *and* 24 | /// connection via the [`Driver`]. Both phases have 25 | /// separate timeouts and failure conditions. 26 | /// 27 | /// This future ***must not*** be `await`ed while 28 | /// holding the lock around a [`Call`]. 29 | /// 30 | /// [`Call::join`]: crate::Call::join 31 | /// [`Call`]: crate::Call 32 | /// [`Driver`]: crate::driver::Driver 33 | #[pin_project] 34 | pub struct Join { 35 | #[pin] 36 | gw: JoinClass<()>, 37 | #[pin] 38 | driver: JoinClass>, 39 | state: JoinState, 40 | } 41 | 42 | #[cfg(feature = "driver")] 43 | impl Join { 44 | pub(crate) fn new( 45 | driver: RecvFut<'static, ConnectionResult<()>>, 46 | gw_recv: RecvFut<'static, ()>, 47 | timeout: Option, 48 | ) -> Self { 49 | Self { 50 | gw: JoinClass::new(gw_recv, timeout), 51 | driver: JoinClass::new(driver, None), 52 | state: JoinState::BeforeGw, 53 | } 54 | } 55 | } 56 | 57 | #[cfg(feature = "driver")] 58 | impl Future for Join { 59 | type Output = JoinResult<()>; 60 | 61 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 62 | let this = self.project(); 63 | 64 | if *this.state == JoinState::BeforeGw { 65 | let poll = this.gw.poll(cx); 66 | match poll { 67 | Poll::Ready(a) if a.is_ok() => { 68 | *this.state = JoinState::AfterGw; 69 | }, 70 | Poll::Ready(a) => { 71 | *this.state = JoinState::Finalised; 72 | return Poll::Ready(a); 73 | }, 74 | Poll::Pending => return Poll::Pending, 75 | } 76 | } 77 | 78 | if *this.state == JoinState::AfterGw { 79 | let poll = this 80 | .driver 81 | .poll(cx) 82 | .map_ok(|res| res.map_err(JoinError::Driver)) 83 | .map(|res| res.and_then(convert::identity)); 84 | 85 | match poll { 86 | Poll::Ready(a) => { 87 | *this.state = JoinState::Finalised; 88 | return Poll::Ready(a); 89 | }, 90 | Poll::Pending => return Poll::Pending, 91 | } 92 | } 93 | 94 | Poll::Pending 95 | } 96 | } 97 | 98 | #[cfg(feature = "driver")] 99 | #[derive(Copy, Clone, Eq, PartialEq)] 100 | enum JoinState { 101 | BeforeGw, 102 | AfterGw, 103 | Finalised, 104 | } 105 | 106 | /// Future for a call to [`Call::join_gateway`]. 107 | /// 108 | /// This future `await`s Discord's gateway response, subject 109 | /// to any timeouts. 110 | /// 111 | /// This future ***must not*** be `await`ed while 112 | /// holding the lock around a [`Call`]. 113 | /// 114 | /// [`Call::join_gateway`]: crate::Call::join_gateway 115 | /// [`Call`]: crate::Call 116 | /// [`Driver`]: crate::driver::Driver 117 | #[pin_project] 118 | pub struct JoinGateway { 119 | #[pin] 120 | inner: JoinClass, 121 | } 122 | 123 | impl JoinGateway { 124 | pub(crate) fn new(recv: RecvFut<'static, ConnectionInfo>, timeout: Option) -> Self { 125 | Self { 126 | inner: JoinClass::new(recv, timeout), 127 | } 128 | } 129 | } 130 | 131 | impl Future for JoinGateway { 132 | type Output = JoinResult; 133 | 134 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 135 | self.project().inner.poll(cx) 136 | } 137 | } 138 | 139 | #[allow(clippy::large_enum_variant)] 140 | #[pin_project(project = JoinClassProj)] 141 | enum JoinClass { 142 | WithTimeout(#[pin] Timeout>), 143 | Vanilla(RecvFut<'static, T>), 144 | } 145 | 146 | impl JoinClass { 147 | pub(crate) fn new(recv: RecvFut<'static, T>, timeout: Option) -> Self { 148 | match timeout { 149 | Some(t) => JoinClass::WithTimeout(time::timeout(t, recv)), 150 | None => JoinClass::Vanilla(recv), 151 | } 152 | } 153 | } 154 | 155 | impl Future for JoinClass 156 | where 157 | T: Unpin, 158 | { 159 | type Output = JoinResult; 160 | 161 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 162 | match self.project() { 163 | JoinClassProj::WithTimeout(t) => t 164 | .poll(cx) 165 | .map_err(|_| JoinError::TimedOut) 166 | .map_ok(|res| res.map_err(|_| JoinError::Dropped)) 167 | .map(|m| m.and_then(convert::identity)), 168 | JoinClassProj::Vanilla(t) => Pin::new(t).poll(cx).map_err(|_| JoinError::Dropped), 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc( 2 | html_logo_url = "https://raw.githubusercontent.com/serenity-rs/songbird/current/songbird.png", 3 | html_favicon_url = "https://raw.githubusercontent.com/serenity-rs/songbird/current/songbird-ico.png" 4 | )] 5 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 6 | #![deny(missing_docs)] 7 | #![deny(rustdoc::broken_intra_doc_links)] 8 | //! ![project logo][logo] 9 | //! 10 | //! Songbird is an async, cross-library compatible voice system for Discord, written in Rust. 11 | //! The library offers: 12 | //! * A standalone gateway frontend compatible with [serenity] and [twilight] using the 13 | //! `"gateway"` and `"[serenity/twilight]"` plus `"[rustls/native]"` features. You can even run 14 | //! driverless, to help manage your [lavalink] sessions. 15 | //! * A standalone driver for voice calls, via the `"driver"` feature. If you can create 16 | //! a `ConnectionInfo` using any other gateway, or language for your bot, then you 17 | //! can run the songbird voice driver. 18 | //! * Voice receive and RT(C)P packet handling via the `"receive"` feature. 19 | //! * And, by default, a fully featured voice system featuring events, queues, 20 | //! seeking on compatible streams, shared multithreaded audio stream caches, 21 | //! and direct Opus data passthrough from DCA files. 22 | //! 23 | //! ## Intents 24 | //! Songbird's gateway functionality requires you to specify the `GUILD_VOICE_STATES` intent. 25 | //! 26 | //! ## Examples 27 | //! Full examples showing various types of functionality and integrations can be found 28 | //! in [this crate's examples directory]. 29 | //! 30 | //! ## Codec support 31 | //! Songbird supports all [codecs and formats provided by Symphonia] (pure-Rust), with Opus support 32 | //! provided by [audiopus] (an FFI wrapper for libopus). 33 | //! 34 | //! **By default, *Songbird will not request any codecs from Symphonia*.** To change this, in your own 35 | //! project you will need to depend on Symphonia as well. 36 | //! 37 | //! ```toml 38 | //! # Including songbird alone gives you support for Opus via the DCA file format. 39 | //! [dependencies.songbird] 40 | //! version = "0.5" 41 | //! features = ["builtin-queue"] 42 | //! 43 | //! # To get additional codecs, you *must* add Symphonia yourself. 44 | //! # This includes the default formats (MKV/WebM, Ogg, Wave) and codecs (FLAC, PCM, Vorbis)... 45 | //! [dependencies.symphonia] 46 | //! version = "0.5" 47 | //! features = ["aac", "mp3", "isomp4", "alac"] # ...as well as any extras you need! 48 | //! ``` 49 | //! 50 | //! ## Attribution 51 | //! 52 | //! Songbird's logo is based upon the copyright-free image ["Black-Capped Chickadee"] by George Gorgas White. 53 | //! 54 | //! [logo]: https://raw.githubusercontent.com/serenity-rs/songbird/current/songbird.png 55 | //! [serenity]: https://github.com/serenity-rs/serenity 56 | //! [twilight]: https://github.com/twilight-rs/twilight 57 | //! [this crate's examples directory]: https://github.com/serenity-rs/songbird/tree/current/examples 58 | //! ["Black-Capped Chickadee"]: https://www.oldbookillustrations.com/illustrations/black-capped-chickadee/ 59 | //! [`ConnectionInfo`]: struct@ConnectionInfo 60 | //! [lavalink]: https://github.com/freyacodes/Lavalink 61 | //! [codecs and formats provided by Symphonia]: https://github.com/pdeljanov/Symphonia#formats-demuxers 62 | //! [audiopus]: https://github.com/lakelezz/audiopus 63 | 64 | #![warn(clippy::pedantic, rust_2018_idioms)] 65 | #![allow( 66 | // Allowed as they are too pedantic 67 | clippy::module_name_repetitions, 68 | clippy::wildcard_imports, 69 | clippy::too_many_lines, 70 | clippy::cast_lossless, 71 | clippy::cast_sign_loss, 72 | clippy::cast_possible_wrap, 73 | clippy::cast_precision_loss, 74 | clippy::cast_possible_truncation, 75 | // TODO: would require significant rewriting of all existing docs 76 | clippy::missing_errors_doc, 77 | clippy::missing_panics_doc, 78 | clippy::doc_link_with_quotes, 79 | clippy::doc_markdown, 80 | // Allowed as they cannot be fixed without breaking 81 | clippy::result_large_err, 82 | clippy::large_enum_variant, 83 | )] 84 | 85 | mod config; 86 | pub mod constants; 87 | #[cfg(feature = "driver")] 88 | pub mod driver; 89 | pub mod error; 90 | #[cfg(feature = "driver")] 91 | pub mod events; 92 | #[cfg(feature = "gateway")] 93 | mod handler; 94 | pub mod id; 95 | pub(crate) mod info; 96 | #[cfg(feature = "driver")] 97 | pub mod input; 98 | #[cfg(feature = "gateway")] 99 | pub mod join; 100 | #[cfg(feature = "gateway")] 101 | mod manager; 102 | #[cfg(feature = "serenity")] 103 | pub mod serenity; 104 | #[cfg(feature = "gateway")] 105 | pub mod shards; 106 | #[cfg(any(test, feature = "internals"))] 107 | pub mod test_utils; 108 | #[cfg(feature = "driver")] 109 | pub mod tracks; 110 | #[cfg(feature = "driver")] 111 | mod ws; 112 | 113 | #[cfg(all(feature = "driver", feature = "receive"))] 114 | pub use discortp as packet; 115 | #[cfg(feature = "driver")] 116 | pub use serenity_voice_model as model; 117 | 118 | pub(crate) use serde_json as json; 119 | 120 | #[cfg(feature = "driver")] 121 | pub use crate::{ 122 | driver::Driver, 123 | events::{CoreEvent, Event, EventContext, EventHandler, TrackEvent}, 124 | }; 125 | 126 | #[cfg(feature = "gateway")] 127 | pub use crate::{handler::*, manager::*}; 128 | 129 | #[cfg(feature = "serenity")] 130 | pub use crate::serenity::*; 131 | 132 | pub use config::Config; 133 | pub use info::ConnectionInfo; 134 | -------------------------------------------------------------------------------- /src/serenity.rs: -------------------------------------------------------------------------------- 1 | //! Compatibility and convenience methods for working with [serenity]. 2 | //! Requires the `"serenity"` feature. 3 | //! 4 | //! [serenity]: https://crates.io/crates/serenity 5 | 6 | use crate::{Config, Songbird}; 7 | use serenity::{ 8 | client::{ClientBuilder, Context}, 9 | prelude::TypeMapKey, 10 | }; 11 | use std::sync::Arc; 12 | 13 | /// Zero-size type used to retrieve the registered [`Songbird`] instance 14 | /// from serenity's inner [`TypeMap`]. 15 | /// 16 | /// [`Songbird`]: Songbird 17 | /// [`TypeMap`]: serenity::prelude::TypeMap 18 | pub struct SongbirdKey; 19 | 20 | impl TypeMapKey for SongbirdKey { 21 | type Value = Arc; 22 | } 23 | 24 | /// Installs a new songbird instance into the serenity client. 25 | /// 26 | /// This should be called after any uses of `ClientBuilder::type_map`. 27 | pub fn register(client_builder: ClientBuilder) -> ClientBuilder { 28 | let voice = Songbird::serenity(); 29 | register_with(client_builder, voice) 30 | } 31 | 32 | /// Installs a given songbird instance into the serenity client. 33 | /// 34 | /// This should be called after any uses of `ClientBuilder::type_map`. 35 | pub fn register_with(client_builder: ClientBuilder, voice: Arc) -> ClientBuilder { 36 | client_builder 37 | .voice_manager_arc(voice.clone()) 38 | .type_map_insert::(voice) 39 | } 40 | 41 | /// Installs a given songbird instance into the serenity client. 42 | /// 43 | /// This should be called after any uses of `ClientBuilder::type_map`. 44 | pub fn register_from_config(client_builder: ClientBuilder, config: Config) -> ClientBuilder { 45 | let voice = Songbird::serenity_from_config(config); 46 | register_with(client_builder, voice) 47 | } 48 | 49 | /// Retrieve the Songbird voice client from a serenity context's 50 | /// shared key-value store. 51 | pub async fn get(ctx: &Context) -> Option> { 52 | let data = ctx.data.read().await; 53 | 54 | data.get::().cloned() 55 | } 56 | 57 | /// Helper trait to add installation/creation methods to serenity's 58 | /// `ClientBuilder`. 59 | /// 60 | /// These install the client to receive gateway voice events, and 61 | /// store an easily accessible reference to Songbird's managers. 62 | pub trait SerenityInit { 63 | /// Registers a new Songbird voice system with serenity, storing it for easy 64 | /// access via [`get`]. 65 | /// 66 | /// [`get`]: get 67 | #[must_use] 68 | fn register_songbird(self) -> Self; 69 | /// Registers a given Songbird voice system with serenity, as above. 70 | #[must_use] 71 | fn register_songbird_with(self, voice: Arc) -> Self; 72 | /// Registers a Songbird voice system serenity, based on the given configuration. 73 | #[must_use] 74 | fn register_songbird_from_config(self, config: Config) -> Self; 75 | } 76 | 77 | impl SerenityInit for ClientBuilder { 78 | fn register_songbird(self) -> Self { 79 | register(self) 80 | } 81 | 82 | fn register_songbird_with(self, voice: Arc) -> Self { 83 | register_with(self, voice) 84 | } 85 | 86 | fn register_songbird_from_config(self, config: Config) -> Self { 87 | register_from_config(self, config) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test_utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use byteorder::{LittleEndian, WriteBytesExt}; 4 | use std::mem; 5 | 6 | #[must_use] 7 | pub fn make_sine(float_len: usize, stereo: bool) -> Vec { 8 | let sample_len = mem::size_of::(); 9 | let byte_len = float_len * sample_len; 10 | 11 | // set period to 100 samples == 480Hz sine. 12 | 13 | let mut out = vec![0u8; byte_len]; 14 | let mut byte_slice = &mut out[..]; 15 | 16 | for i in 0..float_len { 17 | let x_val = (i as f32) * 50.0 / std::f32::consts::PI; 18 | byte_slice.write_f32::(x_val.sin()).unwrap(); 19 | } 20 | 21 | if stereo { 22 | let mut new_out = vec![0u8; byte_len * 2]; 23 | 24 | for (mono_chunk, stereo_chunk) in out[..] 25 | .chunks(sample_len) 26 | .zip(new_out[..].chunks_mut(2 * sample_len)) 27 | { 28 | stereo_chunk[..sample_len].copy_from_slice(mono_chunk); 29 | stereo_chunk[sample_len..].copy_from_slice(mono_chunk); 30 | } 31 | 32 | new_out 33 | } else { 34 | out 35 | } 36 | } 37 | 38 | #[must_use] 39 | pub fn make_pcm_sine(i16_len: usize, stereo: bool) -> Vec { 40 | let sample_len = mem::size_of::(); 41 | let byte_len = i16_len * sample_len; 42 | 43 | // set period to 100 samples == 480Hz sine. 44 | // amplitude = 10_000 45 | 46 | let mut out = vec![0u8; byte_len]; 47 | let mut byte_slice = &mut out[..]; 48 | 49 | for i in 0..i16_len { 50 | let x_val = (i as f32) * 50.0 / std::f32::consts::PI; 51 | byte_slice 52 | .write_i16::((x_val.sin() * 10_000.0) as i16) 53 | .unwrap(); 54 | } 55 | 56 | if stereo { 57 | let mut new_out = vec![0u8; byte_len * 2]; 58 | 59 | for (mono_chunk, stereo_chunk) in out[..] 60 | .chunks(sample_len) 61 | .zip(new_out[..].chunks_mut(2 * sample_len)) 62 | { 63 | stereo_chunk[..sample_len].copy_from_slice(mono_chunk); 64 | stereo_chunk[sample_len..].copy_from_slice(mono_chunk); 65 | } 66 | 67 | new_out 68 | } else { 69 | out 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/tracks/action.rs: -------------------------------------------------------------------------------- 1 | use flume::Sender; 2 | use std::time::Duration; 3 | 4 | use super::{PlayError, SeekRequest}; 5 | 6 | /// Actions for the mixer to take after inspecting track state via 7 | /// [`TrackHandle::action`]. 8 | /// 9 | /// [`TrackHandle::action`]: super::TrackHandle::action 10 | #[derive(Clone, Default)] 11 | pub struct Action { 12 | pub(crate) make_playable: Option>>, 13 | pub(crate) seek_point: Option, 14 | } 15 | 16 | impl Action { 17 | /// Requests a seek to the given time for this track. 18 | #[must_use] 19 | pub fn seek(mut self, time: Duration) -> Self { 20 | let (callback, _) = flume::bounded(1); 21 | self.seek_point = Some(SeekRequest { time, callback }); 22 | 23 | self 24 | } 25 | 26 | /// Readies the track to be playable, if this is not already the case. 27 | #[must_use] 28 | pub fn make_playable(mut self) -> Self { 29 | let (tx, _) = flume::bounded(1); 30 | self.make_playable = Some(tx); 31 | 32 | self 33 | } 34 | 35 | pub(crate) fn combine(&mut self, other: Self) { 36 | if other.make_playable.is_some() { 37 | self.make_playable = other.make_playable; 38 | } 39 | if other.seek_point.is_some() { 40 | self.seek_point = other.seek_point; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tracks/command.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::events::EventData; 3 | use flume::Sender; 4 | use std::fmt::{Debug, Formatter, Result as FmtResult}; 5 | 6 | /// A request from external code using a [`TrackHandle`] to modify 7 | /// or act upon an [`Track`] object. 8 | /// 9 | /// [`Track`]: Track 10 | /// [`TrackHandle`]: TrackHandle 11 | #[non_exhaustive] 12 | pub enum TrackCommand { 13 | /// Set the track's play_mode to play/resume. 14 | Play, 15 | /// Set the track's play_mode to pause. 16 | Pause, 17 | /// Stop the target track. This cannot be undone. 18 | Stop, 19 | /// Set the track's volume. 20 | Volume(f32), 21 | /// Seek to the given duration. 22 | /// 23 | /// On unsupported input types, this can be fatal. 24 | Seek(SeekRequest), 25 | /// Register an event on this track. 26 | AddEvent(EventData), 27 | /// Run some closure on this track, with direct access to the core object. 28 | Do(Box) -> Option + Send + Sync + 'static>), 29 | /// Request a copy of this track's state. 30 | Request(Sender), 31 | /// Change the loop count/strategy of this track. 32 | Loop(LoopState), 33 | /// Prompts a track's input to become live and usable, if it is not already. 34 | MakePlayable(Sender>), 35 | } 36 | 37 | impl Debug for TrackCommand { 38 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 39 | write!( 40 | f, 41 | "TrackCommand::{}", 42 | match self { 43 | Self::Play => "Play".to_string(), 44 | Self::Pause => "Pause".to_string(), 45 | Self::Stop => "Stop".to_string(), 46 | Self::Volume(vol) => format!("Volume({vol})"), 47 | Self::Seek(s) => format!("Seek({:?})", s.time), 48 | Self::AddEvent(evt) => format!("AddEvent({evt:?})"), 49 | Self::Do(_f) => "Do([function])".to_string(), 50 | Self::Request(tx) => format!("Request({tx:?})"), 51 | Self::Loop(loops) => format!("Loop({loops:?})"), 52 | Self::MakePlayable(_) => "MakePlayable".to_string(), 53 | } 54 | ) 55 | } 56 | } 57 | 58 | #[derive(Clone, Debug)] 59 | pub struct SeekRequest { 60 | pub time: Duration, 61 | pub callback: Sender>, 62 | } 63 | -------------------------------------------------------------------------------- /src/tracks/error.rs: -------------------------------------------------------------------------------- 1 | use crate::input::AudioStreamError; 2 | use flume::RecvError; 3 | use std::{ 4 | error::Error, 5 | fmt::{Display, Formatter, Result as FmtResult}, 6 | sync::Arc, 7 | }; 8 | use symphonia_core::errors::Error as SymphoniaError; 9 | 10 | /// Errors associated with control and manipulation of tracks. 11 | /// 12 | /// Unless otherwise stated, these don't invalidate an existing track, 13 | /// but do advise on valid operations and commands. 14 | #[derive(Clone, Debug)] 15 | #[non_exhaustive] 16 | pub enum ControlError { 17 | /// The operation failed because the track has ended, has been removed 18 | /// due to call closure, or some error within the driver. 19 | Finished, 20 | /// The supplied event listener can never be fired by a track, and should 21 | /// be attached to the driver instead. 22 | InvalidTrackEvent, 23 | /// A command to seek or ready the target track failed when parsing or creating the stream. 24 | /// 25 | /// This is a fatal error, and the track will be removed. 26 | Play(PlayError), 27 | /// Another `seek`/`make_playable` request was made, and so this callback handler was dropped. 28 | Dropped, 29 | } 30 | 31 | impl Display for ControlError { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 33 | write!(f, "failed to operate on track (handle): ")?; 34 | match self { 35 | ControlError::Finished => write!(f, "track ended"), 36 | ControlError::InvalidTrackEvent => { 37 | write!(f, "given event listener can't be fired on a track") 38 | }, 39 | ControlError::Play(p) => { 40 | write!(f, "i/o request on track failed: {p}") 41 | }, 42 | ControlError::Dropped => write!(f, "request was replaced by another of same type"), 43 | } 44 | } 45 | } 46 | 47 | impl Error for ControlError {} 48 | 49 | impl From for ControlError { 50 | fn from(_: RecvError) -> Self { 51 | ControlError::Dropped 52 | } 53 | } 54 | 55 | /// Alias for most calls to a [`TrackHandle`]. 56 | /// 57 | /// [`TrackHandle`]: super::TrackHandle 58 | pub type TrackResult = Result; 59 | 60 | /// Errors reported by the mixer while attempting to play (or ready) a [`Track`]. 61 | /// 62 | /// [`Track`]: super::Track 63 | #[derive(Clone, Debug)] 64 | #[non_exhaustive] 65 | pub enum PlayError { 66 | /// Failed to create a live bytestream from the lazy [`Compose`]. 67 | /// 68 | /// [`Compose`]: crate::input::Compose 69 | Create(Arc), 70 | /// Failed to read headers, codecs, or a valid stream from an [`Input`]. 71 | /// 72 | /// [`Input`]: crate::input::Input 73 | Parse(Arc), 74 | /// Failed to decode a frame received from an [`Input`]. 75 | /// 76 | /// [`Input`]: crate::input::Input 77 | Decode(Arc), 78 | /// Failed to seek to the requested location. 79 | Seek(Arc), 80 | } 81 | 82 | impl Display for PlayError { 83 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 84 | f.write_str("runtime error while playing track: ")?; 85 | match self { 86 | Self::Create(c) => { 87 | f.write_str("input creation [")?; 88 | f.write_fmt(format_args!("{}", &c))?; 89 | f.write_str("]") 90 | }, 91 | Self::Parse(p) => { 92 | f.write_str("parsing formats/codecs [")?; 93 | f.write_fmt(format_args!("{}", &p))?; 94 | f.write_str("]") 95 | }, 96 | Self::Decode(d) => { 97 | f.write_str("decoding packets [")?; 98 | f.write_fmt(format_args!("{}", &d))?; 99 | f.write_str("]") 100 | }, 101 | Self::Seek(s) => { 102 | f.write_str("seeking along input [")?; 103 | f.write_fmt(format_args!("{}", &s))?; 104 | f.write_str("]") 105 | }, 106 | } 107 | } 108 | } 109 | 110 | impl Error for PlayError {} 111 | -------------------------------------------------------------------------------- /src/tracks/looping.rs: -------------------------------------------------------------------------------- 1 | /// Looping behaviour for a [`Track`]. 2 | /// 3 | /// [`Track`]: struct.Track.html 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 5 | pub enum LoopState { 6 | /// Track will loop endlessly until loop state is changed or 7 | /// manually stopped. 8 | Infinite, 9 | 10 | /// Track will loop `n` more times. 11 | /// 12 | /// `Finite(0)` is the `Default`, stopping the track once its [`Input`] ends. 13 | /// 14 | /// [`Input`]: crate::input::Input 15 | Finite(usize), 16 | } 17 | 18 | impl Default for LoopState { 19 | fn default() -> Self { 20 | Self::Finite(0) 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::*; 27 | use crate::{ 28 | constants::test_data::FILE_WAV_TARGET, 29 | driver::Driver, 30 | input::File, 31 | tracks::{PlayMode, Track, TrackState}, 32 | Config, 33 | Event, 34 | EventContext, 35 | EventHandler, 36 | TrackEvent, 37 | }; 38 | use flume::Sender; 39 | 40 | struct Looper { 41 | tx: Sender, 42 | } 43 | 44 | #[async_trait::async_trait] 45 | impl EventHandler for Looper { 46 | async fn act(&self, ctx: &crate::EventContext<'_>) -> Option { 47 | if let EventContext::Track(&[(state, _)]) = ctx { 48 | drop(self.tx.send(state.clone())); 49 | } 50 | 51 | None 52 | } 53 | } 54 | 55 | #[tokio::test] 56 | #[ntest::timeout(15_000)] 57 | async fn finite_track_loops_work() { 58 | let (t_handle, config) = Config::test_cfg(true); 59 | let mut driver = Driver::new(config.clone()); 60 | 61 | let file = File::new(FILE_WAV_TARGET); 62 | let handle = driver.play(Track::from(file).loops(LoopState::Finite(2))); 63 | 64 | let (l_tx, l_rx) = flume::unbounded(); 65 | let (e_tx, e_rx) = flume::unbounded(); 66 | let _ = handle.add_event(Event::Track(TrackEvent::Loop), Looper { tx: l_tx }); 67 | let _ = handle.add_event(Event::Track(TrackEvent::End), Looper { tx: e_tx }); 68 | 69 | t_handle.spawn_ticker(); 70 | 71 | // CONDITIONS: 72 | // 1) 2 loop events, each changes the loop count. 73 | // 2) Track ends. 74 | // 3) Playtime >> Position 75 | assert_eq!( 76 | l_rx.recv_async().await.map(|v| v.loops), 77 | Ok(LoopState::Finite(1)) 78 | ); 79 | assert_eq!( 80 | l_rx.recv_async().await.map(|v| v.loops), 81 | Ok(LoopState::Finite(0)) 82 | ); 83 | let ended = e_rx.recv_async().await; 84 | 85 | assert!(ended.is_ok()); 86 | 87 | let ended = ended.unwrap(); 88 | assert!(ended.play_time > 2 * ended.position); 89 | } 90 | 91 | #[tokio::test] 92 | #[ntest::timeout(15_000)] 93 | async fn infinite_track_loops_work() { 94 | let (t_handle, config) = Config::test_cfg(true); 95 | let mut driver = Driver::new(config.clone()); 96 | 97 | let file = File::new(FILE_WAV_TARGET); 98 | let handle = driver.play(Track::from(file).loops(LoopState::Infinite)); 99 | 100 | let (l_tx, l_rx) = flume::unbounded(); 101 | let _ = handle.add_event(Event::Track(TrackEvent::Loop), Looper { tx: l_tx }); 102 | 103 | t_handle.spawn_ticker(); 104 | 105 | // CONDITIONS: 106 | // 1) 3 loop events, each does not change the loop count. 107 | // 2) Track still playing at final 108 | // 3) Playtime >> Position 109 | assert_eq!( 110 | l_rx.recv_async().await.map(|v| v.loops), 111 | Ok(LoopState::Infinite) 112 | ); 113 | assert_eq!( 114 | l_rx.recv_async().await.map(|v| v.loops), 115 | Ok(LoopState::Infinite) 116 | ); 117 | 118 | let final_state = l_rx.recv_async().await; 119 | assert_eq!( 120 | final_state.as_ref().map(|v| v.loops), 121 | Ok(LoopState::Infinite) 122 | ); 123 | let final_state = final_state.unwrap(); 124 | 125 | assert_eq!(final_state.playing, PlayMode::Play); 126 | assert!(final_state.play_time > 2 * final_state.position); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/tracks/mode.rs: -------------------------------------------------------------------------------- 1 | use super::PlayError; 2 | use crate::events::TrackEvent; 3 | 4 | /// Playback status of a track. 5 | #[derive(Clone, Debug)] 6 | #[non_exhaustive] 7 | #[derive(Default)] 8 | pub enum PlayMode { 9 | /// The track is currently playing. 10 | #[default] 11 | Play, 12 | /// The track is currently paused, and may be resumed. 13 | Pause, 14 | /// The track has been manually stopped, and cannot be restarted. 15 | Stop, 16 | /// The track has naturally ended, and cannot be restarted. 17 | End, 18 | /// The track has encountered a runtime or initialisation error, and cannot be restarted. 19 | Errored(PlayError), 20 | } 21 | 22 | impl PlayMode { 23 | /// Returns whether the track has irreversibly stopped. 24 | #[must_use] 25 | pub fn is_done(&self) -> bool { 26 | matches!(self, PlayMode::Stop | PlayMode::End | PlayMode::Errored(_)) 27 | } 28 | 29 | /// Returns whether the track has irreversibly stopped. 30 | #[must_use] 31 | pub(crate) fn is_playing(&self) -> bool { 32 | matches!(self, PlayMode::Play) 33 | } 34 | 35 | #[must_use] 36 | pub(crate) fn next_state(self, other: Self) -> Self { 37 | // Idea: a finished track cannot be restarted -- this action is final. 38 | // We may want to change this in future so that seekable tracks can uncancel 39 | // themselves, perhaps, but this requires a bit more machinery to readd... 40 | match self { 41 | Self::Play | Self::Pause => other, 42 | state => state, 43 | } 44 | } 45 | 46 | pub(crate) fn change_to(&mut self, other: Self) { 47 | *self = self.clone().next_state(other); 48 | } 49 | 50 | #[must_use] 51 | pub(crate) fn as_track_event(&self) -> TrackEvent { 52 | match self { 53 | Self::Play => TrackEvent::Play, 54 | Self::Pause => TrackEvent::Pause, 55 | Self::Stop | Self::End => TrackEvent::End, 56 | Self::Errored(_) => TrackEvent::Error, 57 | } 58 | } 59 | 60 | // The above fn COULD just return a Vec, but the below means we only allocate a Vec 61 | // in the rare error case. 62 | // Also, see discussion on bitsets in src/events/track.rs 63 | #[must_use] 64 | pub(crate) fn also_fired_track_events(&self) -> Option> { 65 | match self { 66 | Self::Errored(_) => Some(vec![TrackEvent::End]), 67 | _ => None, 68 | } 69 | } 70 | } 71 | 72 | impl PartialEq for PlayMode { 73 | fn eq(&self, other: &Self) -> bool { 74 | self.as_track_event() == other.as_track_event() 75 | } 76 | } 77 | 78 | impl Eq for PlayMode {} 79 | -------------------------------------------------------------------------------- /src/tracks/ready.rs: -------------------------------------------------------------------------------- 1 | /// Whether this track has been made live, is being processed, or is 2 | /// currently uninitialised. 3 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 4 | pub enum ReadyState { 5 | /// This track is still a lazy [`Compose`] object, and hasn't been made playable. 6 | /// 7 | /// [`Compose`]: crate::input::Compose 8 | Uninitialised, 9 | 10 | /// The mixer is currently creating and parsing this track's bytestream. 11 | Preparing, 12 | 13 | /// This track is fully initialised and usable. 14 | Playable, 15 | } 16 | 17 | impl Default for ReadyState { 18 | fn default() -> Self { 19 | Self::Uninitialised 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/tracks/state.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// State of an [`Track`] object, designed to be passed to event handlers 4 | /// and retrieved remotely via [`TrackHandle::get_info`]. 5 | /// 6 | /// [`Track`]: Track 7 | /// [`TrackHandle::get_info`]: TrackHandle::get_info 8 | #[derive(Clone, Debug, Default, PartialEq)] 9 | pub struct TrackState { 10 | /// Play status (e.g., active, paused, stopped) of this track. 11 | pub playing: PlayMode, 12 | 13 | /// Current volume of this track. 14 | pub volume: f32, 15 | 16 | /// Current playback position in the source. 17 | /// 18 | /// This is altered by loops and seeks, and represents this track's 19 | /// position in its underlying input stream. 20 | pub position: Duration, 21 | 22 | /// Total playback time, increasing monotonically. 23 | pub play_time: Duration, 24 | 25 | /// Remaining loops on this track. 26 | pub loops: LoopState, 27 | 28 | /// Whether this track has been made live, is being processed, or is 29 | /// currently uninitialised. 30 | pub ready: ReadyState, 31 | } 32 | 33 | impl TrackState { 34 | pub(crate) fn step_frame(&mut self) { 35 | self.position += TIMESTEP_LENGTH; 36 | self.play_time += TIMESTEP_LENGTH; 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | use crate::{constants::test_data::YTDL_TARGET, driver::Driver, input::YoutubeDl, Config}; 44 | use reqwest::Client; 45 | 46 | #[tokio::test] 47 | #[ntest::timeout(10_000)] 48 | async fn times_unchanged_while_not_ready() { 49 | let (t_handle, config) = Config::test_cfg(true); 50 | let mut driver = Driver::new(config.clone()); 51 | 52 | let file = YoutubeDl::new(Client::new(), YTDL_TARGET); 53 | let handle = driver.play(Track::from(file)); 54 | 55 | let state = t_handle 56 | .ready_track(&handle, Some(Duration::from_millis(5))) 57 | .await; 58 | 59 | // As state is `play`, the instant we ready we'll have playout. 60 | // Naturally, fetching a ytdl request takes far longer than this. 61 | assert_eq!(state.position, Duration::from_millis(20)); 62 | assert_eq!(state.play_time, Duration::from_millis(20)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/tracks/view.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::input::Metadata; 3 | 4 | /// Live track and input state exposed during [`TrackHandle::action`]. 5 | /// 6 | /// [`TrackHandle::action`]: super::[`TrackHandle::action`] 7 | #[non_exhaustive] 8 | pub struct View<'a> { 9 | /// The current position within this track. 10 | pub position: &'a Duration, 11 | 12 | /// The total time a track has been played for. 13 | pub play_time: &'a Duration, 14 | 15 | /// The current mixing volume of this track. 16 | pub volume: &'a mut f32, 17 | 18 | /// In-stream metadata for this track, if it is fully readied. 19 | pub meta: Option>, 20 | 21 | /// The current play status of this track. 22 | pub playing: &'a mut PlayMode, 23 | 24 | /// Whether this track has been made live, is being processed, or is 25 | /// currently uninitialised. 26 | pub ready: ReadyState, 27 | 28 | /// The number of remaning loops on this track. 29 | pub loops: &'a mut LoopState, 30 | } 31 | -------------------------------------------------------------------------------- /src/ws.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::JsonError, model::Event}; 2 | 3 | use bytes::Bytes; 4 | use futures::{SinkExt, StreamExt, TryStreamExt}; 5 | use tokio::{ 6 | net::TcpStream, 7 | time::{timeout, Duration}, 8 | }; 9 | #[cfg(feature = "tungstenite")] 10 | use tokio_tungstenite::{ 11 | tungstenite::{ 12 | error::Error as TungsteniteError, 13 | protocol::{CloseFrame, WebSocketConfig as Config}, 14 | Message, 15 | }, 16 | MaybeTlsStream, 17 | WebSocketStream, 18 | }; 19 | #[cfg(feature = "tws")] 20 | use tokio_websockets::{ 21 | CloseCode, 22 | Error as TwsError, 23 | Limits, 24 | MaybeTlsStream, 25 | Message, 26 | WebSocketStream, 27 | }; 28 | use tracing::{debug, instrument}; 29 | use url::Url; 30 | 31 | pub struct WsStream(WebSocketStream>); 32 | 33 | impl WsStream { 34 | #[instrument] 35 | pub(crate) async fn connect(url: Url) -> Result { 36 | #[cfg(feature = "tungstenite")] 37 | let (stream, _) = tokio_tungstenite::connect_async_with_config::( 38 | url, 39 | Some( 40 | Config::default() 41 | .max_message_size(None) 42 | .max_frame_size(None), 43 | ), 44 | true, 45 | ) 46 | .await?; 47 | #[cfg(feature = "tws")] 48 | let (stream, _) = tokio_websockets::ClientBuilder::new() 49 | .limits(Limits::unlimited()) 50 | .uri(url.as_str()) 51 | .unwrap() // Any valid URL is a valid URI. 52 | .connect() 53 | .await?; 54 | 55 | Ok(Self(stream)) 56 | } 57 | 58 | pub(crate) async fn recv_json(&mut self) -> Result> { 59 | const TIMEOUT: Duration = Duration::from_millis(500); 60 | 61 | let ws_message = match timeout(TIMEOUT, self.0.next()).await { 62 | Ok(Some(Ok(v))) => Some(v), 63 | Ok(Some(Err(e))) => return Err(e.into()), 64 | Ok(None) | Err(_) => None, 65 | }; 66 | 67 | convert_ws_message(ws_message) 68 | } 69 | 70 | pub(crate) async fn recv_json_no_timeout(&mut self) -> Result> { 71 | convert_ws_message(self.0.try_next().await?) 72 | } 73 | 74 | pub(crate) async fn send_json(&mut self, value: &Event) -> Result<()> { 75 | let res = crate::json::to_string(value); 76 | let res = res.map(Message::text); 77 | Ok(res.map_err(Error::from).map(|m| self.0.send(m))?.await?) 78 | } 79 | } 80 | 81 | pub type Result = std::result::Result; 82 | 83 | #[derive(Debug)] 84 | pub enum Error { 85 | Json(JsonError), 86 | 87 | /// The discord voice gateway does not support or offer zlib compression. 88 | /// As a result, only text messages are expected. 89 | UnexpectedBinaryMessage(Bytes), 90 | 91 | #[cfg(feature = "tungstenite")] 92 | Ws(TungsteniteError), 93 | #[cfg(feature = "tws")] 94 | Ws(TwsError), 95 | 96 | #[cfg(feature = "tungstenite")] 97 | WsClosed(Option), 98 | #[cfg(feature = "tws")] 99 | WsClosed(Option), 100 | } 101 | 102 | impl From for Error { 103 | fn from(e: JsonError) -> Error { 104 | Error::Json(e) 105 | } 106 | } 107 | 108 | #[cfg(feature = "tungstenite")] 109 | impl From for Error { 110 | fn from(e: TungsteniteError) -> Error { 111 | Error::Ws(e) 112 | } 113 | } 114 | 115 | #[cfg(feature = "tws")] 116 | impl From for Error { 117 | fn from(e: TwsError) -> Self { 118 | Error::Ws(e) 119 | } 120 | } 121 | 122 | #[inline] 123 | pub(crate) fn convert_ws_message(message: Option) -> Result> { 124 | #[cfg(feature = "tungstenite")] 125 | let text = match message { 126 | Some(Message::Text(ref payload)) => payload, 127 | Some(Message::Binary(bytes)) => { 128 | return Err(Error::UnexpectedBinaryMessage(bytes)); 129 | }, 130 | Some(Message::Close(Some(frame))) => { 131 | return Err(Error::WsClosed(Some(frame))); 132 | }, 133 | // Ping/Pong message behaviour is internally handled by tungstenite. 134 | _ => return Ok(None), 135 | }; 136 | #[cfg(feature = "tws")] 137 | let text = match message { 138 | Some(ref message) if message.is_text() => 139 | if let Some(text) = message.as_text() { 140 | text 141 | } else { 142 | return Ok(None); 143 | }, 144 | Some(message) if message.is_binary() => { 145 | return Err(Error::UnexpectedBinaryMessage( 146 | message.into_payload().into(), 147 | )); 148 | }, 149 | Some(message) if message.is_close() => { 150 | return Err(Error::WsClosed(message.as_close().map(|(c, _)| c))); 151 | }, 152 | // ping/pong; will also be internally handled by tokio-websockets. 153 | _ => return Ok(None), 154 | }; 155 | 156 | Ok(serde_json::from_str(text) 157 | .map_err(|e| { 158 | debug!("Unexpected JSON: {e}. Payload: {text}"); 159 | e 160 | }) 161 | .ok()) 162 | } 163 | --------------------------------------------------------------------------------