sysaudio: move to github.com/hexops/mach-sysaudio
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
47936cc7f9
commit
7d89a0f07b
25 changed files with 0 additions and 6803 deletions
2
libs/sysaudio/.gitattributes
vendored
2
libs/sysaudio/.gitattributes
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
* text=auto eol=lf
|
|
||||||
upstream/** linguist-vendored
|
|
||||||
1
libs/sysaudio/.github/FUNDING.yml
vendored
1
libs/sysaudio/.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
||||||
github: slimsag
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
Please send your change to [the main repository](https://github.com/hexops/mach/tree/main/libs/sysaudio) instead, sorry for the trouble!
|
|
||||||
|
|
||||||
This helps us avoid some complex merge conflicts we run into when changes are made to both repositories and history needs to be reconciled. Keeping PRs in just that repository enables us to use `git subtree` to trivially keep the two repositories in sync.
|
|
||||||
|
|
||||||
Once your PR is merged over there, it'll automatically sync to this repository.
|
|
||||||
53
libs/sysaudio/.github/workflows/ci.yml
vendored
53
libs/sysaudio/.github/workflows/ci.yml
vendored
|
|
@ -1,53 +0,0 @@
|
||||||
name: CI
|
|
||||||
on:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
jobs:
|
|
||||||
x86_64-linux:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: 'true'
|
|
||||||
- name: Setup Zig
|
|
||||||
run: |
|
|
||||||
sudo apt install xz-utils
|
|
||||||
sudo sh -c 'wget -c https://ziglang.org/builds/zig-linux-x86_64-0.11.0-dev.3883+7166407d8.tar.xz -O - | tar -xJ --strip-components=1 -C /usr/local/bin'
|
|
||||||
- name: build
|
|
||||||
run: zig build
|
|
||||||
- name: test
|
|
||||||
run: zig build test
|
|
||||||
x86_64-windows:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: 'true'
|
|
||||||
- name: Setup Zig
|
|
||||||
run: |
|
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
|
||||||
Invoke-WebRequest -Uri "https://ziglang.org/builds/zig-windows-x86_64-0.11.0-dev.3883+7166407d8.zip" -OutFile "C:\zig.zip"
|
|
||||||
cd C:\
|
|
||||||
7z x zig.zip
|
|
||||||
Add-Content $env:GITHUB_PATH "C:\zig-windows-x86_64-0.11.0-dev.3883+7166407d8\"
|
|
||||||
- name: build
|
|
||||||
run: zig build
|
|
||||||
- name: test
|
|
||||||
run: zig build test
|
|
||||||
x86_64-macos:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
submodules: 'true'
|
|
||||||
- name: Setup Zig
|
|
||||||
run: |
|
|
||||||
brew install xz
|
|
||||||
sudo sh -c 'wget -c https://ziglang.org/builds/zig-macos-x86_64-0.11.0-dev.3883+7166407d8.tar.xz -O - | tar -xJ --strip-components=1 -C /usr/local/bin'
|
|
||||||
- name: build
|
|
||||||
run: zig build
|
|
||||||
- name: test
|
|
||||||
run: zig build test
|
|
||||||
18
libs/sysaudio/.gitignore
vendored
18
libs/sysaudio/.gitignore
vendored
|
|
@ -1,18 +0,0 @@
|
||||||
# This file is for zig-specific build artifacts.
|
|
||||||
# If you have OS-specific or editor-specific files to ignore,
|
|
||||||
# such as *.swp or .DS_Store, put those in your global
|
|
||||||
# ~/.gitignore and put this in your ~/.gitconfig:
|
|
||||||
#
|
|
||||||
# [core]
|
|
||||||
# excludesfile = ~/.gitignore
|
|
||||||
#
|
|
||||||
# Cheers!
|
|
||||||
# -andrewrk
|
|
||||||
|
|
||||||
zig-cache/
|
|
||||||
zig-out/
|
|
||||||
/release/
|
|
||||||
/debug/
|
|
||||||
/build/
|
|
||||||
/build-*/
|
|
||||||
/docgen_tmp/
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
Copyright 2021, Hexops Contributors (given via the Git commit history).
|
|
||||||
|
|
||||||
All documentation, image, sound, font, and 2D/3D model files are CC-BY-4.0 licensed unless
|
|
||||||
otherwise noted. You may get a copy of this license at https://creativecommons.org/licenses/by/4.0
|
|
||||||
|
|
||||||
Files in a directory with a separate LICENSE file may contain files under different license terms,
|
|
||||||
described within that LICENSE file.
|
|
||||||
|
|
||||||
All other files are licensed under the Apache License, Version 2.0 (see LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
|
||||||
or the MIT license (see LICENSE-MIT or http://opensource.org/licenses/MIT), at your option.
|
|
||||||
|
|
||||||
All files in the project without exclusions may not be copied, modified, or distributed except
|
|
||||||
according to the terms above.
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
Copyright (c) 2021 Hexops Contributors (given via the Git commit history).
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
|
||||||
person obtaining a copy of this software and associated
|
|
||||||
documentation files (the "Software"), to deal in the
|
|
||||||
Software without restriction, including without
|
|
||||||
limitation the rights to use, copy, modify, merge,
|
|
||||||
publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software
|
|
||||||
is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice
|
|
||||||
shall be included in all copies or substantial portions
|
|
||||||
of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
||||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
||||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
||||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
||||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# mach/sysaudio - cross-platform low-level audio IO in Zig
|
|
||||||
|
|
||||||
This repository is a separate copy of the same library in the [main Mach repository](https://github.com/hexops/mach), and is automatically kept in sync, so that anyone can use this library in their own project if they like!
|
|
||||||
|
|
||||||
## Experimental
|
|
||||||
|
|
||||||
This is an _experimental_ Mach library, according to our [stability guarantees](https://machengine.org/next/docs/libs/):
|
|
||||||
|
|
||||||
> Experimental libraries may have their APIs change without much notice, and you may have to look at recent changes in order to update your code.
|
|
||||||
|
|
||||||
[Why this library is not declared stable yet](https://machengine.org/next/docs/libs/experimental/#sysaudio)
|
|
||||||
|
|
||||||
## Join the community
|
|
||||||
|
|
||||||
Join the Mach community [on Discord](https://discord.gg/XNG3NZgCqp) to discuss this project, ask questions, get help, etc.
|
|
||||||
|
|
||||||
## Issues
|
|
||||||
|
|
||||||
Issues are tracked in the [main Mach repository](https://github.com/hexops/mach/issues?q=is%3Aissue+is%3Aopen+label%3Asysaudio).
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are very welcome. Pull requests must be sent to [the main repository](https://github.com/hexops/mach/tree/main/libs/sysaudio) to avoid some complex merge conflicts we'd get by accepting contributions in both repositories. Once the changes are merged there, they'll get sync'd to this repository automatically.
|
|
||||||
|
|
@ -1,236 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
|
||||||
const target = b.standardTargetOptions(.{});
|
|
||||||
|
|
||||||
const test_step = b.step("test", "Run library tests");
|
|
||||||
test_step.dependOn(&testStep(b, optimize, target).step);
|
|
||||||
|
|
||||||
inline for ([_][]const u8{
|
|
||||||
"sine-wave",
|
|
||||||
}) |example| {
|
|
||||||
const example_exe = b.addExecutable(.{
|
|
||||||
.name = "example-" ++ example,
|
|
||||||
.root_source_file = .{ .path = "examples/" ++ example ++ ".zig" },
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
example_exe.addModule("sysaudio", module(b, optimize, target));
|
|
||||||
link(b, example_exe, .{});
|
|
||||||
b.installArtifact(example_exe);
|
|
||||||
|
|
||||||
const example_compile_step = b.step("example-" ++ example, "Compile '" ++ example ++ "' example");
|
|
||||||
example_compile_step.dependOn(b.getInstallStep());
|
|
||||||
|
|
||||||
const example_run_cmd = b.addRunArtifact(example_exe);
|
|
||||||
example_run_cmd.step.dependOn(b.getInstallStep());
|
|
||||||
if (b.args) |args| {
|
|
||||||
example_run_cmd.addArgs(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
const example_run_step = b.step("run-example-" ++ example, "Run '" ++ example ++ "' example");
|
|
||||||
example_run_step.dependOn(&example_run_cmd.step);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Options = struct {
|
|
||||||
install_libs: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
var _module: ?*std.build.Module = null;
|
|
||||||
|
|
||||||
pub fn module(b: *std.Build, optimize: std.builtin.OptimizeMode, target: std.zig.CrossTarget) *std.build.Module {
|
|
||||||
if (_module) |m| return m;
|
|
||||||
|
|
||||||
if (target.getCpuArch() == .wasm32) {
|
|
||||||
const sysjs_dep = b.dependency("mach_sysjs", .{
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
_module = b.createModule(.{
|
|
||||||
.source_file = .{ .path = sdkPath("/src/main.zig") },
|
|
||||||
.dependencies = &.{
|
|
||||||
.{ .name = "sysjs", .module = sysjs_dep.module("mach-sysjs") },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_module = b.createModule(.{
|
|
||||||
.source_file = .{ .path = sdkPath("/src/main.zig") },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _module.?;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn testStep(b: *std.Build, optimize: std.builtin.OptimizeMode, target: std.zig.CrossTarget) *std.build.RunStep {
|
|
||||||
const main_tests = b.addTest(.{
|
|
||||||
.name = "sysaudio-tests",
|
|
||||||
.root_source_file = .{ .path = sdkPath("/src/main.zig") },
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
link(b, main_tests, .{});
|
|
||||||
b.installArtifact(main_tests);
|
|
||||||
return b.addRunArtifact(main_tests);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link(b: *std.Build, step: *std.build.CompileStep, options: Options) void {
|
|
||||||
if (step.target.toTarget().cpu.arch != .wasm32) {
|
|
||||||
if (step.target.toTarget().isDarwin()) {
|
|
||||||
// TODO(build-system): This cannot be imported with the Zig package manager
|
|
||||||
// error: TarUnsupportedFileType
|
|
||||||
//
|
|
||||||
// step.linkLibrary(b.dependency("xcode_frameworks", .{
|
|
||||||
// .target = step.target,
|
|
||||||
// .optimize = step.optimize,
|
|
||||||
// }).artifact("xcode-frameworks"));
|
|
||||||
// @import("xcode_frameworks").addPaths(step);
|
|
||||||
xcode_frameworks.addPaths(b, step);
|
|
||||||
|
|
||||||
step.linkFramework("AudioToolbox");
|
|
||||||
step.linkFramework("CoreFoundation");
|
|
||||||
step.linkFramework("CoreAudio");
|
|
||||||
} else if (step.target.toTarget().os.tag == .linux) {
|
|
||||||
step.linkLibrary(b.dependency("linux_audio_headers", .{
|
|
||||||
.target = step.target,
|
|
||||||
.optimize = step.optimize,
|
|
||||||
}).artifact("linux-audio-headers"));
|
|
||||||
step.addCSourceFile(sdkPath("/src/pipewire/sysaudio.c"), &.{"-std=gnu99"});
|
|
||||||
step.linkLibC();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options.install_libs) {
|
|
||||||
b.installArtifact(step);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensureDependencySubmodule(allocator: std.mem.Allocator, path: []const u8) !void {
|
|
||||||
if (std.process.getEnvVarOwned(allocator, "NO_ENSURE_SUBMODULES")) |no_ensure_submodules| {
|
|
||||||
defer allocator.free(no_ensure_submodules);
|
|
||||||
if (std.mem.eql(u8, no_ensure_submodules, "true")) return;
|
|
||||||
} else |_| {}
|
|
||||||
var child = std.ChildProcess.init(&.{ "git", "submodule", "update", "--init", path }, allocator);
|
|
||||||
child.cwd = sdkPath("/");
|
|
||||||
child.stderr = std.io.getStdErr();
|
|
||||||
child.stdout = std.io.getStdOut();
|
|
||||||
|
|
||||||
_ = try child.spawnAndWait();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sdkPath(comptime suffix: []const u8) []const u8 {
|
|
||||||
if (suffix[0] != '/') @compileError("suffix must be an absolute path");
|
|
||||||
return comptime blk: {
|
|
||||||
const root_dir = std.fs.path.dirname(@src().file) orelse ".";
|
|
||||||
break :blk root_dir ++ suffix;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(build-system): This is a workaround that we copy anywhere xcode_frameworks needs to be used.
|
|
||||||
// With the Zig package manager, it should be possible to remove this entirely and instead just
|
|
||||||
// write:
|
|
||||||
//
|
|
||||||
// ```
|
|
||||||
// step.linkLibrary(b.dependency("xcode_frameworks", .{
|
|
||||||
// .target = step.target,
|
|
||||||
// .optimize = step.optimize,
|
|
||||||
// }).artifact("xcode-frameworks"));
|
|
||||||
// @import("xcode_frameworks").addPaths(step);
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// However, today this package cannot be imported with the Zig package manager due to `error: TarUnsupportedFileType`
|
|
||||||
// which would be fixed by https://github.com/ziglang/zig/pull/15382 - so instead for now you must
|
|
||||||
// copy+paste this struct into your `build.zig` and write:
|
|
||||||
//
|
|
||||||
// ```
|
|
||||||
// try xcode_frameworks.addPaths(b, step);
|
|
||||||
// ```
|
|
||||||
const xcode_frameworks = struct {
|
|
||||||
pub fn addPaths(b: *std.Build, step: *std.build.CompileStep) void {
|
|
||||||
// branch: mach
|
|
||||||
xEnsureGitRepoCloned(b.allocator, "https://github.com/hexops/xcode-frameworks", "723aa55e9752c8c6c25d3413722b5fe13d72ac4f", xSdkPath("/zig-cache/xcode_frameworks")) catch |err| @panic(@errorName(err));
|
|
||||||
|
|
||||||
step.addFrameworkPath("zig-cache/xcode_frameworks/Frameworks");
|
|
||||||
step.addSystemIncludePath("zig-cache/xcode_frameworks/include");
|
|
||||||
step.addLibraryPath("zig-cache/xcode_frameworks/lib");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xEnsureGitRepoCloned(allocator: std.mem.Allocator, clone_url: []const u8, revision: []const u8, dir: []const u8) !void {
|
|
||||||
if (xIsEnvVarTruthy(allocator, "NO_ENSURE_SUBMODULES") or xIsEnvVarTruthy(allocator, "NO_ENSURE_GIT")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
xEnsureGit(allocator);
|
|
||||||
|
|
||||||
if (std.fs.openDirAbsolute(dir, .{})) |_| {
|
|
||||||
const current_revision = try xGetCurrentGitRevision(allocator, dir);
|
|
||||||
if (!std.mem.eql(u8, current_revision, revision)) {
|
|
||||||
// Reset to the desired revision
|
|
||||||
xExec(allocator, &[_][]const u8{ "git", "fetch" }, dir) catch |err| std.debug.print("warning: failed to 'git fetch' in {s}: {s}\n", .{ dir, @errorName(err) });
|
|
||||||
try xExec(allocator, &[_][]const u8{ "git", "checkout", "--quiet", "--force", revision }, dir);
|
|
||||||
try xExec(allocator, &[_][]const u8{ "git", "submodule", "update", "--init", "--recursive" }, dir);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else |err| return switch (err) {
|
|
||||||
error.FileNotFound => {
|
|
||||||
std.log.info("cloning required dependency..\ngit clone {s} {s}..\n", .{ clone_url, dir });
|
|
||||||
|
|
||||||
try xExec(allocator, &[_][]const u8{ "git", "clone", "-c", "core.longpaths=true", clone_url, dir }, ".");
|
|
||||||
try xExec(allocator, &[_][]const u8{ "git", "checkout", "--quiet", "--force", revision }, dir);
|
|
||||||
try xExec(allocator, &[_][]const u8{ "git", "submodule", "update", "--init", "--recursive" }, dir);
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
else => err,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xExec(allocator: std.mem.Allocator, argv: []const []const u8, cwd: []const u8) !void {
|
|
||||||
var child = std.ChildProcess.init(argv, allocator);
|
|
||||||
child.cwd = cwd;
|
|
||||||
_ = try child.spawnAndWait();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xGetCurrentGitRevision(allocator: std.mem.Allocator, cwd: []const u8) ![]const u8 {
|
|
||||||
const result = try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &.{ "git", "rev-parse", "HEAD" }, .cwd = cwd });
|
|
||||||
allocator.free(result.stderr);
|
|
||||||
if (result.stdout.len > 0) return result.stdout[0 .. result.stdout.len - 1]; // trim newline
|
|
||||||
return result.stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xEnsureGit(allocator: std.mem.Allocator) void {
|
|
||||||
const argv = &[_][]const u8{ "git", "--version" };
|
|
||||||
const result = std.ChildProcess.exec(.{
|
|
||||||
.allocator = allocator,
|
|
||||||
.argv = argv,
|
|
||||||
.cwd = ".",
|
|
||||||
}) catch { // e.g. FileNotFound
|
|
||||||
std.log.err("mach: error: 'git --version' failed. Is git not installed?", .{});
|
|
||||||
std.process.exit(1);
|
|
||||||
};
|
|
||||||
defer {
|
|
||||||
allocator.free(result.stderr);
|
|
||||||
allocator.free(result.stdout);
|
|
||||||
}
|
|
||||||
if (result.term.Exited != 0) {
|
|
||||||
std.log.err("mach: error: 'git --version' failed. Is git not installed?", .{});
|
|
||||||
std.process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xIsEnvVarTruthy(allocator: std.mem.Allocator, name: []const u8) bool {
|
|
||||||
if (std.process.getEnvVarOwned(allocator, name)) |truthy| {
|
|
||||||
defer allocator.free(truthy);
|
|
||||||
if (std.mem.eql(u8, truthy, "true")) return true;
|
|
||||||
return false;
|
|
||||||
} else |_| {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xSdkPath(comptime suffix: []const u8) []const u8 {
|
|
||||||
if (suffix[0] != '/') @compileError("suffix must be an absolute path");
|
|
||||||
return comptime blk: {
|
|
||||||
const root_dir = std.fs.path.dirname(@src().file) orelse ".";
|
|
||||||
break :blk root_dir ++ suffix;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
.{
|
|
||||||
.name = "mach-sysaudio",
|
|
||||||
.version = "0.2.0",
|
|
||||||
.dependencies = .{
|
|
||||||
.mach_sysjs = .{
|
|
||||||
.url = "https://github.com/hexops/mach-sysjs/archive/b71eb0531f337fcca5a2b245f595355260109a34.tar.gz",
|
|
||||||
.hash = "12208b30f1d9c229d1e64483354610207c9aa06350a46558560b818d597800ed86e0",
|
|
||||||
},
|
|
||||||
.linux_audio_headers = .{
|
|
||||||
.url = "https://github.com/hexops/linux-audio-headers/archive/d643d20f87e621b4cae1b04a2a112a881334e8b9.tar.gz",
|
|
||||||
.hash = "1220cac816e759824c7f70b6ffd0e6852a33d943b763349421657fd0d8de0587f667",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const sysaudio = @import("sysaudio");
|
|
||||||
|
|
||||||
var player: sysaudio.Player = undefined;
|
|
||||||
|
|
||||||
pub fn main() !void {
|
|
||||||
var timer = try std.time.Timer.start();
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
const allocator = gpa.allocator();
|
|
||||||
|
|
||||||
var ctx = try sysaudio.Context.init(null, allocator, .{ .deviceChangeFn = deviceChange });
|
|
||||||
std.log.info("Took {} to initialize the context...", .{std.fmt.fmtDuration(timer.lap())});
|
|
||||||
defer ctx.deinit();
|
|
||||||
try ctx.refresh();
|
|
||||||
std.log.info("Took {} to refresh the context...", .{std.fmt.fmtDuration(timer.lap())});
|
|
||||||
|
|
||||||
const device = ctx.defaultDevice(.playback) orelse return error.NoDevice;
|
|
||||||
std.log.info("Took {} to get the default playback device...", .{std.fmt.fmtDuration(timer.lap())});
|
|
||||||
|
|
||||||
player = try ctx.createPlayer(device, writeCallback, .{});
|
|
||||||
std.log.info("Took {} to create a player...", .{std.fmt.fmtDuration(timer.lap())});
|
|
||||||
defer player.deinit();
|
|
||||||
try player.start();
|
|
||||||
std.log.info("Took {} to start the player...", .{std.fmt.fmtDuration(timer.lap())});
|
|
||||||
|
|
||||||
try player.setVolume(0.85);
|
|
||||||
std.log.info("Took {} to set the volume...", .{std.fmt.fmtDuration(timer.lap())});
|
|
||||||
|
|
||||||
var buf: [16]u8 = undefined;
|
|
||||||
std.log.info("player created & entering i/o loop...", .{});
|
|
||||||
while (true) {
|
|
||||||
std.debug.print("( paused = {}, volume = {d} )\n> ", .{ player.paused(), try player.volume() });
|
|
||||||
const line = (try std.io.getStdIn().reader().readUntilDelimiterOrEof(&buf, '\n')) orelse break;
|
|
||||||
var iter = std.mem.split(u8, line, ":");
|
|
||||||
const cmd = std.mem.trimRight(u8, iter.first(), &std.ascii.whitespace);
|
|
||||||
if (std.mem.eql(u8, cmd, "vol")) {
|
|
||||||
var vol = try std.fmt.parseFloat(f32, std.mem.trim(u8, iter.next().?, &std.ascii.whitespace));
|
|
||||||
try player.setVolume(vol);
|
|
||||||
} else if (std.mem.eql(u8, cmd, "pause")) {
|
|
||||||
try player.pause();
|
|
||||||
try std.testing.expect(player.paused());
|
|
||||||
} else if (std.mem.eql(u8, cmd, "play")) {
|
|
||||||
try player.play();
|
|
||||||
try std.testing.expect(!player.paused());
|
|
||||||
} else if (std.mem.eql(u8, cmd, "exit")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pitch = 440.0;
|
|
||||||
const radians_per_second = pitch * 2.0 * std.math.pi;
|
|
||||||
var seconds_offset: f32 = 0.0;
|
|
||||||
fn writeCallback(_: ?*anyopaque, frames: usize) void {
|
|
||||||
const seconds_per_frame = 1.0 / @as(f32, @floatFromInt(player.sampleRate()));
|
|
||||||
for (0..frames) |fi| {
|
|
||||||
const sample = std.math.sin((seconds_offset + @as(f32, @floatFromInt(fi)) * seconds_per_frame) * radians_per_second);
|
|
||||||
player.writeAll(fi, sample);
|
|
||||||
}
|
|
||||||
seconds_offset = @mod(seconds_offset + seconds_per_frame * @as(f32, @floatFromInt(frames)), 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deviceChange(_: ?*anyopaque) void {
|
|
||||||
std.debug.print("Device change detected!\n", .{});
|
|
||||||
}
|
|
||||||
|
|
@ -1,735 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const c = @cImport(@cInclude("alsa/asoundlib.h"));
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
const inotify_event = std.os.linux.inotify_event;
|
|
||||||
const is_little = @import("builtin").cpu.arch.endian() == .Little;
|
|
||||||
|
|
||||||
const lib = struct {
|
|
||||||
var handle: std.DynLib = undefined;
|
|
||||||
|
|
||||||
var snd_lib_error_set_handler: *const fn (c.snd_lib_error_handler_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_info_malloc: *const fn ([*c]?*c.snd_pcm_info_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_info_free: *const fn (?*c.snd_pcm_info_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_open: *const fn ([*c]?*c.snd_pcm_t, [*c]const u8, c.snd_pcm_stream_t, c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_close: *const fn (?*c.snd_pcm_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_state: *const fn (?*c.snd_pcm_t) callconv(.C) c.snd_pcm_state_t = undefined;
|
|
||||||
var snd_pcm_pause: *const fn (?*c.snd_pcm_t, c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_writei: *const fn (?*c.snd_pcm_t, ?*const anyopaque, c.snd_pcm_uframes_t) callconv(.C) c.snd_pcm_sframes_t = undefined;
|
|
||||||
var snd_pcm_prepare: *const fn (?*c.snd_pcm_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_info_set_device: *const fn (?*c.snd_pcm_info_t, c_uint) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_info_set_subdevice: *const fn (?*c.snd_pcm_info_t, c_uint) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_info_get_name: *const fn (?*const c.snd_pcm_info_t) callconv(.C) [*c]const u8 = undefined;
|
|
||||||
var snd_pcm_info_set_stream: *const fn (?*c.snd_pcm_info_t, c.snd_pcm_stream_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_hw_free: *const fn (?*c.snd_pcm_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_malloc: *const fn ([*c]?*c.snd_pcm_hw_params_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_free: *const fn (?*c.snd_pcm_hw_params_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_set_params: *const fn (?*c.snd_pcm_t, c.snd_pcm_format_t, c.snd_pcm_access_t, c_uint, c_uint, c_int, c_uint) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_any: *const fn (?*c.snd_pcm_t, ?*c.snd_pcm_hw_params_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_can_pause: *const fn (?*const c.snd_pcm_hw_params_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_current: *const fn (?*c.snd_pcm_t, ?*c.snd_pcm_hw_params_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_get_format_mask: *const fn (?*c.snd_pcm_hw_params_t, ?*c.snd_pcm_format_mask_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_hw_params_get_rate_min: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c_uint, [*c]c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_get_rate_max: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c_uint, [*c]c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_hw_params_get_period_size: *const fn (?*const c.snd_pcm_hw_params_t, [*c]c.snd_pcm_uframes_t, [*c]c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_query_chmaps: *const fn (?*c.snd_pcm_t) callconv(.C) [*c][*c]c.snd_pcm_chmap_query_t = undefined;
|
|
||||||
var snd_pcm_free_chmaps: *const fn ([*c][*c]c.snd_pcm_chmap_query_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_format_mask_malloc: *const fn ([*c]?*c.snd_pcm_format_mask_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_pcm_format_mask_free: *const fn (?*c.snd_pcm_format_mask_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_format_mask_none: *const fn (?*c.snd_pcm_format_mask_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_format_mask_set: *const fn (?*c.snd_pcm_format_mask_t, c.snd_pcm_format_t) callconv(.C) void = undefined;
|
|
||||||
var snd_pcm_format_mask_test: *const fn (?*const c.snd_pcm_format_mask_t, c.snd_pcm_format_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_card_next: *const fn ([*c]c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_ctl_open: *const fn ([*c]?*c.snd_ctl_t, [*c]const u8, c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_ctl_close: *const fn (?*c.snd_ctl_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_ctl_pcm_next_device: *const fn (?*c.snd_ctl_t, [*c]c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_ctl_pcm_info: *const fn (?*c.snd_ctl_t, ?*c.snd_pcm_info_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_open: *const fn ([*c]?*c.snd_mixer_t, c_int) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_close: *const fn (?*c.snd_mixer_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_load: *const fn (?*c.snd_mixer_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_attach: *const fn (?*c.snd_mixer_t, [*c]const u8) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_find_selem: *const fn (?*c.snd_mixer_t, ?*const c.snd_mixer_selem_id_t) callconv(.C) ?*c.snd_mixer_elem_t = undefined;
|
|
||||||
var snd_mixer_selem_register: *const fn (?*c.snd_mixer_t, [*c]c.struct_snd_mixer_selem_regopt, [*c]?*c.snd_mixer_class_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_selem_id_malloc: *const fn ([*c]?*c.snd_mixer_selem_id_t) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_selem_id_free: *const fn (?*c.snd_mixer_selem_id_t) callconv(.C) void = undefined;
|
|
||||||
var snd_mixer_selem_id_set_index: *const fn (?*c.snd_mixer_selem_id_t, c_uint) callconv(.C) void = undefined;
|
|
||||||
var snd_mixer_selem_id_set_name: *const fn (?*c.snd_mixer_selem_id_t, [*c]const u8) callconv(.C) void = undefined;
|
|
||||||
var snd_mixer_selem_set_playback_volume_all: *const fn (?*c.snd_mixer_elem_t, c_long) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_selem_get_playback_volume: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t, [*c]c_long) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_selem_get_playback_volume_range: *const fn (?*c.snd_mixer_elem_t, [*c]c_long, [*c]c_long) callconv(.C) c_int = undefined;
|
|
||||||
var snd_mixer_selem_has_playback_channel: *const fn (?*c.snd_mixer_elem_t, c.snd_mixer_selem_channel_id_t) callconv(.C) c_int = undefined;
|
|
||||||
|
|
||||||
pub fn load() !void {
|
|
||||||
handle = std.DynLib.openZ("libasound.so") catch return error.LibraryNotFound;
|
|
||||||
|
|
||||||
snd_lib_error_set_handler = handle.lookup(@TypeOf(snd_lib_error_set_handler), "snd_lib_error_set_handler") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_info_malloc = handle.lookup(@TypeOf(snd_pcm_info_malloc), "snd_pcm_info_malloc") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_info_free = handle.lookup(@TypeOf(snd_pcm_info_free), "snd_pcm_info_free") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_open = handle.lookup(@TypeOf(snd_pcm_open), "snd_pcm_open") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_close = handle.lookup(@TypeOf(snd_pcm_close), "snd_pcm_close") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_state = handle.lookup(@TypeOf(snd_pcm_state), "snd_pcm_state") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_pause = handle.lookup(@TypeOf(snd_pcm_pause), "snd_pcm_pause") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_writei = handle.lookup(@TypeOf(snd_pcm_writei), "snd_pcm_writei") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_prepare = handle.lookup(@TypeOf(snd_pcm_prepare), "snd_pcm_prepare") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_info_set_device = handle.lookup(@TypeOf(snd_pcm_info_set_device), "snd_pcm_info_set_device") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_info_set_subdevice = handle.lookup(@TypeOf(snd_pcm_info_set_subdevice), "snd_pcm_info_set_subdevice") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_info_get_name = handle.lookup(@TypeOf(snd_pcm_info_get_name), "snd_pcm_info_get_name") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_info_set_stream = handle.lookup(@TypeOf(snd_pcm_info_set_stream), "snd_pcm_info_set_stream") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_free = handle.lookup(@TypeOf(snd_pcm_hw_free), "snd_pcm_hw_free") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_malloc = handle.lookup(@TypeOf(snd_pcm_hw_params_malloc), "snd_pcm_hw_params_malloc") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_free = handle.lookup(@TypeOf(snd_pcm_hw_params_free), "snd_pcm_hw_params_free") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_set_params = handle.lookup(@TypeOf(snd_pcm_set_params), "snd_pcm_set_params") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_any = handle.lookup(@TypeOf(snd_pcm_hw_params_any), "snd_pcm_hw_params_any") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_can_pause = handle.lookup(@TypeOf(snd_pcm_hw_params_can_pause), "snd_pcm_hw_params_can_pause") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_current = handle.lookup(@TypeOf(snd_pcm_hw_params_current), "snd_pcm_hw_params_current") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_get_format_mask = handle.lookup(@TypeOf(snd_pcm_hw_params_get_format_mask), "snd_pcm_hw_params_get_format_mask") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_get_rate_min = handle.lookup(@TypeOf(snd_pcm_hw_params_get_rate_min), "snd_pcm_hw_params_get_rate_min") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_get_rate_max = handle.lookup(@TypeOf(snd_pcm_hw_params_get_rate_max), "snd_pcm_hw_params_get_rate_max") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_hw_params_get_period_size = handle.lookup(@TypeOf(snd_pcm_hw_params_get_period_size), "snd_pcm_hw_params_get_period_size") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_query_chmaps = handle.lookup(@TypeOf(snd_pcm_query_chmaps), "snd_pcm_query_chmaps") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_free_chmaps = handle.lookup(@TypeOf(snd_pcm_free_chmaps), "snd_pcm_free_chmaps") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_format_mask_malloc = handle.lookup(@TypeOf(snd_pcm_format_mask_malloc), "snd_pcm_format_mask_malloc") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_format_mask_free = handle.lookup(@TypeOf(snd_pcm_format_mask_free), "snd_pcm_format_mask_free") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_format_mask_none = handle.lookup(@TypeOf(snd_pcm_format_mask_none), "snd_pcm_format_mask_none") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_format_mask_set = handle.lookup(@TypeOf(snd_pcm_format_mask_set), "snd_pcm_format_mask_set") orelse return error.SymbolLookup;
|
|
||||||
snd_pcm_format_mask_test = handle.lookup(@TypeOf(snd_pcm_format_mask_test), "snd_pcm_format_mask_test") orelse return error.SymbolLookup;
|
|
||||||
snd_card_next = handle.lookup(@TypeOf(snd_card_next), "snd_card_next") orelse return error.SymbolLookup;
|
|
||||||
snd_ctl_open = handle.lookup(@TypeOf(snd_ctl_open), "snd_ctl_open") orelse return error.SymbolLookup;
|
|
||||||
snd_ctl_close = handle.lookup(@TypeOf(snd_ctl_close), "snd_ctl_close") orelse return error.SymbolLookup;
|
|
||||||
snd_ctl_pcm_next_device = handle.lookup(@TypeOf(snd_ctl_pcm_next_device), "snd_ctl_pcm_next_device") orelse return error.SymbolLookup;
|
|
||||||
snd_ctl_pcm_info = handle.lookup(@TypeOf(snd_ctl_pcm_info), "snd_ctl_pcm_info") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_open = handle.lookup(@TypeOf(snd_mixer_open), "snd_mixer_open") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_close = handle.lookup(@TypeOf(snd_mixer_close), "snd_mixer_close") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_load = handle.lookup(@TypeOf(snd_mixer_load), "snd_mixer_load") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_attach = handle.lookup(@TypeOf(snd_mixer_attach), "snd_mixer_attach") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_find_selem = handle.lookup(@TypeOf(snd_mixer_find_selem), "snd_mixer_find_selem") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_register = handle.lookup(@TypeOf(snd_mixer_selem_register), "snd_mixer_selem_register") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_id_malloc = handle.lookup(@TypeOf(snd_mixer_selem_id_malloc), "snd_mixer_selem_id_malloc") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_id_free = handle.lookup(@TypeOf(snd_mixer_selem_id_free), "snd_mixer_selem_id_free") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_id_set_index = handle.lookup(@TypeOf(snd_mixer_selem_id_set_index), "snd_mixer_selem_id_set_index") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_id_set_name = handle.lookup(@TypeOf(snd_mixer_selem_id_set_name), "snd_mixer_selem_id_set_name") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_set_playback_volume_all = handle.lookup(@TypeOf(snd_mixer_selem_set_playback_volume_all), "snd_mixer_selem_set_playback_volume_all") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_get_playback_volume = handle.lookup(@TypeOf(snd_mixer_selem_get_playback_volume), "snd_mixer_selem_get_playback_volume") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_get_playback_volume_range = handle.lookup(@TypeOf(snd_mixer_selem_get_playback_volume_range), "snd_mixer_selem_get_playback_volume_range") orelse return error.SymbolLookup;
|
|
||||||
snd_mixer_selem_has_playback_channel = handle.lookup(@TypeOf(snd_mixer_selem_has_playback_channel), "snd_mixer_selem_has_playback_channel") orelse return error.SymbolLookup;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
watcher: ?Watcher,
|
|
||||||
|
|
||||||
const Watcher = struct {
|
|
||||||
deviceChangeFn: main.Context.DeviceChangeFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
thread: std.Thread,
|
|
||||||
aborted: std.atomic.Atomic(bool),
|
|
||||||
notify_fd: std.os.fd_t,
|
|
||||||
notify_wd: std.os.fd_t,
|
|
||||||
notify_pipe_fd: [2]std.os.fd_t,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
try lib.load();
|
|
||||||
|
|
||||||
_ = lib.snd_lib_error_set_handler(@as(c.snd_lib_error_handler_t, @ptrCast(&util.doNothing)));
|
|
||||||
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
.watcher = blk: {
|
|
||||||
if (options.deviceChangeFn) |deviceChangeFn| {
|
|
||||||
const notify_fd = std.os.inotify_init1(std.os.linux.IN.NONBLOCK) catch |err| switch (err) {
|
|
||||||
error.ProcessFdQuotaExceeded,
|
|
||||||
error.SystemFdQuotaExceeded,
|
|
||||||
error.SystemResources,
|
|
||||||
=> return error.SystemResources,
|
|
||||||
error.Unexpected => unreachable,
|
|
||||||
};
|
|
||||||
errdefer std.os.close(notify_fd);
|
|
||||||
|
|
||||||
const notify_wd = std.os.inotify_add_watch(
|
|
||||||
notify_fd,
|
|
||||||
"/dev/snd",
|
|
||||||
std.os.linux.IN.CREATE | std.os.linux.IN.DELETE,
|
|
||||||
) catch |err| switch (err) {
|
|
||||||
error.AccessDenied => return error.AccessDenied,
|
|
||||||
error.UserResourceLimitReached,
|
|
||||||
error.NotDir,
|
|
||||||
error.FileNotFound,
|
|
||||||
error.SystemResources,
|
|
||||||
=> return error.SystemResources,
|
|
||||||
error.NameTooLong,
|
|
||||||
error.WatchAlreadyExists,
|
|
||||||
error.Unexpected,
|
|
||||||
=> unreachable,
|
|
||||||
};
|
|
||||||
errdefer std.os.inotify_rm_watch(notify_fd, notify_wd);
|
|
||||||
|
|
||||||
const notify_pipe_fd = std.os.pipe2(std.os.O.NONBLOCK) catch |err| switch (err) {
|
|
||||||
error.ProcessFdQuotaExceeded,
|
|
||||||
error.SystemFdQuotaExceeded,
|
|
||||||
=> return error.SystemResources,
|
|
||||||
error.Unexpected => unreachable,
|
|
||||||
};
|
|
||||||
errdefer {
|
|
||||||
std.os.close(notify_pipe_fd[0]);
|
|
||||||
std.os.close(notify_pipe_fd[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk .{
|
|
||||||
.deviceChangeFn = deviceChangeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.aborted = .{ .value = false },
|
|
||||||
.notify_fd = notify_fd,
|
|
||||||
.notify_wd = notify_wd,
|
|
||||||
.notify_pipe_fd = notify_pipe_fd,
|
|
||||||
.thread = std.Thread.spawn(.{}, deviceEventsLoop, .{self}) catch |err| switch (err) {
|
|
||||||
error.ThreadQuotaExceeded,
|
|
||||||
error.SystemResources,
|
|
||||||
error.LockedMemoryLimitExceeded,
|
|
||||||
=> return error.SystemResources,
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
error.Unexpected => unreachable,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .alsa = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
if (self.watcher) |*watcher| {
|
|
||||||
watcher.aborted.store(true, .Unordered);
|
|
||||||
_ = std.os.write(watcher.notify_pipe_fd[1], "a") catch {};
|
|
||||||
watcher.thread.join();
|
|
||||||
|
|
||||||
std.os.close(watcher.notify_pipe_fd[0]);
|
|
||||||
std.os.close(watcher.notify_pipe_fd[1]);
|
|
||||||
std.os.inotify_rm_watch(watcher.notify_fd, watcher.notify_wd);
|
|
||||||
std.os.close(watcher.notify_fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
lib.handle.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deviceEventsLoop(self: *Context) void {
|
|
||||||
var watcher = self.watcher.?;
|
|
||||||
var scan = false;
|
|
||||||
var last_crash: ?i64 = null;
|
|
||||||
var buf: [2048]u8 = undefined;
|
|
||||||
var fds = [2]std.os.pollfd{
|
|
||||||
.{
|
|
||||||
.fd = watcher.notify_fd,
|
|
||||||
.events = std.os.POLL.IN,
|
|
||||||
.revents = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.fd = watcher.notify_pipe_fd[0],
|
|
||||||
.events = std.os.POLL.IN,
|
|
||||||
.revents = 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
while (!watcher.aborted.load(.Unordered)) {
|
|
||||||
_ = std.os.poll(&fds, -1) catch |err| switch (err) {
|
|
||||||
error.NetworkSubsystemFailed,
|
|
||||||
error.SystemResources,
|
|
||||||
=> {
|
|
||||||
const ts = std.time.milliTimestamp();
|
|
||||||
if (last_crash) |lc| {
|
|
||||||
if (ts - lc < 500) return;
|
|
||||||
}
|
|
||||||
last_crash = ts;
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
error.Unexpected => unreachable,
|
|
||||||
};
|
|
||||||
if (watcher.notify_fd & std.os.POLL.IN != 0) {
|
|
||||||
while (true) {
|
|
||||||
const len = std.os.read(watcher.notify_fd, &buf) catch |err| {
|
|
||||||
if (err == error.WouldBlock) break;
|
|
||||||
const ts = std.time.milliTimestamp();
|
|
||||||
if (last_crash) |lc| {
|
|
||||||
if (ts - lc < 500) return;
|
|
||||||
}
|
|
||||||
last_crash = ts;
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
if (len == 0) break;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
var evt: *inotify_event = undefined;
|
|
||||||
while (i < buf.len) : (i += @sizeOf(inotify_event) + evt.len) {
|
|
||||||
evt = @as(*inotify_event, @ptrCast(@alignCast(buf[i..])));
|
|
||||||
const evt_name = @as([*]u8, @ptrCast(buf[i..]))[@sizeOf(inotify_event) .. @sizeOf(inotify_event) + 8];
|
|
||||||
|
|
||||||
if (evt.mask & std.os.linux.IN.ISDIR != 0 or !std.mem.startsWith(u8, evt_name, "pcm"))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
scan = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scan) {
|
|
||||||
watcher.deviceChangeFn(self.watcher.?.user_data);
|
|
||||||
scan = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.clear(self.allocator);
|
|
||||||
|
|
||||||
var pcm_info: ?*c.snd_pcm_info_t = null;
|
|
||||||
_ = lib.snd_pcm_info_malloc(&pcm_info);
|
|
||||||
defer lib.snd_pcm_info_free(pcm_info);
|
|
||||||
|
|
||||||
var card_idx: c_int = -1;
|
|
||||||
if (lib.snd_card_next(&card_idx) < 0)
|
|
||||||
return error.SystemResources;
|
|
||||||
|
|
||||||
while (card_idx >= 0) {
|
|
||||||
var card_id_buf: [8]u8 = undefined;
|
|
||||||
const card_id = std.fmt.bufPrintZ(&card_id_buf, "hw:{d}", .{card_idx}) catch break;
|
|
||||||
|
|
||||||
var ctl: ?*c.snd_ctl_t = undefined;
|
|
||||||
_ = switch (-lib.snd_ctl_open(&ctl, card_id.ptr, 0)) {
|
|
||||||
0 => {},
|
|
||||||
@intFromEnum(std.os.E.NOENT) => break,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
};
|
|
||||||
defer _ = lib.snd_ctl_close(ctl);
|
|
||||||
|
|
||||||
var dev_idx: c_int = -1;
|
|
||||||
if (lib.snd_ctl_pcm_next_device(ctl, &dev_idx) < 0)
|
|
||||||
return error.SystemResources;
|
|
||||||
|
|
||||||
lib.snd_pcm_info_set_device(pcm_info, @as(c_uint, @intCast(dev_idx)));
|
|
||||||
lib.snd_pcm_info_set_subdevice(pcm_info, 0);
|
|
||||||
const name = std.mem.span(lib.snd_pcm_info_get_name(pcm_info) orelse continue);
|
|
||||||
|
|
||||||
for (&[_]main.Device.Mode{ .playback, .capture }) |mode| {
|
|
||||||
const snd_stream = modeToStream(mode);
|
|
||||||
lib.snd_pcm_info_set_stream(pcm_info, snd_stream);
|
|
||||||
const err = lib.snd_ctl_pcm_info(ctl, pcm_info);
|
|
||||||
switch (@as(std.os.E, @enumFromInt(-err))) {
|
|
||||||
.SUCCESS => {},
|
|
||||||
.NOENT,
|
|
||||||
.NXIO,
|
|
||||||
.NODEV,
|
|
||||||
=> break,
|
|
||||||
else => return error.SystemResources,
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf: [9]u8 = undefined; // 'hw' + max(card|device) * 2 + ':' + \0
|
|
||||||
const id = std.fmt.bufPrintZ(&buf, "hw:{d},{d}", .{ card_idx, dev_idx }) catch continue;
|
|
||||||
|
|
||||||
var pcm: ?*c.snd_pcm_t = null;
|
|
||||||
if (lib.snd_pcm_open(&pcm, id.ptr, snd_stream, 0) < 0)
|
|
||||||
continue;
|
|
||||||
defer _ = lib.snd_pcm_close(pcm);
|
|
||||||
|
|
||||||
var params: ?*c.snd_pcm_hw_params_t = null;
|
|
||||||
_ = lib.snd_pcm_hw_params_malloc(¶ms);
|
|
||||||
defer lib.snd_pcm_hw_params_free(params);
|
|
||||||
if (lib.snd_pcm_hw_params_any(pcm, params) < 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (lib.snd_pcm_hw_params_can_pause(params) == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const device = main.Device{
|
|
||||||
.mode = mode,
|
|
||||||
.channels = blk: {
|
|
||||||
const chmap = lib.snd_pcm_query_chmaps(pcm);
|
|
||||||
if (chmap) |_| {
|
|
||||||
defer lib.snd_pcm_free_chmaps(chmap);
|
|
||||||
|
|
||||||
if (chmap[0] == null) continue;
|
|
||||||
|
|
||||||
var channels = try self.allocator.alloc(main.Channel, chmap.*.*.map.channels);
|
|
||||||
for (channels, 0..) |*ch, i|
|
|
||||||
ch.*.id = fromAlsaChannel(chmap[0][0].map.pos()[i]) catch return error.OpeningDevice;
|
|
||||||
break :blk channels;
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.formats = blk: {
|
|
||||||
var fmt_mask: ?*c.snd_pcm_format_mask_t = null;
|
|
||||||
_ = lib.snd_pcm_format_mask_malloc(&fmt_mask);
|
|
||||||
defer lib.snd_pcm_format_mask_free(fmt_mask);
|
|
||||||
lib.snd_pcm_format_mask_none(fmt_mask);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S8);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U8);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S16_BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U16_BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_3BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_3BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S24_BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U24_BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_S32_BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_U32_BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT_BE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_LE);
|
|
||||||
lib.snd_pcm_format_mask_set(fmt_mask, c.SND_PCM_FORMAT_FLOAT64_BE);
|
|
||||||
lib.snd_pcm_hw_params_get_format_mask(params, fmt_mask);
|
|
||||||
|
|
||||||
var fmt_arr = std.ArrayList(main.Format).init(self.allocator);
|
|
||||||
inline for (std.meta.tags(main.Format)) |format| {
|
|
||||||
if (lib.snd_pcm_format_mask_test(fmt_mask, toAlsaFormat(format)) != 0) {
|
|
||||||
try fmt_arr.append(format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk try fmt_arr.toOwnedSlice();
|
|
||||||
},
|
|
||||||
.sample_rate = blk: {
|
|
||||||
var rate_min: c_uint = 0;
|
|
||||||
var rate_max: c_uint = 0;
|
|
||||||
if (lib.snd_pcm_hw_params_get_rate_min(params, &rate_min, null) < 0)
|
|
||||||
continue;
|
|
||||||
if (lib.snd_pcm_hw_params_get_rate_max(params, &rate_max, null) < 0)
|
|
||||||
continue;
|
|
||||||
break :blk .{
|
|
||||||
.min = @as(u24, @intCast(rate_min)),
|
|
||||||
.max = @as(u24, @intCast(rate_max)),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.id = try self.allocator.dupeZ(u8, id),
|
|
||||||
.name = try self.allocator.dupeZ(u8, name),
|
|
||||||
};
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, device);
|
|
||||||
|
|
||||||
if (self.devices_info.default(mode) == null and dev_idx == 0) {
|
|
||||||
self.devices_info.setDefault(mode, self.devices_info.list.items.len - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lib.snd_card_next(&card_idx) < 0)
|
|
||||||
return error.SystemResources;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createPlayer(self: Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
const format = device.preferredFormat(options.format);
|
|
||||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
|
||||||
var pcm: ?*c.snd_pcm_t = null;
|
|
||||||
var mixer: ?*c.snd_mixer_t = null;
|
|
||||||
var selem: ?*c.snd_mixer_selem_id_t = null;
|
|
||||||
var mixer_elm: ?*c.snd_mixer_elem_t = null;
|
|
||||||
var period_size: c_ulong = 0;
|
|
||||||
|
|
||||||
if (lib.snd_pcm_open(&pcm, device.id.ptr, modeToStream(device.mode), 0) < 0)
|
|
||||||
return error.OpeningDevice;
|
|
||||||
errdefer _ = lib.snd_pcm_close(pcm);
|
|
||||||
{
|
|
||||||
var hw_params: ?*c.snd_pcm_hw_params_t = null;
|
|
||||||
|
|
||||||
if ((lib.snd_pcm_set_params(
|
|
||||||
pcm,
|
|
||||||
toAlsaFormat(format),
|
|
||||||
c.SND_PCM_ACCESS_RW_INTERLEAVED,
|
|
||||||
@as(c_uint, @intCast(device.channels.len)),
|
|
||||||
sample_rate,
|
|
||||||
1,
|
|
||||||
main.default_latency,
|
|
||||||
)) < 0)
|
|
||||||
return error.OpeningDevice;
|
|
||||||
errdefer _ = lib.snd_pcm_hw_free(pcm);
|
|
||||||
|
|
||||||
if (lib.snd_pcm_hw_params_malloc(&hw_params) < 0)
|
|
||||||
return error.OpeningDevice;
|
|
||||||
defer lib.snd_pcm_hw_params_free(hw_params);
|
|
||||||
|
|
||||||
if (lib.snd_pcm_hw_params_current(pcm, hw_params) < 0)
|
|
||||||
return error.OpeningDevice;
|
|
||||||
|
|
||||||
if (lib.snd_pcm_hw_params_get_period_size(hw_params, &period_size, null) < 0)
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
if (lib.snd_mixer_open(&mixer, 0) < 0)
|
|
||||||
return error.OutOfMemory;
|
|
||||||
|
|
||||||
const card_id = try self.allocator.dupeZ(u8, std.mem.sliceTo(device.id, ','));
|
|
||||||
defer self.allocator.free(card_id);
|
|
||||||
|
|
||||||
if (lib.snd_mixer_attach(mixer, card_id.ptr) < 0)
|
|
||||||
return error.IncompatibleDevice;
|
|
||||||
|
|
||||||
if (lib.snd_mixer_selem_register(mixer, null, null) < 0)
|
|
||||||
return error.OpeningDevice;
|
|
||||||
|
|
||||||
if (lib.snd_mixer_load(mixer) < 0)
|
|
||||||
return error.OpeningDevice;
|
|
||||||
|
|
||||||
if (lib.snd_mixer_selem_id_malloc(&selem) < 0)
|
|
||||||
return error.OutOfMemory;
|
|
||||||
errdefer lib.snd_mixer_selem_id_free(selem);
|
|
||||||
|
|
||||||
lib.snd_mixer_selem_id_set_index(selem, 0);
|
|
||||||
lib.snd_mixer_selem_id_set_name(selem, "Master");
|
|
||||||
|
|
||||||
mixer_elm = lib.snd_mixer_find_selem(mixer, selem) orelse
|
|
||||||
return error.IncompatibleDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.thread = undefined,
|
|
||||||
.mutex = .{},
|
|
||||||
.aborted = .{ .value = false },
|
|
||||||
.sample_buffer = try self.allocator.alloc(u8, period_size * format.frameSize(device.channels.len)),
|
|
||||||
.period_size = period_size,
|
|
||||||
.pcm = pcm.?,
|
|
||||||
.mixer = mixer.?,
|
|
||||||
.selem = selem.?,
|
|
||||||
.mixer_elm = mixer_elm.?,
|
|
||||||
.writeFn = writeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = format,
|
|
||||||
.sample_rate = sample_rate,
|
|
||||||
.write_step = format.frameSize(device.channels.len),
|
|
||||||
};
|
|
||||||
return .{ .alsa = player };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
thread: std.Thread,
|
|
||||||
mutex: std.Thread.Mutex,
|
|
||||||
aborted: std.atomic.Atomic(bool),
|
|
||||||
sample_buffer: []u8,
|
|
||||||
period_size: c_ulong,
|
|
||||||
pcm: *c.snd_pcm_t,
|
|
||||||
mixer: *c.snd_mixer_t,
|
|
||||||
selem: *c.snd_mixer_selem_id_t,
|
|
||||||
mixer_elm: *c.snd_mixer_elem_t,
|
|
||||||
writeFn: main.WriteFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
sample_rate: u24,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
self.aborted.store(true, .Unordered);
|
|
||||||
self.thread.join();
|
|
||||||
|
|
||||||
_ = lib.snd_mixer_close(self.mixer);
|
|
||||||
lib.snd_mixer_selem_id_free(self.selem);
|
|
||||||
_ = lib.snd_pcm_close(self.pcm);
|
|
||||||
_ = lib.snd_pcm_hw_free(self.pcm);
|
|
||||||
|
|
||||||
self.allocator.free(self.sample_buffer);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: *Player) !void {
|
|
||||||
self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) {
|
|
||||||
error.ThreadQuotaExceeded,
|
|
||||||
error.SystemResources,
|
|
||||||
error.LockedMemoryLimitExceeded,
|
|
||||||
=> return error.SystemResources,
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
error.Unexpected => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeLoop(self: *Player) void {
|
|
||||||
for (self.channels, 0..) |*ch, i| {
|
|
||||||
ch.*.ptr = self.sample_buffer.ptr + self.format.frameSize(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
var underrun = false;
|
|
||||||
while (!self.aborted.load(.Unordered)) {
|
|
||||||
if (!underrun) {
|
|
||||||
self.writeFn(self.user_data, self.period_size);
|
|
||||||
}
|
|
||||||
underrun = false;
|
|
||||||
const n = lib.snd_pcm_writei(self.pcm, self.sample_buffer.ptr, self.period_size);
|
|
||||||
if (n < 0) {
|
|
||||||
_ = lib.snd_pcm_prepare(self.pcm);
|
|
||||||
underrun = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: Player) !void {
|
|
||||||
if (lib.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED) {
|
|
||||||
if (lib.snd_pcm_pause(self.pcm, 0) < 0)
|
|
||||||
return error.CannotPlay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: Player) !void {
|
|
||||||
if (lib.snd_pcm_state(self.pcm) != c.SND_PCM_STATE_PAUSED) {
|
|
||||||
if (lib.snd_pcm_pause(self.pcm, 1) < 0)
|
|
||||||
return error.CannotPause;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: Player) bool {
|
|
||||||
return lib.snd_pcm_state(self.pcm) == c.SND_PCM_STATE_PAUSED;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
var min_vol: c_long = 0;
|
|
||||||
var max_vol: c_long = 0;
|
|
||||||
if (lib.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0)
|
|
||||||
return error.CannotSetVolume;
|
|
||||||
|
|
||||||
const dist = @as(f32, @floatFromInt(max_vol - min_vol));
|
|
||||||
if (lib.snd_mixer_selem_set_playback_volume_all(
|
|
||||||
self.mixer_elm,
|
|
||||||
@as(c_long, @intFromFloat(dist * vol)) + min_vol,
|
|
||||||
) < 0)
|
|
||||||
return error.CannotSetVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: *Player) !f32 {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
var vol: c_long = 0;
|
|
||||||
var channel: c_int = 0;
|
|
||||||
|
|
||||||
while (channel < c.SND_MIXER_SCHN_LAST) : (channel += 1) {
|
|
||||||
if (lib.snd_mixer_selem_has_playback_channel(self.mixer_elm, channel) == 1) {
|
|
||||||
if (lib.snd_mixer_selem_get_playback_volume(self.mixer_elm, channel, &vol) == 0)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channel == c.SND_MIXER_SCHN_LAST)
|
|
||||||
return error.CannotGetVolume;
|
|
||||||
|
|
||||||
var min_vol: c_long = 0;
|
|
||||||
var max_vol: c_long = 0;
|
|
||||||
if (lib.snd_mixer_selem_get_playback_volume_range(self.mixer_elm, &min_vol, &max_vol) < 0)
|
|
||||||
return error.CannotGetVolume;
|
|
||||||
|
|
||||||
return @as(f32, @floatFromInt(vol)) / @as(f32, @floatFromInt(max_vol - min_vol));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
||||||
allocator.free(device.id);
|
|
||||||
allocator.free(device.name);
|
|
||||||
allocator.free(device.formats);
|
|
||||||
allocator.free(device.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn modeToStream(mode: main.Device.Mode) c_uint {
|
|
||||||
return switch (mode) {
|
|
||||||
.playback => c.SND_PCM_STREAM_PLAYBACK,
|
|
||||||
.capture => c.SND_PCM_STREAM_CAPTURE,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toAlsaFormat(format: main.Format) c.snd_pcm_format_t {
|
|
||||||
return switch (format) {
|
|
||||||
.u8 => c.SND_PCM_FORMAT_U8,
|
|
||||||
.i16 => if (is_little) c.SND_PCM_FORMAT_S16_LE else c.SND_PCM_FORMAT_S16_BE,
|
|
||||||
.i24 => if (is_little) c.SND_PCM_FORMAT_S24_3LE else c.SND_PCM_FORMAT_S24_3BE,
|
|
||||||
.i24_4b => if (is_little) c.SND_PCM_FORMAT_S24_LE else c.SND_PCM_FORMAT_S24_BE,
|
|
||||||
.i32 => if (is_little) c.SND_PCM_FORMAT_S32_LE else c.SND_PCM_FORMAT_S32_BE,
|
|
||||||
.f32 => if (is_little) c.SND_PCM_FORMAT_FLOAT_LE else c.SND_PCM_FORMAT_FLOAT_BE,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fromAlsaChannel(pos: c_uint) !main.Channel.Id {
|
|
||||||
return switch (pos) {
|
|
||||||
c.SND_CHMAP_UNKNOWN, c.SND_CHMAP_NA => return error.Invalid,
|
|
||||||
c.SND_CHMAP_MONO, c.SND_CHMAP_FC => .front_center,
|
|
||||||
c.SND_CHMAP_FL => .front_left,
|
|
||||||
c.SND_CHMAP_FR => .front_right,
|
|
||||||
c.SND_CHMAP_LFE => .lfe,
|
|
||||||
c.SND_CHMAP_SL => .side_left,
|
|
||||||
c.SND_CHMAP_SR => .side_right,
|
|
||||||
c.SND_CHMAP_RC => .back_center,
|
|
||||||
c.SND_CHMAP_RLC => .back_left,
|
|
||||||
c.SND_CHMAP_RRC => .back_right,
|
|
||||||
c.SND_CHMAP_FLC => .front_left_center,
|
|
||||||
c.SND_CHMAP_FRC => .front_right_center,
|
|
||||||
c.SND_CHMAP_TC => .top_center,
|
|
||||||
c.SND_CHMAP_TFL => .top_front_left,
|
|
||||||
c.SND_CHMAP_TFR => .top_front_right,
|
|
||||||
c.SND_CHMAP_TFC => .top_front_center,
|
|
||||||
c.SND_CHMAP_TRL => .top_back_left,
|
|
||||||
c.SND_CHMAP_TRR => .top_back_right,
|
|
||||||
c.SND_CHMAP_TRC => .top_back_center,
|
|
||||||
|
|
||||||
else => return error.Invalid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toCHMAP(pos: main.Channel.Id) c_uint {
|
|
||||||
return switch (pos) {
|
|
||||||
.front_center => c.SND_CHMAP_FC,
|
|
||||||
.front_left => c.SND_CHMAP_FL,
|
|
||||||
.front_right => c.SND_CHMAP_FR,
|
|
||||||
.lfe => c.SND_CHMAP_LFE,
|
|
||||||
.side_left => c.SND_CHMAP_SL,
|
|
||||||
.side_right => c.SND_CHMAP_SR,
|
|
||||||
.back_center => c.SND_CHMAP_RC,
|
|
||||||
.back_left => c.SND_CHMAP_RLC,
|
|
||||||
.back_right => c.SND_CHMAP_RRC,
|
|
||||||
.front_left_center => c.SND_CHMAP_FLC,
|
|
||||||
.front_right_center => c.SND_CHMAP_FRC,
|
|
||||||
.top_center => c.SND_CHMAP_TC,
|
|
||||||
.top_front_left => c.SND_CHMAP_TFL,
|
|
||||||
.top_front_right => c.SND_CHMAP_TFR,
|
|
||||||
.top_front_center => c.SND_CHMAP_TFC,
|
|
||||||
.top_back_left => c.SND_CHMAP_TRL,
|
|
||||||
.top_back_right => c.SND_CHMAP_TRR,
|
|
||||||
.top_back_center => c.SND_CHMAP_TRC,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
const builtin = @import("builtin");
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub const Backend = std.meta.Tag(BackendContext);
|
|
||||||
pub const BackendContext = switch (builtin.os.tag) {
|
|
||||||
.linux => union(enum) {
|
|
||||||
alsa: *@import("alsa.zig").Context,
|
|
||||||
pipewire: *@import("pipewire.zig").Context,
|
|
||||||
jack: *@import("jack.zig").Context,
|
|
||||||
pulseaudio: *@import("pulseaudio.zig").Context,
|
|
||||||
dummy: *@import("dummy.zig").Context,
|
|
||||||
},
|
|
||||||
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
|
|
||||||
pipewire: *@import("pipewire.zig").Context,
|
|
||||||
jack: *@import("jack.zig").Context,
|
|
||||||
pulseaudio: *@import("pulseaudio.zig").Context,
|
|
||||||
dummy: *@import("dummy.zig").Context,
|
|
||||||
},
|
|
||||||
.macos, .ios, .watchos, .tvos => union(enum) {
|
|
||||||
coreaudio: *@import("coreaudio.zig").Context,
|
|
||||||
dummy: *@import("dummy.zig").Context,
|
|
||||||
},
|
|
||||||
.windows => union(enum) {
|
|
||||||
wasapi: *@import("wasapi.zig").Context,
|
|
||||||
dummy: *@import("dummy.zig").Context,
|
|
||||||
},
|
|
||||||
.freestanding => switch (builtin.cpu.arch) {
|
|
||||||
.wasm32 => union(enum) {
|
|
||||||
webaudio: *@import("webaudio.zig").Context,
|
|
||||||
dummy: *@import("dummy.zig").Context,
|
|
||||||
},
|
|
||||||
else => union(enum) {
|
|
||||||
dummy: *@import("dummy.zig").Context,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
else => union(enum) {
|
|
||||||
dummy: *@import("dummy.zig").Context,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
pub const BackendPlayer = switch (builtin.os.tag) {
|
|
||||||
.linux => union(enum) {
|
|
||||||
alsa: *@import("alsa.zig").Player,
|
|
||||||
pipewire: *@import("pipewire.zig").Player,
|
|
||||||
jack: *@import("jack.zig").Player,
|
|
||||||
pulseaudio: *@import("pulseaudio.zig").Player,
|
|
||||||
dummy: *@import("dummy.zig").Player,
|
|
||||||
},
|
|
||||||
.freebsd, .netbsd, .openbsd, .solaris => union(enum) {
|
|
||||||
pipewire: *@import("pipewire.zig").Player,
|
|
||||||
jack: *@import("jack.zig").Player,
|
|
||||||
pulseaudio: *@import("pulseaudio.zig").Player,
|
|
||||||
dummy: *@import("dummy.zig").Player,
|
|
||||||
},
|
|
||||||
.macos, .ios, .watchos, .tvos => union(enum) {
|
|
||||||
coreaudio: *@import("coreaudio.zig").Player,
|
|
||||||
dummy: *@import("dummy.zig").Player,
|
|
||||||
},
|
|
||||||
.windows => union(enum) {
|
|
||||||
wasapi: *@import("wasapi.zig").Player,
|
|
||||||
dummy: *@import("dummy.zig").Player,
|
|
||||||
},
|
|
||||||
.freestanding => switch (builtin.cpu.arch) {
|
|
||||||
.wasm32 => union(enum) {
|
|
||||||
webaudio: *@import("webaudio.zig").Player,
|
|
||||||
dummy: *@import("dummy.zig").Player,
|
|
||||||
},
|
|
||||||
else => union(enum) {
|
|
||||||
dummy: *@import("dummy.zig").Player,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
else => union(enum) {
|
|
||||||
dummy: *@import("dummy.zig").Player,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,449 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
// const c = @import("cimport.zig");
|
|
||||||
const c = @cImport({
|
|
||||||
@cInclude("CoreAudio/CoreAudio.h");
|
|
||||||
@cInclude("AudioUnit/AudioUnit.h");
|
|
||||||
});
|
|
||||||
const native_endian = builtin.cpu.arch.endian();
|
|
||||||
var is_darling = false;
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
_ = options;
|
|
||||||
|
|
||||||
if (std.fs.accessAbsolute("/usr/lib/darling", .{})) {
|
|
||||||
is_darling = true;
|
|
||||||
} else |_| {}
|
|
||||||
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .coreaudio = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.clear(self.allocator);
|
|
||||||
|
|
||||||
var prop_address = c.AudioObjectPropertyAddress{
|
|
||||||
.mSelector = c.kAudioHardwarePropertyDevices,
|
|
||||||
.mScope = c.kAudioObjectPropertyScopeGlobal,
|
|
||||||
.mElement = c.kAudioObjectPropertyElementMaster,
|
|
||||||
};
|
|
||||||
|
|
||||||
var io_size: u32 = 0;
|
|
||||||
if (c.AudioObjectGetPropertyDataSize(
|
|
||||||
c.kAudioObjectSystemObject,
|
|
||||||
&prop_address,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
&io_size,
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
const devices_count = io_size / @sizeOf(c.AudioObjectID);
|
|
||||||
if (devices_count == 0) return;
|
|
||||||
|
|
||||||
var devs = try self.allocator.alloc(c.AudioObjectID, devices_count);
|
|
||||||
defer self.allocator.free(devs);
|
|
||||||
if (c.AudioObjectGetPropertyData(
|
|
||||||
c.kAudioObjectSystemObject,
|
|
||||||
&prop_address,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
&io_size,
|
|
||||||
@as(*anyopaque, @ptrCast(devs)),
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
var default_input_id: c.AudioObjectID = undefined;
|
|
||||||
var default_output_id: c.AudioObjectID = undefined;
|
|
||||||
|
|
||||||
io_size = @sizeOf(c.AudioObjectID);
|
|
||||||
if (c.AudioHardwareGetProperty(
|
|
||||||
c.kAudioHardwarePropertyDefaultInputDevice,
|
|
||||||
&io_size,
|
|
||||||
&default_input_id,
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
io_size = @sizeOf(c.AudioObjectID);
|
|
||||||
if (c.AudioHardwareGetProperty(
|
|
||||||
c.kAudioHardwarePropertyDefaultOutputDevice,
|
|
||||||
&io_size,
|
|
||||||
&default_output_id,
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (devs) |id| {
|
|
||||||
var buf_list: *c.AudioBufferList = undefined;
|
|
||||||
defer self.allocator.destroy(buf_list);
|
|
||||||
var mode: main.Device.Mode = undefined;
|
|
||||||
for (std.meta.tags(main.Device.Mode)) |m| {
|
|
||||||
mode = m;
|
|
||||||
|
|
||||||
io_size = 0;
|
|
||||||
prop_address.mSelector = c.kAudioDevicePropertyStreamConfiguration;
|
|
||||||
prop_address.mScope = switch (mode) {
|
|
||||||
.playback => c.kAudioObjectPropertyScopeOutput,
|
|
||||||
.capture => c.kAudioObjectPropertyScopeInput,
|
|
||||||
};
|
|
||||||
if (c.AudioObjectGetPropertyDataSize(
|
|
||||||
id,
|
|
||||||
&prop_address,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
&io_size,
|
|
||||||
) != c.noErr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf_list = try self.allocator.create(c.AudioBufferList);
|
|
||||||
if (c.AudioObjectGetPropertyData(
|
|
||||||
id,
|
|
||||||
&prop_address,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
&io_size,
|
|
||||||
@as(*anyopaque, @ptrCast(buf_list)),
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf_list.mBuffers[0].mNumberChannels > 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for now, only set `channels` to the output channels
|
|
||||||
const audio_buffer_list_property_address = c.AudioObjectPropertyAddress{
|
|
||||||
.mSelector = c.kAudioDevicePropertyStreamConfiguration,
|
|
||||||
.mScope = c.kAudioDevicePropertyScopeOutput,
|
|
||||||
.mElement = c.kAudioObjectPropertyElementMain,
|
|
||||||
};
|
|
||||||
var output_audio_buffer_list: c.AudioBufferList = undefined;
|
|
||||||
var audio_buffer_list_size: c_uint = undefined;
|
|
||||||
|
|
||||||
if (c.AudioObjectGetPropertyDataSize(id, &audio_buffer_list_property_address, 0, null, &audio_buffer_list_size) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c.AudioObjectGetPropertyData(id, &prop_address, 0, null, &audio_buffer_list_size, &output_audio_buffer_list) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
var output_channel_count: usize = 0;
|
|
||||||
for (0..output_audio_buffer_list.mNumberBuffers) |mBufferIndex| {
|
|
||||||
output_channel_count += output_audio_buffer_list.mBuffers[mBufferIndex].mNumberChannels;
|
|
||||||
}
|
|
||||||
|
|
||||||
var channels = try self.allocator.alloc(main.Channel, output_channel_count);
|
|
||||||
|
|
||||||
prop_address.mSelector = c.kAudioDevicePropertyNominalSampleRate;
|
|
||||||
io_size = @sizeOf(f64);
|
|
||||||
var sample_rate: f64 = undefined;
|
|
||||||
if (c.AudioObjectGetPropertyData(
|
|
||||||
id,
|
|
||||||
&prop_address,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
&io_size,
|
|
||||||
&sample_rate,
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
io_size = @sizeOf([*]const u8);
|
|
||||||
if (c.AudioDeviceGetPropertyInfo(
|
|
||||||
id,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
c.kAudioDevicePropertyDeviceName,
|
|
||||||
&io_size,
|
|
||||||
null,
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = try self.allocator.allocSentinel(u8, io_size, 0);
|
|
||||||
errdefer self.allocator.free(name);
|
|
||||||
if (c.AudioDeviceGetProperty(
|
|
||||||
id,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
c.kAudioDevicePropertyDeviceName,
|
|
||||||
&io_size,
|
|
||||||
name.ptr,
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
const id_str = try std.fmt.allocPrintZ(self.allocator, "{d}", .{id});
|
|
||||||
errdefer self.allocator.free(id_str);
|
|
||||||
|
|
||||||
var dev = main.Device{
|
|
||||||
.id = id_str,
|
|
||||||
.name = name,
|
|
||||||
.mode = mode,
|
|
||||||
.channels = channels,
|
|
||||||
.formats = &.{ .i16, .i32, .f32 },
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = @as(u24, @intFromFloat(@floor(sample_rate))),
|
|
||||||
.max = @as(u24, @intFromFloat(@floor(sample_rate))),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, dev);
|
|
||||||
if (id == default_output_id) {
|
|
||||||
self.devices_info.default_output = self.devices_info.list.items.len - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id == default_input_id) {
|
|
||||||
self.devices_info.default_input = self.devices_info.list.items.len - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
|
|
||||||
var component_desc = c.AudioComponentDescription{
|
|
||||||
.componentType = c.kAudioUnitType_Output,
|
|
||||||
.componentSubType = c.kAudioUnitSubType_HALOutput,
|
|
||||||
.componentManufacturer = c.kAudioUnitManufacturer_Apple,
|
|
||||||
.componentFlags = 0,
|
|
||||||
.componentFlagsMask = 0,
|
|
||||||
};
|
|
||||||
const component = c.AudioComponentFindNext(null, &component_desc);
|
|
||||||
if (component == null) return error.OpeningDevice;
|
|
||||||
|
|
||||||
var audio_unit: c.AudioComponentInstance = undefined;
|
|
||||||
if (c.AudioComponentInstanceNew(component, &audio_unit) != c.noErr) return error.OpeningDevice;
|
|
||||||
|
|
||||||
if (c.AudioUnitInitialize(audio_unit) != c.noErr) return error.OpeningDevice;
|
|
||||||
errdefer _ = c.AudioUnitUninitialize(audio_unit);
|
|
||||||
|
|
||||||
const device_id = std.fmt.parseInt(c.AudioDeviceID, device.id, 10) catch unreachable;
|
|
||||||
if (c.AudioUnitSetProperty(
|
|
||||||
audio_unit,
|
|
||||||
c.kAudioOutputUnitProperty_CurrentDevice,
|
|
||||||
c.kAudioUnitScope_Input,
|
|
||||||
0,
|
|
||||||
&device_id,
|
|
||||||
@sizeOf(c.AudioDeviceID),
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream_desc = try createStreamDesc(options.format, options.sample_rate, device.channels.len);
|
|
||||||
if (c.AudioUnitSetProperty(
|
|
||||||
audio_unit,
|
|
||||||
c.kAudioUnitProperty_StreamFormat,
|
|
||||||
c.kAudioUnitScope_Input,
|
|
||||||
0,
|
|
||||||
&stream_desc,
|
|
||||||
@sizeOf(c.AudioStreamBasicDescription),
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
const render_callback = c.AURenderCallbackStruct{
|
|
||||||
.inputProc = Player.renderCallback,
|
|
||||||
.inputProcRefCon = player,
|
|
||||||
};
|
|
||||||
if (c.AudioUnitSetProperty(
|
|
||||||
audio_unit,
|
|
||||||
c.kAudioUnitProperty_SetRenderCallback,
|
|
||||||
c.kAudioUnitScope_Input,
|
|
||||||
0,
|
|
||||||
&render_callback,
|
|
||||||
@sizeOf(c.AURenderCallbackStruct),
|
|
||||||
) != c.noErr) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.audio_unit = audio_unit.?,
|
|
||||||
.is_paused = false,
|
|
||||||
.vol = 1.0,
|
|
||||||
.writeFn = writeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = options.format,
|
|
||||||
.sample_rate = options.sample_rate,
|
|
||||||
.write_step = options.format.frameSize(device.channels.len),
|
|
||||||
};
|
|
||||||
return .{ .coreaudio = player };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
audio_unit: c.AudioUnit,
|
|
||||||
is_paused: bool,
|
|
||||||
vol: f32,
|
|
||||||
writeFn: main.WriteFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
sample_rate: u24,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn renderCallback(
|
|
||||||
self_opaque: ?*anyopaque,
|
|
||||||
action_flags: [*c]c.AudioUnitRenderActionFlags,
|
|
||||||
time_stamp: [*c]const c.AudioTimeStamp,
|
|
||||||
bus_number: u32,
|
|
||||||
frames_left: u32,
|
|
||||||
buf: [*c]c.AudioBufferList,
|
|
||||||
) callconv(.C) c.OSStatus {
|
|
||||||
_ = action_flags;
|
|
||||||
_ = time_stamp;
|
|
||||||
_ = bus_number;
|
|
||||||
_ = frames_left;
|
|
||||||
|
|
||||||
const self = @as(*Player, @ptrCast(@alignCast(self_opaque.?)));
|
|
||||||
|
|
||||||
for (self.channels, 0..) |*ch, i| {
|
|
||||||
ch.ptr = @as([*]u8, @ptrCast(buf.*.mBuffers[0].mData.?)) + self.format.frameSize(i);
|
|
||||||
}
|
|
||||||
const frames = buf.*.mBuffers[0].mDataByteSize / self.format.frameSize(self.channels.len);
|
|
||||||
self.writeFn(self.user_data, frames);
|
|
||||||
|
|
||||||
return c.noErr;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
_ = c.AudioOutputUnitStop(self.audio_unit);
|
|
||||||
_ = c.AudioUnitUninitialize(self.audio_unit);
|
|
||||||
_ = c.AudioComponentInstanceDispose(self.audio_unit);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: *Player) !void {
|
|
||||||
return self.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: *Player) !void {
|
|
||||||
if (c.AudioOutputUnitStart(self.audio_unit) != c.noErr) {
|
|
||||||
return error.CannotPlay;
|
|
||||||
}
|
|
||||||
self.is_paused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: *Player) !void {
|
|
||||||
if (c.AudioOutputUnitStop(self.audio_unit) != c.noErr) {
|
|
||||||
return error.CannotPause;
|
|
||||||
}
|
|
||||||
self.is_paused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: Player) bool {
|
|
||||||
return self.is_paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
if (c.AudioUnitSetParameter(
|
|
||||||
self.audio_unit,
|
|
||||||
c.kHALOutputParam_Volume,
|
|
||||||
c.kAudioUnitScope_Global,
|
|
||||||
0,
|
|
||||||
vol,
|
|
||||||
0,
|
|
||||||
) != c.noErr) {
|
|
||||||
if (is_darling) return;
|
|
||||||
return error.CannotSetVolume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: Player) !f32 {
|
|
||||||
var vol: f32 = 0;
|
|
||||||
if (c.AudioUnitGetParameter(
|
|
||||||
self.audio_unit,
|
|
||||||
c.kHALOutputParam_Volume,
|
|
||||||
c.kAudioUnitScope_Global,
|
|
||||||
0,
|
|
||||||
&vol,
|
|
||||||
) != c.noErr) {
|
|
||||||
if (is_darling) return 1;
|
|
||||||
return error.CannotGetVolume;
|
|
||||||
}
|
|
||||||
return vol;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
||||||
allocator.free(device.id);
|
|
||||||
allocator.free(device.name);
|
|
||||||
allocator.free(device.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn createStreamDesc(format: main.Format, sample_rate: u24, ch_count: usize) !c.AudioStreamBasicDescription {
|
|
||||||
var desc = c.AudioStreamBasicDescription{
|
|
||||||
.mSampleRate = @as(f64, @floatFromInt(sample_rate)),
|
|
||||||
.mFormatID = c.kAudioFormatLinearPCM,
|
|
||||||
.mFormatFlags = switch (format) {
|
|
||||||
.i16 => c.kAudioFormatFlagIsSignedInteger,
|
|
||||||
.i24 => c.kAudioFormatFlagIsSignedInteger,
|
|
||||||
.i32 => c.kAudioFormatFlagIsSignedInteger,
|
|
||||||
.f32 => c.kAudioFormatFlagIsFloat,
|
|
||||||
.u8 => return error.IncompatibleDevice,
|
|
||||||
.i24_4b => return error.IncompatibleDevice,
|
|
||||||
},
|
|
||||||
.mBytesPerPacket = format.frameSize(ch_count),
|
|
||||||
.mFramesPerPacket = 1,
|
|
||||||
.mBytesPerFrame = format.frameSize(ch_count),
|
|
||||||
.mChannelsPerFrame = @as(c_uint, @intCast(ch_count)),
|
|
||||||
.mBitsPerChannel = switch (format) {
|
|
||||||
.i16 => 16,
|
|
||||||
.i24 => 24,
|
|
||||||
.i32 => 32,
|
|
||||||
.f32 => 32,
|
|
||||||
.u8 => unreachable,
|
|
||||||
.i24_4b => unreachable,
|
|
||||||
},
|
|
||||||
.mReserved = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (native_endian == .Big) {
|
|
||||||
desc.mFormatFlags |= c.kAudioFormatFlagIsBigEndian;
|
|
||||||
}
|
|
||||||
|
|
||||||
return desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
|
|
||||||
const dummy_playback = main.Device{
|
|
||||||
.id = "dummy-playback",
|
|
||||||
.name = "Dummy Device",
|
|
||||||
.mode = .playback,
|
|
||||||
.channels = undefined,
|
|
||||||
.formats = std.meta.tags(main.Format),
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = main.min_sample_rate,
|
|
||||||
.max = main.max_sample_rate,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const dummy_capture = main.Device{
|
|
||||||
.id = "dummy-capture",
|
|
||||||
.name = "Dummy Device",
|
|
||||||
.mode = .capture,
|
|
||||||
.channels = undefined,
|
|
||||||
.formats = std.meta.tags(main.Format),
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = main.min_sample_rate,
|
|
||||||
.max = main.max_sample_rate,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
_ = options;
|
|
||||||
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .dummy = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.clear(self.allocator);
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, dummy_playback);
|
|
||||||
try self.devices_info.list.append(self.allocator, dummy_capture);
|
|
||||||
|
|
||||||
self.devices_info.setDefault(.playback, 0);
|
|
||||||
self.devices_info.setDefault(.capture, 1);
|
|
||||||
|
|
||||||
self.devices_info.list.items[0].channels = try self.allocator.alloc(main.Channel, 1);
|
|
||||||
self.devices_info.list.items[1].channels = try self.allocator.alloc(main.Channel, 1);
|
|
||||||
|
|
||||||
self.devices_info.list.items[0].channels[0] = .{ .id = .front_center };
|
|
||||||
self.devices_info.list.items[1].channels[0] = .{ .id = .front_center };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
_ = writeFn;
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.is_paused = false,
|
|
||||||
.vol = 1.0,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = options.format,
|
|
||||||
.sample_rate = options.sample_rate,
|
|
||||||
.write_step = 0,
|
|
||||||
};
|
|
||||||
return .{ .dummy = player };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
is_paused: bool,
|
|
||||||
vol: f32,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
sample_rate: u24,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: Player) !void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: *Player) !void {
|
|
||||||
self.is_paused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: *Player) !void {
|
|
||||||
self.is_paused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: Player) bool {
|
|
||||||
return self.is_paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
self.vol = vol;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: Player) !f32 {
|
|
||||||
return self.vol;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
||||||
allocator.free(device.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
|
|
@ -1,327 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const c = @cImport(@cInclude("jack/jack.h"));
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
|
|
||||||
const lib = struct {
|
|
||||||
var handle: std.DynLib = undefined;
|
|
||||||
|
|
||||||
var jack_free: *const fn (ptr: ?*anyopaque) callconv(.C) void = undefined;
|
|
||||||
var jack_set_error_function: *const fn (?*const fn ([*c]const u8) callconv(.C) void) callconv(.C) void = undefined;
|
|
||||||
var jack_set_info_function: *const fn (?*const fn ([*c]const u8) callconv(.C) void) callconv(.C) void = undefined;
|
|
||||||
var jack_client_open: *const fn ([*c]const u8, c.jack_options_t, [*c]c.jack_status_t, ...) callconv(.C) ?*c.jack_client_t = undefined;
|
|
||||||
var jack_client_close: *const fn (?*c.jack_client_t) callconv(.C) c_int = undefined;
|
|
||||||
var jack_connect: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8) callconv(.C) c_int = undefined;
|
|
||||||
var jack_disconnect: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8) callconv(.C) c_int = undefined;
|
|
||||||
var jack_activate: *const fn (?*c.jack_client_t) callconv(.C) c_int = undefined;
|
|
||||||
var jack_deactivate: *const fn (?*c.jack_client_t) callconv(.C) c_int = undefined;
|
|
||||||
var jack_port_by_name: *const fn (?*c.jack_client_t, [*c]const u8) callconv(.C) ?*c.jack_port_t = undefined;
|
|
||||||
var jack_port_register: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8, c_ulong, c_ulong) callconv(.C) ?*c.jack_port_t = undefined;
|
|
||||||
var jack_set_sample_rate_callback: *const fn (?*c.jack_client_t, c.JackSampleRateCallback, ?*anyopaque) callconv(.C) c_int = undefined;
|
|
||||||
var jack_set_port_registration_callback: *const fn (?*c.jack_client_t, c.JackPortRegistrationCallback, ?*anyopaque) callconv(.C) c_int = undefined;
|
|
||||||
var jack_set_process_callback: *const fn (?*c.jack_client_t, c.JackProcessCallback, ?*anyopaque) callconv(.C) c_int = undefined;
|
|
||||||
var jack_set_port_rename_callback: *const fn (?*c.jack_client_t, c.JackPortRenameCallback, ?*anyopaque) callconv(.C) c_int = undefined;
|
|
||||||
var jack_get_sample_rate: *const fn (?*c.jack_client_t) callconv(.C) c.jack_nframes_t = undefined;
|
|
||||||
var jack_get_ports: *const fn (?*c.jack_client_t, [*c]const u8, [*c]const u8, c_ulong) callconv(.C) [*c][*c]const u8 = undefined;
|
|
||||||
var jack_port_type: *const fn (port: ?*const c.jack_port_t) callconv(.C) [*c]const u8 = undefined;
|
|
||||||
var jack_port_flags: *const fn (port: ?*const c.jack_port_t) callconv(.C) c_int = undefined;
|
|
||||||
var jack_port_name: *const fn (?*const c.jack_port_t) callconv(.C) [*c]const u8 = undefined;
|
|
||||||
var jack_port_get_buffer: *const fn (?*c.jack_port_t, c.jack_nframes_t) callconv(.C) ?*anyopaque = undefined;
|
|
||||||
var jack_port_connected_to: *const fn (?*const c.jack_port_t, [*c]const u8) callconv(.C) c_int = undefined;
|
|
||||||
var jack_port_type_size: *const fn () c_int = undefined;
|
|
||||||
|
|
||||||
pub fn load() !void {
|
|
||||||
handle = std.DynLib.openZ("libjack.so") catch return error.LibraryNotFound;
|
|
||||||
|
|
||||||
jack_free = handle.lookup(@TypeOf(jack_free), "jack_free") orelse return error.SymbolLookup;
|
|
||||||
jack_set_error_function = handle.lookup(@TypeOf(jack_set_error_function), "jack_set_error_function") orelse return error.SymbolLookup;
|
|
||||||
jack_set_info_function = handle.lookup(@TypeOf(jack_set_info_function), "jack_set_info_function") orelse return error.SymbolLookup;
|
|
||||||
jack_client_open = handle.lookup(@TypeOf(jack_client_open), "jack_client_open") orelse return error.SymbolLookup;
|
|
||||||
jack_client_close = handle.lookup(@TypeOf(jack_client_close), "jack_client_close") orelse return error.SymbolLookup;
|
|
||||||
jack_connect = handle.lookup(@TypeOf(jack_connect), "jack_connect") orelse return error.SymbolLookup;
|
|
||||||
jack_disconnect = handle.lookup(@TypeOf(jack_disconnect), "jack_disconnect") orelse return error.SymbolLookup;
|
|
||||||
jack_activate = handle.lookup(@TypeOf(jack_activate), "jack_activate") orelse return error.SymbolLookup;
|
|
||||||
jack_deactivate = handle.lookup(@TypeOf(jack_deactivate), "jack_deactivate") orelse return error.SymbolLookup;
|
|
||||||
jack_port_by_name = handle.lookup(@TypeOf(jack_port_by_name), "jack_port_by_name") orelse return error.SymbolLookup;
|
|
||||||
jack_port_register = handle.lookup(@TypeOf(jack_port_register), "jack_port_register") orelse return error.SymbolLookup;
|
|
||||||
jack_set_sample_rate_callback = handle.lookup(@TypeOf(jack_set_sample_rate_callback), "jack_set_sample_rate_callback") orelse return error.SymbolLookup;
|
|
||||||
jack_set_port_registration_callback = handle.lookup(@TypeOf(jack_set_port_registration_callback), "jack_set_port_registration_callback") orelse return error.SymbolLookup;
|
|
||||||
jack_set_process_callback = handle.lookup(@TypeOf(jack_set_process_callback), "jack_set_process_callback") orelse return error.SymbolLookup;
|
|
||||||
jack_set_port_rename_callback = handle.lookup(@TypeOf(jack_set_port_rename_callback), "jack_set_port_rename_callback") orelse return error.SymbolLookup;
|
|
||||||
jack_get_sample_rate = handle.lookup(@TypeOf(jack_get_sample_rate), "jack_get_sample_rate") orelse return error.SymbolLookup;
|
|
||||||
jack_get_ports = handle.lookup(@TypeOf(jack_get_ports), "jack_get_ports") orelse return error.SymbolLookup;
|
|
||||||
jack_port_type = handle.lookup(@TypeOf(jack_port_type), "jack_port_type") orelse return error.SymbolLookup;
|
|
||||||
jack_port_flags = handle.lookup(@TypeOf(jack_port_flags), "jack_port_flags") orelse return error.SymbolLookup;
|
|
||||||
jack_port_name = handle.lookup(@TypeOf(jack_port_name), "jack_port_name") orelse return error.SymbolLookup;
|
|
||||||
jack_port_get_buffer = handle.lookup(@TypeOf(jack_port_get_buffer), "jack_port_get_buffer") orelse return error.SymbolLookup;
|
|
||||||
jack_port_connected_to = handle.lookup(@TypeOf(jack_port_connected_to), "jack_port_connected_to") orelse return error.SymbolLookup;
|
|
||||||
jack_port_type_size = handle.lookup(@TypeOf(jack_port_type_size), "jack_port_type_size") orelse return error.SymbolLookup;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
client: *c.jack_client_t,
|
|
||||||
watcher: ?Watcher,
|
|
||||||
|
|
||||||
const Watcher = struct {
|
|
||||||
deviceChangeFn: main.Context.DeviceChangeFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
try lib.load();
|
|
||||||
|
|
||||||
lib.jack_set_error_function(@as(?*const fn ([*c]const u8) callconv(.C) void, @ptrCast(&util.doNothing)));
|
|
||||||
lib.jack_set_info_function(@as(?*const fn ([*c]const u8) callconv(.C) void, @ptrCast(&util.doNothing)));
|
|
||||||
|
|
||||||
var status: c.jack_status_t = 0;
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
.client = lib.jack_client_open(options.app_name.ptr, c.JackNoStartServer, &status) orelse {
|
|
||||||
std.debug.assert(status & c.JackInvalidOption == 0);
|
|
||||||
return if (status & c.JackShmFailure != 0)
|
|
||||||
error.SystemResources
|
|
||||||
else
|
|
||||||
error.ConnectionRefused;
|
|
||||||
},
|
|
||||||
.watcher = if (options.deviceChangeFn) |deviceChangeFn| .{
|
|
||||||
.deviceChangeFn = deviceChangeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
} else null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.deviceChangeFn) |_| {
|
|
||||||
if (lib.jack_set_sample_rate_callback(self.client, sampleRateCallback, self) != 0 or
|
|
||||||
lib.jack_set_port_registration_callback(self.client, portRegistrationCallback, self) != 0 or
|
|
||||||
lib.jack_set_port_rename_callback(self.client, portRenameCalllback, self) != 0)
|
|
||||||
return error.ConnectionRefused;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .jack = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
for (self.devices_info.list.items) |device|
|
|
||||||
freeDevice(self.allocator, device);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
_ = lib.jack_client_close(self.client);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
lib.handle.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.clear(self.allocator);
|
|
||||||
|
|
||||||
const sample_rate = @as(u24, @intCast(lib.jack_get_sample_rate(self.client)));
|
|
||||||
|
|
||||||
const port_names = lib.jack_get_ports(self.client, null, null, 0) orelse
|
|
||||||
return error.OutOfMemory;
|
|
||||||
defer lib.jack_free(@as(?*anyopaque, @ptrCast(port_names)));
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
outer: while (port_names[i] != null) : (i += 1) {
|
|
||||||
const port = lib.jack_port_by_name(self.client, port_names[i]) orelse break;
|
|
||||||
const port_type = lib.jack_port_type(port)[0..@as(usize, @intCast(lib.jack_port_type_size()))];
|
|
||||||
if (!std.mem.startsWith(u8, port_type, c.JACK_DEFAULT_AUDIO_TYPE))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const flags = lib.jack_port_flags(port);
|
|
||||||
const mode: main.Device.Mode = if (flags & c.JackPortIsInput != 0) .capture else .playback;
|
|
||||||
|
|
||||||
const name = std.mem.span(port_names[i]);
|
|
||||||
const id = std.mem.sliceTo(name, ':');
|
|
||||||
|
|
||||||
for (self.devices_info.list.items) |*dev| {
|
|
||||||
if (std.mem.eql(u8, dev.id, id) and mode == dev.mode) {
|
|
||||||
const new_ch = main.Channel{
|
|
||||||
.id = @as(main.Channel.Id, @enumFromInt(dev.channels.len)),
|
|
||||||
};
|
|
||||||
dev.channels = try self.allocator.realloc(dev.channels, dev.channels.len + 1);
|
|
||||||
dev.channels[dev.channels.len - 1] = new_ch;
|
|
||||||
break :outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var device = main.Device{
|
|
||||||
.id = try self.allocator.dupeZ(u8, id),
|
|
||||||
.name = name,
|
|
||||||
.mode = mode,
|
|
||||||
.channels = blk: {
|
|
||||||
var channels = try self.allocator.alloc(main.Channel, 1);
|
|
||||||
channels[0] = .{ .id = @as(main.Channel.Id, @enumFromInt(0)) };
|
|
||||||
break :blk channels;
|
|
||||||
},
|
|
||||||
.formats = &.{.f32},
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = sample_rate,
|
|
||||||
.max = sample_rate,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, device);
|
|
||||||
if (self.devices_info.default(mode) == null) {
|
|
||||||
self.devices_info.setDefault(mode, self.devices_info.list.items.len - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sampleRateCallback(_: c.jack_nframes_t, arg: ?*anyopaque) callconv(.C) c_int {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(arg.?)));
|
|
||||||
self.watcher.?.deviceChangeFn(self.watcher.?.user_data);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn portRegistrationCallback(_: c.jack_port_id_t, _: c_int, arg: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(arg.?)));
|
|
||||||
self.watcher.?.deviceChangeFn(self.watcher.?.user_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn portRenameCalllback(_: c.jack_port_id_t, _: [*c]const u8, _: [*c]const u8, arg: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(arg.?)));
|
|
||||||
self.watcher.?.deviceChangeFn(self.watcher.?.user_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: *Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: *Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
var ports = try self.allocator.alloc(*c.jack_port_t, device.channels.len);
|
|
||||||
var dest_ports = try self.allocator.alloc([:0]const u8, ports.len);
|
|
||||||
var buf: [64]u8 = undefined;
|
|
||||||
for (device.channels, 0..) |_, i| {
|
|
||||||
const port_name = std.fmt.bufPrintZ(&buf, "playback_{d}", .{i + 1}) catch unreachable;
|
|
||||||
const dest_name = try std.fmt.allocPrintZ(self.allocator, "{s}:{s}", .{ device.id, port_name });
|
|
||||||
ports[i] = lib.jack_port_register(self.client, port_name.ptr, c.JACK_DEFAULT_AUDIO_TYPE, c.JackPortIsOutput, 0) orelse
|
|
||||||
return error.OpeningDevice;
|
|
||||||
dest_ports[i] = dest_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.mutex = .{},
|
|
||||||
.client = self.client,
|
|
||||||
.ports = ports,
|
|
||||||
.dest_ports = dest_ports,
|
|
||||||
.device = device,
|
|
||||||
.vol = 1.0,
|
|
||||||
.writeFn = writeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = .f32,
|
|
||||||
.write_step = main.Format.size(.f32),
|
|
||||||
};
|
|
||||||
return .{ .jack = player };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
mutex: std.Thread.Mutex,
|
|
||||||
client: *c.jack_client_t,
|
|
||||||
ports: []const *c.jack_port_t,
|
|
||||||
dest_ports: []const [:0]const u8,
|
|
||||||
device: main.Device,
|
|
||||||
vol: f32,
|
|
||||||
writeFn: main.WriteFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
self.allocator.free(self.ports);
|
|
||||||
for (self.dest_ports) |d|
|
|
||||||
self.allocator.free(d);
|
|
||||||
self.allocator.free(self.dest_ports);
|
|
||||||
_ = lib.jack_deactivate(self.client);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: *Player) !void {
|
|
||||||
if (lib.jack_set_process_callback(self.client, processCallback, self) != 0)
|
|
||||||
return error.CannotPlay;
|
|
||||||
|
|
||||||
if (lib.jack_activate(self.client) != 0)
|
|
||||||
return error.CannotPlay;
|
|
||||||
|
|
||||||
for (self.ports, 0..) |port, i| {
|
|
||||||
if (lib.jack_connect(self.client, lib.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
|
||||||
return error.CannotPlay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn processCallback(n_frames: c.jack_nframes_t, self_opaque: ?*anyopaque) callconv(.C) c_int {
|
|
||||||
const self = @as(*Player, @ptrCast(@alignCast(self_opaque.?)));
|
|
||||||
|
|
||||||
for (self.channels, 0..) |*ch, i| {
|
|
||||||
ch.*.ptr = @as([*]u8, @ptrCast(lib.jack_port_get_buffer(self.ports[i], n_frames)));
|
|
||||||
}
|
|
||||||
self.writeFn(self.user_data, n_frames);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: *Player) !void {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
for (self.ports, 0..) |port, i| {
|
|
||||||
if (lib.jack_connect(self.client, lib.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
|
||||||
return error.CannotPlay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: *Player) !void {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
for (self.ports, 0..) |port, i| {
|
|
||||||
if (lib.jack_disconnect(self.client, lib.jack_port_name(port), self.dest_ports[i].ptr) != 0)
|
|
||||||
return error.CannotPause;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: *Player) bool {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
for (self.ports, 0..) |port, i| {
|
|
||||||
if (lib.jack_port_connected_to(port, self.dest_ports[i].ptr) == 1)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
self.vol = vol;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: *Player) !f32 {
|
|
||||||
return self.vol;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sampleRate(self: Player) u24 {
|
|
||||||
return @as(u24, @intCast(lib.jack_get_sample_rate(self.client)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
||||||
allocator.free(device.id);
|
|
||||||
allocator.free(device.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
|
|
@ -1,413 +0,0 @@
|
||||||
const builtin = @import("builtin");
|
|
||||||
const std = @import("std");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
|
|
||||||
pub const Backend = backends.Backend;
|
|
||||||
pub const default_sample_rate = 44_100; // Hz
|
|
||||||
pub const default_latency = 500 * std.time.us_per_ms; // μs
|
|
||||||
pub const min_sample_rate = 8_000; // Hz
|
|
||||||
pub const max_sample_rate = 5_644_800; // Hz
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
pub const DeviceChangeFn = *const fn (self: ?*anyopaque) void;
|
|
||||||
pub const Options = struct {
|
|
||||||
app_name: [:0]const u8 = "Mach Game",
|
|
||||||
deviceChangeFn: ?DeviceChangeFn = null,
|
|
||||||
user_data: ?*anyopaque = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
data: backends.BackendContext,
|
|
||||||
|
|
||||||
pub const InitError = error{
|
|
||||||
OutOfMemory,
|
|
||||||
AccessDenied,
|
|
||||||
LibraryNotFound,
|
|
||||||
SymbolLookup,
|
|
||||||
SystemResources,
|
|
||||||
ConnectionRefused,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(comptime backend: ?Backend, allocator: std.mem.Allocator, options: Options) InitError!Context {
|
|
||||||
var data: backends.BackendContext = blk: {
|
|
||||||
if (backend) |b| {
|
|
||||||
break :blk try @typeInfo(
|
|
||||||
std.meta.fieldInfo(backends.BackendContext, b).type,
|
|
||||||
).Pointer.child.init(allocator, options);
|
|
||||||
} else {
|
|
||||||
inline for (std.meta.fields(Backend), 0..) |b, i| {
|
|
||||||
if (@typeInfo(
|
|
||||||
std.meta.fieldInfo(backends.BackendContext, @as(Backend, @enumFromInt(b.value))).type,
|
|
||||||
).Pointer.child.init(allocator, options)) |d| {
|
|
||||||
break :blk d;
|
|
||||||
} else |err| {
|
|
||||||
if (i == std.meta.fields(Backend).len - 1)
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unreachable;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .data = data };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Context) void {
|
|
||||||
switch (self.data) {
|
|
||||||
inline else => |b| b.deinit(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const RefreshError = error{
|
|
||||||
OutOfMemory,
|
|
||||||
SystemResources,
|
|
||||||
OpeningDevice,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn refresh(self: Context) RefreshError!void {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.refresh(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const Device {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.devices(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: Device.Mode) ?Device {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.defaultDevice(mode),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const CreateStreamError = error{
|
|
||||||
OutOfMemory,
|
|
||||||
SystemResources,
|
|
||||||
OpeningDevice,
|
|
||||||
IncompatibleDevice,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn createPlayer(self: Context, device: Device, writeFn: WriteFn, options: StreamOptions) CreateStreamError!Player {
|
|
||||||
std.debug.assert(device.mode == .playback);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.data = switch (self.data) {
|
|
||||||
inline else => |b| try b.createPlayer(device, writeFn, options),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const StreamOptions = struct {
|
|
||||||
format: Format = .f32,
|
|
||||||
sample_rate: u24 = default_sample_rate,
|
|
||||||
media_role: MediaRole = .default,
|
|
||||||
user_data: ?*anyopaque = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MediaRole = enum {
|
|
||||||
default,
|
|
||||||
game,
|
|
||||||
music,
|
|
||||||
movie,
|
|
||||||
communication,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: `*Player` instead `*anyopaque`
|
|
||||||
// https://github.com/ziglang/zig/issues/12325
|
|
||||||
pub const WriteFn = *const fn (user_data: ?*anyopaque, frame_count_max: usize) void;
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
data: backends.BackendPlayer,
|
|
||||||
|
|
||||||
pub fn deinit(self: Player) void {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.deinit(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const StartError = error{
|
|
||||||
CannotPlay,
|
|
||||||
OutOfMemory,
|
|
||||||
SystemResources,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn start(self: Player) StartError!void {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.start(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PlayError = error{
|
|
||||||
CannotPlay,
|
|
||||||
OutOfMemory,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn play(self: Player) PlayError!void {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.play(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PauseError = error{
|
|
||||||
CannotPause,
|
|
||||||
OutOfMemory,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn pause(self: Player) PauseError!void {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.pause(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: Player) bool {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.paused(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const SetVolumeError = error{
|
|
||||||
CannotSetVolume,
|
|
||||||
};
|
|
||||||
|
|
||||||
// confidence interval (±) depends on the device
|
|
||||||
pub fn setVolume(self: Player, vol: f32) SetVolumeError!void {
|
|
||||||
std.debug.assert(vol <= 1.0);
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.setVolume(vol),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const GetVolumeError = error{
|
|
||||||
CannotGetVolume,
|
|
||||||
};
|
|
||||||
|
|
||||||
// confidence interval (±) depends on the device
|
|
||||||
pub fn volume(self: Player) GetVolumeError!f32 {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.volume(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writeAll(self: Player, frame: usize, value: anytype) void {
|
|
||||||
for (self.channels()) |ch|
|
|
||||||
self.write(ch, frame, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(self: Player, channel: Channel, frame: usize, sample: anytype) void {
|
|
||||||
switch (@TypeOf(sample)) {
|
|
||||||
u8, i8, i16, i24, i32, f32 => {},
|
|
||||||
else => @compileError(
|
|
||||||
\\invalid sample type. supported types are:
|
|
||||||
\\u8, i8, i16, i24, i32, f32
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
var ptr = channel.ptr + frame * self.writeStep();
|
|
||||||
switch (self.format()) {
|
|
||||||
.u8 => std.mem.bytesAsValue(u8, ptr[0..@sizeOf(u8)]).* = switch (@TypeOf(sample)) {
|
|
||||||
u8 => sample,
|
|
||||||
i8, i16, i24, i32 => signedToUnsigned(u8, sample),
|
|
||||||
f32 => floatToUnsigned(u8, sample),
|
|
||||||
else => unreachable,
|
|
||||||
},
|
|
||||||
.i16 => std.mem.bytesAsValue(i16, ptr[0..@sizeOf(i16)]).* = switch (@TypeOf(sample)) {
|
|
||||||
i16 => sample,
|
|
||||||
u8 => unsignedToSigned(i16, sample),
|
|
||||||
// i8, i24, i32 => signedToSigned(i16, sample),
|
|
||||||
f32 => floatToSigned(i16, sample),
|
|
||||||
else => unreachable,
|
|
||||||
},
|
|
||||||
.i24 => std.mem.bytesAsValue(i24, ptr[0..@sizeOf(i24)]).* = switch (@TypeOf(sample)) {
|
|
||||||
i24 => sample,
|
|
||||||
u8 => unsignedToSigned(i24, sample),
|
|
||||||
// i8, i16, i32 => signedToSigned(i24, sample),
|
|
||||||
f32 => floatToSigned(i24, sample),
|
|
||||||
else => unreachable,
|
|
||||||
},
|
|
||||||
.i24_4b => @panic("TODO"),
|
|
||||||
.i32 => std.mem.bytesAsValue(i32, ptr[0..@sizeOf(i32)]).* = switch (@TypeOf(sample)) {
|
|
||||||
i32 => sample,
|
|
||||||
u8 => unsignedToSigned(i32, sample),
|
|
||||||
// i8, i16, i24 => signedToSigned(i32, sample),
|
|
||||||
f32 => floatToSigned(i32, sample),
|
|
||||||
else => unreachable,
|
|
||||||
},
|
|
||||||
.f32 => std.mem.bytesAsValue(f32, ptr[0..@sizeOf(f32)]).* = switch (@TypeOf(sample)) {
|
|
||||||
f32 => sample,
|
|
||||||
u8 => unsignedToFloat(f32, sample),
|
|
||||||
i8, i16, i24, i32 => signedToFloat(f32, sample),
|
|
||||||
else => unreachable,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sampleRate(self: Player) u24 {
|
|
||||||
return if (@hasField(Backend, "jack")) switch (self.data) {
|
|
||||||
.jack => |b| b.sampleRate(),
|
|
||||||
inline else => |b| b.sample_rate,
|
|
||||||
} else switch (self.data) {
|
|
||||||
inline else => |b| b.sample_rate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn channels(self: Player) []Channel {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.channels,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format(self: Player) Format {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.format,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writeStep(self: Player) u8 {
|
|
||||||
return switch (self.data) {
|
|
||||||
inline else => |b| b.write_step,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn unsignedToSigned(comptime T: type, sample: anytype) T {
|
|
||||||
const half = 1 << (@bitSizeOf(@TypeOf(sample)) - 1);
|
|
||||||
const trunc = @bitSizeOf(T) - @bitSizeOf(@TypeOf(sample));
|
|
||||||
return @as(T, @intCast(sample -% half)) << trunc;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unsignedToFloat(comptime T: type, sample: anytype) T {
|
|
||||||
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
|
|
||||||
return (@as(T, @floatFromInt(sample)) - max_int) * 1.0 / max_int;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signedToSigned(comptime T: type, sample: anytype) T {
|
|
||||||
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
|
|
||||||
return @as(T, @intCast(sample >> trunc));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signedToUnsigned(comptime T: type, sample: anytype) T {
|
|
||||||
const half = 1 << (@bitSizeOf(T) - 1);
|
|
||||||
const trunc = @bitSizeOf(@TypeOf(sample)) - @bitSizeOf(T);
|
|
||||||
return @as(T, @intCast((sample >> trunc) + half));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signedToFloat(comptime T: type, sample: anytype) T {
|
|
||||||
const max_int = std.math.maxInt(@TypeOf(sample)) + 1.0;
|
|
||||||
return @as(T, @floatFromInt(sample)) * 1.0 / max_int;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn floatToSigned(comptime T: type, sample: f64) T {
|
|
||||||
return @as(T, @intFromFloat(sample * std.math.maxInt(T)));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn floatToUnsigned(comptime T: type, sample: f64) T {
|
|
||||||
const half = 1 << @bitSizeOf(T) - 1;
|
|
||||||
return @as(T, @intFromFloat(sample * (half - 1) + half));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Device = struct {
|
|
||||||
id: [:0]const u8,
|
|
||||||
name: [:0]const u8,
|
|
||||||
mode: Mode,
|
|
||||||
// NOTE(haze): we should elaborate on the `channels` field and seperate them into input and output channels
|
|
||||||
channels: []Channel,
|
|
||||||
formats: []const Format,
|
|
||||||
sample_rate: util.Range(u24),
|
|
||||||
|
|
||||||
pub const Mode = enum {
|
|
||||||
playback,
|
|
||||||
capture,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn preferredFormat(self: Device, format: ?Format) Format {
|
|
||||||
if (format) |f| {
|
|
||||||
for (self.formats) |fmt| {
|
|
||||||
if (f == fmt) {
|
|
||||||
return fmt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var best: Format = self.formats[0];
|
|
||||||
for (self.formats) |fmt| {
|
|
||||||
if (fmt.size() >= best.size()) {
|
|
||||||
if (fmt == .i24_4b and best == .i24)
|
|
||||||
continue;
|
|
||||||
best = fmt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Channel = struct {
|
|
||||||
ptr: [*]u8 = undefined,
|
|
||||||
id: Id,
|
|
||||||
|
|
||||||
pub const Id = enum {
|
|
||||||
front_center,
|
|
||||||
front_left,
|
|
||||||
front_right,
|
|
||||||
front_left_center,
|
|
||||||
front_right_center,
|
|
||||||
back_center,
|
|
||||||
back_left,
|
|
||||||
back_right,
|
|
||||||
side_left,
|
|
||||||
side_right,
|
|
||||||
top_center,
|
|
||||||
top_front_center,
|
|
||||||
top_front_left,
|
|
||||||
top_front_right,
|
|
||||||
top_back_center,
|
|
||||||
top_back_left,
|
|
||||||
top_back_right,
|
|
||||||
lfe,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Format = enum {
|
|
||||||
u8,
|
|
||||||
i16,
|
|
||||||
i24,
|
|
||||||
i24_4b,
|
|
||||||
i32,
|
|
||||||
f32,
|
|
||||||
|
|
||||||
pub fn size(self: Format) u8 {
|
|
||||||
return switch (self) {
|
|
||||||
.u8 => 1,
|
|
||||||
.i16 => 2,
|
|
||||||
.i24 => 3,
|
|
||||||
.i24_4b, .i32, .f32 => 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validSize(self: Format) u8 {
|
|
||||||
return switch (self) {
|
|
||||||
.u8 => 1,
|
|
||||||
.i16 => 2,
|
|
||||||
.i24, .i24_4b => 3,
|
|
||||||
.i32, .f32 => 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sizeBits(self: Format) u8 {
|
|
||||||
return self.size() * 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validSizeBits(self: Format) u8 {
|
|
||||||
return self.validSize() * 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn frameSize(self: Format, ch_count: usize) u8 {
|
|
||||||
return self.size() * @as(u5, @intCast(ch_count));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const c = @cImport({
|
|
||||||
@cInclude("pipewire/pipewire.h");
|
|
||||||
@cInclude("spa/param/audio/format-utils.h");
|
|
||||||
});
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
|
|
||||||
const lib = struct {
|
|
||||||
var handle: std.DynLib = undefined;
|
|
||||||
|
|
||||||
var pw_init: *const fn ([*c]c_int, [*c][*c][*c]u8) callconv(.C) void = undefined;
|
|
||||||
var pw_deinit: *const fn () callconv(.C) void = undefined;
|
|
||||||
var pw_thread_loop_new: *const fn ([*c]const u8, [*c]const c.spa_dict) callconv(.C) ?*c.pw_thread_loop = undefined;
|
|
||||||
var pw_thread_loop_destroy: *const fn (?*c.pw_thread_loop) callconv(.C) void = undefined;
|
|
||||||
var pw_thread_loop_start: *const fn (?*c.pw_thread_loop) callconv(.C) c_int = undefined;
|
|
||||||
var pw_thread_loop_stop: *const fn (?*c.pw_thread_loop) callconv(.C) void = undefined;
|
|
||||||
var pw_thread_loop_signal: *const fn (?*c.pw_thread_loop, bool) callconv(.C) void = undefined;
|
|
||||||
var pw_thread_loop_wait: *const fn (?*c.pw_thread_loop) callconv(.C) void = undefined;
|
|
||||||
var pw_thread_loop_lock: *const fn (?*c.pw_thread_loop) callconv(.C) void = undefined;
|
|
||||||
var pw_thread_loop_unlock: *const fn (?*c.pw_thread_loop) callconv(.C) void = undefined;
|
|
||||||
var pw_thread_loop_get_loop: *const fn (?*c.pw_thread_loop) callconv(.C) [*c]c.pw_loop = undefined;
|
|
||||||
var pw_properties_new: *const fn ([*c]const u8, ...) callconv(.C) [*c]c.pw_properties = undefined;
|
|
||||||
var pw_stream_new_simple: *const fn ([*c]c.pw_loop, [*c]const u8, [*c]c.pw_properties, [*c]const c.pw_stream_events, ?*anyopaque) callconv(.C) ?*c.pw_stream = undefined;
|
|
||||||
var pw_stream_destroy: *const fn (?*c.pw_stream) callconv(.C) void = undefined;
|
|
||||||
var pw_stream_connect: *const fn (?*c.pw_stream, c.spa_direction, u32, c.pw_stream_flags, [*c][*c]const c.spa_pod, u32) callconv(.C) c_int = undefined;
|
|
||||||
var pw_stream_queue_buffer: *const fn (?*c.pw_stream, [*c]c.pw_buffer) callconv(.C) c_int = undefined;
|
|
||||||
var pw_stream_dequeue_buffer: *const fn (?*c.pw_stream) callconv(.C) [*c]c.pw_buffer = undefined;
|
|
||||||
var pw_stream_get_state: *const fn (?*c.pw_stream, [*c][*c]const u8) callconv(.C) c.pw_stream_state = undefined;
|
|
||||||
|
|
||||||
pub fn load() !void {
|
|
||||||
handle = std.DynLib.openZ("libpipewire-0.3.so") catch return error.LibraryNotFound;
|
|
||||||
|
|
||||||
pw_init = handle.lookup(@TypeOf(pw_init), "pw_init") orelse return error.SymbolLookup;
|
|
||||||
pw_deinit = handle.lookup(@TypeOf(pw_deinit), "pw_deinit") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_new = handle.lookup(@TypeOf(pw_thread_loop_new), "pw_thread_loop_new") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_destroy = handle.lookup(@TypeOf(pw_thread_loop_destroy), "pw_thread_loop_destroy") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_start = handle.lookup(@TypeOf(pw_thread_loop_start), "pw_thread_loop_start") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_stop = handle.lookup(@TypeOf(pw_thread_loop_stop), "pw_thread_loop_stop") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_signal = handle.lookup(@TypeOf(pw_thread_loop_signal), "pw_thread_loop_signal") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_wait = handle.lookup(@TypeOf(pw_thread_loop_wait), "pw_thread_loop_wait") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_lock = handle.lookup(@TypeOf(pw_thread_loop_lock), "pw_thread_loop_lock") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_unlock = handle.lookup(@TypeOf(pw_thread_loop_unlock), "pw_thread_loop_unlock") orelse return error.SymbolLookup;
|
|
||||||
pw_thread_loop_get_loop = handle.lookup(@TypeOf(pw_thread_loop_get_loop), "pw_thread_loop_get_loop") orelse return error.SymbolLookup;
|
|
||||||
pw_properties_new = handle.lookup(@TypeOf(pw_properties_new), "pw_properties_new") orelse return error.SymbolLookup;
|
|
||||||
pw_stream_new_simple = handle.lookup(@TypeOf(pw_stream_new_simple), "pw_stream_new_simple") orelse return error.SymbolLookup;
|
|
||||||
pw_stream_destroy = handle.lookup(@TypeOf(pw_stream_destroy), "pw_stream_destroy") orelse return error.SymbolLookup;
|
|
||||||
pw_stream_connect = handle.lookup(@TypeOf(pw_stream_connect), "pw_stream_connect") orelse return error.SymbolLookup;
|
|
||||||
pw_stream_queue_buffer = handle.lookup(@TypeOf(pw_stream_queue_buffer), "pw_stream_queue_buffer") orelse return error.SymbolLookup;
|
|
||||||
pw_stream_dequeue_buffer = handle.lookup(@TypeOf(pw_stream_dequeue_buffer), "pw_stream_dequeue_buffer") orelse return error.SymbolLookup;
|
|
||||||
pw_stream_get_state = handle.lookup(@TypeOf(pw_stream_get_state), "pw_stream_get_state") orelse return error.SymbolLookup;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const default_playback = main.Device{
|
|
||||||
.id = "default-playback",
|
|
||||||
.name = "Default Device",
|
|
||||||
.mode = .playback,
|
|
||||||
.channels = undefined,
|
|
||||||
.formats = std.meta.tags(main.Format),
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = main.min_sample_rate,
|
|
||||||
.max = main.max_sample_rate,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const default_capture = main.Device{
|
|
||||||
.id = "default-capture",
|
|
||||||
.name = "Default Device",
|
|
||||||
.mode = .capture,
|
|
||||||
.channels = undefined,
|
|
||||||
.formats = std.meta.tags(main.Format),
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = main.min_sample_rate,
|
|
||||||
.max = main.max_sample_rate,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
app_name: [:0]const u8,
|
|
||||||
// watcher: ?Watcher,
|
|
||||||
|
|
||||||
const Watcher = struct {
|
|
||||||
deviceChangeFn: main.Context.DeviceChangeFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
thread: *c.pw_thread_loop,
|
|
||||||
aborted: std.atomic.Atomic(bool),
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
try lib.load();
|
|
||||||
|
|
||||||
lib.pw_init(null, null);
|
|
||||||
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
.app_name = options.app_name,
|
|
||||||
// TODO: device change watcher
|
|
||||||
// .watcher = blk: {
|
|
||||||
// if (options.deviceChangeFn != null) {
|
|
||||||
// const thread = c.pw_thread_loop_new("device-change-watcher", null) orelse return error.SystemResources;
|
|
||||||
// const context = c.pw_context_new(c.pw_thread_loop_get_loop(thread), null, 0);
|
|
||||||
// const core = c.pw_context_connect(context, null, 0);
|
|
||||||
// const registry = c.pw_core_get_registry(core, c.PW_VERSION_REGISTRY, 0);
|
|
||||||
// _ = c.spa_zero(registry);
|
|
||||||
|
|
||||||
// var registry_listener: c.spa_hook = undefined;
|
|
||||||
// _ = c.pw_registry_add_listener(registry, registry_listener);
|
|
||||||
|
|
||||||
// break :blk .{
|
|
||||||
// .deviceChangeFn = options.deviceChangeFn.?,
|
|
||||||
// .user_data = options.user_data,
|
|
||||||
// .thread = thread,
|
|
||||||
// .aborted = .{ .value = false },
|
|
||||||
// };
|
|
||||||
// } else break :blk null;
|
|
||||||
// },
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .pipewire = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
lib.pw_deinit();
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
lib.handle.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.clear(self.allocator);
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, default_playback);
|
|
||||||
try self.devices_info.list.append(self.allocator, default_capture);
|
|
||||||
|
|
||||||
self.devices_info.setDefault(.playback, 0);
|
|
||||||
self.devices_info.setDefault(.capture, 1);
|
|
||||||
|
|
||||||
self.devices_info.list.items[0].channels = try self.allocator.alloc(main.Channel, 2);
|
|
||||||
self.devices_info.list.items[1].channels = try self.allocator.alloc(main.Channel, 2);
|
|
||||||
|
|
||||||
self.devices_info.list.items[0].channels[0] = .{ .id = .front_right };
|
|
||||||
self.devices_info.list.items[0].channels[1] = .{ .id = .front_left };
|
|
||||||
self.devices_info.list.items[1].channels[0] = .{ .id = .front_right };
|
|
||||||
self.devices_info.list.items[1].channels[1] = .{ .id = .front_left };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream_events = c.pw_stream_events{
|
|
||||||
.version = c.PW_VERSION_STREAM_EVENTS,
|
|
||||||
.process = Player.processCb,
|
|
||||||
.destroy = null,
|
|
||||||
.state_changed = Player.stateChangedCb,
|
|
||||||
.control_info = null,
|
|
||||||
.io_changed = null,
|
|
||||||
.param_changed = null,
|
|
||||||
.add_buffer = null,
|
|
||||||
.remove_buffer = null,
|
|
||||||
.drained = null,
|
|
||||||
.command = null,
|
|
||||||
.trigger_done = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources;
|
|
||||||
|
|
||||||
const media_role = switch (options.media_role) {
|
|
||||||
.default => "Screen",
|
|
||||||
.game => "Game",
|
|
||||||
.music => "Music",
|
|
||||||
.movie => "Movie",
|
|
||||||
.communication => "Communication",
|
|
||||||
};
|
|
||||||
|
|
||||||
var buf: [8]u8 = undefined;
|
|
||||||
const audio_rate = std.fmt.bufPrintZ(&buf, "{d}", .{options.sample_rate}) catch unreachable;
|
|
||||||
|
|
||||||
const props = lib.pw_properties_new(
|
|
||||||
c.PW_KEY_MEDIA_TYPE,
|
|
||||||
"Audio",
|
|
||||||
|
|
||||||
c.PW_KEY_MEDIA_CATEGORY,
|
|
||||||
"Playback",
|
|
||||||
|
|
||||||
c.PW_KEY_MEDIA_ROLE,
|
|
||||||
media_role.ptr,
|
|
||||||
|
|
||||||
c.PW_KEY_MEDIA_NAME,
|
|
||||||
self.app_name.ptr,
|
|
||||||
|
|
||||||
c.PW_KEY_AUDIO_RATE,
|
|
||||||
audio_rate.ptr,
|
|
||||||
|
|
||||||
@as(*allowzero u0, @ptrFromInt(0)),
|
|
||||||
);
|
|
||||||
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
errdefer self.allocator.destroy(player);
|
|
||||||
|
|
||||||
const stream = lib.pw_stream_new_simple(
|
|
||||||
lib.pw_thread_loop_get_loop(thread),
|
|
||||||
"audio-src",
|
|
||||||
props,
|
|
||||||
&stream_events,
|
|
||||||
player,
|
|
||||||
) orelse return error.OpeningDevice;
|
|
||||||
|
|
||||||
var builder_buf: [256]u8 = undefined;
|
|
||||||
var pod_builder = c.spa_pod_builder{
|
|
||||||
.data = &builder_buf,
|
|
||||||
.size = builder_buf.len,
|
|
||||||
._padding = 0,
|
|
||||||
.state = .{
|
|
||||||
.offset = 0,
|
|
||||||
.flags = 0,
|
|
||||||
.frame = null,
|
|
||||||
},
|
|
||||||
.callbacks = .{ .funcs = null, .data = null },
|
|
||||||
};
|
|
||||||
var info = c.spa_audio_info_raw{
|
|
||||||
.format = c.SPA_AUDIO_FORMAT_F32,
|
|
||||||
.channels = @as(u32, @intCast(device.channels.len)),
|
|
||||||
.rate = options.sample_rate,
|
|
||||||
.flags = 0,
|
|
||||||
.position = undefined,
|
|
||||||
};
|
|
||||||
var params = [1][*c]c.spa_pod{
|
|
||||||
sysaudio_spa_format_audio_raw_build(&pod_builder, c.SPA_PARAM_EnumFormat, &info),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (lib.pw_stream_connect(
|
|
||||||
stream,
|
|
||||||
c.PW_DIRECTION_OUTPUT,
|
|
||||||
c.PW_ID_ANY,
|
|
||||||
c.PW_STREAM_FLAG_AUTOCONNECT | c.PW_STREAM_FLAG_MAP_BUFFERS | c.PW_STREAM_FLAG_RT_PROCESS,
|
|
||||||
¶ms,
|
|
||||||
params.len,
|
|
||||||
) < 0) return error.OpeningDevice;
|
|
||||||
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.thread = thread,
|
|
||||||
.stream = stream,
|
|
||||||
.is_paused = .{ .value = false },
|
|
||||||
.vol = 1.0,
|
|
||||||
.writeFn = writeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = .f32,
|
|
||||||
.sample_rate = options.sample_rate,
|
|
||||||
.write_step = main.Format.frameSize(.f32, device.channels.len),
|
|
||||||
};
|
|
||||||
return .{ .pipewire = player };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
thread: *c.pw_thread_loop,
|
|
||||||
stream: *c.pw_stream,
|
|
||||||
is_paused: std.atomic.Atomic(bool),
|
|
||||||
vol: f32,
|
|
||||||
writeFn: main.WriteFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
sample_rate: u24,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn stateChangedCb(self_opaque: ?*anyopaque, old_state: c.pw_stream_state, state: c.pw_stream_state, err: [*c]const u8) callconv(.C) void {
|
|
||||||
_ = old_state;
|
|
||||||
_ = err;
|
|
||||||
|
|
||||||
var self = @as(*Player, @ptrCast(@alignCast(self_opaque.?)));
|
|
||||||
|
|
||||||
if (state == c.PW_STREAM_STATE_STREAMING or state == c.PW_STREAM_STATE_ERROR) {
|
|
||||||
lib.pw_thread_loop_signal(self.thread, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn processCb(self_opaque: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Player, @ptrCast(@alignCast(self_opaque.?)));
|
|
||||||
|
|
||||||
const buf = lib.pw_stream_dequeue_buffer(self.stream) orelse unreachable;
|
|
||||||
if (buf.*.buffer.*.datas[0].data == null) return;
|
|
||||||
defer _ = lib.pw_stream_queue_buffer(self.stream, buf);
|
|
||||||
|
|
||||||
buf.*.buffer.*.datas[0].chunk.*.offset = 0;
|
|
||||||
if (self.is_paused.load(.Unordered)) {
|
|
||||||
buf.*.buffer.*.datas[0].chunk.*.stride = 0;
|
|
||||||
buf.*.buffer.*.datas[0].chunk.*.size = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stride = self.format.frameSize(self.channels.len);
|
|
||||||
const n_frames = @min(buf.*.requested, buf.*.buffer.*.datas[0].maxsize / stride);
|
|
||||||
buf.*.buffer.*.datas[0].chunk.*.stride = stride;
|
|
||||||
buf.*.buffer.*.datas[0].chunk.*.size = @as(u32, @intCast(n_frames * stride));
|
|
||||||
|
|
||||||
for (self.channels, 0..) |*ch, i| {
|
|
||||||
ch.ptr = @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?)) + self.format.frameSize(i);
|
|
||||||
}
|
|
||||||
self.writeFn(self.user_data, n_frames);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
lib.pw_thread_loop_stop(self.thread);
|
|
||||||
lib.pw_thread_loop_destroy(self.thread);
|
|
||||||
lib.pw_stream_destroy(self.stream);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: *Player) !void {
|
|
||||||
if (lib.pw_thread_loop_start(self.thread) < 0) return error.SystemResources;
|
|
||||||
|
|
||||||
lib.pw_thread_loop_lock(self.thread);
|
|
||||||
lib.pw_thread_loop_wait(self.thread);
|
|
||||||
lib.pw_thread_loop_unlock(self.thread);
|
|
||||||
|
|
||||||
if (lib.pw_stream_get_state(self.stream, null) == c.PW_STREAM_STATE_ERROR) {
|
|
||||||
return error.CannotPlay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: *Player) !void {
|
|
||||||
self.is_paused.store(false, .Unordered);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: *Player) !void {
|
|
||||||
self.is_paused.store(true, .Unordered);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: Player) bool {
|
|
||||||
return self.is_paused.load(.Unordered);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
self.vol = vol;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: Player) !f32 {
|
|
||||||
return self.vol;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
||||||
allocator.free(device.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern fn sysaudio_spa_format_audio_raw_build(builder: [*c]c.spa_pod_builder, id: u32, info: [*c]c.spa_audio_info_raw) callconv(.C) [*c]c.spa_pod;
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#include <pipewire/core.h>
|
|
||||||
#include <spa/param/audio/format-utils.h>
|
|
||||||
|
|
||||||
struct spa_pod *sysaudio_spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_raw *info)
|
|
||||||
{
|
|
||||||
return spa_format_audio_raw_build(builder, id, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
void sysaudio_pw_registry_add_listener(struct pw_registry *reg, struct spa_hook *reg_listener, struct pw_registry_events *events) {
|
|
||||||
pw_registry_add_listener(reg, reg_listener, events, NULL);
|
|
||||||
}
|
|
||||||
|
|
@ -1,672 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const c = @cImport(@cInclude("pulse/pulseaudio.h"));
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
const is_little = @import("builtin").cpu.arch.endian() == .Little;
|
|
||||||
|
|
||||||
const lib = struct {
|
|
||||||
var handle: std.DynLib = undefined;
|
|
||||||
|
|
||||||
var pa_threaded_mainloop_new: *const fn () callconv(.C) ?*c.pa_threaded_mainloop = undefined;
|
|
||||||
var pa_threaded_mainloop_free: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void = undefined;
|
|
||||||
var pa_threaded_mainloop_start: *const fn (?*c.pa_threaded_mainloop) callconv(.C) c_int = undefined;
|
|
||||||
var pa_threaded_mainloop_stop: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void = undefined;
|
|
||||||
var pa_threaded_mainloop_signal: *const fn (?*c.pa_threaded_mainloop, c_int) callconv(.C) void = undefined;
|
|
||||||
var pa_threaded_mainloop_wait: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void = undefined;
|
|
||||||
var pa_threaded_mainloop_lock: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void = undefined;
|
|
||||||
var pa_threaded_mainloop_unlock: *const fn (?*c.pa_threaded_mainloop) callconv(.C) void = undefined;
|
|
||||||
var pa_threaded_mainloop_get_api: *const fn (?*c.pa_threaded_mainloop) callconv(.C) [*c]c.pa_mainloop_api = undefined;
|
|
||||||
var pa_operation_unref: *const fn (?*c.pa_operation) callconv(.C) void = undefined;
|
|
||||||
var pa_operation_get_state: *const fn (?*const c.pa_operation) callconv(.C) c.pa_operation_state_t = undefined;
|
|
||||||
var pa_context_new_with_proplist: *const fn ([*c]c.pa_mainloop_api, [*c]const u8, ?*const c.pa_proplist) callconv(.C) ?*c.pa_context = undefined;
|
|
||||||
var pa_context_unref: *const fn (?*c.pa_context) callconv(.C) void = undefined;
|
|
||||||
var pa_context_connect: *const fn (?*c.pa_context, [*c]const u8, c.pa_context_flags_t, [*c]const c.pa_spawn_api) callconv(.C) c_int = undefined;
|
|
||||||
var pa_context_disconnect: *const fn (?*c.pa_context) callconv(.C) void = undefined;
|
|
||||||
var pa_context_subscribe: *const fn (?*c.pa_context, c.pa_subscription_mask_t, c.pa_context_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation = undefined;
|
|
||||||
var pa_context_get_state: *const fn (?*const c.pa_context) callconv(.C) c.pa_context_state_t = undefined;
|
|
||||||
var pa_context_set_state_callback: *const fn (?*c.pa_context, c.pa_context_notify_cb_t, ?*anyopaque) callconv(.C) void = undefined;
|
|
||||||
var pa_context_set_subscribe_callback: *const fn (?*c.pa_context, c.pa_context_subscribe_cb_t, ?*anyopaque) callconv(.C) void = undefined;
|
|
||||||
var pa_context_get_sink_input_info: *const fn (?*c.pa_context, u32, c.pa_sink_input_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation = undefined;
|
|
||||||
var pa_context_get_sink_info_list: *const fn (?*c.pa_context, c.pa_sink_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation = undefined;
|
|
||||||
var pa_context_set_sink_input_volume: *const fn (?*c.pa_context, u32, [*c]const c.pa_cvolume, c.pa_context_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation = undefined;
|
|
||||||
var pa_context_get_source_info_list: *const fn (?*c.pa_context, c.pa_source_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation = undefined;
|
|
||||||
var pa_context_get_server_info: *const fn (?*c.pa_context, c.pa_server_info_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation = undefined;
|
|
||||||
var pa_stream_new: *const fn (?*c.pa_context, [*c]const u8, [*c]const c.pa_sample_spec, [*c]const c.pa_channel_map) callconv(.C) ?*c.pa_stream = undefined;
|
|
||||||
var pa_stream_unref: *const fn (?*c.pa_stream) callconv(.C) void = undefined;
|
|
||||||
var pa_stream_connect_playback: *const fn (?*c.pa_stream, [*c]const u8, [*c]const c.pa_buffer_attr, c.pa_stream_flags_t, [*c]const c.pa_cvolume, ?*c.pa_stream) callconv(.C) c_int = undefined;
|
|
||||||
var pa_stream_disconnect: *const fn (?*c.pa_stream) callconv(.C) c_int = undefined;
|
|
||||||
var pa_stream_cork: *const fn (?*c.pa_stream, c_int, c.pa_stream_success_cb_t, ?*anyopaque) callconv(.C) ?*c.pa_operation = undefined;
|
|
||||||
var pa_stream_is_corked: *const fn (?*const c.pa_stream) callconv(.C) c_int = undefined;
|
|
||||||
var pa_stream_begin_write: *const fn (?*c.pa_stream, [*c]?*anyopaque, [*c]usize) callconv(.C) c_int = undefined;
|
|
||||||
var pa_stream_write: *const fn (?*c.pa_stream, ?*const anyopaque, usize, c.pa_free_cb_t, i64, c.pa_seek_mode_t) callconv(.C) c_int = undefined;
|
|
||||||
var pa_stream_get_state: *const fn (?*const c.pa_stream) callconv(.C) c.pa_stream_state_t = undefined;
|
|
||||||
var pa_stream_get_index: *const fn (?*const c.pa_stream) callconv(.C) u32 = undefined;
|
|
||||||
var pa_stream_set_state_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void = undefined;
|
|
||||||
var pa_stream_set_write_callback: *const fn (?*c.pa_stream, c.pa_stream_request_cb_t, ?*anyopaque) callconv(.C) void = undefined;
|
|
||||||
var pa_stream_set_underflow_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void = undefined;
|
|
||||||
var pa_stream_set_overflow_callback: *const fn (?*c.pa_stream, c.pa_stream_notify_cb_t, ?*anyopaque) callconv(.C) void = undefined;
|
|
||||||
var pa_cvolume_init: *const fn ([*c]c.pa_cvolume) callconv(.C) [*c]c.pa_cvolume = undefined;
|
|
||||||
var pa_cvolume_set: *const fn ([*c]c.pa_cvolume, c_uint, c.pa_volume_t) callconv(.C) [*c]c.pa_cvolume = undefined;
|
|
||||||
var pa_sw_volume_from_linear: *const fn (f64) callconv(.C) c.pa_volume_t = undefined;
|
|
||||||
|
|
||||||
pub fn load() !void {
|
|
||||||
handle = std.DynLib.openZ("libpulse.so") catch return error.LibraryNotFound;
|
|
||||||
|
|
||||||
pa_threaded_mainloop_new = handle.lookup(@TypeOf(pa_threaded_mainloop_new), "pa_threaded_mainloop_new") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_free = handle.lookup(@TypeOf(pa_threaded_mainloop_free), "pa_threaded_mainloop_free") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_start = handle.lookup(@TypeOf(pa_threaded_mainloop_start), "pa_threaded_mainloop_start") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_stop = handle.lookup(@TypeOf(pa_threaded_mainloop_stop), "pa_threaded_mainloop_stop") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_signal = handle.lookup(@TypeOf(pa_threaded_mainloop_signal), "pa_threaded_mainloop_signal") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_wait = handle.lookup(@TypeOf(pa_threaded_mainloop_wait), "pa_threaded_mainloop_wait") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_lock = handle.lookup(@TypeOf(pa_threaded_mainloop_lock), "pa_threaded_mainloop_lock") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_unlock = handle.lookup(@TypeOf(pa_threaded_mainloop_unlock), "pa_threaded_mainloop_unlock") orelse return error.SymbolLookup;
|
|
||||||
pa_threaded_mainloop_get_api = handle.lookup(@TypeOf(pa_threaded_mainloop_get_api), "pa_threaded_mainloop_get_api") orelse return error.SymbolLookup;
|
|
||||||
pa_operation_unref = handle.lookup(@TypeOf(pa_operation_unref), "pa_operation_unref") orelse return error.SymbolLookup;
|
|
||||||
pa_operation_get_state = handle.lookup(@TypeOf(pa_operation_get_state), "pa_operation_get_state") orelse return error.SymbolLookup;
|
|
||||||
pa_context_new_with_proplist = handle.lookup(@TypeOf(pa_context_new_with_proplist), "pa_context_new_with_proplist") orelse return error.SymbolLookup;
|
|
||||||
pa_context_unref = handle.lookup(@TypeOf(pa_context_unref), "pa_context_unref") orelse return error.SymbolLookup;
|
|
||||||
pa_context_connect = handle.lookup(@TypeOf(pa_context_connect), "pa_context_connect") orelse return error.SymbolLookup;
|
|
||||||
pa_context_disconnect = handle.lookup(@TypeOf(pa_context_disconnect), "pa_context_disconnect") orelse return error.SymbolLookup;
|
|
||||||
pa_context_subscribe = handle.lookup(@TypeOf(pa_context_subscribe), "pa_context_subscribe") orelse return error.SymbolLookup;
|
|
||||||
pa_context_get_state = handle.lookup(@TypeOf(pa_context_get_state), "pa_context_get_state") orelse return error.SymbolLookup;
|
|
||||||
pa_context_set_state_callback = handle.lookup(@TypeOf(pa_context_set_state_callback), "pa_context_set_state_callback") orelse return error.SymbolLookup;
|
|
||||||
pa_context_set_subscribe_callback = handle.lookup(@TypeOf(pa_context_set_subscribe_callback), "pa_context_set_subscribe_callback") orelse return error.SymbolLookup;
|
|
||||||
pa_context_get_sink_input_info = handle.lookup(@TypeOf(pa_context_get_sink_input_info), "pa_context_get_sink_input_info") orelse return error.SymbolLookup;
|
|
||||||
pa_context_get_sink_info_list = handle.lookup(@TypeOf(pa_context_get_sink_info_list), "pa_context_get_sink_info_list") orelse return error.SymbolLookup;
|
|
||||||
pa_context_set_sink_input_volume = handle.lookup(@TypeOf(pa_context_set_sink_input_volume), "pa_context_set_sink_input_volume") orelse return error.SymbolLookup;
|
|
||||||
pa_context_get_source_info_list = handle.lookup(@TypeOf(pa_context_get_source_info_list), "pa_context_get_source_info_list") orelse return error.SymbolLookup;
|
|
||||||
pa_context_get_server_info = handle.lookup(@TypeOf(pa_context_get_server_info), "pa_context_get_server_info") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_new = handle.lookup(@TypeOf(pa_stream_new), "pa_stream_new") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_unref = handle.lookup(@TypeOf(pa_stream_unref), "pa_stream_unref") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_connect_playback = handle.lookup(@TypeOf(pa_stream_connect_playback), "pa_stream_connect_playback") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_disconnect = handle.lookup(@TypeOf(pa_stream_disconnect), "pa_stream_disconnect") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_cork = handle.lookup(@TypeOf(pa_stream_cork), "pa_stream_cork") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_is_corked = handle.lookup(@TypeOf(pa_stream_is_corked), "pa_stream_is_corked") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_begin_write = handle.lookup(@TypeOf(pa_stream_begin_write), "pa_stream_begin_write") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_write = handle.lookup(@TypeOf(pa_stream_write), "pa_stream_write") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_get_state = handle.lookup(@TypeOf(pa_stream_get_state), "pa_stream_get_state") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_get_index = handle.lookup(@TypeOf(pa_stream_get_index), "pa_stream_get_index") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_set_state_callback = handle.lookup(@TypeOf(pa_stream_set_state_callback), "pa_stream_set_state_callback") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_set_write_callback = handle.lookup(@TypeOf(pa_stream_set_write_callback), "pa_stream_set_write_callback") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_set_underflow_callback = handle.lookup(@TypeOf(pa_stream_set_underflow_callback), "pa_stream_set_underflow_callback") orelse return error.SymbolLookup;
|
|
||||||
pa_stream_set_overflow_callback = handle.lookup(@TypeOf(pa_stream_set_overflow_callback), "pa_stream_set_overflow_callback") orelse return error.SymbolLookup;
|
|
||||||
pa_cvolume_init = handle.lookup(@TypeOf(pa_cvolume_init), "pa_cvolume_init") orelse return error.SymbolLookup;
|
|
||||||
pa_cvolume_set = handle.lookup(@TypeOf(pa_cvolume_set), "pa_cvolume_set") orelse return error.SymbolLookup;
|
|
||||||
pa_sw_volume_from_linear = handle.lookup(@TypeOf(pa_sw_volume_from_linear), "pa_sw_volume_from_linear") orelse return error.SymbolLookup;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
app_name: [:0]const u8,
|
|
||||||
main_loop: *c.pa_threaded_mainloop,
|
|
||||||
ctx: *c.pa_context,
|
|
||||||
ctx_state: c.pa_context_state_t,
|
|
||||||
default_sink: ?[:0]const u8,
|
|
||||||
default_source: ?[:0]const u8,
|
|
||||||
watcher: ?Watcher,
|
|
||||||
|
|
||||||
const Watcher = struct {
|
|
||||||
deviceChangeFn: main.Context.DeviceChangeFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
try lib.load();
|
|
||||||
|
|
||||||
const main_loop = lib.pa_threaded_mainloop_new() orelse
|
|
||||||
return error.OutOfMemory;
|
|
||||||
errdefer lib.pa_threaded_mainloop_free(main_loop);
|
|
||||||
var main_loop_api = lib.pa_threaded_mainloop_get_api(main_loop);
|
|
||||||
|
|
||||||
const ctx = lib.pa_context_new_with_proplist(main_loop_api, options.app_name.ptr, null) orelse
|
|
||||||
return error.OutOfMemory;
|
|
||||||
errdefer lib.pa_context_unref(ctx);
|
|
||||||
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = Context{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
.app_name = options.app_name,
|
|
||||||
.main_loop = main_loop,
|
|
||||||
.ctx = ctx,
|
|
||||||
.ctx_state = c.PA_CONTEXT_UNCONNECTED,
|
|
||||||
.default_sink = null,
|
|
||||||
.default_source = null,
|
|
||||||
.watcher = if (options.deviceChangeFn) |dcf| .{
|
|
||||||
.deviceChangeFn = dcf,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
} else null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (lib.pa_context_connect(ctx, null, 0, null) != 0)
|
|
||||||
return error.ConnectionRefused;
|
|
||||||
errdefer lib.pa_context_disconnect(ctx);
|
|
||||||
lib.pa_context_set_state_callback(ctx, contextStateOp, self);
|
|
||||||
|
|
||||||
if (lib.pa_threaded_mainloop_start(main_loop) != 0)
|
|
||||||
return error.SystemResources;
|
|
||||||
errdefer lib.pa_threaded_mainloop_stop(main_loop);
|
|
||||||
|
|
||||||
lib.pa_threaded_mainloop_lock(main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(main_loop);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
switch (self.ctx_state) {
|
|
||||||
// The context hasn't been connected yet.
|
|
||||||
c.PA_CONTEXT_UNCONNECTED,
|
|
||||||
// A connection is being established.
|
|
||||||
c.PA_CONTEXT_CONNECTING,
|
|
||||||
// The client is authorizing itself to the daemon.
|
|
||||||
c.PA_CONTEXT_AUTHORIZING,
|
|
||||||
// The client is passing its application name to the daemon.
|
|
||||||
c.PA_CONTEXT_SETTING_NAME,
|
|
||||||
=> lib.pa_threaded_mainloop_wait(main_loop),
|
|
||||||
|
|
||||||
// The connection is established, the context is ready to execute operations.
|
|
||||||
c.PA_CONTEXT_READY => break,
|
|
||||||
|
|
||||||
// The connection was terminated cleanly.
|
|
||||||
c.PA_CONTEXT_TERMINATED,
|
|
||||||
// The connection failed or was disconnected.
|
|
||||||
c.PA_CONTEXT_FAILED,
|
|
||||||
=> return error.ConnectionRefused,
|
|
||||||
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// subscribe to events
|
|
||||||
if (options.deviceChangeFn != null) {
|
|
||||||
lib.pa_context_set_subscribe_callback(ctx, subscribeOp, self);
|
|
||||||
const events = c.PA_SUBSCRIPTION_MASK_SINK | c.PA_SUBSCRIPTION_MASK_SOURCE;
|
|
||||||
const subscribe_op = lib.pa_context_subscribe(ctx, events, null, self) orelse
|
|
||||||
return error.OutOfMemory;
|
|
||||||
lib.pa_operation_unref(subscribe_op);
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .pulseaudio = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribeOp(_: ?*c.pa_context, _: c.pa_subscription_event_type_t, _: u32, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
self.watcher.?.deviceChangeFn(self.watcher.?.user_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn contextStateOp(ctx: ?*c.pa_context, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
|
|
||||||
self.ctx_state = lib.pa_context_get_state(ctx);
|
|
||||||
lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
lib.pa_context_set_subscribe_callback(self.ctx, null, null);
|
|
||||||
lib.pa_context_set_state_callback(self.ctx, null, null);
|
|
||||||
lib.pa_context_disconnect(self.ctx);
|
|
||||||
lib.pa_context_unref(self.ctx);
|
|
||||||
lib.pa_threaded_mainloop_stop(self.main_loop);
|
|
||||||
lib.pa_threaded_mainloop_free(self.main_loop);
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
lib.handle.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.clear(self.allocator);
|
|
||||||
|
|
||||||
const list_sink_op = lib.pa_context_get_sink_info_list(self.ctx, sinkInfoOp, self);
|
|
||||||
const list_source_op = lib.pa_context_get_source_info_list(self.ctx, sourceInfoOp, self);
|
|
||||||
const server_info_op = lib.pa_context_get_server_info(self.ctx, serverInfoOp, self);
|
|
||||||
|
|
||||||
performOperation(self.main_loop, list_sink_op);
|
|
||||||
performOperation(self.main_loop, list_source_op);
|
|
||||||
performOperation(self.main_loop, server_info_op);
|
|
||||||
|
|
||||||
defer {
|
|
||||||
if (self.default_sink) |d|
|
|
||||||
self.allocator.free(d);
|
|
||||||
if (self.default_source) |d|
|
|
||||||
self.allocator.free(d);
|
|
||||||
}
|
|
||||||
for (self.devices_info.list.items, 0..) |device, i| {
|
|
||||||
if ((device.mode == .playback and
|
|
||||||
self.default_sink != null and
|
|
||||||
std.mem.eql(u8, device.id, self.default_sink.?)) or
|
|
||||||
//
|
|
||||||
(device.mode == .capture and
|
|
||||||
self.default_source != null and
|
|
||||||
std.mem.eql(u8, device.id, self.default_source.?)))
|
|
||||||
{
|
|
||||||
self.devices_info.setDefault(device.mode, i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serverInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_server_info, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
|
|
||||||
defer lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
self.default_sink = self.allocator.dupeZ(u8, std.mem.span(info.*.default_sink_name)) catch return;
|
|
||||||
self.default_source = self.allocator.dupeZ(u8, std.mem.span(info.*.default_source_name)) catch {
|
|
||||||
self.allocator.free(self.default_sink.?);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sinkInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
if (eol != 0) {
|
|
||||||
lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.deviceInfoOp(info, .playback) catch return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sourceInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_source_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Context, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
if (eol != 0) {
|
|
||||||
lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.deviceInfoOp(info, .capture) catch return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deviceInfoOp(self: *Context, info: anytype, mode: main.Device.Mode) !void {
|
|
||||||
var id = try self.allocator.dupeZ(u8, std.mem.span(info.*.name));
|
|
||||||
errdefer self.allocator.free(id);
|
|
||||||
var name = try self.allocator.dupeZ(u8, std.mem.span(info.*.description));
|
|
||||||
errdefer self.allocator.free(name);
|
|
||||||
|
|
||||||
var device = main.Device{
|
|
||||||
.mode = mode,
|
|
||||||
.channels = blk: {
|
|
||||||
var channels = try self.allocator.alloc(main.Channel, info.*.channel_map.channels);
|
|
||||||
for (channels, 0..) |*ch, i|
|
|
||||||
ch.*.id = fromPAChannelPos(info.*.channel_map.map[i]) catch unreachable;
|
|
||||||
break :blk channels;
|
|
||||||
},
|
|
||||||
.formats = available_formats,
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = @as(u24, @intCast(info.*.sample_spec.rate)),
|
|
||||||
.max = @as(u24, @intCast(info.*.sample_spec.rate)),
|
|
||||||
},
|
|
||||||
.id = id,
|
|
||||||
.name = name,
|
|
||||||
};
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, device);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
const format = device.preferredFormat(options.format);
|
|
||||||
const sample_rate = device.sample_rate.clamp(options.sample_rate);
|
|
||||||
|
|
||||||
const sample_spec = c.pa_sample_spec{
|
|
||||||
.format = toPAFormat(format),
|
|
||||||
.rate = sample_rate,
|
|
||||||
.channels = @as(u5, @intCast(device.channels.len)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const channel_map = try toPAChannelMap(device.channels);
|
|
||||||
|
|
||||||
var stream = lib.pa_stream_new(self.ctx, self.app_name.ptr, &sample_spec, &channel_map);
|
|
||||||
if (stream == null)
|
|
||||||
return error.OutOfMemory;
|
|
||||||
errdefer lib.pa_stream_unref(stream);
|
|
||||||
|
|
||||||
var status: StreamStatus = .{ .main_loop = self.main_loop, .status = .unknown };
|
|
||||||
lib.pa_stream_set_state_callback(stream, streamStateOp, &status);
|
|
||||||
|
|
||||||
const buf_attr = c.pa_buffer_attr{
|
|
||||||
.maxlength = std.math.maxInt(u32),
|
|
||||||
.tlength = std.math.maxInt(u32),
|
|
||||||
.prebuf = 0,
|
|
||||||
.minreq = std.math.maxInt(u32),
|
|
||||||
.fragsize = std.math.maxInt(u32),
|
|
||||||
};
|
|
||||||
|
|
||||||
const flags =
|
|
||||||
c.PA_STREAM_START_CORKED |
|
|
||||||
c.PA_STREAM_AUTO_TIMING_UPDATE |
|
|
||||||
c.PA_STREAM_INTERPOLATE_TIMING |
|
|
||||||
c.PA_STREAM_ADJUST_LATENCY;
|
|
||||||
|
|
||||||
if (lib.pa_stream_connect_playback(stream, device.id.ptr, &buf_attr, flags, null, null) != 0) {
|
|
||||||
return error.OpeningDevice;
|
|
||||||
}
|
|
||||||
errdefer _ = lib.pa_stream_disconnect(stream);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
switch (status.status) {
|
|
||||||
.unknown => lib.pa_threaded_mainloop_wait(self.main_loop),
|
|
||||||
.ready => break,
|
|
||||||
.failure => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.main_loop = self.main_loop,
|
|
||||||
.ctx = self.ctx,
|
|
||||||
.stream = stream.?,
|
|
||||||
.write_ptr = undefined,
|
|
||||||
.vol = 1.0,
|
|
||||||
.writeFn = writeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = format,
|
|
||||||
.sample_rate = sample_rate,
|
|
||||||
.write_step = format.frameSize(device.channels.len),
|
|
||||||
};
|
|
||||||
return .{ .pulseaudio = player };
|
|
||||||
}
|
|
||||||
|
|
||||||
const StreamStatus = struct {
|
|
||||||
main_loop: *c.pa_threaded_mainloop,
|
|
||||||
status: enum(u8) {
|
|
||||||
unknown,
|
|
||||||
ready,
|
|
||||||
failure,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
fn streamStateOp(stream: ?*c.pa_stream, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*StreamStatus, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
|
|
||||||
switch (lib.pa_stream_get_state(stream)) {
|
|
||||||
c.PA_STREAM_UNCONNECTED,
|
|
||||||
c.PA_STREAM_CREATING,
|
|
||||||
c.PA_STREAM_TERMINATED,
|
|
||||||
=> {},
|
|
||||||
c.PA_STREAM_READY => {
|
|
||||||
self.status = .ready;
|
|
||||||
lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
},
|
|
||||||
c.PA_STREAM_FAILED => {
|
|
||||||
self.status = .failure;
|
|
||||||
lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
},
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
main_loop: *c.pa_threaded_mainloop,
|
|
||||||
ctx: *c.pa_context,
|
|
||||||
stream: *c.pa_stream,
|
|
||||||
write_ptr: [*]u8,
|
|
||||||
vol: f32,
|
|
||||||
writeFn: main.WriteFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
sample_rate: u24,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
lib.pa_stream_set_write_callback(self.stream, null, null);
|
|
||||||
lib.pa_stream_set_state_callback(self.stream, null, null);
|
|
||||||
lib.pa_stream_set_underflow_callback(self.stream, null, null);
|
|
||||||
lib.pa_stream_set_overflow_callback(self.stream, null, null);
|
|
||||||
_ = lib.pa_stream_disconnect(self.stream);
|
|
||||||
lib.pa_stream_unref(self.stream);
|
|
||||||
lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: *Player) !void {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
const op = lib.pa_stream_cork(self.stream, 0, null, null) orelse
|
|
||||||
return error.CannotPlay;
|
|
||||||
lib.pa_operation_unref(op);
|
|
||||||
lib.pa_stream_set_write_callback(self.stream, playbackStreamWriteOp, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn playbackStreamWriteOp(_: ?*c.pa_stream, nbytes: usize, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Player, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
|
|
||||||
var frames_left = nbytes;
|
|
||||||
if (lib.pa_stream_begin_write(
|
|
||||||
self.stream,
|
|
||||||
@as(
|
|
||||||
[*c]?*anyopaque,
|
|
||||||
@ptrCast(@alignCast(&self.write_ptr)),
|
|
||||||
),
|
|
||||||
&frames_left,
|
|
||||||
) != 0) {
|
|
||||||
if (std.debug.runtime_safety) unreachable;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.channels, 0..) |*ch, i| {
|
|
||||||
ch.*.ptr = self.write_ptr + self.format.frameSize(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
const frames = frames_left / self.format.frameSize(self.channels.len);
|
|
||||||
self.writeFn(self.user_data, frames);
|
|
||||||
|
|
||||||
if (lib.pa_stream_write(self.stream, self.write_ptr, frames_left, null, 0, c.PA_SEEK_RELATIVE) != 0) {
|
|
||||||
if (std.debug.runtime_safety) unreachable;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: *Player) !void {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
if (lib.pa_stream_is_corked(self.stream) > 0) {
|
|
||||||
const op = lib.pa_stream_cork(self.stream, 0, null, null) orelse
|
|
||||||
return error.CannotPlay;
|
|
||||||
lib.pa_operation_unref(op);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: *Player) !void {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
if (lib.pa_stream_is_corked(self.stream) == 0) {
|
|
||||||
const op = lib.pa_stream_cork(self.stream, 1, null, null) orelse
|
|
||||||
return error.CannotPause;
|
|
||||||
lib.pa_operation_unref(op);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: *Player) bool {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
return lib.pa_stream_is_corked(self.stream) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
var cvolume: c.pa_cvolume = undefined;
|
|
||||||
_ = lib.pa_cvolume_init(&cvolume);
|
|
||||||
_ = lib.pa_cvolume_set(&cvolume, @as(c_uint, @intCast(self.channels.len)), lib.pa_sw_volume_from_linear(vol));
|
|
||||||
|
|
||||||
performOperation(
|
|
||||||
self.main_loop,
|
|
||||||
lib.pa_context_set_sink_input_volume(
|
|
||||||
self.ctx,
|
|
||||||
lib.pa_stream_get_index(self.stream),
|
|
||||||
&cvolume,
|
|
||||||
successOp,
|
|
||||||
self,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn successOp(_: ?*c.pa_context, success: c_int, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Player, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
if (success == 1)
|
|
||||||
lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: *Player) !f32 {
|
|
||||||
lib.pa_threaded_mainloop_lock(self.main_loop);
|
|
||||||
defer lib.pa_threaded_mainloop_unlock(self.main_loop);
|
|
||||||
|
|
||||||
performOperation(
|
|
||||||
self.main_loop,
|
|
||||||
lib.pa_context_get_sink_input_info(
|
|
||||||
self.ctx,
|
|
||||||
lib.pa_stream_get_index(self.stream),
|
|
||||||
sinkInputInfoOp,
|
|
||||||
self,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return self.vol;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sinkInputInfoOp(_: ?*c.pa_context, info: [*c]const c.pa_sink_input_info, eol: c_int, user_data: ?*anyopaque) callconv(.C) void {
|
|
||||||
var self = @as(*Player, @ptrCast(@alignCast(user_data.?)));
|
|
||||||
|
|
||||||
if (eol != 0) {
|
|
||||||
lib.pa_threaded_mainloop_signal(self.main_loop, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.vol = @as(f32, @floatFromInt(info.*.volume.values[0])) / @as(f32, @floatFromInt(c.PA_VOLUME_NORM));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
||||||
allocator.free(device.id);
|
|
||||||
allocator.free(device.name);
|
|
||||||
allocator.free(device.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn performOperation(main_loop: *c.pa_threaded_mainloop, op: ?*c.pa_operation) void {
|
|
||||||
while (true) {
|
|
||||||
switch (lib.pa_operation_get_state(op)) {
|
|
||||||
c.PA_OPERATION_RUNNING => lib.pa_threaded_mainloop_wait(main_loop),
|
|
||||||
c.PA_OPERATION_DONE => return lib.pa_operation_unref(op),
|
|
||||||
c.PA_OPERATION_CANCELLED => {
|
|
||||||
std.debug.assert(false);
|
|
||||||
lib.pa_operation_unref(op);
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const available_formats = &[_]main.Format{
|
|
||||||
.u8, .i16,
|
|
||||||
.i24, .i24_4b,
|
|
||||||
.i32, .f32,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn fromPAChannelPos(pos: c.pa_channel_position_t) !main.Channel.Id {
|
|
||||||
return switch (pos) {
|
|
||||||
c.PA_CHANNEL_POSITION_MONO => .front_center,
|
|
||||||
c.PA_CHANNEL_POSITION_FRONT_LEFT => .front_left, // PA_CHANNEL_POSITION_LEFT
|
|
||||||
c.PA_CHANNEL_POSITION_FRONT_RIGHT => .front_right, // PA_CHANNEL_POSITION_RIGHT
|
|
||||||
c.PA_CHANNEL_POSITION_FRONT_CENTER => .front_center, // PA_CHANNEL_POSITION_CENTER
|
|
||||||
c.PA_CHANNEL_POSITION_REAR_CENTER => .back_center,
|
|
||||||
c.PA_CHANNEL_POSITION_REAR_LEFT => .back_left,
|
|
||||||
c.PA_CHANNEL_POSITION_REAR_RIGHT => .back_right,
|
|
||||||
c.PA_CHANNEL_POSITION_LFE => .lfe, // PA_CHANNEL_POSITION_SUBWOOFER
|
|
||||||
c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER => .front_left_center,
|
|
||||||
c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER => .front_right_center,
|
|
||||||
c.PA_CHANNEL_POSITION_SIDE_LEFT => .side_left,
|
|
||||||
c.PA_CHANNEL_POSITION_SIDE_RIGHT => .side_right,
|
|
||||||
|
|
||||||
// TODO: .front_center?
|
|
||||||
c.PA_CHANNEL_POSITION_AUX0...c.PA_CHANNEL_POSITION_AUX31 => .front_center,
|
|
||||||
|
|
||||||
c.PA_CHANNEL_POSITION_TOP_CENTER => .top_center,
|
|
||||||
c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT => .top_front_left,
|
|
||||||
c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT => .top_front_right,
|
|
||||||
c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER => .top_front_center,
|
|
||||||
c.PA_CHANNEL_POSITION_TOP_REAR_LEFT => .top_back_left,
|
|
||||||
c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT => .top_back_right,
|
|
||||||
c.PA_CHANNEL_POSITION_TOP_REAR_CENTER => .top_back_center,
|
|
||||||
|
|
||||||
else => error.Invalid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toPAFormat(format: main.Format) c.pa_sample_format_t {
|
|
||||||
return switch (format) {
|
|
||||||
.u8 => c.PA_SAMPLE_U8,
|
|
||||||
.i16 => if (is_little) c.PA_SAMPLE_S16LE else c.PA_SAMPLE_S16BE,
|
|
||||||
.i24 => if (is_little) c.PA_SAMPLE_S24LE else c.PA_SAMPLE_S24LE,
|
|
||||||
.i24_4b => if (is_little) c.PA_SAMPLE_S24_32LE else c.PA_SAMPLE_S24_32BE,
|
|
||||||
.i32 => if (is_little) c.PA_SAMPLE_S32LE else c.PA_SAMPLE_S32BE,
|
|
||||||
.f32 => if (is_little) c.PA_SAMPLE_FLOAT32LE else c.PA_SAMPLE_FLOAT32BE,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toPAChannelMap(channels: []const main.Channel) !c.pa_channel_map {
|
|
||||||
var channel_map: c.pa_channel_map = undefined;
|
|
||||||
channel_map.channels = @as(u5, @intCast(channels.len));
|
|
||||||
for (channels, 0..) |ch, i|
|
|
||||||
channel_map.map[i] = try toPAChannelPos(ch.id);
|
|
||||||
return channel_map;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toPAChannelPos(channel_id: main.Channel.Id) !c.pa_channel_position_t {
|
|
||||||
return switch (channel_id) {
|
|
||||||
.lfe => c.PA_CHANNEL_POSITION_LFE,
|
|
||||||
.front_center => c.PA_CHANNEL_POSITION_FRONT_CENTER,
|
|
||||||
.front_left => c.PA_CHANNEL_POSITION_FRONT_LEFT,
|
|
||||||
.front_right => c.PA_CHANNEL_POSITION_FRONT_RIGHT,
|
|
||||||
.front_left_center => c.PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
|
|
||||||
.front_right_center => c.PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
|
|
||||||
.back_center => c.PA_CHANNEL_POSITION_REAR_CENTER,
|
|
||||||
.back_left => c.PA_CHANNEL_POSITION_REAR_LEFT,
|
|
||||||
.back_right => c.PA_CHANNEL_POSITION_REAR_RIGHT,
|
|
||||||
.side_left => c.PA_CHANNEL_POSITION_SIDE_LEFT,
|
|
||||||
.side_right => c.PA_CHANNEL_POSITION_SIDE_RIGHT,
|
|
||||||
.top_center => c.PA_CHANNEL_POSITION_TOP_CENTER,
|
|
||||||
.top_front_center => c.PA_CHANNEL_POSITION_TOP_FRONT_CENTER,
|
|
||||||
.top_front_left => c.PA_CHANNEL_POSITION_TOP_FRONT_LEFT,
|
|
||||||
.top_front_right => c.PA_CHANNEL_POSITION_TOP_FRONT_RIGHT,
|
|
||||||
.top_back_center => c.PA_CHANNEL_POSITION_TOP_REAR_CENTER,
|
|
||||||
.top_back_left => c.PA_CHANNEL_POSITION_TOP_REAR_LEFT,
|
|
||||||
.top_back_right => c.PA_CHANNEL_POSITION_TOP_REAR_RIGHT,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const main = @import("main.zig");
|
|
||||||
|
|
||||||
pub const DevicesInfo = struct {
|
|
||||||
list: std.ArrayListUnmanaged(main.Device),
|
|
||||||
default_output: ?usize,
|
|
||||||
default_input: ?usize,
|
|
||||||
|
|
||||||
pub fn init() DevicesInfo {
|
|
||||||
return .{
|
|
||||||
.list = .{},
|
|
||||||
.default_output = null,
|
|
||||||
.default_input = null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(self: *DevicesInfo, allocator: std.mem.Allocator) void {
|
|
||||||
self.default_output = null;
|
|
||||||
self.default_input = null;
|
|
||||||
self.list.clearAndFree(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(self: DevicesInfo, i: usize) main.Device {
|
|
||||||
return self.list.items[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default(self: DevicesInfo, mode: main.Device.Mode) ?main.Device {
|
|
||||||
const index = switch (mode) {
|
|
||||||
.playback => self.default_output,
|
|
||||||
.capture => self.default_input,
|
|
||||||
} orelse return null;
|
|
||||||
return self.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setDefault(self: *DevicesInfo, mode: main.Device.Mode, i: usize) void {
|
|
||||||
switch (mode) {
|
|
||||||
.playback => self.default_output = i,
|
|
||||||
.capture => self.default_input = i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn Range(comptime T: type) type {
|
|
||||||
return struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
min: T,
|
|
||||||
max: T,
|
|
||||||
|
|
||||||
pub fn clamp(self: Self, val: T) T {
|
|
||||||
return std.math.clamp(val, self.min, self.max);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn doNothing() callconv(.C) void {}
|
|
||||||
|
|
@ -1,833 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const win32 = @import("wasapi/win32.zig");
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
enumerator: ?*win32.IMMDeviceEnumerator,
|
|
||||||
watcher: ?Watcher,
|
|
||||||
is_wine: bool,
|
|
||||||
|
|
||||||
const Watcher = struct {
|
|
||||||
deviceChangeFn: main.Context.DeviceChangeFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
notif_client: win32.IMMNotificationClient,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
const flags = win32.COINIT_APARTMENTTHREADED | win32.COINIT_DISABLE_OLE1DDE;
|
|
||||||
var hr = win32.CoInitializeEx(null, flags);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK,
|
|
||||||
win32.S_FALSE,
|
|
||||||
win32.RPC_E_CHANGED_MODE,
|
|
||||||
=> {},
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
win32.E_UNEXPECTED => return error.SystemResources,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
.enumerator = blk: {
|
|
||||||
var enumerator: ?*win32.IMMDeviceEnumerator = null;
|
|
||||||
hr = win32.CoCreateInstance(
|
|
||||||
win32.CLSID_MMDeviceEnumerator,
|
|
||||||
null,
|
|
||||||
win32.CLSCTX_ALL,
|
|
||||||
win32.IID_IMMDeviceEnumerator,
|
|
||||||
@as(*?*anyopaque, @ptrCast(&enumerator)),
|
|
||||||
);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_NOINTERFACE => unreachable,
|
|
||||||
win32.CLASS_E_NOAGGREGATION => return error.SystemResources,
|
|
||||||
win32.REGDB_E_CLASSNOTREG => unreachable,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
break :blk enumerator;
|
|
||||||
},
|
|
||||||
.watcher = if (options.deviceChangeFn) |deviceChangeFn| .{
|
|
||||||
.deviceChangeFn = deviceChangeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.notif_client = win32.IMMNotificationClient{
|
|
||||||
.vtable = &.{
|
|
||||||
.base = .{
|
|
||||||
.QueryInterface = queryInterfaceCB,
|
|
||||||
.AddRef = addRefCB,
|
|
||||||
.Release = releaseCB,
|
|
||||||
},
|
|
||||||
.OnDeviceStateChanged = onDeviceStateChangedCB,
|
|
||||||
.OnDeviceAdded = onDeviceAddedCB,
|
|
||||||
.OnDeviceRemoved = onDeviceRemovedCB,
|
|
||||||
.OnDefaultDeviceChanged = onDefaultDeviceChangedCB,
|
|
||||||
.OnPropertyValueChanged = onPropertyValueChangedCB,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} else null,
|
|
||||||
.is_wine = blk: {
|
|
||||||
const hntdll = win32.GetModuleHandleA("ntdll.dll");
|
|
||||||
if (hntdll) |_| {
|
|
||||||
if (win32.GetProcAddress(hntdll, "wine_get_version")) |_| {
|
|
||||||
break :blk true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break :blk false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.deviceChangeFn) |_| {
|
|
||||||
hr = self.enumerator.?.RegisterEndpointNotificationCallback(&self.watcher.?.notif_client);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
else => return error.SystemResources,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .wasapi = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn queryInterfaceCB(self: *const win32.IUnknown, riid: ?*const win32.Guid, ppv: ?*?*anyopaque) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
|
||||||
if (riid.?.eql(win32.IID_IUnknown.*) or riid.?.eql(win32.IID_IMMNotificationClient.*)) {
|
|
||||||
ppv.?.* = @as(?*anyopaque, @ptrFromInt(@intFromPtr(self)));
|
|
||||||
_ = self.AddRef();
|
|
||||||
return win32.S_OK;
|
|
||||||
} else {
|
|
||||||
ppv.?.* = null;
|
|
||||||
return win32.E_NOINTERFACE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn addRefCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn releaseCB(_: *const win32.IUnknown) callconv(std.os.windows.WINAPI) u32 {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onDeviceStateChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: u32) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
|
||||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
|
||||||
watcher.deviceChangeFn(watcher.user_data);
|
|
||||||
return win32.S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onDeviceAddedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
|
||||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
|
||||||
watcher.deviceChangeFn(watcher.user_data);
|
|
||||||
return win32.S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onDeviceRemovedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
|
||||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
|
||||||
watcher.deviceChangeFn(watcher.user_data);
|
|
||||||
return win32.S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onDefaultDeviceChangedCB(self: *const win32.IMMNotificationClient, _: win32.DataFlow, _: win32.Role, _: ?[*:0]const u16) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
|
||||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
|
||||||
watcher.deviceChangeFn(watcher.user_data);
|
|
||||||
return win32.S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onPropertyValueChangedCB(self: *const win32.IMMNotificationClient, _: ?[*:0]const u16, _: win32.PROPERTYKEY) callconv(std.os.windows.WINAPI) win32.HRESULT {
|
|
||||||
var watcher = @fieldParentPtr(Watcher, "notif_client", self);
|
|
||||||
watcher.deviceChangeFn(watcher.user_data);
|
|
||||||
return win32.S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
if (self.watcher) |*watcher| {
|
|
||||||
_ = self.enumerator.?.UnregisterEndpointNotificationCallback(&watcher.notif_client);
|
|
||||||
}
|
|
||||||
_ = self.enumerator.?.Release();
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
// get default devices id
|
|
||||||
var default_playback_device: ?*win32.IMMDevice = null;
|
|
||||||
var hr = self.enumerator.?.GetDefaultAudioEndpoint(.render, .multimedia, &default_playback_device);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
// TODO: win32.E_NOTFOUND!?
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
defer _ = default_playback_device.?.Release();
|
|
||||||
|
|
||||||
var default_capture_device: ?*win32.IMMDevice = null;
|
|
||||||
hr = self.enumerator.?.GetDefaultAudioEndpoint(.capture, .multimedia, &default_capture_device);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
// TODO: win32.E_NOTFOUND!?
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
defer _ = default_capture_device.?.Release();
|
|
||||||
|
|
||||||
var default_playback_id_u16: ?[*:0]u16 = undefined;
|
|
||||||
hr = default_playback_device.?.GetId(&default_playback_id_u16);
|
|
||||||
defer win32.CoTaskMemFree(default_playback_id_u16);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
const default_playback_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_playback_id_u16.?)) catch |err| switch (err) {
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
defer self.allocator.free(default_playback_id);
|
|
||||||
|
|
||||||
var default_capture_id_u16: ?[*:0]u16 = undefined;
|
|
||||||
hr = default_capture_device.?.GetId(&default_capture_id_u16);
|
|
||||||
defer win32.CoTaskMemFree(default_capture_id_u16);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
const default_capture_id = std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(default_capture_id_u16.?)) catch |err| switch (err) {
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
defer self.allocator.free(default_capture_id);
|
|
||||||
|
|
||||||
// enumerate
|
|
||||||
var collection: ?*win32.IMMDeviceCollection = null;
|
|
||||||
hr = self.enumerator.?.EnumAudioEndpoints(
|
|
||||||
win32.DataFlow.all,
|
|
||||||
win32.DEVICE_STATE_ACTIVE,
|
|
||||||
&collection,
|
|
||||||
);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
defer _ = collection.?.Release();
|
|
||||||
|
|
||||||
var device_count: u32 = 0;
|
|
||||||
hr = collection.?.GetCount(&device_count);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
var i: u32 = 0;
|
|
||||||
while (i < device_count) : (i += 1) {
|
|
||||||
var imm_device: ?*win32.IMMDevice = null;
|
|
||||||
hr = collection.?.Item(i, &imm_device);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
defer _ = imm_device.?.Release();
|
|
||||||
|
|
||||||
var property_store: ?*win32.IPropertyStore = null;
|
|
||||||
var variant: win32.PROPVARIANT = undefined;
|
|
||||||
hr = imm_device.?.OpenPropertyStore(win32.STGM_READ, &property_store);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
defer _ = property_store.?.Release();
|
|
||||||
|
|
||||||
hr = property_store.?.GetValue(&win32.PKEY_AudioEngine_DeviceFormat, &variant);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK, win32.INPLACE_S_TRUNCATED => {},
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
var wf = @as(
|
|
||||||
*win32.WAVEFORMATEXTENSIBLE,
|
|
||||||
@ptrCast(variant.anon.anon.anon.blob.pBlobData),
|
|
||||||
);
|
|
||||||
defer win32.CoTaskMemFree(variant.anon.anon.anon.blob.pBlobData);
|
|
||||||
|
|
||||||
var device = main.Device{
|
|
||||||
.mode = blk: {
|
|
||||||
var endpoint: ?*win32.IMMEndpoint = null;
|
|
||||||
hr = imm_device.?.QueryInterface(win32.IID_IMMEndpoint, @as(?*?*anyopaque, @ptrCast(&endpoint)));
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_NOINTERFACE => unreachable,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
defer _ = endpoint.?.Release();
|
|
||||||
|
|
||||||
var dataflow: win32.DataFlow = undefined;
|
|
||||||
hr = endpoint.?.GetDataFlow(&dataflow);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk switch (dataflow) {
|
|
||||||
.render => .playback,
|
|
||||||
.capture => .capture,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.channels = blk: {
|
|
||||||
var chn_arr = std.ArrayList(main.Channel).init(self.allocator);
|
|
||||||
var channel: u32 = win32.SPEAKER_FRONT_LEFT;
|
|
||||||
while (channel < win32.SPEAKER_ALL) : (channel <<= 1) {
|
|
||||||
if (wf.dwChannelMask & channel != 0)
|
|
||||||
try chn_arr.append(.{ .id = fromWASApiChannel(channel) });
|
|
||||||
}
|
|
||||||
break :blk try chn_arr.toOwnedSlice();
|
|
||||||
},
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = @as(u24, @intCast(wf.Format.nSamplesPerSec)),
|
|
||||||
.max = @as(u24, @intCast(wf.Format.nSamplesPerSec)),
|
|
||||||
},
|
|
||||||
.formats = blk: {
|
|
||||||
var audio_client: ?*win32.IAudioClient = null;
|
|
||||||
hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @as(?*?*anyopaque, @ptrCast(&audio_client)));
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.E_NOINTERFACE => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
var fmt_arr = std.ArrayList(main.Format).init(self.allocator);
|
|
||||||
var closest_match: ?*win32.WAVEFORMATEX = null;
|
|
||||||
for (std.meta.tags(main.Format)) |format| {
|
|
||||||
setWaveFormatFormat(wf, format);
|
|
||||||
if (audio_client.?.IsFormatSupported(
|
|
||||||
.SHARED,
|
|
||||||
@as(?*const win32.WAVEFORMATEX, @ptrCast(@alignCast(wf))),
|
|
||||||
&closest_match,
|
|
||||||
) == win32.S_OK) {
|
|
||||||
try fmt_arr.append(format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break :blk try fmt_arr.toOwnedSlice();
|
|
||||||
},
|
|
||||||
.id = blk: {
|
|
||||||
var id_u16: ?[*:0]u16 = undefined;
|
|
||||||
hr = imm_device.?.GetId(&id_u16);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
defer win32.CoTaskMemFree(id_u16);
|
|
||||||
|
|
||||||
break :blk std.unicode.utf16leToUtf8AllocZ(self.allocator, std.mem.span(id_u16.?)) catch |err| switch (err) {
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.name = blk: {
|
|
||||||
hr = property_store.?.GetValue(&win32.PKEY_Device_FriendlyName, &variant);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK, win32.INPLACE_S_TRUNCATED => {},
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
defer win32.CoTaskMemFree(variant.anon.anon.anon.pwszVal);
|
|
||||||
|
|
||||||
break :blk std.unicode.utf16leToUtf8AllocZ(
|
|
||||||
self.allocator,
|
|
||||||
std.mem.span(variant.anon.anon.anon.pwszVal.?),
|
|
||||||
) catch |err| switch (err) {
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, device);
|
|
||||||
if (self.devices_info.default(device.mode) == null) {
|
|
||||||
switch (device.mode) {
|
|
||||||
.playback => if (std.mem.eql(u8, device.id, default_playback_id)) {
|
|
||||||
self.devices_info.setDefault(.playback, self.devices_info.list.items.len - 1);
|
|
||||||
},
|
|
||||||
.capture => if (std.mem.eql(u8, device.id, default_capture_id)) {
|
|
||||||
self.devices_info.setDefault(.capture, self.devices_info.list.items.len - 1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fromWASApiChannel(speaker: u32) main.Channel.Id {
|
|
||||||
return switch (speaker) {
|
|
||||||
win32.SPEAKER_FRONT_CENTER => .front_center,
|
|
||||||
win32.SPEAKER_FRONT_LEFT => .front_left,
|
|
||||||
win32.SPEAKER_FRONT_RIGHT => .front_right,
|
|
||||||
win32.SPEAKER_FRONT_LEFT_OF_CENTER => .front_left_center,
|
|
||||||
win32.SPEAKER_FRONT_RIGHT_OF_CENTER => .front_right_center,
|
|
||||||
win32.SPEAKER_BACK_CENTER => .back_center,
|
|
||||||
win32.SPEAKER_SIDE_LEFT => .side_left,
|
|
||||||
win32.SPEAKER_SIDE_RIGHT => .side_right,
|
|
||||||
win32.SPEAKER_TOP_CENTER => .top_center,
|
|
||||||
win32.SPEAKER_TOP_FRONT_CENTER => .top_front_center,
|
|
||||||
win32.SPEAKER_TOP_FRONT_LEFT => .top_front_left,
|
|
||||||
win32.SPEAKER_TOP_FRONT_RIGHT => .top_front_right,
|
|
||||||
win32.SPEAKER_TOP_BACK_CENTER => .top_back_center,
|
|
||||||
win32.SPEAKER_TOP_BACK_LEFT => .top_back_left,
|
|
||||||
win32.SPEAKER_TOP_BACK_RIGHT => .top_back_right,
|
|
||||||
win32.SPEAKER_LOW_FREQUENCY => .lfe,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setWaveFormatFormat(wf: *win32.WAVEFORMATEXTENSIBLE, format: main.Format) void {
|
|
||||||
switch (format) {
|
|
||||||
.u8, .i16, .i24, .i24_4b, .i32 => {
|
|
||||||
wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*;
|
|
||||||
},
|
|
||||||
.f32 => {
|
|
||||||
wf.SubFormat = win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
wf.Format.wBitsPerSample = format.sizeBits();
|
|
||||||
wf.Samples.wValidBitsPerSample = format.validSizeBits();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
var imm_device: ?*win32.IMMDevice = null;
|
|
||||||
var id_u16 = std.unicode.utf8ToUtf16LeWithNull(self.allocator, device.id) catch |err| switch (err) {
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
defer self.allocator.free(id_u16);
|
|
||||||
var hr = self.enumerator.?.GetDevice(id_u16, &imm_device);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
// TODO: win32.E_NOTFOUND!?
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
var audio_client: ?*win32.IAudioClient = null;
|
|
||||||
var audio_client3: ?*win32.IAudioClient3 = null;
|
|
||||||
hr = imm_device.?.Activate(win32.IID_IAudioClient3, win32.CLSCTX_ALL, null, @as(?*?*anyopaque, @ptrCast(&audio_client3)));
|
|
||||||
if (hr == win32.S_OK) {
|
|
||||||
hr = audio_client3.?.QueryInterface(win32.IID_IAudioClient, @as(?*?*anyopaque, @ptrCast(&audio_client)));
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_NOINTERFACE => unreachable,
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hr = imm_device.?.Activate(win32.IID_IAudioClient, win32.CLSCTX_ALL, null, @as(?*?*anyopaque, @ptrCast(&audio_client)));
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.E_NOINTERFACE => unreachable,
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => unreachable,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const format = device.preferredFormat(options.format);
|
|
||||||
const sample_rate = device.sample_rate.min;
|
|
||||||
|
|
||||||
const wave_format = win32.WAVEFORMATEXTENSIBLE{
|
|
||||||
.Format = .{
|
|
||||||
.wFormatTag = win32.WAVE_FORMAT_EXTENSIBLE,
|
|
||||||
.nChannels = @as(u16, @intCast(device.channels.len)),
|
|
||||||
.nSamplesPerSec = sample_rate,
|
|
||||||
.nAvgBytesPerSec = sample_rate * format.frameSize(device.channels.len),
|
|
||||||
.nBlockAlign = format.frameSize(device.channels.len),
|
|
||||||
.wBitsPerSample = format.sizeBits(),
|
|
||||||
.cbSize = 0x16,
|
|
||||||
},
|
|
||||||
.Samples = .{
|
|
||||||
.wValidBitsPerSample = format.validSizeBits(),
|
|
||||||
},
|
|
||||||
.dwChannelMask = toChannelMask(device.channels),
|
|
||||||
.SubFormat = toSubFormat(format),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!self.is_wine and audio_client3 != null) {
|
|
||||||
hr = audio_client3.?.InitializeSharedAudioStream(
|
|
||||||
win32.AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
|
||||||
0, // TODO: use the advantage of AudioClient3
|
|
||||||
@as(?*const win32.WAVEFORMATEX, @ptrCast(@alignCast(&wave_format))),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
|
||||||
win32.AUDCLNT_E_CPUUSAGE_EXCEEDED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_DEVICE_IN_USE => unreachable,
|
|
||||||
win32.AUDCLNT_E_ENGINE_FORMAT_LOCKED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_ENGINE_PERIODICITY_LOCKED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_ENDPOINT_CREATE_FAILED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_INVALID_DEVICE_PERIOD => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_UNSUPPORTED_FORMAT => unreachable,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hr = audio_client.?.Initialize(
|
|
||||||
.SHARED,
|
|
||||||
win32.AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
@as(?*const win32.WAVEFORMATEX, @ptrCast(@alignCast(&wave_format))),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_OUTOFMEMORY => return error.OutOfMemory,
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.AUDCLNT_E_ALREADY_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
|
||||||
win32.AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED => return error.OpeningDevice, // TODO: some libs handle this better
|
|
||||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_CPUUSAGE_EXCEEDED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_DEVICE_IN_USE => unreachable,
|
|
||||||
win32.AUDCLNT_E_ENDPOINT_CREATE_FAILED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_INVALID_DEVICE_PERIOD => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_UNSUPPORTED_FORMAT => unreachable,
|
|
||||||
win32.AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED => unreachable,
|
|
||||||
win32.AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL => unreachable,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var render_client: ?*win32.IAudioRenderClient = null;
|
|
||||||
hr = audio_client.?.GetService(win32.IID_IAudioRenderClient, @as(?*?*anyopaque, @ptrCast(&render_client)));
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_NOINTERFACE => unreachable,
|
|
||||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
var simple_volume: ?*win32.ISimpleAudioVolume = null;
|
|
||||||
hr = audio_client.?.GetService(win32.IID_ISimpleAudioVolume, @as(?*?*anyopaque, @ptrCast(&simple_volume)));
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.E_NOINTERFACE => unreachable,
|
|
||||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_WRONG_ENDPOINT_TYPE => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ready_event = win32.CreateEventA(null, 0, 0, null) orelse return error.SystemResources;
|
|
||||||
hr = audio_client.?.SetEventHandle(ready_event);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED => unreachable,
|
|
||||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.OpeningDevice,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.OpeningDevice,
|
|
||||||
else => return error.OpeningDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.thread = undefined,
|
|
||||||
.mutex = .{},
|
|
||||||
.audio_client = audio_client,
|
|
||||||
.audio_client3 = audio_client3,
|
|
||||||
.simple_volume = simple_volume,
|
|
||||||
.imm_device = imm_device,
|
|
||||||
.render_client = render_client,
|
|
||||||
.ready_event = ready_event,
|
|
||||||
.aborted = .{ .value = false },
|
|
||||||
.is_paused = false,
|
|
||||||
.writeFn = writeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = format,
|
|
||||||
.sample_rate = sample_rate,
|
|
||||||
.write_step = format.frameSize(device.channels.len),
|
|
||||||
};
|
|
||||||
return .{ .wasapi = player };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toSubFormat(format: main.Format) win32.Guid {
|
|
||||||
return switch (format) {
|
|
||||||
.u8,
|
|
||||||
.i16,
|
|
||||||
.i24,
|
|
||||||
.i24_4b,
|
|
||||||
.i32,
|
|
||||||
=> win32.CLSID_KSDATAFORMAT_SUBTYPE_PCM.*,
|
|
||||||
.f32 => win32.CLSID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT.*,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toChannelMask(channels: []const main.Channel) u32 {
|
|
||||||
var mask: u32 = 0;
|
|
||||||
for (channels) |ch| {
|
|
||||||
mask |= switch (ch.id) {
|
|
||||||
.front_center => win32.SPEAKER_FRONT_CENTER,
|
|
||||||
.front_left => win32.SPEAKER_FRONT_LEFT,
|
|
||||||
.front_right => win32.SPEAKER_FRONT_RIGHT,
|
|
||||||
.front_left_center => win32.SPEAKER_FRONT_LEFT_OF_CENTER,
|
|
||||||
.front_right_center => win32.SPEAKER_FRONT_RIGHT_OF_CENTER,
|
|
||||||
.back_center => win32.SPEAKER_BACK_CENTER,
|
|
||||||
.back_left => win32.SPEAKER_BACK_LEFT,
|
|
||||||
.back_right => win32.SPEAKER_BACK_RIGHT,
|
|
||||||
.side_left => win32.SPEAKER_SIDE_LEFT,
|
|
||||||
.side_right => win32.SPEAKER_SIDE_RIGHT,
|
|
||||||
.top_center => win32.SPEAKER_TOP_CENTER,
|
|
||||||
.top_front_center => win32.SPEAKER_TOP_FRONT_CENTER,
|
|
||||||
.top_front_left => win32.SPEAKER_TOP_FRONT_LEFT,
|
|
||||||
.top_front_right => win32.SPEAKER_TOP_FRONT_RIGHT,
|
|
||||||
.top_back_center => win32.SPEAKER_TOP_BACK_CENTER,
|
|
||||||
.top_back_left => win32.SPEAKER_TOP_BACK_LEFT,
|
|
||||||
.top_back_right => win32.SPEAKER_TOP_BACK_RIGHT,
|
|
||||||
.lfe => win32.SPEAKER_LOW_FREQUENCY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
thread: std.Thread,
|
|
||||||
mutex: std.Thread.Mutex,
|
|
||||||
simple_volume: ?*win32.ISimpleAudioVolume,
|
|
||||||
imm_device: ?*win32.IMMDevice,
|
|
||||||
audio_client: ?*win32.IAudioClient,
|
|
||||||
audio_client3: ?*win32.IAudioClient3,
|
|
||||||
render_client: ?*win32.IAudioRenderClient,
|
|
||||||
ready_event: *anyopaque,
|
|
||||||
aborted: std.atomic.Atomic(bool),
|
|
||||||
is_paused: bool,
|
|
||||||
writeFn: main.WriteFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
sample_rate: u24,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
self.aborted.store(true, .Unordered);
|
|
||||||
self.thread.join();
|
|
||||||
_ = self.simple_volume.?.Release();
|
|
||||||
_ = self.render_client.?.Release();
|
|
||||||
_ = self.audio_client.?.Release();
|
|
||||||
_ = self.audio_client3.?.Release();
|
|
||||||
_ = self.imm_device.?.Release();
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: *Player) !void {
|
|
||||||
self.thread = std.Thread.spawn(.{}, writeLoop, .{self}) catch |err| switch (err) {
|
|
||||||
error.ThreadQuotaExceeded,
|
|
||||||
error.SystemResources,
|
|
||||||
error.LockedMemoryLimitExceeded,
|
|
||||||
=> return error.SystemResources,
|
|
||||||
error.OutOfMemory => return error.OutOfMemory,
|
|
||||||
error.Unexpected => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeLoop(self: *Player) void {
|
|
||||||
var hr = self.audio_client.?.Start();
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_NOT_STOPPED => unreachable,
|
|
||||||
win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!self.aborted.load(.Unordered)) {
|
|
||||||
_ = win32.WaitForSingleObject(self.ready_event, win32.INFINITE);
|
|
||||||
|
|
||||||
var buf_frames: u32 = 0;
|
|
||||||
hr = self.audio_client.?.GetBufferSize(&buf_frames);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
|
|
||||||
var padding_frames: u32 = 0;
|
|
||||||
hr = self.audio_client.?.GetCurrentPadding(&padding_frames);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
const frames = buf_frames - padding_frames;
|
|
||||||
if (frames > 0) {
|
|
||||||
var data: [*]u8 = undefined;
|
|
||||||
hr = self.render_client.?.GetBuffer(frames, @as(?*?*u8, @ptrCast(&data)));
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.AUDCLNT_E_BUFFER_ERROR => unreachable,
|
|
||||||
win32.AUDCLNT_E_BUFFER_TOO_LARGE => unreachable,
|
|
||||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable,
|
|
||||||
win32.AUDCLNT_E_OUT_OF_ORDER => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
|
||||||
win32.AUDCLNT_E_BUFFER_OPERATION_PENDING => continue,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.channels, 0..) |*ch, i| {
|
|
||||||
ch.*.ptr = data + self.format.frameSize(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.writeFn(self.user_data, frames);
|
|
||||||
|
|
||||||
hr = self.render_client.?.ReleaseBuffer(frames, 0);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.AUDCLNT_E_INVALID_SIZE => unreachable,
|
|
||||||
win32.AUDCLNT_E_BUFFER_SIZE_ERROR => unreachable,
|
|
||||||
win32.AUDCLNT_E_OUT_OF_ORDER => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: *Player) !void {
|
|
||||||
if (self.paused()) {
|
|
||||||
const hr = self.audio_client.?.Start();
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.AUDCLNT_E_NOT_INITIALIZED => unreachable,
|
|
||||||
win32.AUDCLNT_E_NOT_STOPPED => unreachable,
|
|
||||||
win32.AUDCLNT_E_EVENTHANDLE_NOT_SET => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPlay,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPlay,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
self.is_paused = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: *Player) !void {
|
|
||||||
if (!self.paused()) {
|
|
||||||
const hr = self.audio_client.?.Stop();
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotPause,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotPause,
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
self.is_paused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: Player) bool {
|
|
||||||
return self.is_paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
const hr = self.simple_volume.?.SetMasterVolume(vol, null);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_INVALIDARG => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotSetVolume,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotSetVolume,
|
|
||||||
else => return error.CannotSetVolume,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: Player) !f32 {
|
|
||||||
var vol: f32 = 0;
|
|
||||||
const hr = self.simple_volume.?.GetMasterVolume(&vol);
|
|
||||||
switch (hr) {
|
|
||||||
win32.S_OK => {},
|
|
||||||
win32.E_POINTER => unreachable,
|
|
||||||
win32.AUDCLNT_E_DEVICE_INVALIDATED => return error.CannotGetVolume,
|
|
||||||
win32.AUDCLNT_E_SERVICE_NOT_RUNNING => return error.CannotGetVolume,
|
|
||||||
else => return error.CannotGetVolume,
|
|
||||||
}
|
|
||||||
return vol;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn freeDevice(allocator: std.mem.Allocator, self: main.Device) void {
|
|
||||||
allocator.free(self.id);
|
|
||||||
allocator.free(self.name);
|
|
||||||
allocator.free(self.formats);
|
|
||||||
allocator.free(self.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,232 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const js = @import("sysjs");
|
|
||||||
const main = @import("main.zig");
|
|
||||||
const backends = @import("backends.zig");
|
|
||||||
const util = @import("util.zig");
|
|
||||||
|
|
||||||
const channel_size = 1024;
|
|
||||||
const channel_size_bytes = channel_size * @sizeOf(f32);
|
|
||||||
|
|
||||||
const default_playback = main.Device{
|
|
||||||
.id = "default-playback",
|
|
||||||
.name = "Default Device",
|
|
||||||
.mode = .playback,
|
|
||||||
.channels = undefined,
|
|
||||||
.formats = &.{.f32},
|
|
||||||
.sample_rate = .{
|
|
||||||
.min = 8_000,
|
|
||||||
.max = 96_000,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Context = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
devices_info: util.DevicesInfo,
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext {
|
|
||||||
_ = options;
|
|
||||||
|
|
||||||
const audio_context = js.global().get("AudioContext");
|
|
||||||
if (audio_context.is(.undefined))
|
|
||||||
return error.ConnectionRefused;
|
|
||||||
|
|
||||||
var self = try allocator.create(Context);
|
|
||||||
errdefer allocator.destroy(self);
|
|
||||||
self.* = .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.devices_info = util.DevicesInfo.init(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .webaudio = self };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.list.deinit(self.allocator);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self: *Context) !void {
|
|
||||||
for (self.devices_info.list.items) |d|
|
|
||||||
freeDevice(self.allocator, d);
|
|
||||||
self.devices_info.clear(self.allocator);
|
|
||||||
|
|
||||||
try self.devices_info.list.append(self.allocator, default_playback);
|
|
||||||
self.devices_info.list.items[0].channels = try self.allocator.alloc(main.Channel, 2);
|
|
||||||
self.devices_info.list.items[0].channels[0] = .{ .id = .front_left };
|
|
||||||
self.devices_info.list.items[0].channels[1] = .{ .id = .front_right };
|
|
||||||
self.devices_info.setDefault(.playback, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn devices(self: Context) []const main.Device {
|
|
||||||
return self.devices_info.list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device {
|
|
||||||
return self.devices_info.default(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.BackendPlayer {
|
|
||||||
const context_options = js.createMap();
|
|
||||||
defer context_options.deinit();
|
|
||||||
context_options.set("sampleRate", js.createNumber(options.sample_rate));
|
|
||||||
|
|
||||||
const audio_context = js.constructType("AudioContext", &.{context_options.toValue()});
|
|
||||||
const gain_node = audio_context.call("createGain", &.{
|
|
||||||
js.createNumber(1),
|
|
||||||
js.createNumber(0),
|
|
||||||
js.createNumber(device.channels.len),
|
|
||||||
}).view(.object);
|
|
||||||
const process_node = audio_context.call("createScriptProcessor", &.{
|
|
||||||
js.createNumber(channel_size),
|
|
||||||
js.createNumber(device.channels.len),
|
|
||||||
}).view(.object);
|
|
||||||
|
|
||||||
var player = try self.allocator.create(Player);
|
|
||||||
errdefer self.allocator.destroy(player);
|
|
||||||
|
|
||||||
var captures = try self.allocator.alloc(js.Value, 1);
|
|
||||||
captures[0] = js.createNumber(@intFromPtr(player));
|
|
||||||
|
|
||||||
const document = js.global().get("document").view(.object);
|
|
||||||
defer document.deinit();
|
|
||||||
const click_event_str = js.createString("click");
|
|
||||||
defer click_event_str.deinit();
|
|
||||||
const resume_on_click = js.createFunction(Player.resumeOnClick, captures);
|
|
||||||
_ = document.call("addEventListener", &.{ click_event_str.toValue(), resume_on_click.toValue() });
|
|
||||||
|
|
||||||
const audio_process_event = js.createFunction(Player.audioProcessEvent, captures);
|
|
||||||
defer audio_process_event.deinit();
|
|
||||||
process_node.set("onaudioprocess", audio_process_event.toValue());
|
|
||||||
|
|
||||||
player.* = .{
|
|
||||||
.allocator = self.allocator,
|
|
||||||
.audio_context = audio_context,
|
|
||||||
.process_node = process_node,
|
|
||||||
.gain_node = gain_node,
|
|
||||||
.process_captures = captures,
|
|
||||||
.resume_on_click = resume_on_click,
|
|
||||||
.buf = try self.allocator.alloc(u8, channel_size_bytes * device.channels.len),
|
|
||||||
.buf_js = js.constructType("Uint8Array", &.{js.createNumber(channel_size_bytes)}),
|
|
||||||
.is_paused = false,
|
|
||||||
.writeFn = writeFn,
|
|
||||||
.user_data = options.user_data,
|
|
||||||
.channels = device.channels,
|
|
||||||
.format = .f32,
|
|
||||||
.sample_rate = options.sample_rate,
|
|
||||||
.write_step = main.Format.size(.f32),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (player.channels, 0..) |*ch, i| {
|
|
||||||
ch.*.ptr = player.buf.ptr + i * channel_size_bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .webaudio = player };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Player = struct {
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
audio_context: js.Object,
|
|
||||||
process_node: js.Object,
|
|
||||||
gain_node: js.Object,
|
|
||||||
process_captures: []js.Value,
|
|
||||||
resume_on_click: js.Function,
|
|
||||||
buf: []u8,
|
|
||||||
buf_js: js.Object,
|
|
||||||
is_paused: bool,
|
|
||||||
writeFn: main.WriteFn,
|
|
||||||
user_data: ?*anyopaque,
|
|
||||||
|
|
||||||
channels: []main.Channel,
|
|
||||||
format: main.Format,
|
|
||||||
sample_rate: u24,
|
|
||||||
write_step: u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: *Player) void {
|
|
||||||
self.resume_on_click.deinit();
|
|
||||||
self.buf_js.deinit();
|
|
||||||
self.gain_node.deinit();
|
|
||||||
self.process_node.deinit();
|
|
||||||
self.audio_context.deinit();
|
|
||||||
self.allocator.free(self.process_captures);
|
|
||||||
self.allocator.free(self.buf);
|
|
||||||
self.allocator.destroy(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(self: Player) !void {
|
|
||||||
const destination = self.audio_context.get("destination").view(.object);
|
|
||||||
defer destination.deinit();
|
|
||||||
_ = self.gain_node.call("connect", &.{destination.toValue()});
|
|
||||||
_ = self.process_node.call("connect", &.{self.gain_node.toValue()});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resumeOnClick(args: js.Object, _: usize, captures: []js.Value) js.Value {
|
|
||||||
const self = @as(*Player, @ptrFromInt(@as(usize, @intFromFloat(captures[0].view(.num)))));
|
|
||||||
self.play() catch {};
|
|
||||||
|
|
||||||
const document = js.global().get("document").view(.object);
|
|
||||||
defer document.deinit();
|
|
||||||
|
|
||||||
const event = args.getIndex(0).view(.object);
|
|
||||||
defer event.deinit();
|
|
||||||
_ = document.call("removeEventListener", &.{ event.toValue(), self.resume_on_click.toValue() });
|
|
||||||
|
|
||||||
return js.createUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audioProcessEvent(args: js.Object, _: usize, captures: []js.Value) js.Value {
|
|
||||||
const self = @as(*Player, @ptrFromInt(@as(usize, @intFromFloat(captures[0].view(.num)))));
|
|
||||||
|
|
||||||
const event = args.getIndex(0).view(.object);
|
|
||||||
defer event.deinit();
|
|
||||||
const output_buffer = event.get("outputBuffer").view(.object);
|
|
||||||
defer output_buffer.deinit();
|
|
||||||
|
|
||||||
self.writeFn(self.user_data, channel_size);
|
|
||||||
|
|
||||||
for (self.channels, 0..) |_, i| {
|
|
||||||
self.buf_js.copyBytes(self.buf[i * channel_size_bytes .. (i + 1) * channel_size_bytes]);
|
|
||||||
const buf_f32_js = js.constructType("Float32Array", &.{ self.buf_js.get("buffer"), self.buf_js.get("byteOffset"), js.createNumber(channel_size) });
|
|
||||||
defer buf_f32_js.deinit();
|
|
||||||
_ = output_buffer.call("copyToChannel", &.{ buf_f32_js.toValue(), js.createNumber(i) });
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.createUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(self: *Player) !void {
|
|
||||||
_ = self.audio_context.call("resume", &.{js.createUndefined()});
|
|
||||||
self.is_paused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(self: *Player) !void {
|
|
||||||
_ = self.audio_context.call("suspend", &.{js.createUndefined()});
|
|
||||||
self.is_paused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(self: Player) bool {
|
|
||||||
return self.is_paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setVolume(self: *Player, vol: f32) !void {
|
|
||||||
const gain = self.gain_node.get("gain").view(.object);
|
|
||||||
defer gain.deinit();
|
|
||||||
gain.set("value", js.createNumber(vol));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(self: Player) !f32 {
|
|
||||||
const gain = self.gain_node.get("gain").view(.object);
|
|
||||||
defer gain.deinit();
|
|
||||||
return @as(f32, @floatCast(gain.get("value").view(.num)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void {
|
|
||||||
allocator.free(device.channels);
|
|
||||||
}
|
|
||||||
|
|
||||||
test {
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue