├── .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 | 
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