mirror of
https://github.com/ivabus/speakersafetyd
synced 2024-11-10 02:15:16 +03:00
initial commit
Signed-off-by: James Calligeros <jcalligeros99@gmail.com>
This commit is contained in:
commit
e8a37a847b
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
*.kate-swp
|
||||
*/target
|
88
Cargo.lock
generated
Normal file
88
Cargo.lock
generated
Normal file
|
@ -0,0 +1,88 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags",
|
||||
"libc",
|
||||
"nix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "configparser"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
|
||||
[[package]]
|
||||
name = "speakersafety-2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"configparser",
|
||||
"half",
|
||||
]
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "speakersafety-2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
half = "^2.1.0"
|
||||
alsa = { path = "./alsa" }
|
||||
configparser = "^3.0.2"
|
42
README.md
Normal file
42
README.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
## Asahi Linux speaker safety daemon
|
||||
|
||||
This is still very much a work in progress, is probably not "proper" Rust,
|
||||
and almost definitely makes competent developers extremely sad.
|
||||
|
||||
We currently rely on a local version of the `alsa` crate, pending the merge of
|
||||
bindings to `snd_ctl_elem_value_{read,write}` and `snd_ctl_elem_set_id`.
|
||||
|
||||
## What works
|
||||
* Parsing config file
|
||||
* All borrows seem to work fine
|
||||
* Volume getting/setting
|
||||
|
||||
## Needs improvement
|
||||
* Probably everything
|
||||
|
||||
## Need to implement
|
||||
* Daemonise and loop
|
||||
* Threading (should probably make sure it works as intended first)
|
||||
* Getting V/ISENSE (pending changes to the codec drivers, we have mock implementations)
|
||||
* Actually fail safe (see below)
|
||||
|
||||
## On failing safe
|
||||
We need a way to guarantee safety on _any_ fail condition. The TAS codecs have a safe
|
||||
mode which cuts all outputs down by 18 dB. This works out to being about half their
|
||||
full output capabiltiy. It might be worth having the `macaudio` driver start them
|
||||
explicitly in this mode, and only unlock full output capability with an IOCTL that
|
||||
can be sent by `speakersafetyd` when it's sure it has started correctly. We would
|
||||
then of course also need an IOCTL to do the opposite if we encounter a runtime error.
|
||||
|
||||
It was suggested by someone on IRC that this would be conducive to some sort of
|
||||
keepalive IOCTL, where the driver would automatically put the codecs into safe mode
|
||||
if it didn't hear from us for a while. This seems like it would suck to implement.
|
||||
|
||||
Like any SLA, it is likely that we will never be able to guarantee 100% safety for all
|
||||
nonstandard setups. The reference PipeWire DSP graph plus this should be enough for 99% of
|
||||
users, but I feel at some point those who insist on using Pulse or raw ALSA are just going
|
||||
to have to put up with a best effort service and accept the (small) risk of this failing.
|
||||
|
||||
## Sundry
|
||||
The `alsa` crate is Copyright (c) 2015-2021 David Henningsson, and other
|
||||
contributors. Redistributed under the MIT license.
|
58
alsa/Cargo.lock
generated
Normal file
58
alsa/Cargo.lock
generated
Normal file
|
@ -0,0 +1,58 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags",
|
||||
"libc",
|
||||
"nix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
24
alsa/Cargo.toml
Normal file
24
alsa/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "alsa"
|
||||
version = "0.7.0"
|
||||
authors = ["David Henningsson <coding@diwic.se>"]
|
||||
|
||||
description = "Thin but safe wrappers for ALSA (Linux sound API)"
|
||||
repository = "https://github.com/diwic/alsa-rs"
|
||||
documentation = "http://docs.rs/alsa"
|
||||
keywords = ["ALSA", "audio", "sound"]
|
||||
license = "Apache-2.0/MIT"
|
||||
categories = ["multimedia::audio", "api-bindings"]
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
include = ["README.md", "LICENSE-*", "Cargo.toml", "src/"]
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
alsa-sys = "0.3.1"
|
||||
bitflags = "1.3.2"
|
||||
nix = { version = "^0.24", default-features = false, features = ["ioctl"] }
|
||||
|
||||
[badges]
|
||||
is-it-maintained-issue-resolution = { repository = "diwic/alsa-rs" }
|
||||
is-it-maintained-open-issues = { repository = "diwic/alsa-rs" }
|
177
alsa/LICENSE-APACHE
Normal file
177
alsa/LICENSE-APACHE
Normal file
|
@ -0,0 +1,177 @@
|
|||
|
||||
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
|
21
alsa/LICENSE-MIT
Normal file
21
alsa/LICENSE-MIT
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2015-2021 David Henningsson, and other contributors.
|
||||
|
||||
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.
|
62
alsa/README.md
Normal file
62
alsa/README.md
Normal file
|
@ -0,0 +1,62 @@
|
|||
ALSA bindings for Rust
|
||||
=======================
|
||||
|
||||
Thin but safe wrappers for [ALSA](https://alsa-project.org), the most
|
||||
common API for accessing audio devices on Linux.
|
||||
|
||||
[![crates.io](https://img.shields.io/crates/v/alsa.svg)](https://crates.io/crates/alsa)
|
||||
[![API documentation](https://docs.rs/alsa/badge.svg)](https://docs.rs/alsa)
|
||||
[![license](https://img.shields.io/crates/l/alsa.svg)](https://crates.io/crates/alsa)
|
||||
|
||||
The ALSA API is rather big, so everything is not covered yet, but expect the following to work:
|
||||
|
||||
* Audio Playback (example in `pcm` module docs)
|
||||
|
||||
* Audio Recording
|
||||
|
||||
* Mixer controls
|
||||
|
||||
* HCtl API (jack detection example in `hctl` module docs)
|
||||
|
||||
* Raw midi
|
||||
|
||||
* Midi sequencer (most of it)
|
||||
|
||||
* Ctl API
|
||||
|
||||
* Device name hints (example in `device_name` module docs)
|
||||
|
||||
* Enumerations of all of the above
|
||||
|
||||
* Poll and/or wait for all of the above
|
||||
|
||||
The following is not yet implemented (mostly because nobody asked for them) :
|
||||
|
||||
* Separate timer API (snd_timer_*)
|
||||
|
||||
* Config API (snd_config_*)
|
||||
|
||||
* Plug-in API
|
||||
|
||||
Quickstart guide / API design:
|
||||
|
||||
* Most functions map 1-to-1 to alsa-lib functions, e g, `ctl::CardInfo::get_id()` is a wrapper around
|
||||
`snd_ctl_card_info_get_id` and the [alsa-lib documentation](https://www.alsa-project.org/alsa-doc/alsa-lib/)
|
||||
can be consulted for additional information.
|
||||
|
||||
* Structs are RAII and closed/freed on drop, e g, when a `PCM` struct is dropped, `snd_pcm_close` is called.
|
||||
|
||||
* To read and write buffers, call the `io_*` methods. It will return a separate struct from which you can
|
||||
read or write, and which can also be used for mmap (if supported by the driver).
|
||||
|
||||
* Error handling - most alsa-lib functions can return errors, so the return value from these is a `Result`.
|
||||
|
||||
* Enumeration of cards, devices etc is done through structs implementing `Iterator`.
|
||||
|
||||
* Many structs implement `poll::Descriptors`, to combine with poll or mio.
|
||||
Or just use `wait` if you don't need non-blocking functionality.
|
||||
|
||||
Notes:
|
||||
|
||||
* To run the tests successfully, there must be a "default" sound card configured. This is usually not a problem when running on normal hardware, but some CI systems, docker images etc, might not have that configured by default.
|
||||
|
50
alsa/examples/record.rs
Normal file
50
alsa/examples/record.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
//! Example that continously reads data and displays its RMS volume.
|
||||
|
||||
use alsa::pcm::*;
|
||||
use alsa::{Direction, ValueOr, Error};
|
||||
|
||||
fn start_capture(device: &str) -> Result<PCM, Error> {
|
||||
let pcm = PCM::new(device, Direction::Capture, false)?;
|
||||
{
|
||||
// For this example, we assume 44100Hz, one channel, 16 bit audio.
|
||||
let hwp = HwParams::any(&pcm)?;
|
||||
hwp.set_channels(1)?;
|
||||
hwp.set_rate(44100, ValueOr::Nearest)?;
|
||||
hwp.set_format(Format::s16())?;
|
||||
hwp.set_access(Access::RWInterleaved)?;
|
||||
pcm.hw_params(&hwp)?;
|
||||
}
|
||||
pcm.start()?;
|
||||
Ok(pcm)
|
||||
}
|
||||
|
||||
// Calculates RMS (root mean square) as a way to determine volume
|
||||
fn rms(buf: &[i16]) -> f64 {
|
||||
if buf.len() == 0 { return 0f64; }
|
||||
let mut sum = 0f64;
|
||||
for &x in buf {
|
||||
sum += (x as f64) * (x as f64);
|
||||
}
|
||||
let r = (sum / (buf.len() as f64)).sqrt();
|
||||
// Convert value to decibels
|
||||
20.0 * (r / (i16::MAX as f64)).log10()
|
||||
}
|
||||
|
||||
|
||||
fn read_loop(pcm: &PCM) -> Result<(), Error> {
|
||||
let io = pcm.io_i16()?;
|
||||
let mut buf = [0i16; 8192];
|
||||
loop {
|
||||
// Block while waiting for 8192 samples to be read from the device.
|
||||
assert_eq!(io.readi(&mut buf)?, buf.len());
|
||||
let r = rms(&buf);
|
||||
println!("RMS: {:.1} dB", r);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// The "default" device is usually directed to the sound server process,
|
||||
// e g PulseAudio or PipeWire.
|
||||
let capture = start_capture("default").unwrap();
|
||||
read_loop(&capture).unwrap();
|
||||
}
|
54
alsa/src/card.rs
Normal file
54
alsa/src/card.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
//! Sound card enumeration
|
||||
use libc::{c_int, c_char};
|
||||
use super::error::*;
|
||||
use crate::alsa;
|
||||
use std::ffi::CStr;
|
||||
|
||||
/// An ALSA sound card, uniquely identified by its index.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Card(c_int);
|
||||
|
||||
/// Iterate over existing sound cards.
|
||||
pub struct Iter(c_int);
|
||||
|
||||
impl Iter {
|
||||
pub fn new() -> Iter { Iter(-1) }
|
||||
}
|
||||
|
||||
impl Iterator for Iter {
|
||||
type Item = Result<Card>;
|
||||
|
||||
fn next(&mut self) -> Option<Result<Card>> {
|
||||
match acheck!(snd_card_next(&mut self.0)) {
|
||||
Ok(_) if self.0 == -1 => None,
|
||||
Ok(_) => Some(Ok(Card(self.0))),
|
||||
Err(e) => Some(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Card {
|
||||
pub fn new(index: c_int) -> Card { Card(index) }
|
||||
pub fn from_str(s: &CStr) -> Result<Card> {
|
||||
acheck!(snd_card_get_index(s.as_ptr())).map(Card)
|
||||
}
|
||||
pub fn get_name(&self) -> Result<String> {
|
||||
let mut c: *mut c_char = ::std::ptr::null_mut();
|
||||
acheck!(snd_card_get_name(self.0, &mut c))
|
||||
.and_then(|_| from_alloc("snd_card_get_name", c))
|
||||
}
|
||||
pub fn get_longname(&self) -> Result<String> {
|
||||
let mut c: *mut c_char = ::std::ptr::null_mut();
|
||||
acheck!(snd_card_get_longname(self.0, &mut c))
|
||||
.and_then(|_| from_alloc("snd_card_get_longname", c))
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> c_int { self.0 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_cards() {
|
||||
for a in Iter::new().map(|a| a.unwrap()) {
|
||||
println!("Card #{}: {} ({})", a.get_index(), a.get_name().unwrap(), a.get_longname().unwrap())
|
||||
}
|
||||
}
|
148
alsa/src/chmap.rs
Normal file
148
alsa/src/chmap.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use crate::alsa;
|
||||
use std::{fmt, mem, slice};
|
||||
use super::error::*;
|
||||
|
||||
alsa_enum!(
|
||||
/// [SND_CHMAP_TYPE_xxx](http://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html) constants
|
||||
ChmapType, ALL_CHMAP_TYPES[4],
|
||||
|
||||
None = SND_CHMAP_TYPE_NONE,
|
||||
Fixed = SND_CHMAP_TYPE_FIXED,
|
||||
Var = SND_CHMAP_TYPE_VAR,
|
||||
Paired = SND_CHMAP_TYPE_PAIRED,
|
||||
);
|
||||
|
||||
alsa_enum!(
|
||||
/// [SND_CHMAP_xxx](http://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html) constants
|
||||
ChmapPosition, ALL_CHMAP_POSITIONS[33],
|
||||
|
||||
Unknown = SND_CHMAP_UNKNOWN,
|
||||
NA = SND_CHMAP_NA,
|
||||
Mono = SND_CHMAP_MONO,
|
||||
FL = SND_CHMAP_FL,
|
||||
FR = SND_CHMAP_FR,
|
||||
RL = SND_CHMAP_RL,
|
||||
SR = SND_CHMAP_SR,
|
||||
RC = SND_CHMAP_RC,
|
||||
FLC = SND_CHMAP_FLC,
|
||||
FRC = SND_CHMAP_FRC,
|
||||
RLC = SND_CHMAP_RLC,
|
||||
RRC = SND_CHMAP_RRC,
|
||||
FLW = SND_CHMAP_FLW,
|
||||
FRW = SND_CHMAP_FRW,
|
||||
FLH = SND_CHMAP_FLH,
|
||||
FCH = SND_CHMAP_FCH,
|
||||
FRH = SND_CHMAP_FRH,
|
||||
TC = SND_CHMAP_TC,
|
||||
TFL = SND_CHMAP_TFL,
|
||||
TFR = SND_CHMAP_TFR,
|
||||
TFC = SND_CHMAP_TFC,
|
||||
TRL = SND_CHMAP_TRL,
|
||||
TRR = SND_CHMAP_TRR,
|
||||
TRC = SND_CHMAP_TRC,
|
||||
TFLC = SND_CHMAP_TFLC,
|
||||
TFRC = SND_CHMAP_TFRC,
|
||||
TSL = SND_CHMAP_TSL,
|
||||
TSR = SND_CHMAP_TSR,
|
||||
LLFE = SND_CHMAP_LLFE,
|
||||
RLFE = SND_CHMAP_RLFE,
|
||||
BC = SND_CHMAP_BC,
|
||||
BLC = SND_CHMAP_BLC,
|
||||
BRC = SND_CHMAP_BRC,
|
||||
);
|
||||
|
||||
impl fmt::Display for ChmapPosition {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = unsafe { alsa::snd_pcm_chmap_long_name(*self as libc::c_uint) };
|
||||
let s = from_const("snd_pcm_chmap_long_name", s)?;
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [snd_pcm_chmap_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html) wrapper
|
||||
pub struct Chmap(*mut alsa::snd_pcm_chmap_t, bool);
|
||||
|
||||
impl Drop for Chmap {
|
||||
fn drop(&mut self) { if self.1 { unsafe { libc::free(self.0 as *mut libc::c_void) }}}
|
||||
}
|
||||
|
||||
impl Chmap {
|
||||
fn set_channels(&mut self, c: libc::c_uint) { unsafe { (*self.0) .channels = c }}
|
||||
fn as_slice_mut(&mut self) -> &mut [libc::c_uint] {
|
||||
unsafe { slice::from_raw_parts_mut((*self.0).pos.as_mut_ptr(), (*self.0).channels as usize) }
|
||||
}
|
||||
fn as_slice(&self) -> &[libc::c_uint] {
|
||||
unsafe { slice::from_raw_parts((*self.0).pos.as_ptr(), (*self.0).channels as usize) }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Chmap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut buf: Vec<libc::c_char> = vec![0; 512];
|
||||
acheck!(snd_pcm_chmap_print(self.0, buf.len() as libc::size_t, buf.as_mut_ptr()))?;
|
||||
let s = from_const("snd_pcm_chmap_print", buf.as_mut_ptr())?;
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [ChmapPosition]> for Chmap {
|
||||
fn from(a: &'a [ChmapPosition]) -> Chmap {
|
||||
let p = unsafe { libc::malloc((mem::size_of::<alsa::snd_pcm_chmap_t>() + mem::size_of::<libc::c_uint>() * a.len()) as libc::size_t) };
|
||||
if p.is_null() { panic!("Out of memory") }
|
||||
let mut r = Chmap(p as *mut alsa::snd_pcm_chmap_t, true);
|
||||
r.set_channels(a.len() as libc::c_uint);
|
||||
for (i,v) in r.as_slice_mut().iter_mut().enumerate() { *v = a[i] as libc::c_uint }
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Chmap> for Vec<ChmapPosition> {
|
||||
fn from(a: &'a Chmap) -> Vec<ChmapPosition> {
|
||||
a.as_slice().iter().map(|&v| ChmapPosition::from_c_int(v as libc::c_int, "").unwrap()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chmap_new(a: *mut alsa::snd_pcm_chmap_t) -> Chmap { Chmap(a, true) }
|
||||
pub fn chmap_handle(a: &Chmap) -> *mut alsa::snd_pcm_chmap_t { a.0 }
|
||||
|
||||
|
||||
/// Iterator over available channel maps - see [snd_pcm_chmap_query_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html)
|
||||
pub struct ChmapsQuery(*mut *mut alsa::snd_pcm_chmap_query_t, isize);
|
||||
|
||||
impl Drop for ChmapsQuery {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_pcm_free_chmaps(self.0) }}
|
||||
}
|
||||
|
||||
pub fn chmaps_query_new(a: *mut *mut alsa::snd_pcm_chmap_query_t) -> ChmapsQuery { ChmapsQuery(a, 0) }
|
||||
|
||||
impl Iterator for ChmapsQuery {
|
||||
type Item = (ChmapType, Chmap);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.0.is_null() { return None; }
|
||||
let p = unsafe { *self.0.offset(self.1) };
|
||||
if p.is_null() { return None; }
|
||||
self.1 += 1;
|
||||
let t = ChmapType::from_c_int(unsafe { (*p).type_ } as libc::c_int, "snd_pcm_query_chmaps").unwrap();
|
||||
let m = Chmap(unsafe { &mut (*p).map }, false);
|
||||
Some((t, m))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn chmap_for_first_pcm() {
|
||||
use super::*;
|
||||
use std::ffi::CString;
|
||||
use crate::device_name::HintIter;
|
||||
let i = HintIter::new(None, &*CString::new("pcm").unwrap()).unwrap();
|
||||
for p in i.map(|n| n.name.unwrap()) {
|
||||
println!("Chmaps for {:?}:", p);
|
||||
match PCM::open(&CString::new(p).unwrap(), Direction::Playback, false) {
|
||||
Ok(a) => for c in a.query_chmaps() {
|
||||
println!(" {:?}, {}", c.0, c.1);
|
||||
},
|
||||
Err(a) => println!(" {}", a) // It's okay to have entries in the name hint array that can't be opened
|
||||
}
|
||||
}
|
||||
}
|
455
alsa/src/ctl_int.rs
Normal file
455
alsa/src/ctl_int.rs
Normal file
|
@ -0,0 +1,455 @@
|
|||
|
||||
use crate::alsa;
|
||||
use std::ffi::{CStr, CString};
|
||||
use super::error::*;
|
||||
use super::mixer::MilliBel;
|
||||
use super::Round;
|
||||
use std::{ptr, mem, fmt, cmp};
|
||||
use crate::{Card, poll};
|
||||
use std::cell::UnsafeCell;
|
||||
use libc::{c_uint, c_void, size_t, c_long, c_int, pollfd, c_short};
|
||||
|
||||
/// We prefer not to allocate for every ElemId, ElemInfo or ElemValue.
|
||||
/// But we don't know if these will increase in the future or on other platforms.
|
||||
/// Unfortunately, Rust does not support alloca, so hard-code the sizes for now.
|
||||
|
||||
const ELEM_ID_SIZE: usize = 64;
|
||||
// const ELEM_VALUE_SIZE: usize = 1224;
|
||||
// const ELEM_INFO_SIZE: usize = 272;
|
||||
|
||||
/// [snd_ctl_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct Ctl(*mut alsa::snd_ctl_t);
|
||||
|
||||
unsafe impl Send for Ctl {}
|
||||
|
||||
impl Ctl {
|
||||
/// Wrapper around open that takes a &str instead of a &CStr
|
||||
pub fn new(c: &str, nonblock: bool) -> Result<Self> {
|
||||
Self::open(&CString::new(c).unwrap(), nonblock)
|
||||
}
|
||||
|
||||
/// Open does not support async mode (it's not very Rustic anyway)
|
||||
pub fn open(c: &CStr, nonblock: bool) -> Result<Ctl> {
|
||||
let mut r = ptr::null_mut();
|
||||
let flags = if nonblock { 1 } else { 0 }; // FIXME: alsa::SND_CTL_NONBLOCK does not exist in alsa-sys
|
||||
acheck!(snd_ctl_open(&mut r, c.as_ptr(), flags)).map(|_| Ctl(r))
|
||||
}
|
||||
|
||||
pub fn from_card(c: &Card, nonblock: bool) -> Result<Ctl> {
|
||||
let s = format!("hw:{}", c.get_index());
|
||||
Ctl::open(&CString::new(s).unwrap(), nonblock)
|
||||
}
|
||||
|
||||
pub fn card_info(&self) -> Result<CardInfo> { CardInfo::new().and_then(|c|
|
||||
acheck!(snd_ctl_card_info(self.0, c.0)).map(|_| c)) }
|
||||
|
||||
pub fn wait(&self, timeout_ms: Option<u32>) -> Result<bool> {
|
||||
acheck!(snd_ctl_wait(self.0, timeout_ms.map(|x| x as c_int).unwrap_or(-1))).map(|i| i == 1) }
|
||||
|
||||
pub fn get_db_range(&self, id: &ElemId) -> Result<(MilliBel, MilliBel)> {
|
||||
let mut min: c_long = 0;
|
||||
let mut max: c_long = 0;
|
||||
acheck!(snd_ctl_get_dB_range(self.0, elem_id_ptr(id), &mut min, &mut max))
|
||||
.map(|_| (MilliBel(min as i64), MilliBel(max as i64)))
|
||||
}
|
||||
|
||||
pub fn convert_to_db(&self, id: &ElemId, volume: i64) -> Result<MilliBel> {
|
||||
let mut m: c_long = 0;
|
||||
acheck!(snd_ctl_convert_to_dB(self.0, elem_id_ptr(id), volume as c_long, &mut m))
|
||||
.map(|_| (MilliBel(m as i64)))
|
||||
}
|
||||
|
||||
pub fn convert_from_db(&self, id: &ElemId, mb: MilliBel, dir: Round) -> Result<i64> {
|
||||
let mut m: c_long = 0;
|
||||
acheck!(snd_ctl_convert_from_dB(self.0, elem_id_ptr(id), mb.0 as c_long, &mut m, dir as c_int))
|
||||
.map(|_| m as i64)
|
||||
}
|
||||
|
||||
pub fn elem_read(&self, val: &mut ElemValue) -> Result<()> {
|
||||
acheck!(snd_ctl_elem_read(self.0, elem_value_ptr(val))).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn elem_write(&self, val: &ElemValue) -> Result<()> {
|
||||
acheck!(snd_ctl_elem_write(self.0, elem_value_ptr(val))).map(|_| ())
|
||||
}
|
||||
|
||||
/// Note: According to alsa-lib documentation, you're also supposed to have functionality for
|
||||
/// returning whether or not you are subscribed. This does not work in practice, so I'm not
|
||||
/// including that here.
|
||||
pub fn subscribe_events(&self, subscribe: bool) -> Result<()> {
|
||||
acheck!(snd_ctl_subscribe_events(self.0, if subscribe { 1 } else { 0 })).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Result<Option<Event>> {
|
||||
let e = event_new()?;
|
||||
acheck!(snd_ctl_read(self.0, e.0)).map(|r| if r == 1 { Some(e) } else { None })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ctl {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_ctl_close(self.0) }; }
|
||||
}
|
||||
|
||||
impl poll::Descriptors for Ctl {
|
||||
fn count(&self) -> usize {
|
||||
unsafe { alsa::snd_ctl_poll_descriptors_count(self.0) as usize }
|
||||
}
|
||||
fn fill(&self, p: &mut [pollfd]) -> Result<usize> {
|
||||
let z = unsafe { alsa::snd_ctl_poll_descriptors(self.0, p.as_mut_ptr(), p.len() as c_uint) };
|
||||
from_code("snd_ctl_poll_descriptors", z).map(|_| z as usize)
|
||||
}
|
||||
fn revents(&self, p: &[pollfd]) -> Result<poll::Flags> {
|
||||
let mut r = 0;
|
||||
let z = unsafe { alsa::snd_ctl_poll_descriptors_revents(self.0, p.as_ptr() as *mut pollfd, p.len() as c_uint, &mut r) };
|
||||
from_code("snd_ctl_poll_descriptors_revents", z).map(|_| poll::Flags::from_bits_truncate(r as c_short))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn ctl_ptr(a: &Ctl) -> *mut alsa::snd_ctl_t { a.0 }
|
||||
|
||||
/// [snd_ctl_card_info_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct CardInfo(*mut alsa::snd_ctl_card_info_t);
|
||||
|
||||
impl Drop for CardInfo {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_ctl_card_info_free(self.0) }}
|
||||
}
|
||||
|
||||
impl CardInfo {
|
||||
fn new() -> Result<CardInfo> {
|
||||
let mut p = ptr::null_mut();
|
||||
acheck!(snd_ctl_card_info_malloc(&mut p)).map(|_| CardInfo(p))
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> Result<&str> {
|
||||
from_const("snd_ctl_card_info_get_id", unsafe { alsa::snd_ctl_card_info_get_id(self.0) })}
|
||||
pub fn get_driver(&self) -> Result<&str> {
|
||||
from_const("snd_ctl_card_info_get_driver", unsafe { alsa::snd_ctl_card_info_get_driver(self.0) })}
|
||||
pub fn get_components(&self) -> Result<&str> {
|
||||
from_const("snd_ctl_card_info_get_components", unsafe { alsa::snd_ctl_card_info_get_components(self.0) })}
|
||||
pub fn get_longname(&self) -> Result<&str> {
|
||||
from_const("snd_ctl_card_info_get_longname", unsafe { alsa::snd_ctl_card_info_get_longname(self.0) })}
|
||||
pub fn get_name(&self) -> Result<&str> {
|
||||
from_const("snd_ctl_card_info_get_name", unsafe { alsa::snd_ctl_card_info_get_name(self.0) })}
|
||||
pub fn get_mixername(&self) -> Result<&str> {
|
||||
from_const("snd_ctl_card_info_get_mixername", unsafe { alsa::snd_ctl_card_info_get_mixername(self.0) })}
|
||||
pub fn get_card(&self) -> Card { Card::new(unsafe { alsa::snd_ctl_card_info_get_card(self.0) })}
|
||||
}
|
||||
|
||||
alsa_enum!(
|
||||
/// [SND_CTL_ELEM_IFACE_xxx](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) constants
|
||||
ElemIface, ALL_ELEMIFACE[7],
|
||||
|
||||
Card = SND_CTL_ELEM_IFACE_CARD,
|
||||
Hwdep = SND_CTL_ELEM_IFACE_HWDEP,
|
||||
Mixer = SND_CTL_ELEM_IFACE_MIXER,
|
||||
PCM = SND_CTL_ELEM_IFACE_PCM,
|
||||
Rawmidi = SND_CTL_ELEM_IFACE_RAWMIDI,
|
||||
Timer = SND_CTL_ELEM_IFACE_TIMER,
|
||||
Sequencer = SND_CTL_ELEM_IFACE_SEQUENCER,
|
||||
);
|
||||
|
||||
alsa_enum!(
|
||||
/// [SND_CTL_ELEM_TYPE_xxx](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) constants
|
||||
ElemType, ALL_ELEMTYPE[7],
|
||||
|
||||
None = SND_CTL_ELEM_TYPE_NONE,
|
||||
Boolean = SND_CTL_ELEM_TYPE_BOOLEAN,
|
||||
Integer = SND_CTL_ELEM_TYPE_INTEGER,
|
||||
Enumerated = SND_CTL_ELEM_TYPE_ENUMERATED,
|
||||
Bytes = SND_CTL_ELEM_TYPE_BYTES,
|
||||
IEC958 = SND_CTL_ELEM_TYPE_IEC958,
|
||||
Integer64 = SND_CTL_ELEM_TYPE_INTEGER64,
|
||||
);
|
||||
|
||||
/// [snd_ctl_elem_value_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct ElemValue {
|
||||
ptr: *mut alsa::snd_ctl_elem_value_t,
|
||||
etype: ElemType,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
impl Drop for ElemValue {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_ctl_elem_value_free(self.ptr) }; }
|
||||
}
|
||||
|
||||
pub fn elem_value_ptr(a: &ElemValue) -> *mut alsa::snd_ctl_elem_value_t { a.ptr }
|
||||
|
||||
pub fn elem_value_new(t: ElemType, count: u32) -> Result<ElemValue> {
|
||||
let mut p = ptr::null_mut();
|
||||
acheck!(snd_ctl_elem_value_malloc(&mut p))
|
||||
.map(|_| ElemValue { ptr: p, etype: t, count })
|
||||
}
|
||||
|
||||
impl ElemValue {
|
||||
|
||||
pub fn set_id(&mut self, id: &ElemId) {
|
||||
unsafe { alsa::snd_ctl_elem_value_set_id(self.ptr, elem_id_ptr(id)) }
|
||||
}
|
||||
|
||||
// Note: The get_bytes hands out a reference to inside the object. Therefore, we can't treat
|
||||
// the content as "cell"ed, but must take a "&mut self" (to make sure the reference
|
||||
// from get_bytes has been dropped when calling a set_* function).
|
||||
|
||||
pub fn get_boolean(&self, idx: u32) -> Option<bool> {
|
||||
if self.etype != ElemType::Boolean || idx >= self.count { None }
|
||||
else { Some( unsafe { alsa::snd_ctl_elem_value_get_boolean(self.ptr, idx as c_uint) } != 0) }
|
||||
}
|
||||
|
||||
pub fn set_boolean(&mut self, idx: u32, val: bool) -> Option<()> {
|
||||
if self.etype != ElemType::Boolean || idx >= self.count { None }
|
||||
else { unsafe { alsa::snd_ctl_elem_value_set_boolean(self.ptr, idx as c_uint, if val {1} else {0}) }; Some(()) }
|
||||
}
|
||||
|
||||
pub fn get_integer(&self, idx: u32) -> Option<i32> {
|
||||
if self.etype != ElemType::Integer || idx >= self.count { None }
|
||||
else { Some( unsafe { alsa::snd_ctl_elem_value_get_integer(self.ptr, idx as c_uint) } as i32) }
|
||||
}
|
||||
|
||||
pub fn set_integer(&mut self, idx: u32, val: i32) -> Option<()> {
|
||||
if self.etype != ElemType::Integer || idx >= self.count { None }
|
||||
else { unsafe { alsa::snd_ctl_elem_value_set_integer(self.ptr, idx as c_uint, val as c_long) }; Some(()) }
|
||||
}
|
||||
|
||||
pub fn get_integer64(&self, idx: u32) -> Option<i64> {
|
||||
if self.etype != ElemType::Integer64 || idx >= self.count { None }
|
||||
else { Some( unsafe { alsa::snd_ctl_elem_value_get_integer64(self.ptr, idx as c_uint) } as i64) }
|
||||
}
|
||||
|
||||
pub fn set_integer64(&mut self, idx: u32, val: i64) -> Option<()> {
|
||||
if self.etype != ElemType::Integer || idx >= self.count { None }
|
||||
else { unsafe { alsa::snd_ctl_elem_value_set_integer64(self.ptr, idx as c_uint, val) }; Some(()) }
|
||||
}
|
||||
|
||||
pub fn get_enumerated(&self, idx: u32) -> Option<u32> {
|
||||
if self.etype != ElemType::Enumerated || idx >= self.count { None }
|
||||
else { Some( unsafe { alsa::snd_ctl_elem_value_get_enumerated(self.ptr, idx as c_uint) } as u32) }
|
||||
}
|
||||
|
||||
pub fn set_enumerated(&mut self, idx: u32, val: u32) -> Option<()> {
|
||||
if self.etype != ElemType::Enumerated || idx >= self.count { None }
|
||||
else { unsafe { alsa::snd_ctl_elem_value_set_enumerated(self.ptr, idx as c_uint, val as c_uint) }; Some(()) }
|
||||
}
|
||||
|
||||
pub fn get_byte(&self, idx: u32) -> Option<u8> {
|
||||
if self.etype != ElemType::Bytes || idx >= self.count { None }
|
||||
else { Some( unsafe { alsa::snd_ctl_elem_value_get_byte(self.ptr, idx as c_uint) } as u8) }
|
||||
}
|
||||
|
||||
pub fn set_byte(&mut self, idx: u32, val: u8) -> Option<()> {
|
||||
if self.etype != ElemType::Bytes || idx >= self.count { None }
|
||||
else { unsafe { alsa::snd_ctl_elem_value_set_byte(self.ptr, idx as c_uint, val) }; Some(()) }
|
||||
}
|
||||
|
||||
pub fn get_bytes(&self) -> Option<&[u8]> {
|
||||
if self.etype != ElemType::Bytes { None }
|
||||
else { Some( unsafe { ::std::slice::from_raw_parts(
|
||||
alsa::snd_ctl_elem_value_get_bytes(self.ptr) as *const u8, self.count as usize) } ) }
|
||||
}
|
||||
|
||||
pub fn set_bytes(&mut self, val: &[u8]) -> Option<()> {
|
||||
if self.etype != ElemType::Bytes || val.len() != self.count as usize { None }
|
||||
|
||||
// Note: the alsa-lib function definition is broken. First, the pointer is declared as mut even
|
||||
// though it's const, and second, there is a "value" missing between "elem" and "set_bytes".
|
||||
else { unsafe { alsa::snd_ctl_elem_set_bytes(self.ptr, val.as_ptr() as *mut c_void, val.len() as size_t) }; Some(()) }
|
||||
}
|
||||
|
||||
/// Creates a new ElemValue.
|
||||
pub fn new(t: ElemType) -> Result<ElemValue> {
|
||||
// See max length in include/uapi/sound/asound.h in linux kernel for these values
|
||||
let count = match t {
|
||||
ElemType::None => 1,
|
||||
ElemType::Boolean => 128,
|
||||
ElemType::Integer => 128,
|
||||
ElemType::Enumerated => 128,
|
||||
ElemType::Bytes => 512,
|
||||
ElemType::IEC958 => 1,
|
||||
ElemType::Integer64 => 64,
|
||||
};
|
||||
// if count > maxcount { return Err(Error::new(Some("ElemValue::new - count too large".into()), 1)) }
|
||||
let ev = elem_value_new(t, count)?;
|
||||
unsafe { alsa::snd_ctl_elem_value_clear(elem_value_ptr(&ev)) };
|
||||
Ok(ev)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl fmt::Debug for ElemValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::ElemType::*;
|
||||
write!(f, "ElemValue({:?}", self.etype)?;
|
||||
for a in 0..self.count { match self.etype {
|
||||
Boolean => write!(f, ",{:?}", self.get_boolean(a).unwrap()),
|
||||
Integer => write!(f, ",{:?}", self.get_integer(a).unwrap()),
|
||||
Integer64 => write!(f, ",{:?}", self.get_integer64(a).unwrap()),
|
||||
Enumerated => write!(f, ",{:?}", self.get_enumerated(a).unwrap()),
|
||||
Bytes => write!(f, ",{:?}", self.get_byte(a).unwrap()),
|
||||
_ => Ok(()),
|
||||
}?};
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
/// [snd_ctl_elem_info_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct ElemInfo(*mut alsa::snd_ctl_elem_info_t);
|
||||
|
||||
pub fn elem_info_ptr(a: &ElemInfo) -> *mut alsa::snd_ctl_elem_info_t { a.0 }
|
||||
|
||||
impl Drop for ElemInfo {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_ctl_elem_info_free(self.0) }; }
|
||||
}
|
||||
|
||||
pub fn elem_info_new() -> Result<ElemInfo> {
|
||||
let mut p = ptr::null_mut();
|
||||
acheck!(snd_ctl_elem_info_malloc(&mut p)).map(|_| ElemInfo(p))
|
||||
}
|
||||
|
||||
impl ElemInfo {
|
||||
pub fn get_type(&self) -> ElemType { ElemType::from_c_int(
|
||||
unsafe { alsa::snd_ctl_elem_info_get_type(self.0) } as c_int, "snd_ctl_elem_info_get_type").unwrap() }
|
||||
pub fn get_count(&self) -> u32 { unsafe { alsa::snd_ctl_elem_info_get_count(self.0) as u32 } }
|
||||
}
|
||||
|
||||
//
|
||||
// Non-allocating version of ElemId
|
||||
//
|
||||
|
||||
/// [snd_ctl_elem_id_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct ElemId(UnsafeCell<[u8; ELEM_ID_SIZE]>);
|
||||
|
||||
pub fn elem_id_new() -> Result<ElemId> {
|
||||
assert!(unsafe { alsa::snd_ctl_elem_id_sizeof() } as usize <= ELEM_ID_SIZE);
|
||||
Ok(ElemId(UnsafeCell::new(unsafe { mem::zeroed() })))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn elem_id_ptr(a: &ElemId) -> *mut alsa::snd_ctl_elem_id_t { a.0.get() as *mut _ as *mut alsa::snd_ctl_elem_id_t }
|
||||
|
||||
unsafe impl Send for ElemId {}
|
||||
|
||||
impl Clone for ElemId {
|
||||
fn clone(&self) -> Self {
|
||||
ElemId(UnsafeCell::new(unsafe { *self.0.get() }))
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Allocating version of ElemId
|
||||
//
|
||||
|
||||
/*
|
||||
|
||||
/// [snd_ctl_elem_id_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct ElemId(*mut alsa::snd_ctl_elem_id_t);
|
||||
|
||||
impl Drop for ElemId {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_ctl_elem_id_free(self.0) }; }
|
||||
}
|
||||
|
||||
pub fn elem_id_new() -> Result<ElemId> {
|
||||
let mut p = ptr::null_mut();
|
||||
acheck!(snd_ctl_elem_id_malloc(&mut p)).map(|_| ElemId(p))
|
||||
}
|
||||
|
||||
pub fn elem_id_ptr(a: &ElemId) -> *mut alsa::snd_ctl_elem_id_t { a.0 }
|
||||
|
||||
*/
|
||||
|
||||
impl ElemId {
|
||||
pub fn get_name(&self) -> Result<&str> {
|
||||
from_const("snd_hctl_elem_id_get_name", unsafe { alsa::snd_ctl_elem_id_get_name(elem_id_ptr(self)) })}
|
||||
pub fn get_device(&self) -> u32 { unsafe { alsa::snd_ctl_elem_id_get_device(elem_id_ptr(self)) as u32 }}
|
||||
pub fn get_subdevice(&self) -> u32 { unsafe { alsa::snd_ctl_elem_id_get_subdevice(elem_id_ptr(self)) as u32 }}
|
||||
pub fn get_numid(&self) -> u32 { unsafe { alsa::snd_ctl_elem_id_get_numid(elem_id_ptr(self)) as u32 }}
|
||||
pub fn get_index(&self) -> u32 { unsafe { alsa::snd_ctl_elem_id_get_index(elem_id_ptr(self)) as u32 }}
|
||||
pub fn get_interface(&self) -> ElemIface { ElemIface::from_c_int(
|
||||
unsafe { alsa::snd_ctl_elem_id_get_interface(elem_id_ptr(self)) } as c_int, "snd_ctl_elem_id_get_interface").unwrap() }
|
||||
|
||||
pub fn set_device(&mut self, v: u32) { unsafe { alsa::snd_ctl_elem_id_set_device(elem_id_ptr(self), v) }}
|
||||
pub fn set_subdevice(&mut self, v: u32) { unsafe { alsa::snd_ctl_elem_id_set_subdevice(elem_id_ptr(self), v) }}
|
||||
pub fn set_numid(&mut self, v: u32) { unsafe { alsa::snd_ctl_elem_id_set_numid(elem_id_ptr(self), v) }}
|
||||
pub fn set_index(&mut self, v: u32) { unsafe { alsa::snd_ctl_elem_id_set_index(elem_id_ptr(self), v) }}
|
||||
pub fn set_interface(&mut self, v: ElemIface) { unsafe { alsa::snd_ctl_elem_id_set_interface(elem_id_ptr(self), v as u32) }}
|
||||
pub fn set_name(&mut self, v: &CStr) { unsafe { alsa::snd_ctl_elem_id_set_name(elem_id_ptr(self), v.as_ptr()) }}
|
||||
|
||||
/// Creates a new ElemId.
|
||||
///
|
||||
/// To ensure safety (i e make sure we never have an invalid interface enum), we need to supply it to the "new" function.
|
||||
pub fn new(iface: ElemIface) -> Self {
|
||||
let mut r = elem_id_new().unwrap();
|
||||
r.set_interface(iface);
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::Eq for ElemId {}
|
||||
|
||||
impl cmp::PartialEq for ElemId {
|
||||
fn eq(&self, a: &ElemId) -> bool {
|
||||
self.get_numid() == a.get_numid() && self.get_interface() == a.get_interface() &&
|
||||
self.get_index() == a.get_index() && self.get_device() == a.get_device() &&
|
||||
self.get_subdevice() == a.get_subdevice() && self.get_name().ok() == a.get_name().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ElemId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let index = self.get_index();
|
||||
let device = self.get_device();
|
||||
let subdevice = self.get_subdevice();
|
||||
|
||||
write!(f, "ElemId(#{}, {:?}, {:?}", self.get_numid(), self.get_interface(), self.get_name())?;
|
||||
if index > 0 { write!(f, ", index={}", index)? };
|
||||
if device > 0 || subdevice > 0 { write!(f, ", device={}", device)? };
|
||||
if subdevice > 0 { write!(f, ", subdevice={}", device)? };
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
/// [snd_ctl_event_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct Event(*mut alsa::snd_ctl_event_t);
|
||||
|
||||
impl Drop for Event {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_ctl_event_free(self.0) }; }
|
||||
}
|
||||
|
||||
pub fn event_new() -> Result<Event> {
|
||||
let mut p = ptr::null_mut();
|
||||
acheck!(snd_ctl_event_malloc(&mut p)).map(|_| Event(p))
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn get_mask(&self) -> EventMask { EventMask(unsafe { alsa::snd_ctl_event_elem_get_mask(self.0) as u32 })}
|
||||
pub fn get_id(&self) -> ElemId {
|
||||
let r = elem_id_new().unwrap();
|
||||
unsafe { alsa::snd_ctl_event_elem_get_id(self.0, elem_id_ptr(&r)) };
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [SND_CTL_EVENT_MASK_XXX](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) bitmask
|
||||
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct EventMask(pub u32);
|
||||
|
||||
impl EventMask {
|
||||
pub fn remove(&self) -> bool { return self.0 & 0xffffffff == 0xffffffff }
|
||||
pub fn value(&self) -> bool { return (!self.remove()) && (self.0 & (1 << 0) != 0); }
|
||||
pub fn info(&self) -> bool { return (!self.remove()) && (self.0 & (1 << 1) != 0); }
|
||||
pub fn add(&self) -> bool { return (!self.remove()) && (self.0 & (1 << 2) != 0); }
|
||||
pub fn tlv(&self) -> bool { return (!self.remove()) && (self.0 & (1 << 3) != 0); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_sizeof() {
|
||||
let elemid = unsafe { alsa::snd_ctl_elem_id_sizeof() } as usize;
|
||||
let elemvalue = unsafe { alsa::snd_ctl_elem_value_sizeof() } as usize;
|
||||
let eleminfo = unsafe { alsa::snd_ctl_elem_info_sizeof() } as usize;
|
||||
|
||||
assert!(elemid <= ELEM_ID_SIZE);
|
||||
// assert!(elemvalue <= ELEM_VALUE_SIZE);
|
||||
// assert!(eleminfo <= ELEM_INFO_SIZE);
|
||||
|
||||
println!("Elem id: {}, Elem value: {}, Elem info: {}", elemid, elemvalue, eleminfo);
|
||||
}
|
90
alsa/src/device_name.rs
Normal file
90
alsa/src/device_name.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
//! Enumerate devices in the alsa library configuration
|
||||
//!
|
||||
//! # Example
|
||||
//! Print all devices found in various categories.
|
||||
//!
|
||||
//! ```
|
||||
//! use std::ffi::CString;
|
||||
//! use alsa::device_name::HintIter;
|
||||
//!
|
||||
//! for t in &["pcm", "ctl", "rawmidi", "timer", "seq", "hwdep"] {
|
||||
//! println!("{} devices:", t);
|
||||
//! let i = HintIter::new(None, &*CString::new(*t).unwrap()).unwrap();
|
||||
//! for a in i { println!(" {:?}", a) }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::ptr;
|
||||
use libc::{c_void, c_int};
|
||||
use crate::alsa;
|
||||
use super::{Card, Direction};
|
||||
use super::error::*;
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
/// [snd_device_name_hint](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
pub struct HintIter(*mut *mut c_void, isize);
|
||||
|
||||
impl Drop for HintIter {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_device_name_free_hint(self.0); }}
|
||||
}
|
||||
|
||||
impl HintIter {
|
||||
/// typical interfaces are: "pcm", "ctl", "rawmidi", "timer", "seq" and "hwdep".
|
||||
pub fn new(card: Option<&Card>, iface: &CStr) -> Result<HintIter> {
|
||||
let mut p = ptr::null_mut();
|
||||
let cnr = card.map(|c| c.get_index()).unwrap_or(-1) as c_int;
|
||||
acheck!(snd_device_name_hint(cnr, iface.as_ptr(), &mut p))
|
||||
.map(|_| HintIter(p, 0))
|
||||
}
|
||||
|
||||
/// A constructor variant that takes the interface as a Rust string slice.
|
||||
pub fn new_str(card: Option<&Card>, iface: &str) -> Result<HintIter> {
|
||||
HintIter::new(card, &CString::new(iface).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for HintIter {
|
||||
type Item = Hint;
|
||||
fn next(&mut self) -> Option<Hint> {
|
||||
if self.0.is_null() { return None; }
|
||||
let p = unsafe { *self.0.offset(self.1) };
|
||||
if p.is_null() { return None; }
|
||||
self.1 += 1;
|
||||
Some(Hint::new(p))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [snd_device_name_get_hint](http://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html) wrapper
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Hint {
|
||||
pub name: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub direction: Option<Direction>,
|
||||
}
|
||||
|
||||
impl Hint {
|
||||
fn get_str(p: *const c_void, name: &str) -> Option<String> {
|
||||
let name = CString::new(name).unwrap();
|
||||
let c = unsafe { alsa::snd_device_name_get_hint(p, name.as_ptr()) };
|
||||
from_alloc("snd_device_name_get_hint", c).ok()
|
||||
}
|
||||
|
||||
fn new(p: *const c_void) -> Hint {
|
||||
let d = Hint::get_str(p, "IOID").and_then(|x| match &*x {
|
||||
"Input" => Some(Direction::Capture),
|
||||
"Output" => Some(Direction::Playback),
|
||||
_ => None,
|
||||
});
|
||||
Hint { name: Hint::get_str(p, "NAME"), desc: Hint::get_str(p, "DESC"), direction: d }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_hints() {
|
||||
for t in &["pcm", "ctl", "rawmidi", "timer", "seq", "hwdep"] {
|
||||
println!("{} devices:", t);
|
||||
let i = HintIter::new(None, &*CString::new(*t).unwrap()).unwrap();
|
||||
for a in i { println!(" {:?}", a) }
|
||||
}
|
||||
}
|
5
alsa/src/direct.rs
Normal file
5
alsa/src/direct.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
//! Functions that bypass alsa-lib and talk directly to the kernel.
|
||||
|
||||
pub mod pcm;
|
||||
|
||||
mod ffi;
|
6713
alsa/src/direct/asound_ioctl.rs
Normal file
6713
alsa/src/direct/asound_ioctl.rs
Normal file
File diff suppressed because it is too large
Load diff
79
alsa/src/direct/ffi.rs
Normal file
79
alsa/src/direct/ffi.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
|
||||
// Some definitions from the kernel headers
|
||||
|
||||
// const SNDRV_PCM_MMAP_OFFSET_DATA: c_uint = 0x00000000;
|
||||
pub const SNDRV_PCM_MMAP_OFFSET_STATUS: libc::c_uint = 0x80000000;
|
||||
pub const SNDRV_PCM_MMAP_OFFSET_CONTROL: libc::c_uint = 0x81000000;
|
||||
|
||||
|
||||
pub const SNDRV_PCM_SYNC_PTR_HWSYNC: libc::c_uint = 1;
|
||||
pub const SNDRV_PCM_SYNC_PTR_APPL: libc::c_uint = 2;
|
||||
pub const SNDRV_PCM_SYNC_PTR_AVAIL_MIN: libc::c_uint = 4;
|
||||
|
||||
// #[repr(C)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub type snd_pcm_state_t = libc::c_int;
|
||||
|
||||
// #[repr(C)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub type snd_pcm_uframes_t = libc::c_ulong;
|
||||
|
||||
// I think?! Not sure how this will work with X32 ABI?!
|
||||
#[allow(non_camel_case_types)]
|
||||
pub type __kernel_off_t = libc::c_long;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct snd_pcm_mmap_status {
|
||||
pub state: snd_pcm_state_t, /* RO: state - SNDRV_PCM_STATE_XXXX */
|
||||
pub pad1: libc::c_int, /* Needed for 64 bit alignment */
|
||||
pub hw_ptr: snd_pcm_uframes_t, /* RO: hw ptr (0...boundary-1) */
|
||||
pub tstamp: libc::timespec, /* Timestamp */
|
||||
pub suspended_state: snd_pcm_state_t, /* RO: suspended stream state */
|
||||
pub audio_tstamp: libc::timespec, /* from sample counter or wall clock */
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct snd_pcm_mmap_control {
|
||||
pub appl_ptr: snd_pcm_uframes_t, /* RW: appl ptr (0...boundary-1) */
|
||||
pub avail_min: snd_pcm_uframes_t, /* RW: min available frames for wakeup */
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub struct snd_pcm_channel_info {
|
||||
pub channel: libc::c_uint,
|
||||
pub offset: __kernel_off_t, /* mmap offset */
|
||||
pub first: libc::c_uint, /* offset to first sample in bits */
|
||||
pub step: libc::c_uint, /* samples distance in bits */
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub union snd_pcm_mmap_status_r {
|
||||
pub status: snd_pcm_mmap_status,
|
||||
pub reserved: [libc::c_uchar; 64],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub union snd_pcm_mmap_control_r {
|
||||
pub control: snd_pcm_mmap_control,
|
||||
pub reserved: [libc::c_uchar; 64],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct snd_pcm_sync_ptr {
|
||||
pub flags: libc::c_uint,
|
||||
pub s: snd_pcm_mmap_status_r,
|
||||
pub c: snd_pcm_mmap_control_r,
|
||||
}
|
||||
|
||||
ioctl_read!(sndrv_pcm_ioctl_channel_info, b'A', 0x32, snd_pcm_channel_info);
|
||||
ioctl_readwrite!(sndrv_pcm_ioctl_sync_ptr, b'A', 0x23, snd_pcm_sync_ptr);
|
||||
|
||||
pub fn pagesize() -> usize {
|
||||
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
|
||||
}
|
630
alsa/src/direct/pcm.rs
Normal file
630
alsa/src/direct/pcm.rs
Normal file
|
@ -0,0 +1,630 @@
|
|||
/*!
|
||||
This module bypasses alsa-lib and directly read and write into memory mapped kernel memory.
|
||||
In case of the sample memory, this is in many cases the DMA buffers that is transferred to the sound card.
|
||||
|
||||
The reasons for doing this are:
|
||||
|
||||
* Minimum overhead where it matters most: let alsa-lib do the code heavy setup -
|
||||
then steal its file descriptor and deal with sample streaming from Rust.
|
||||
* RT-safety to the maximum extent possible. Creating/dropping any of these structs causes syscalls,
|
||||
but function calls on these are just read and write from memory. No syscalls, no memory allocations,
|
||||
not even loops (with the exception of `MmapPlayback::write` that loops over samples to write).
|
||||
* Possibility to allow Send + Sync for structs
|
||||
* It's a fun experiment and an interesting deep dive into how alsa-lib does things.
|
||||
|
||||
Note: Not all sound card drivers support this direct method of communication; although almost all
|
||||
modern/common ones do. It only works with hardware devices though (such as "hw:xxx" device strings),
|
||||
don't expect it to work with, e g, the PulseAudio plugin or so.
|
||||
|
||||
For an example of how to use this mode, look in the "synth-example" directory.
|
||||
*/
|
||||
|
||||
use libc;
|
||||
use std::{mem, ptr, fmt, cmp};
|
||||
use crate::error::{Error, Result};
|
||||
use std::os::unix::io::RawFd;
|
||||
use crate::{pcm, PollDescriptors, Direction};
|
||||
use crate::pcm::Frames;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use super::ffi::*;
|
||||
|
||||
/// Read PCM status via a simple kernel syscall, bypassing alsa-lib.
|
||||
///
|
||||
/// If Status is not available on your architecture, this is the second best option.
|
||||
pub struct SyncPtrStatus(snd_pcm_mmap_status);
|
||||
|
||||
impl SyncPtrStatus {
|
||||
/// Executes sync_ptr syscall.
|
||||
///
|
||||
/// Unsafe because
|
||||
/// - setting appl_ptr and avail_min might make alsa-lib confused
|
||||
/// - no check that the fd is really a PCM
|
||||
pub unsafe fn sync_ptr(fd: RawFd, hwsync: bool, appl_ptr: Option<pcm::Frames>, avail_min: Option<pcm::Frames>) -> Result<Self> {
|
||||
let mut data = snd_pcm_sync_ptr {
|
||||
flags: (if hwsync { SNDRV_PCM_SYNC_PTR_HWSYNC } else { 0 }) +
|
||||
(if appl_ptr.is_some() { SNDRV_PCM_SYNC_PTR_APPL } else { 0 }) +
|
||||
(if avail_min.is_some() { SNDRV_PCM_SYNC_PTR_AVAIL_MIN } else { 0 }),
|
||||
c: snd_pcm_mmap_control_r {
|
||||
control: snd_pcm_mmap_control {
|
||||
appl_ptr: appl_ptr.unwrap_or(0) as snd_pcm_uframes_t,
|
||||
avail_min: avail_min.unwrap_or(0) as snd_pcm_uframes_t,
|
||||
}
|
||||
},
|
||||
s: mem::zeroed()
|
||||
};
|
||||
|
||||
sndrv_pcm_ioctl_sync_ptr(fd, &mut data).map_err(|_|
|
||||
Error::new("SNDRV_PCM_IOCTL_SYNC_PTR", nix::errno::Errno::last() as i32))?;
|
||||
|
||||
let i = data.s.status.state;
|
||||
if (i >= (pcm::State::Open as snd_pcm_state_t)) && (i <= (pcm::State::Disconnected as snd_pcm_state_t)) {
|
||||
Ok(SyncPtrStatus(data.s.status))
|
||||
} else {
|
||||
Err(Error::unsupported("SNDRV_PCM_IOCTL_SYNC_PTR returned broken state"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hw_ptr(&self) -> pcm::Frames { self.0.hw_ptr as pcm::Frames }
|
||||
pub fn state(&self) -> pcm::State { unsafe { mem::transmute(self.0.state as u8) } /* valid range checked in sync_ptr */ }
|
||||
pub fn htstamp(&self) -> libc::timespec { self.0.tstamp }
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Read PCM status directly from memory, bypassing alsa-lib.
|
||||
///
|
||||
/// This means that it's
|
||||
/// 1) less overhead for reading status (no syscall, no allocations, no virtual dispatch, just a read from memory)
|
||||
/// 2) Send + Sync, and
|
||||
/// 3) will only work for "hw" / "plughw" devices (not e g PulseAudio plugins), and not
|
||||
/// all of those are supported, although all common ones are (as of 2017, and a kernel from the same decade).
|
||||
/// Kernel supported archs are: x86, PowerPC, Alpha. Use "SyncPtrStatus" for other archs.
|
||||
///
|
||||
/// The values are updated every now and then by the kernel. Many functions will force an update to happen,
|
||||
/// e g `PCM::avail()` and `PCM::delay()`.
|
||||
///
|
||||
/// Note: Even if you close the original PCM device, ALSA will not actually close the device until all
|
||||
/// Status structs are dropped too.
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub struct Status(DriverMemory<snd_pcm_mmap_status>);
|
||||
|
||||
fn pcm_to_fd(p: &pcm::PCM) -> Result<RawFd> {
|
||||
let mut fds: [libc::pollfd; 1] = unsafe { mem::zeroed() };
|
||||
let c = PollDescriptors::fill(p, &mut fds)?;
|
||||
if c != 1 {
|
||||
return Err(Error::unsupported("snd_pcm_poll_descriptors returned wrong number of fds"))
|
||||
}
|
||||
Ok(fds[0].fd)
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn new(p: &pcm::PCM) -> Result<Self> { Status::from_fd(pcm_to_fd(p)?) }
|
||||
|
||||
pub fn from_fd(fd: RawFd) -> Result<Self> {
|
||||
DriverMemory::new(fd, 1, SNDRV_PCM_MMAP_OFFSET_STATUS as libc::off_t, false).map(Status)
|
||||
}
|
||||
|
||||
/// Current PCM state.
|
||||
pub fn state(&self) -> pcm::State {
|
||||
unsafe {
|
||||
let i = ptr::read_volatile(&(*self.0.ptr).state);
|
||||
assert!((i >= (pcm::State::Open as snd_pcm_state_t)) && (i <= (pcm::State::Disconnected as snd_pcm_state_t)));
|
||||
mem::transmute(i as u8)
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of frames hardware has read or written
|
||||
///
|
||||
/// This number is updated every now and then by the kernel.
|
||||
/// Calling most functions on the PCM will update it, so will usually a period interrupt.
|
||||
/// No guarantees given.
|
||||
///
|
||||
/// This value wraps at "boundary" (a large value you can read from SwParams).
|
||||
pub fn hw_ptr(&self) -> pcm::Frames {
|
||||
unsafe {
|
||||
ptr::read_volatile(&(*self.0.ptr).hw_ptr) as pcm::Frames
|
||||
}
|
||||
}
|
||||
|
||||
/// Timestamp - fast version of alsa-lib's Status::get_htstamp
|
||||
///
|
||||
/// Note: This just reads the actual value in memory.
|
||||
/// Unfortunately, the timespec is too big to be read atomically on most archs.
|
||||
/// Therefore, this function can potentially give bogus result at times, at least in theory...?
|
||||
pub fn htstamp(&self) -> libc::timespec {
|
||||
unsafe {
|
||||
ptr::read_volatile(&(*self.0.ptr).tstamp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio timestamp - fast version of alsa-lib's Status::get_audio_htstamp
|
||||
///
|
||||
/// Note: This just reads the actual value in memory.
|
||||
/// Unfortunately, the timespec is too big to be read atomically on most archs.
|
||||
/// Therefore, this function can potentially give bogus result at times, at least in theory...?
|
||||
pub fn audio_htstamp(&self) -> libc::timespec {
|
||||
unsafe {
|
||||
ptr::read_volatile(&(*self.0.ptr).audio_tstamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write PCM appl ptr directly, bypassing alsa-lib.
|
||||
///
|
||||
/// Provides direct access to appl ptr and avail min, without the overhead of
|
||||
/// alsa-lib or a syscall. Caveats that apply to Status applies to this struct too.
|
||||
#[derive(Debug)]
|
||||
pub struct Control(DriverMemory<snd_pcm_mmap_control>);
|
||||
|
||||
impl Control {
|
||||
pub fn new(p: &pcm::PCM) -> Result<Self> { Self::from_fd(pcm_to_fd(p)?) }
|
||||
|
||||
pub fn from_fd(fd: RawFd) -> Result<Self> {
|
||||
DriverMemory::new(fd, 1, SNDRV_PCM_MMAP_OFFSET_CONTROL as libc::off_t, true).map(Control)
|
||||
}
|
||||
|
||||
/// Read number of frames application has read or written
|
||||
///
|
||||
/// This value wraps at "boundary" (a large value you can read from SwParams).
|
||||
pub fn appl_ptr(&self) -> pcm::Frames {
|
||||
unsafe {
|
||||
ptr::read_volatile(&(*self.0.ptr).appl_ptr) as pcm::Frames
|
||||
}
|
||||
}
|
||||
|
||||
/// Set number of frames application has read or written
|
||||
///
|
||||
/// When the kernel wakes up due to a period interrupt, this value will
|
||||
/// be checked by the kernel. An XRUN will happen in case the application
|
||||
/// has not read or written enough data.
|
||||
pub fn set_appl_ptr(&self, value: pcm::Frames) {
|
||||
unsafe {
|
||||
ptr::write_volatile(&mut (*self.0.ptr).appl_ptr, value as snd_pcm_uframes_t)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read minimum number of frames in buffer in order to wakeup process
|
||||
pub fn avail_min(&self) -> pcm::Frames {
|
||||
unsafe {
|
||||
ptr::read_volatile(&(*self.0.ptr).avail_min) as pcm::Frames
|
||||
}
|
||||
}
|
||||
|
||||
/// Write minimum number of frames in buffer in order to wakeup process
|
||||
pub fn set_avail_min(&self, value: pcm::Frames) {
|
||||
unsafe {
|
||||
ptr::write_volatile(&mut (*self.0.ptr).avail_min, value as snd_pcm_uframes_t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DriverMemory<S> {
|
||||
ptr: *mut S,
|
||||
size: libc::size_t,
|
||||
}
|
||||
|
||||
impl<S> fmt::Debug for DriverMemory<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "DriverMemory({:?})", self.ptr) }
|
||||
}
|
||||
|
||||
impl<S> DriverMemory<S> {
|
||||
fn new(fd: RawFd, count: usize, offs: libc::off_t, writable: bool) -> Result<Self> {
|
||||
let mut total = count * mem::size_of::<S>();
|
||||
let ps = pagesize();
|
||||
assert!(total > 0);
|
||||
if total % ps != 0 { total += ps - total % ps };
|
||||
let flags = if writable { libc::PROT_WRITE | libc::PROT_READ } else { libc::PROT_READ };
|
||||
let p = unsafe { libc::mmap(ptr::null_mut(), total, flags, libc::MAP_FILE | libc::MAP_SHARED, fd, offs) };
|
||||
if p.is_null() || p == libc::MAP_FAILED {
|
||||
Err(Error::new("mmap (of driver memory)", nix::errno::Errno::last() as i32))
|
||||
} else {
|
||||
Ok(DriverMemory { ptr: p as *mut S, size: total })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<S> Send for DriverMemory<S> {}
|
||||
unsafe impl<S> Sync for DriverMemory<S> {}
|
||||
|
||||
impl<S> Drop for DriverMemory<S> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {{ libc::munmap(self.ptr as *mut libc::c_void, self.size); } }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SampleData<S> {
|
||||
mem: DriverMemory<S>,
|
||||
frames: pcm::Frames,
|
||||
channels: u32,
|
||||
}
|
||||
|
||||
impl<S> SampleData<S> {
|
||||
pub fn new(p: &pcm::PCM) -> Result<Self> {
|
||||
let params = p.hw_params_current()?;
|
||||
let bufsize = params.get_buffer_size()?;
|
||||
let channels = params.get_channels()?;
|
||||
if params.get_access()? != pcm::Access::MMapInterleaved {
|
||||
return Err(Error::unsupported("Not MMAP interleaved data"))
|
||||
}
|
||||
|
||||
let fd = pcm_to_fd(p)?;
|
||||
let info = unsafe {
|
||||
let mut info: snd_pcm_channel_info = mem::zeroed();
|
||||
sndrv_pcm_ioctl_channel_info(fd, &mut info).map_err(|_|
|
||||
Error::new("SNDRV_PCM_IOCTL_CHANNEL_INFO", nix::errno::Errno::last() as i32))?;
|
||||
info
|
||||
};
|
||||
// println!("{:?}", info);
|
||||
if (info.step != channels * mem::size_of::<S>() as u32 * 8) || (info.first != 0) {
|
||||
return Err(Error::unsupported("MMAP data size mismatch"))
|
||||
}
|
||||
Ok(SampleData {
|
||||
mem: DriverMemory::new(fd, (bufsize as usize) * (channels as usize), info.offset as libc::off_t, true)?,
|
||||
frames: bufsize,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Dummy trait for better generics
|
||||
pub trait MmapDir: fmt::Debug {
|
||||
const DIR: Direction;
|
||||
fn avail(hwptr: Frames, applptr: Frames, buffersize: Frames, boundary: Frames) -> Frames;
|
||||
}
|
||||
|
||||
/// Dummy struct for better generics
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Playback;
|
||||
|
||||
impl MmapDir for Playback {
|
||||
const DIR: Direction = Direction::Playback;
|
||||
#[inline]
|
||||
fn avail(hwptr: Frames, applptr: Frames, buffersize: Frames, boundary: Frames) -> Frames {
|
||||
let r = hwptr.wrapping_add(buffersize).wrapping_sub(applptr);
|
||||
let r = if r < 0 { r.wrapping_add(boundary) } else { r };
|
||||
if r as usize >= boundary as usize { r.wrapping_sub(boundary) } else { r }
|
||||
}
|
||||
}
|
||||
|
||||
/// Dummy struct for better generics
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Capture;
|
||||
|
||||
impl MmapDir for Capture {
|
||||
const DIR: Direction = Direction::Capture;
|
||||
#[inline]
|
||||
fn avail(hwptr: Frames, applptr: Frames, _buffersize: Frames, boundary: Frames) -> Frames {
|
||||
let r = hwptr.wrapping_sub(applptr);
|
||||
if r < 0 { r.wrapping_add(boundary) } else { r }
|
||||
}
|
||||
}
|
||||
|
||||
pub type MmapPlayback<S> = MmapIO<S, Playback>;
|
||||
|
||||
pub type MmapCapture<S> = MmapIO<S, Capture>;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Struct containing direct I/O functions shared between playback and capture.
|
||||
pub struct MmapIO<S, D> {
|
||||
data: SampleData<S>,
|
||||
c: Control,
|
||||
ss: Status,
|
||||
bound: Frames,
|
||||
dir: PhantomData<*const D>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// A raw pointer to samples, and the amount of samples readable or writable.
|
||||
pub struct RawSamples<S> {
|
||||
pub ptr: *mut S,
|
||||
pub frames: Frames,
|
||||
pub channels: u32,
|
||||
}
|
||||
|
||||
impl<S> RawSamples<S> {
|
||||
#[inline]
|
||||
/// Returns `frames` * `channels`, i e the amount of samples (of type `S`) that can be read/written.
|
||||
pub fn samples(&self) -> isize { self.frames as isize * (self.channels as isize) }
|
||||
|
||||
/// Writes samples from an iterator.
|
||||
///
|
||||
/// Returns true if iterator was depleted, and the number of samples written.
|
||||
/// This is just raw read/write of memory.
|
||||
pub unsafe fn write_samples<I: Iterator<Item=S>>(&self, i: &mut I) -> (bool, isize) {
|
||||
let mut z = 0;
|
||||
let max_samples = self.samples();
|
||||
while z < max_samples {
|
||||
let b = if let Some(b) = i.next() { b } else { return (true, z) };
|
||||
ptr::write_volatile(self.ptr.offset(z), b);
|
||||
z += 1;
|
||||
};
|
||||
(false, z)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<S, D: MmapDir> MmapIO<S, D> {
|
||||
fn new(p: &pcm::PCM) -> Result<Self> {
|
||||
if p.info()?.get_stream() != D::DIR {
|
||||
return Err(Error::unsupported("Wrong direction"));
|
||||
}
|
||||
let boundary = p.sw_params_current()?.get_boundary()?;
|
||||
Ok(MmapIO {
|
||||
data: SampleData::new(p)?,
|
||||
c: Control::new(p)?,
|
||||
ss: Status::new(p)?,
|
||||
bound: boundary,
|
||||
dir: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub (crate) fn new_mmap<S, D: MmapDir>(p: &pcm::PCM) -> Result<MmapIO<S, D>> { MmapIO::new(p) }
|
||||
|
||||
impl<S, D: MmapDir> MmapIO<S, D> {
|
||||
/// Read current status
|
||||
pub fn status(&self) -> &Status { &self.ss }
|
||||
|
||||
/// Read current number of frames committed by application
|
||||
///
|
||||
/// This number wraps at 'boundary'.
|
||||
#[inline]
|
||||
pub fn appl_ptr(&self) -> Frames { self.c.appl_ptr() }
|
||||
|
||||
/// Read current number of frames read / written by hardware
|
||||
///
|
||||
/// This number wraps at 'boundary'.
|
||||
#[inline]
|
||||
pub fn hw_ptr(&self) -> Frames { self.ss.hw_ptr() }
|
||||
|
||||
/// The number at which hw_ptr and appl_ptr wraps.
|
||||
#[inline]
|
||||
pub fn boundary(&self) -> Frames { self.bound }
|
||||
|
||||
/// Total number of frames in hardware buffer
|
||||
#[inline]
|
||||
pub fn buffer_size(&self) -> Frames { self.data.frames }
|
||||
|
||||
/// Number of channels in stream
|
||||
#[inline]
|
||||
pub fn channels(&self) -> u32 { self.data.channels }
|
||||
|
||||
/// Notifies the kernel that frames have now been read / written by the application
|
||||
///
|
||||
/// This will allow the kernel to write new data into this part of the buffer.
|
||||
pub fn commit(&self, v: Frames) {
|
||||
let mut z = self.appl_ptr() + v;
|
||||
if z + v >= self.boundary() { z -= self.boundary() };
|
||||
self.c.set_appl_ptr(z)
|
||||
}
|
||||
|
||||
/// Number of frames available to read / write.
|
||||
///
|
||||
/// In case of an underrun, this value might be bigger than the buffer size.
|
||||
pub fn avail(&self) -> Frames { D::avail(self.hw_ptr(), self.appl_ptr(), self.buffer_size(), self.boundary()) }
|
||||
|
||||
/// Returns raw pointers to data to read / write.
|
||||
///
|
||||
/// Use this if you want to read/write data yourself (instead of using iterators). If you do,
|
||||
/// using `write_volatile` or `read_volatile` is recommended, since it's DMA memory and can
|
||||
/// change at any time.
|
||||
///
|
||||
/// Since this is a ring buffer, there might be more data to read/write in the beginning
|
||||
/// of the buffer as well. If so this is returned as the second return value.
|
||||
pub fn data_ptr(&self) -> (RawSamples<S>, Option<RawSamples<S>>) {
|
||||
let (hwptr, applptr) = (self.hw_ptr(), self.appl_ptr());
|
||||
let c = self.channels();
|
||||
let bufsize = self.buffer_size();
|
||||
|
||||
// These formulas mostly mimic the behaviour of
|
||||
// snd_pcm_mmap_begin (in alsa-lib/src/pcm/pcm.c).
|
||||
let offs = applptr % bufsize;
|
||||
let mut a = D::avail(hwptr, applptr, bufsize, self.boundary());
|
||||
a = cmp::min(a, bufsize);
|
||||
let b = bufsize - offs;
|
||||
let more_data = if b < a {
|
||||
let z = a - b;
|
||||
a = b;
|
||||
Some( RawSamples { ptr: self.data.mem.ptr, frames: z, channels: c })
|
||||
} else { None };
|
||||
|
||||
let p = unsafe { self.data.mem.ptr.offset(offs as isize * self.data.channels as isize) };
|
||||
(RawSamples { ptr: p, frames: a, channels: c }, more_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> MmapPlayback<S> {
|
||||
/// Write samples to the kernel ringbuffer.
|
||||
pub fn write<I: Iterator<Item=S>>(&mut self, i: &mut I) -> Frames {
|
||||
let (data, more_data) = self.data_ptr();
|
||||
let (iter_end, samples) = unsafe { data.write_samples(i) };
|
||||
let mut z = samples / data.channels as isize;
|
||||
if !iter_end {
|
||||
if let Some(data2) = more_data {
|
||||
let (_, samples2) = unsafe { data2.write_samples(i) };
|
||||
z += samples2 / data2.channels as isize;
|
||||
}
|
||||
}
|
||||
let z = z as Frames;
|
||||
self.commit(z);
|
||||
z
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> MmapCapture<S> {
|
||||
/// Read samples from the kernel ringbuffer.
|
||||
///
|
||||
/// When the iterator is dropped or depleted, the read samples will be committed, i e,
|
||||
/// the kernel can then write data to the location again. So do this ASAP.
|
||||
pub fn iter(&mut self) -> CaptureIter<S> {
|
||||
let (data, more_data) = self.data_ptr();
|
||||
CaptureIter {
|
||||
m: self,
|
||||
samples: data,
|
||||
p_offs: 0,
|
||||
read_samples: 0,
|
||||
next_p: more_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over captured samples
|
||||
pub struct CaptureIter<'a, S: 'static> {
|
||||
m: &'a MmapCapture<S>,
|
||||
samples: RawSamples<S>,
|
||||
p_offs: isize,
|
||||
read_samples: isize,
|
||||
next_p: Option<RawSamples<S>>,
|
||||
}
|
||||
|
||||
impl<'a, S: 'static + Copy> CaptureIter<'a, S> {
|
||||
fn handle_max(&mut self) {
|
||||
self.p_offs = 0;
|
||||
if let Some(p2) = self.next_p.take() {
|
||||
self.samples = p2;
|
||||
} else {
|
||||
self.m.commit((self.read_samples / self.samples.channels as isize) as Frames);
|
||||
self.read_samples = 0;
|
||||
self.samples.frames = 0; // Shortcut to "None" in case anyone calls us again
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: 'static + Copy> Iterator for CaptureIter<'a, S> {
|
||||
type Item = S;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.p_offs >= self.samples.samples() {
|
||||
self.handle_max();
|
||||
if self.samples.frames <= 0 { return None; }
|
||||
}
|
||||
let s = unsafe { ptr::read_volatile(self.samples.ptr.offset(self.p_offs)) };
|
||||
self.p_offs += 1;
|
||||
self.read_samples += 1;
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: 'static> Drop for CaptureIter<'a, S> {
|
||||
fn drop(&mut self) {
|
||||
self.m.commit((self.read_samples / self.m.data.channels as isize) as Frames);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
#[ignore] // Not everyone has a recording device on plughw:1. So let's ignore this test by default.
|
||||
fn record_from_plughw_rw() {
|
||||
use crate::pcm::*;
|
||||
use crate::{ValueOr, Direction};
|
||||
use std::ffi::CString;
|
||||
let pcm = PCM::open(&*CString::new("plughw:1").unwrap(), Direction::Capture, false).unwrap();
|
||||
let ss = self::Status::new(&pcm).unwrap();
|
||||
let c = self::Control::new(&pcm).unwrap();
|
||||
let hwp = HwParams::any(&pcm).unwrap();
|
||||
hwp.set_channels(2).unwrap();
|
||||
hwp.set_rate(44100, ValueOr::Nearest).unwrap();
|
||||
hwp.set_format(Format::s16()).unwrap();
|
||||
hwp.set_access(Access::RWInterleaved).unwrap();
|
||||
pcm.hw_params(&hwp).unwrap();
|
||||
|
||||
{
|
||||
let swp = pcm.sw_params_current().unwrap();
|
||||
swp.set_tstamp_mode(true).unwrap();
|
||||
pcm.sw_params(&swp).unwrap();
|
||||
}
|
||||
assert_eq!(ss.state(), State::Prepared);
|
||||
pcm.start().unwrap();
|
||||
assert_eq!(c.appl_ptr(), 0);
|
||||
println!("{:?}, {:?}", ss, c);
|
||||
let mut buf = [0i16; 512*2];
|
||||
assert_eq!(pcm.io_i16().unwrap().readi(&mut buf).unwrap(), 512);
|
||||
assert_eq!(c.appl_ptr(), 512);
|
||||
|
||||
assert_eq!(ss.state(), State::Running);
|
||||
assert!(ss.hw_ptr() >= 512);
|
||||
let t2 = ss.htstamp();
|
||||
assert!(t2.tv_sec > 0 || t2.tv_nsec > 0);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
#[ignore] // Not everyone has a record device on plughw:1. So let's ignore this test by default.
|
||||
fn record_from_plughw_mmap() {
|
||||
use crate::pcm::*;
|
||||
use crate::{ValueOr, Direction};
|
||||
use std::ffi::CString;
|
||||
use std::{thread, time};
|
||||
|
||||
let pcm = PCM::open(&*CString::new("plughw:1").unwrap(), Direction::Capture, false).unwrap();
|
||||
let hwp = HwParams::any(&pcm).unwrap();
|
||||
hwp.set_channels(2).unwrap();
|
||||
hwp.set_rate(44100, ValueOr::Nearest).unwrap();
|
||||
hwp.set_format(Format::s16()).unwrap();
|
||||
hwp.set_access(Access::MMapInterleaved).unwrap();
|
||||
pcm.hw_params(&hwp).unwrap();
|
||||
|
||||
let ss = unsafe { SyncPtrStatus::sync_ptr(pcm_to_fd(&pcm).unwrap(), false, None, None).unwrap() };
|
||||
assert_eq!(ss.state(), State::Prepared);
|
||||
|
||||
let mut m = pcm.direct_mmap_capture::<i16>().unwrap();
|
||||
|
||||
assert_eq!(m.status().state(), State::Prepared);
|
||||
assert_eq!(m.appl_ptr(), 0);
|
||||
assert_eq!(m.hw_ptr(), 0);
|
||||
|
||||
|
||||
println!("{:?}", m);
|
||||
|
||||
let now = time::Instant::now();
|
||||
pcm.start().unwrap();
|
||||
while m.avail() < 256 { thread::sleep(time::Duration::from_millis(1)) };
|
||||
assert!(now.elapsed() >= time::Duration::from_millis(256 * 1000 / 44100));
|
||||
let (ptr1, md) = m.data_ptr();
|
||||
assert_eq!(ptr1.channels, 2);
|
||||
assert!(ptr1.frames >= 256);
|
||||
assert!(md.is_none());
|
||||
println!("Has {:?} frames at {:?} in {:?}", m.avail(), ptr1.ptr, now.elapsed());
|
||||
let samples: Vec<i16> = m.iter().collect();
|
||||
assert!(samples.len() >= ptr1.frames as usize * 2);
|
||||
println!("Collected {} samples", samples.len());
|
||||
let (ptr2, _md) = m.data_ptr();
|
||||
assert!(unsafe { ptr1.ptr.offset(256 * 2) } <= ptr2.ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn playback_to_plughw_mmap() {
|
||||
use crate::pcm::*;
|
||||
use crate::{ValueOr, Direction};
|
||||
use std::ffi::CString;
|
||||
|
||||
let pcm = PCM::open(&*CString::new("plughw:1").unwrap(), Direction::Playback, false).unwrap();
|
||||
let hwp = HwParams::any(&pcm).unwrap();
|
||||
hwp.set_channels(2).unwrap();
|
||||
hwp.set_rate(44100, ValueOr::Nearest).unwrap();
|
||||
hwp.set_format(Format::s16()).unwrap();
|
||||
hwp.set_access(Access::MMapInterleaved).unwrap();
|
||||
pcm.hw_params(&hwp).unwrap();
|
||||
let mut m = pcm.direct_mmap_playback::<i16>().unwrap();
|
||||
|
||||
assert_eq!(m.status().state(), State::Prepared);
|
||||
assert_eq!(m.appl_ptr(), 0);
|
||||
assert_eq!(m.hw_ptr(), 0);
|
||||
|
||||
println!("{:?}", m);
|
||||
let mut i = (0..(m.buffer_size() * 2)).map(|i|
|
||||
(((i / 2) as f32 * 2.0 * ::std::f32::consts::PI / 128.0).sin() * 8192.0) as i16);
|
||||
m.write(&mut i);
|
||||
assert_eq!(m.appl_ptr(), m.buffer_size());
|
||||
|
||||
pcm.start().unwrap();
|
||||
pcm.drain().unwrap();
|
||||
assert_eq!(m.appl_ptr(), m.buffer_size());
|
||||
assert!(m.hw_ptr() >= m.buffer_size());
|
||||
}
|
100
alsa/src/error.rs
Normal file
100
alsa/src/error.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
#![macro_use]
|
||||
|
||||
use libc::{c_void, c_int, c_char, free};
|
||||
use std::{fmt, str};
|
||||
use std::ffi::CStr;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
/// ALSA error
|
||||
///
|
||||
/// Most ALSA functions can return a negative error code.
|
||||
/// If so, then that error code is wrapped into this `Error` struct.
|
||||
/// An Error is also returned in case ALSA returns a string that
|
||||
/// cannot be translated into Rust's UTF-8 strings.
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub struct Error(&'static str, nix::Error);
|
||||
|
||||
pub type Result<T> = ::std::result::Result<T, Error>;
|
||||
|
||||
macro_rules! acheck {
|
||||
($f: ident ( $($x: expr),* ) ) => {{
|
||||
let r = unsafe { alsa::$f( $($x),* ) };
|
||||
if r < 0 { Err(Error::new(stringify!($f), -r as ::libc::c_int)) }
|
||||
else { Ok(r) }
|
||||
}}
|
||||
}
|
||||
|
||||
pub fn from_const<'a>(func: &'static str, s: *const c_char) -> Result<&'a str> {
|
||||
if s.is_null() { return Err(invalid_str(func)) };
|
||||
let cc = unsafe { CStr::from_ptr(s) };
|
||||
str::from_utf8(cc.to_bytes()).map_err(|_| invalid_str(func))
|
||||
}
|
||||
|
||||
pub fn from_alloc(func: &'static str, s: *mut c_char) -> Result<String> {
|
||||
if s.is_null() { return Err(invalid_str(func)) };
|
||||
let c = unsafe { CStr::from_ptr(s) };
|
||||
let ss = str::from_utf8(c.to_bytes()).map_err(|_| {
|
||||
unsafe { free(s as *mut c_void); }
|
||||
invalid_str(func)
|
||||
})?.to_string();
|
||||
unsafe { free(s as *mut c_void); }
|
||||
Ok(ss)
|
||||
}
|
||||
|
||||
pub fn from_code(func: &'static str, r: c_int) -> Result<c_int> {
|
||||
if r < 0 { Err(Error::new(func, r)) }
|
||||
else { Ok(r) }
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(func: &'static str, res: c_int) -> Error {
|
||||
let errno = nix::errno::Errno::from_i32(res as i32);
|
||||
Error(func, errno)
|
||||
}
|
||||
|
||||
pub fn unsupported(func: &'static str) -> Error {
|
||||
Error(func, nix::Error::ENOTSUP)
|
||||
}
|
||||
|
||||
/// The function which failed.
|
||||
pub fn func(&self) -> &'static str { self.0 }
|
||||
|
||||
|
||||
/// Underlying error
|
||||
///
|
||||
/// Match this against the re-export of `nix::Error` in this crate, not against a specific version
|
||||
/// of the nix crate. The nix crate version might be updated with minor updates of this library.
|
||||
pub fn errno(&self) -> nix::Error { self.1 }
|
||||
|
||||
/// Underlying error
|
||||
///
|
||||
/// Match this against the re-export of `nix::Error` in this crate, not against a specific version
|
||||
/// of the nix crate. The nix crate version might be updated with minor updates of this library.
|
||||
pub fn nix_error(&self) -> nix::Error { self.1 }
|
||||
}
|
||||
|
||||
pub fn invalid_str(func: &'static str) -> Error { Error(func, nix::Error::EILSEQ) }
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> { Some(&self.1) }
|
||||
fn description(&self) -> &str { "ALSA error" }
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "ALSA function '{}' failed with error '{}'", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for fmt::Error {
|
||||
fn from(_: Error) -> fmt::Error { fmt::Error }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn broken_pcm_name() {
|
||||
use std::ffi::CString;
|
||||
let e = crate::PCM::open(&*CString::new("this_PCM_does_not_exist").unwrap(), crate::Direction::Playback, false).err().unwrap();
|
||||
assert_eq!(e.func(), "snd_pcm_open");
|
||||
assert_eq!(e.errno(), nix::errno::Errno::ENOENT);
|
||||
}
|
162
alsa/src/hctl.rs
Normal file
162
alsa/src/hctl.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
//! HCtl API - for mixer control and jack detection
|
||||
//!
|
||||
//! # Example
|
||||
//! Print all jacks and their status
|
||||
//!
|
||||
//! ```
|
||||
//! for a in ::alsa::card::Iter::new().map(|x| x.unwrap()) {
|
||||
//! use std::ffi::CString;
|
||||
//! use alsa::hctl::HCtl;
|
||||
//! let h = HCtl::open(&CString::new(format!("hw:{}", a.get_index())).unwrap(), false).unwrap();
|
||||
//! h.load().unwrap();
|
||||
//! for b in h.elem_iter() {
|
||||
//! use alsa::ctl::ElemIface;
|
||||
//! let id = b.get_id().unwrap();
|
||||
//! if id.get_interface() != ElemIface::Card { continue; }
|
||||
//! let name = id.get_name().unwrap();
|
||||
//! if !name.ends_with(" Jack") { continue; }
|
||||
//! if name.ends_with(" Phantom Jack") {
|
||||
//! println!("{} is always present", &name[..name.len()-13])
|
||||
//! }
|
||||
//! else { println!("{} is {}", &name[..name.len()-5],
|
||||
//! if b.read().unwrap().get_boolean(0).unwrap() { "plugged in" } else { "unplugged" })
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use crate::alsa;
|
||||
use std::ffi::{CStr, CString};
|
||||
use super::error::*;
|
||||
use std::ptr;
|
||||
use super::{ctl_int, poll};
|
||||
use libc::{c_short, c_uint, c_int, pollfd};
|
||||
|
||||
|
||||
/// [snd_hctl_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___h_control.html) wrapper
|
||||
pub struct HCtl(*mut alsa::snd_hctl_t);
|
||||
|
||||
unsafe impl Send for HCtl {}
|
||||
|
||||
impl Drop for HCtl {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_hctl_close(self.0) }; }
|
||||
}
|
||||
|
||||
impl HCtl {
|
||||
/// Wrapper around open that takes a &str instead of a &CStr
|
||||
pub fn new(c: &str, nonblock: bool) -> Result<HCtl> {
|
||||
Self::open(&CString::new(c).unwrap(), nonblock)
|
||||
}
|
||||
|
||||
/// Open does not support async mode (it's not very Rustic anyway)
|
||||
/// Note: You probably want to call `load` afterwards.
|
||||
pub fn open(c: &CStr, nonblock: bool) -> Result<HCtl> {
|
||||
let mut r = ptr::null_mut();
|
||||
let flags = if nonblock { 1 } else { 0 }; // FIXME: alsa::SND_CTL_NONBLOCK does not exist in alsa-sys
|
||||
acheck!(snd_hctl_open(&mut r, c.as_ptr(), flags))
|
||||
.map(|_| HCtl(r))
|
||||
}
|
||||
|
||||
pub fn load(&self) -> Result<()> { acheck!(snd_hctl_load(self.0)).map(|_| ()) }
|
||||
|
||||
pub fn elem_iter(&self) -> ElemIter { ElemIter(self, ptr::null_mut()) }
|
||||
|
||||
pub fn find_elem(&self, id: &ctl_int::ElemId) -> Option<Elem> {
|
||||
let p = unsafe { alsa::snd_hctl_find_elem(self.0, ctl_int::elem_id_ptr(id)) };
|
||||
if p.is_null() { None } else { Some(Elem(self, p)) }
|
||||
}
|
||||
|
||||
pub fn handle_events(&self) -> Result<u32> {
|
||||
acheck!(snd_hctl_handle_events(self.0)).map(|x| x as u32)
|
||||
}
|
||||
|
||||
pub fn wait(&self, timeout_ms: Option<u32>) -> Result<bool> {
|
||||
acheck!(snd_hctl_wait(self.0, timeout_ms.map(|x| x as c_int).unwrap_or(-1))).map(|i| i == 1) }
|
||||
}
|
||||
|
||||
impl poll::Descriptors for HCtl {
|
||||
fn count(&self) -> usize {
|
||||
unsafe { alsa::snd_hctl_poll_descriptors_count(self.0) as usize }
|
||||
}
|
||||
fn fill(&self, p: &mut [pollfd]) -> Result<usize> {
|
||||
let z = unsafe { alsa::snd_hctl_poll_descriptors(self.0, p.as_mut_ptr(), p.len() as c_uint) };
|
||||
from_code("snd_hctl_poll_descriptors", z).map(|_| z as usize)
|
||||
}
|
||||
fn revents(&self, p: &[pollfd]) -> Result<poll::Flags> {
|
||||
let mut r = 0;
|
||||
let z = unsafe { alsa::snd_hctl_poll_descriptors_revents(self.0, p.as_ptr() as *mut pollfd, p.len() as c_uint, &mut r) };
|
||||
from_code("snd_hctl_poll_descriptors_revents", z).map(|_| poll::Flags::from_bits_truncate(r as c_short))
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over elements for a `HCtl`
|
||||
pub struct ElemIter<'a>(&'a HCtl, *mut alsa::snd_hctl_elem_t);
|
||||
|
||||
impl<'a> Iterator for ElemIter<'a> {
|
||||
type Item = Elem<'a>;
|
||||
fn next(&mut self) -> Option<Elem<'a>> {
|
||||
self.1 = if self.1.is_null() { unsafe { alsa::snd_hctl_first_elem((self.0).0) }}
|
||||
else { unsafe { alsa::snd_hctl_elem_next(self.1) }};
|
||||
if self.1.is_null() { None }
|
||||
else { Some(Elem(self.0, self.1)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [snd_hctl_elem_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___h_control.html) wrapper
|
||||
pub struct Elem<'a>(&'a HCtl, *mut alsa::snd_hctl_elem_t);
|
||||
|
||||
impl<'a> Elem<'a> {
|
||||
pub fn get_id(&self) -> Result<ctl_int::ElemId> {
|
||||
let v = ctl_int::elem_id_new()?;
|
||||
unsafe { alsa::snd_hctl_elem_get_id(self.1, ctl_int::elem_id_ptr(&v)) };
|
||||
Ok(v)
|
||||
}
|
||||
pub fn info(&self) -> Result<ctl_int::ElemInfo> {
|
||||
let v = ctl_int::elem_info_new()?;
|
||||
acheck!(snd_hctl_elem_info(self.1, ctl_int::elem_info_ptr(&v))).map(|_| v)
|
||||
}
|
||||
pub fn read(&self) -> Result<ctl_int::ElemValue> {
|
||||
let i = self.info()?;
|
||||
let v = ctl_int::elem_value_new(i.get_type(), i.get_count())?;
|
||||
acheck!(snd_hctl_elem_read(self.1, ctl_int::elem_value_ptr(&v))).map(|_| v)
|
||||
}
|
||||
|
||||
pub fn write(&self, v: &ctl_int::ElemValue) -> Result<bool> {
|
||||
acheck!(snd_hctl_elem_write(self.1, ctl_int::elem_value_ptr(v))).map(|e| e > 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_hctls() {
|
||||
for a in super::card::Iter::new().map(|x| x.unwrap()) {
|
||||
use std::ffi::CString;
|
||||
let h = HCtl::open(&CString::new(format!("hw:{}", a.get_index())).unwrap(), false).unwrap();
|
||||
h.load().unwrap();
|
||||
println!("Card {}:", a.get_name().unwrap());
|
||||
for b in h.elem_iter() {
|
||||
println!(" {:?} - {:?}", b.get_id().unwrap(), b.read().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_jacks() {
|
||||
for a in super::card::Iter::new().map(|x| x.unwrap()) {
|
||||
use std::ffi::CString;
|
||||
let h = HCtl::open(&CString::new(format!("hw:{}", a.get_index())).unwrap(), false).unwrap();
|
||||
h.load().unwrap();
|
||||
for b in h.elem_iter() {
|
||||
let id = b.get_id().unwrap();
|
||||
if id.get_interface() != super::ctl_int::ElemIface::Card { continue; }
|
||||
let name = id.get_name().unwrap();
|
||||
if !name.ends_with(" Jack") { continue; }
|
||||
if name.ends_with(" Phantom Jack") {
|
||||
println!("{} is always present", &name[..name.len()-13])
|
||||
}
|
||||
else { println!("{} is {}", &name[..name.len()-5],
|
||||
if b.read().unwrap().get_boolean(0).unwrap() { "plugged in" } else { "unplugged" })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
alsa/src/io.rs
Normal file
49
alsa/src/io.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use crate::alsa;
|
||||
use super::error::*;
|
||||
use std::{slice, ptr, fmt};
|
||||
|
||||
/// [snd_output_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___output.html) wrapper
|
||||
pub struct Output(*mut alsa::snd_output_t);
|
||||
|
||||
unsafe impl Send for Output {}
|
||||
|
||||
impl Drop for Output {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_output_close(self.0) }; }
|
||||
}
|
||||
|
||||
impl Output {
|
||||
|
||||
pub fn buffer_open() -> Result<Output> {
|
||||
let mut q = ptr::null_mut();
|
||||
acheck!(snd_output_buffer_open(&mut q)).map(|_| Output(q))
|
||||
}
|
||||
|
||||
pub fn buffer_string<T, F: FnOnce(&[u8]) -> T>(&self, f: F) -> T {
|
||||
let b = unsafe {
|
||||
let mut q = ptr::null_mut();
|
||||
let s = alsa::snd_output_buffer_string(self.0, &mut q);
|
||||
slice::from_raw_parts(q as *const u8, s as usize)
|
||||
};
|
||||
f(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Output {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Output(")?;
|
||||
fmt::Display::fmt(self, f)?;
|
||||
write!(f, ")")
|
||||
/* self.buffer_string(|b| f.write_str(try!(str::from_utf8(b).map_err(|_| fmt::Error)))) */
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Output {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.buffer_string(|b| {
|
||||
let s = String::from_utf8_lossy(b);
|
||||
f.write_str(&*s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_handle(o: &Output) -> *mut alsa::snd_output_t { o.0 }
|
140
alsa/src/lib.rs
Normal file
140
alsa/src/lib.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
//! Thin but safe wrappers for [ALSA](https://alsa-project.org).
|
||||
//!
|
||||
//! [GitHub repo](https://github.com/diwic/alsa-rs)
|
||||
//!
|
||||
//! [Crates.io](https://crates.io/crates/alsa)
|
||||
//!
|
||||
//! This ALSA API wrapper/binding is WIP - the ALSA API is huge, and new
|
||||
//! functions and structs might be added as requested.
|
||||
//!
|
||||
//! Most functions map 1-to-1 to alsa-lib functions, e g, `ctl::CardInfo::get_id()` is a wrapper around
|
||||
//! `snd_ctl_card_info_get_id` and the [alsa-lib documentation](https://www.alsa-project.org/alsa-doc/alsa-lib/)
|
||||
//! can be consulted for additional information.
|
||||
//!
|
||||
//! Enjoy!
|
||||
|
||||
#![allow(clippy::all)]
|
||||
#![warn(clippy::correctness, clippy::suspicious, clippy::perf)]
|
||||
|
||||
extern crate alsa_sys as alsa;
|
||||
extern crate libc;
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate nix as nix_the_crate;
|
||||
|
||||
macro_rules! alsa_enum {
|
||||
($(#[$attr:meta])+ $name:ident, $static_name:ident [$count:expr], $( $a:ident = $b:ident),* ,) =>
|
||||
{
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
$(#[$attr])*
|
||||
pub enum $name {
|
||||
$(
|
||||
$a = alsa::$b as isize,
|
||||
)*
|
||||
}
|
||||
|
||||
static $static_name: [$name; $count] =
|
||||
[ $( $name::$a, )* ];
|
||||
|
||||
impl $name {
|
||||
/// Returns a slice of all possible values; useful for iteration
|
||||
pub fn all() -> &'static [$name] { &$static_name[..] }
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn from_c_int(c: ::libc::c_int, s: &'static str) -> Result<$name> {
|
||||
Self::all().iter().find(|&&x| c == x as ::libc::c_int).map(|&x| x)
|
||||
.ok_or_else(|| Error::unsupported(s))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces constants ending with PLAYBACK/CAPTURE as well as
|
||||
/// INPUT/OUTPUT
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Direction {
|
||||
Playback,
|
||||
Capture
|
||||
}
|
||||
impl Direction {
|
||||
#[inline]
|
||||
pub fn input() -> Direction { Direction::Capture }
|
||||
#[inline]
|
||||
pub fn output() -> Direction { Direction::Playback }
|
||||
}
|
||||
|
||||
/// Used to restrict hw parameters. In case the submitted
|
||||
/// value is unavailable, in which direction should one search
|
||||
/// for available values?
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ValueOr {
|
||||
/// The value set is the submitted value, or less
|
||||
Less = -1,
|
||||
/// The value set is the submitted value, or the nearest
|
||||
Nearest = 0,
|
||||
/// The value set is the submitted value, or greater
|
||||
Greater = 1,
|
||||
}
|
||||
|
||||
/// Rounding mode (used in some mixer related calls)
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Round {
|
||||
/// Round down (towards negative infinity)
|
||||
Floor = 0,
|
||||
/// Round up (towards positive infinity)
|
||||
Ceil = 1,
|
||||
}
|
||||
|
||||
mod error;
|
||||
pub use crate::error::{Error, Result};
|
||||
|
||||
pub mod card;
|
||||
pub use crate::card::Card as Card;
|
||||
|
||||
mod ctl_int;
|
||||
pub mod ctl {
|
||||
//! Control device API
|
||||
pub use super::ctl_int::{Ctl, CardInfo, ElemIface, ElemId, ElemType, ElemValue, ElemInfo};
|
||||
}
|
||||
|
||||
pub use crate::ctl::Ctl as Ctl;
|
||||
|
||||
pub mod hctl;
|
||||
pub use crate::hctl::HCtl as HCtl;
|
||||
|
||||
pub mod pcm;
|
||||
pub use crate::pcm::PCM as PCM;
|
||||
|
||||
pub mod rawmidi;
|
||||
pub use crate::rawmidi::Rawmidi as Rawmidi;
|
||||
|
||||
pub mod device_name;
|
||||
|
||||
pub mod poll;
|
||||
pub use crate::poll::Descriptors as PollDescriptors;
|
||||
|
||||
pub mod mixer;
|
||||
pub use crate::mixer::Mixer as Mixer;
|
||||
|
||||
pub mod seq;
|
||||
pub use crate::seq::Seq as Seq;
|
||||
|
||||
mod io;
|
||||
pub use crate::io::Output;
|
||||
|
||||
// Reexported inside PCM module
|
||||
mod chmap;
|
||||
|
||||
pub mod direct;
|
||||
|
||||
/// Re-exports from the nix crate.
|
||||
///
|
||||
/// Use these re-exports instead of also depending on the nix crate. There
|
||||
/// is no guarantee that these will match a specific nix version, it may
|
||||
/// change between minor updates of the library.
|
||||
pub mod nix {
|
||||
pub use nix_the_crate::Error;
|
||||
pub use nix_the_crate::errno;
|
||||
}
|
639
alsa/src/mixer.rs
Normal file
639
alsa/src/mixer.rs
Normal file
|
@ -0,0 +1,639 @@
|
|||
//! Mixer API - Simple Mixer API for mixer control
|
||||
//!
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::{ptr, mem, fmt, ops};
|
||||
use libc::{c_long, c_int, c_uint, c_short, pollfd};
|
||||
use crate::poll;
|
||||
|
||||
use crate::alsa;
|
||||
use super::Round;
|
||||
use super::error::*;
|
||||
|
||||
const SELEM_ID_SIZE: usize = 64;
|
||||
|
||||
/// wraps [snd_mixer_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___mixer.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Mixer(*mut alsa::snd_mixer_t);
|
||||
|
||||
unsafe impl Send for Mixer {}
|
||||
|
||||
impl Mixer {
|
||||
/// Opens a mixer and attaches it to a card identified by its name (like hw:0) and loads the
|
||||
/// mixer after registering a Selem.
|
||||
pub fn new(name: &str, nonblock: bool) -> Result<Mixer> {
|
||||
let mut mixer = Mixer::open(nonblock)?;
|
||||
mixer.attach(&CString::new(name).unwrap())?;
|
||||
Selem::register(&mut mixer)?;
|
||||
mixer.load()?;
|
||||
Ok(mixer)
|
||||
}
|
||||
|
||||
/// Creates a Selem by looking for a specific selem by name given a mixer (of a card)
|
||||
pub fn find_selem(&self, id: &SelemId) -> Option<Selem> {
|
||||
let selem = unsafe { alsa::snd_mixer_find_selem(self.0, id.as_ptr()) };
|
||||
|
||||
if selem.is_null() { None }
|
||||
else { Some(Selem(Elem {handle: selem, _mixer: self})) }
|
||||
}
|
||||
|
||||
pub fn open(nonblock: bool) -> Result<Mixer> {
|
||||
let mut r = ptr::null_mut();
|
||||
let flags = if nonblock { 1 } else { 0 }; // FIXME: alsa::SND_CTL_NONBLOCK does not exist in alsa-sys
|
||||
acheck!(snd_mixer_open(&mut r, flags)).map(|_| Mixer(r))
|
||||
}
|
||||
|
||||
pub fn attach(&mut self, name: &CStr) -> Result<()> {
|
||||
acheck!(snd_mixer_attach(self.0, name.as_ptr())).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn load(&mut self) -> Result<()> {
|
||||
acheck!(snd_mixer_load(self.0)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter {
|
||||
Iter {
|
||||
last_handle: ptr::null_mut(),
|
||||
mixer: self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_events(&self) -> Result<u32> {
|
||||
acheck!(snd_mixer_handle_events(self.0)).map(|x| x as u32)
|
||||
}
|
||||
|
||||
pub fn wait(&self, timeout_ms: Option<u32>) -> Result<bool> {
|
||||
acheck!(snd_mixer_wait(self.0, timeout_ms.map(|x| x as c_int).unwrap_or(-1))).map(|i| i == 1) }
|
||||
}
|
||||
|
||||
/// Closes mixer and frees used resources
|
||||
impl Drop for Mixer {
|
||||
fn drop(&mut self) {
|
||||
unsafe { alsa::snd_mixer_close(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl poll::Descriptors for Mixer {
|
||||
fn count(&self) -> usize {
|
||||
unsafe { alsa::snd_mixer_poll_descriptors_count(self.0) as usize }
|
||||
}
|
||||
fn fill(&self, p: &mut [pollfd]) -> Result<usize> {
|
||||
let z = unsafe { alsa::snd_mixer_poll_descriptors(self.0, p.as_mut_ptr(), p.len() as c_uint) };
|
||||
from_code("snd_mixer_poll_descriptors", z).map(|_| z as usize)
|
||||
}
|
||||
fn revents(&self, p: &[pollfd]) -> Result<poll::Flags> {
|
||||
let mut r = 0;
|
||||
let z = unsafe { alsa::snd_mixer_poll_descriptors_revents(self.0, p.as_ptr() as *mut pollfd, p.len() as c_uint, &mut r) };
|
||||
from_code("snd_mixer_poll_descriptors_revents", z).map(|_| poll::Flags::from_bits_truncate(r as c_short))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Wrapper for a mB (millibel) value.
|
||||
///
|
||||
/// Despite some ALSA functions named "dB", they actually take mB values instead.
|
||||
/// This is a wrapper type to help with those calculations. Its interior is the
|
||||
/// actual mB value.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct MilliBel(pub i64);
|
||||
|
||||
impl MilliBel {
|
||||
pub fn to_db(self) -> f32 { (self.0 as f32) / 100.0 }
|
||||
pub fn from_db(db: f32) -> Self { MilliBel((db * 100.0) as i64) }
|
||||
}
|
||||
|
||||
impl ops::Deref for MilliBel {
|
||||
type Target = i64;
|
||||
fn deref(&self) -> &i64 { &self.0 }
|
||||
}
|
||||
|
||||
impl ops::Add for MilliBel {
|
||||
type Output = MilliBel;
|
||||
fn add(self, rhs: Self) -> Self { MilliBel(self.0 + rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::AddAssign for MilliBel {
|
||||
fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0 }
|
||||
}
|
||||
|
||||
impl ops::Sub for MilliBel {
|
||||
type Output = MilliBel;
|
||||
fn sub(self, rhs: Self) -> Self { MilliBel(self.0 - rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::SubAssign for MilliBel {
|
||||
fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0 }
|
||||
}
|
||||
|
||||
/// Wraps [snd_mixer_elem_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___mixer.html)
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Elem<'a>{
|
||||
handle: *mut alsa::snd_mixer_elem_t,
|
||||
_mixer: &'a Mixer
|
||||
}
|
||||
|
||||
/// Iterator for all elements of mixer
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Iter<'a>{
|
||||
last_handle: *mut alsa::snd_mixer_elem_t,
|
||||
mixer: &'a Mixer
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = Elem<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Elem<'a>> {
|
||||
let elem = if self.last_handle.is_null() {
|
||||
unsafe { alsa::snd_mixer_first_elem(self.mixer.0) }
|
||||
} else {
|
||||
unsafe { alsa::snd_mixer_elem_next(self.last_handle) }
|
||||
};
|
||||
|
||||
if elem.is_null() {
|
||||
None
|
||||
} else {
|
||||
self.last_handle = elem;
|
||||
Some(Elem { handle: elem, _mixer: self.mixer})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Wrapper for [snd_mixer_selem_id_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___simple_mixer.html)
|
||||
/// No allocation (uses fixed array)
|
||||
// #[derive(Copy, Clone, Debug)]
|
||||
pub struct SelemId([u8; SELEM_ID_SIZE]);
|
||||
|
||||
impl SelemId {
|
||||
|
||||
pub fn new(name: &str, index: u32) -> SelemId {
|
||||
let mut s = SelemId::empty();
|
||||
s.set_name(&CString::new(name).unwrap());
|
||||
s.set_index(index);
|
||||
s
|
||||
}
|
||||
|
||||
/// Returns an empty (zeroed) SelemId. This id is not a usable id and need to be initialized
|
||||
/// like `SelemId::new()` does
|
||||
pub fn empty() -> SelemId {
|
||||
assert!(unsafe { alsa::snd_mixer_selem_id_sizeof() } as usize <= SELEM_ID_SIZE);
|
||||
// Create empty selem_id and fill from mixer
|
||||
SelemId(unsafe { mem::zeroed() })
|
||||
}
|
||||
|
||||
/// Convert SelemId into ``*mut snd_mixer_selem_id_t` that the alsa call needs.
|
||||
/// See [snd_mixer_selem_id_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___simple_mixer.html)
|
||||
#[inline]
|
||||
fn as_ptr(&self) -> *mut alsa::snd_mixer_selem_id_t {
|
||||
self.0.as_ptr() as *const _ as *mut alsa::snd_mixer_selem_id_t
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> Result<&str> {
|
||||
let c = unsafe { alsa::snd_mixer_selem_id_get_name(self.as_ptr()) };
|
||||
from_const("snd_mixer_selem_id_get_name", c)
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> u32 {
|
||||
unsafe { alsa::snd_mixer_selem_id_get_index(self.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: &CStr) {
|
||||
unsafe { alsa::snd_mixer_selem_id_set_name(self.as_ptr(), name.as_ptr()) };
|
||||
}
|
||||
|
||||
pub fn set_index(&mut self, index: u32) {
|
||||
unsafe { alsa::snd_mixer_selem_id_set_index(self.as_ptr(), index) };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Wraps an Elem as a Selem
|
||||
// #[derive(Copy, Clone)]
|
||||
pub struct Selem<'a>(Elem<'a>);
|
||||
|
||||
impl<'a> Selem<'a> {
|
||||
/// Creates a Selem by wrapping `elem`.
|
||||
pub fn new(elem: Elem<'a>) -> Option<Selem<'a>> {
|
||||
if unsafe { alsa::snd_mixer_elem_get_type(elem.handle) } == alsa::SND_MIXER_ELEM_SIMPLE
|
||||
{ Some(Selem(elem)) } else { None }
|
||||
}
|
||||
|
||||
/// TODO: This function might change to support regopt and to return the mixer class
|
||||
pub fn register(mixer: &mut Mixer) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_register(mixer.0, ptr::null_mut(), ptr::null_mut())).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> SelemId {
|
||||
let id = SelemId::empty();
|
||||
unsafe { alsa::snd_mixer_selem_get_id(self.handle, id.as_ptr()) };
|
||||
id
|
||||
}
|
||||
|
||||
pub fn has_capture_volume(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_has_capture_volume(self.handle) > 0 }
|
||||
}
|
||||
|
||||
pub fn has_capture_switch(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_has_capture_switch(self.handle) > 0 }
|
||||
}
|
||||
|
||||
pub fn has_playback_volume(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_has_playback_volume(self.handle) > 0 }
|
||||
}
|
||||
|
||||
pub fn has_playback_switch(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_has_playback_switch(self.handle) > 0 }
|
||||
}
|
||||
|
||||
pub fn can_capture(&self) -> bool {
|
||||
self.has_capture_volume() || self.has_capture_switch()
|
||||
}
|
||||
|
||||
pub fn can_playback(&self) -> bool {
|
||||
self.has_playback_volume() || self.has_playback_switch()
|
||||
}
|
||||
|
||||
pub fn has_volume(&self) -> bool {
|
||||
self.has_capture_volume() || self.has_playback_volume()
|
||||
}
|
||||
|
||||
/// returns range for capture volume as (min, max) values
|
||||
pub fn get_capture_volume_range(&self) -> (i64, i64) {
|
||||
let mut min: c_long = 0;
|
||||
let mut max: c_long = 0;
|
||||
unsafe { alsa::snd_mixer_selem_get_capture_volume_range(self.handle, &mut min, &mut max) };
|
||||
(min as i64, max as i64)
|
||||
}
|
||||
|
||||
/// returns (min, max) values.
|
||||
pub fn get_capture_db_range(&self) -> (MilliBel, MilliBel) {
|
||||
let mut min: c_long = 0;
|
||||
let mut max: c_long = 0;
|
||||
unsafe { alsa::snd_mixer_selem_get_capture_dB_range(self.handle, &mut min, &mut max) };
|
||||
(MilliBel(min as i64), MilliBel(max as i64))
|
||||
}
|
||||
|
||||
/// returns (min, max) values.
|
||||
pub fn get_playback_volume_range(&self) -> (i64, i64) {
|
||||
let mut min: c_long = 0;
|
||||
let mut max: c_long = 0;
|
||||
unsafe { alsa::snd_mixer_selem_get_playback_volume_range(self.handle, &mut min, &mut max) };
|
||||
(min as i64, max as i64)
|
||||
}
|
||||
|
||||
/// returns (min, max) values.
|
||||
pub fn get_playback_db_range(&self) -> (MilliBel, MilliBel) {
|
||||
let mut min: c_long = 0;
|
||||
let mut max: c_long = 0;
|
||||
unsafe { alsa::snd_mixer_selem_get_playback_dB_range(self.handle, &mut min, &mut max) };
|
||||
(MilliBel(min as i64), MilliBel(max as i64))
|
||||
}
|
||||
|
||||
pub fn is_playback_mono(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_is_playback_mono(self.handle) == 1 }
|
||||
}
|
||||
|
||||
pub fn has_capture_channel(&self, channel: SelemChannelId) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_has_capture_channel(self.handle, channel as i32) > 0 }
|
||||
}
|
||||
|
||||
pub fn has_playback_channel(&self, channel: SelemChannelId) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_has_playback_channel(self.handle, channel as i32) > 0 }
|
||||
}
|
||||
|
||||
/// Gets name from snd_mixer_selem_channel_name
|
||||
pub fn channel_name(channel: SelemChannelId) -> Result<&'static str> {
|
||||
let c = unsafe { alsa::snd_mixer_selem_channel_name(channel as i32) };
|
||||
from_const("snd_mixer_selem_channel_name", c)
|
||||
}
|
||||
|
||||
pub fn get_playback_volume(&self, channel: SelemChannelId) -> Result<i64> {
|
||||
let mut value: c_long = 0;
|
||||
acheck!(snd_mixer_selem_get_playback_volume(self.handle, channel as i32, &mut value)).and_then(|_| Ok(value as i64))
|
||||
}
|
||||
|
||||
/// returns volume in millibels.
|
||||
pub fn get_playback_vol_db(&self, channel: SelemChannelId) -> Result<MilliBel> {
|
||||
self.get_playback_volume(channel)
|
||||
.and_then(|volume| self.ask_playback_vol_db(volume))
|
||||
}
|
||||
|
||||
/// Asks alsa to convert playback volume to millibels.
|
||||
pub fn ask_playback_vol_db(&self, volume: i64) -> Result<MilliBel> {
|
||||
let mut decibel_value: c_long = 0;
|
||||
acheck!(snd_mixer_selem_ask_playback_vol_dB(self.handle, volume as c_long, &mut decibel_value))
|
||||
.map(|_| MilliBel(decibel_value as i64))
|
||||
}
|
||||
|
||||
pub fn get_capture_volume(&self, channel: SelemChannelId) -> Result<i64> {
|
||||
let mut value: c_long = 0;
|
||||
acheck!(snd_mixer_selem_get_capture_volume(self.handle, channel as i32, &mut value)).map(|_| value as i64)
|
||||
}
|
||||
|
||||
/// returns volume in millibels.
|
||||
pub fn get_capture_vol_db(&self, channel: SelemChannelId) -> Result<MilliBel> {
|
||||
self.get_capture_volume(channel)
|
||||
.and_then(|volume| self.ask_capture_vol_db(volume))
|
||||
}
|
||||
|
||||
/// Asks alsa to convert capture volume to millibels
|
||||
pub fn ask_capture_vol_db(&self, volume: i64) -> Result<MilliBel> {
|
||||
let mut decibel_value: c_long = 0;
|
||||
acheck!(snd_mixer_selem_ask_capture_vol_dB (self.handle, volume as c_long, &mut decibel_value))
|
||||
.map(|_| MilliBel(decibel_value as i64))
|
||||
}
|
||||
|
||||
pub fn set_playback_volume(&self, channel: SelemChannelId, value: i64) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_playback_volume(self.handle, channel as i32, value as c_long)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_playback_volume_range(&self, min: i64, max: i64) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_playback_volume_range(self.handle, min as c_long, max as c_long)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_playback_volume_all(&self, value: i64) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_playback_volume_all(self.handle, value as c_long)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_playback_db(&self, channel: SelemChannelId, value: MilliBel, dir: Round) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_playback_dB(self.handle, channel as i32, *value as c_long, dir as c_int)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_capture_db(&self, channel: SelemChannelId, value: MilliBel, dir: Round) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_capture_dB(self.handle, channel as i32, *value as c_long, dir as c_int)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_playback_db_all(&self, value: MilliBel, dir: Round) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_playback_dB_all(self.handle, *value as c_long, dir as c_int)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_capture_db_all(&self, value: MilliBel, dir: Round) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_capture_dB_all(self.handle, *value as c_long, dir as c_int)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_capture_volume(&self, channel: SelemChannelId, value: i64) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_capture_volume(self.handle, channel as i32, value as c_long)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_capture_volume_range(&self, min: i64, max: i64) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_capture_volume_range(self.handle, min as c_long, max as c_long)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_playback_switch(&self, channel: SelemChannelId, value: i32) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_playback_switch(self.handle, channel as i32, value)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_playback_switch_all(&self, value: i32) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_playback_switch_all(self.handle, value)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_capture_switch(&self, channel: SelemChannelId, value: i32) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_capture_switch(self.handle, channel as i32, value)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn set_capture_switch_all(&self, value: i32) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_capture_switch_all(self.handle, value)).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn get_playback_switch(&self, channel: SelemChannelId) -> Result<i32> {
|
||||
let mut value: i32 = 0;
|
||||
acheck!(snd_mixer_selem_get_playback_switch(self.handle, channel as i32, &mut value)).map(|_| value)
|
||||
}
|
||||
|
||||
pub fn get_capture_switch(&self, channel: SelemChannelId) -> Result<i32> {
|
||||
let mut value: i32 = 0;
|
||||
acheck!(snd_mixer_selem_get_capture_switch(self.handle, channel as i32, &mut value)).map(|_| value)
|
||||
}
|
||||
|
||||
pub fn is_enumerated(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_is_enumerated(self.handle) == 1 }
|
||||
}
|
||||
|
||||
pub fn is_enum_playback(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_is_enum_playback(self.handle) == 1 }
|
||||
}
|
||||
|
||||
pub fn is_enum_capture(&self) -> bool {
|
||||
unsafe { alsa::snd_mixer_selem_is_enum_capture(self.handle) == 1 }
|
||||
}
|
||||
|
||||
pub fn get_enum_items(&self) -> Result<u32> {
|
||||
acheck!(snd_mixer_selem_get_enum_items(self.handle)).map(|v| v as u32)
|
||||
}
|
||||
|
||||
pub fn get_enum_item_name(&self, idx: u32) -> Result<String> {
|
||||
let mut temp = [0 as ::libc::c_char; 128];
|
||||
acheck!(snd_mixer_selem_get_enum_item_name(self.handle, idx, temp.len()-1, temp.as_mut_ptr()))
|
||||
.and_then(|_| from_const("snd_mixer_selem_get_enum_item_name", temp.as_ptr()))
|
||||
.map(|v| v.into())
|
||||
}
|
||||
|
||||
/// Enumerates over valid Enum values
|
||||
pub fn iter_enum(&self) -> Result<IterEnum> {
|
||||
Ok(IterEnum(self, 0, self.get_enum_items()?))
|
||||
}
|
||||
|
||||
pub fn get_enum_item(&self, channel: SelemChannelId) -> Result<u32> {
|
||||
let mut temp = 0;
|
||||
acheck!(snd_mixer_selem_get_enum_item(self.handle, channel as i32, &mut temp))
|
||||
.map(|_| temp)
|
||||
}
|
||||
|
||||
pub fn set_enum_item(&self, channel: SelemChannelId, idx: u32) -> Result<()> {
|
||||
acheck!(snd_mixer_selem_set_enum_item(self.handle, channel as i32, idx))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ops::Deref for Selem<'a> {
|
||||
type Target = Elem<'a>;
|
||||
|
||||
/// returns the elem of this selem
|
||||
fn deref(&self) -> &Elem<'a> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IterEnum<'a>(&'a Selem<'a>, u32, u32);
|
||||
|
||||
impl<'a> Iterator for IterEnum<'a> {
|
||||
type Item = Result<String>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.1 >= self.2 { None }
|
||||
else { self.1 += 1; Some(self.0.get_enum_item_name(self.1-1)) }
|
||||
}
|
||||
}
|
||||
|
||||
alsa_enum!(
|
||||
/// Wrapper for [SND_MIXER_SCHN_*](http://www.alsa-project.org/alsa-doc/alsa-lib/group___simple_mixer.html) constants
|
||||
SelemChannelId, ALL_SELEM_CHANNEL_ID[11],
|
||||
|
||||
Unknown = SND_MIXER_SCHN_UNKNOWN,
|
||||
FrontLeft = SND_MIXER_SCHN_FRONT_LEFT,
|
||||
FrontRight = SND_MIXER_SCHN_FRONT_RIGHT,
|
||||
RearLeft = SND_MIXER_SCHN_REAR_LEFT,
|
||||
RearRight = SND_MIXER_SCHN_REAR_RIGHT,
|
||||
FrontCenter = SND_MIXER_SCHN_FRONT_CENTER,
|
||||
Woofer = SND_MIXER_SCHN_WOOFER,
|
||||
SideLeft = SND_MIXER_SCHN_SIDE_LEFT,
|
||||
SideRight = SND_MIXER_SCHN_SIDE_RIGHT,
|
||||
RearCenter = SND_MIXER_SCHN_REAR_CENTER,
|
||||
Last = SND_MIXER_SCHN_LAST,
|
||||
);
|
||||
|
||||
impl SelemChannelId {
|
||||
pub fn mono() -> SelemChannelId { SelemChannelId::FrontLeft }
|
||||
}
|
||||
|
||||
impl fmt::Display for SelemChannelId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Selem::channel_name(*self).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_mixer_of_cards() {
|
||||
use super::card;
|
||||
|
||||
for card in card::Iter::new().map(|c| c.unwrap()) {
|
||||
println!("Card #{}: {} ({})", card.get_index(), card.get_name().unwrap(), card.get_longname().unwrap());
|
||||
|
||||
let mixer = Mixer::new(&format!("hw:{}", card.get_index()), false).unwrap();
|
||||
for selem in mixer.iter().filter_map(|e| Selem::new(e)) {
|
||||
|
||||
let sid = selem.get_id();
|
||||
println!("\tMixer element {},{}:", sid.get_name().unwrap(), sid.get_index());
|
||||
|
||||
if selem.has_volume() {
|
||||
print!("\t Volume limits: ");
|
||||
if selem.has_capture_volume() {
|
||||
let (vmin, vmax) = selem.get_capture_volume_range();
|
||||
let (mbmin, mbmax) = selem.get_capture_db_range();
|
||||
print!("Capture = {} - {}", vmin, vmax);
|
||||
print!(" ({} dB - {} dB)", mbmin.to_db(), mbmax.to_db());
|
||||
}
|
||||
if selem.has_playback_volume() {
|
||||
let (vmin, vmax) = selem.get_playback_volume_range();
|
||||
let (mbmin, mbmax) = selem.get_playback_db_range();
|
||||
print!("Playback = {} - {}", vmin, vmax);
|
||||
print!(" ({} dB - {} dB)", mbmin.to_db(), mbmax.to_db());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if selem.is_enumerated() {
|
||||
print!("\t Valid values: ");
|
||||
for v in selem.iter_enum().unwrap() { print!("{}, ", v.unwrap()) };
|
||||
print!("\n\t Current values: ");
|
||||
for v in SelemChannelId::all().iter().filter_map(|&v| selem.get_enum_item(v).ok()) {
|
||||
print!("{}, ", selem.get_enum_item_name(v).unwrap());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if selem.can_capture() {
|
||||
print!("\t Capture channels: ");
|
||||
for channel in SelemChannelId::all() {
|
||||
if selem.has_capture_channel(*channel) { print!("{}, ", channel) };
|
||||
}
|
||||
println!();
|
||||
print!("\t Capture volumes: ");
|
||||
for channel in SelemChannelId::all() {
|
||||
if selem.has_capture_channel(*channel) { print!("{}: {} ({} dB), ", channel,
|
||||
match selem.get_capture_volume(*channel) {Ok(v) => format!("{}", v), Err(_) => "n/a".to_string()},
|
||||
match selem.get_capture_vol_db(*channel) {Ok(v) => format!("{}", v.to_db()), Err(_) => "n/a".to_string()}
|
||||
);}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if selem.can_playback() {
|
||||
print!("\t Playback channels: ");
|
||||
if selem.is_playback_mono() {
|
||||
print!("Mono");
|
||||
} else {
|
||||
for channel in SelemChannelId::all() {
|
||||
if selem.has_playback_channel(*channel) { print!("{}, ", channel) };
|
||||
}
|
||||
}
|
||||
println!();
|
||||
if selem.has_playback_volume() {
|
||||
print!("\t Playback volumes: ");
|
||||
for channel in SelemChannelId::all() {
|
||||
if selem.has_playback_channel(*channel) { print!("{}: {} / {}dB, ",
|
||||
channel,
|
||||
match selem.get_playback_volume(*channel) {Ok(v) => format!("{}", v), Err(_) => "n/a".to_string()},
|
||||
match selem.get_playback_vol_db(*channel) {Ok(v) => format!("{}", v.to_db()), Err(_) => "n/a".to_string()}
|
||||
);}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn get_and_set_playback_volume() {
|
||||
let mixer = Mixer::new("hw:1", false).unwrap();
|
||||
let selem = mixer.find_selem(&SelemId::new("Master", 0)).unwrap();
|
||||
|
||||
let (rmin, rmax) = selem.get_playback_volume_range();
|
||||
let mut channel = SelemChannelId::mono();
|
||||
for c in SelemChannelId::all().iter() {
|
||||
if selem.has_playback_channel(*c) { channel = *c; break }
|
||||
}
|
||||
println!("Testing on {} with limits {}-{} on channel {}", selem.get_id().get_name().unwrap(), rmin, rmax, channel);
|
||||
|
||||
let old: i64 = selem.get_playback_volume(channel).unwrap();
|
||||
let new: i64 = rmax / 2;
|
||||
assert_ne!(new, old);
|
||||
|
||||
println!("Changing volume of {} from {} to {}", channel, old, new);
|
||||
selem.set_playback_volume(channel, new).unwrap();
|
||||
let mut result: i64 = selem.get_playback_volume(channel).unwrap();
|
||||
assert_eq!(new, result);
|
||||
|
||||
// return volume to old value
|
||||
selem.set_playback_volume(channel, old).unwrap();
|
||||
result = selem.get_playback_volume(channel).unwrap();
|
||||
assert_eq!(old, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn get_and_set_capture_volume() {
|
||||
let mixer = Mixer::new("hw:1", false).unwrap();
|
||||
let selem = mixer.find_selem(&SelemId::new("Capture", 0)).unwrap();
|
||||
|
||||
let (rmin, rmax) = selem.get_capture_volume_range();
|
||||
let mut channel = SelemChannelId::mono();
|
||||
for c in SelemChannelId::all().iter() {
|
||||
if selem.has_playback_channel(*c) { channel = *c; break }
|
||||
}
|
||||
println!("Testing on {} with limits {}-{} on channel {}", selem.get_id().get_name().unwrap(), rmin, rmax, channel);
|
||||
|
||||
let old: i64 = selem.get_capture_volume(channel).unwrap();
|
||||
let new: i64 = rmax / 2;
|
||||
assert_ne!(new, old);
|
||||
|
||||
println!("Changing volume of {} from {} to {}", channel, old, new);
|
||||
selem.set_capture_volume(channel, new).unwrap();
|
||||
let mut result: i64 = selem.get_capture_volume(channel).unwrap();
|
||||
assert_eq!(new, result);
|
||||
|
||||
// return volume to old value
|
||||
selem.set_capture_volume(channel, old).unwrap();
|
||||
result = selem.get_capture_volume(channel).unwrap();
|
||||
assert_eq!(old, result);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn print_sizeof() {
|
||||
let selemid = unsafe { alsa::snd_mixer_selem_id_sizeof() } as usize;
|
||||
|
||||
assert!(selemid <= SELEM_ID_SIZE);
|
||||
println!("Selem id: {}", selemid);
|
||||
}
|
1151
alsa/src/pcm.rs
Normal file
1151
alsa/src/pcm.rs
Normal file
File diff suppressed because it is too large
Load diff
68
alsa/src/poll.rs
Normal file
68
alsa/src/poll.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
//! Tiny poll ffi
|
||||
//!
|
||||
//! A tiny wrapper around libc's poll system call.
|
||||
|
||||
use libc;
|
||||
use super::error::*;
|
||||
use std::io;
|
||||
pub use libc::pollfd;
|
||||
|
||||
|
||||
bitflags! {
|
||||
pub struct Flags: ::libc::c_short {
|
||||
const IN = ::libc::POLLIN;
|
||||
const PRI = ::libc::POLLPRI;
|
||||
const OUT = ::libc::POLLOUT;
|
||||
const ERR = ::libc::POLLERR;
|
||||
const HUP = ::libc::POLLHUP;
|
||||
const NVAL = ::libc::POLLNVAL;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Descriptors {
|
||||
fn count(&self) -> usize;
|
||||
fn fill(&self, _: &mut [pollfd]) -> Result<usize>;
|
||||
fn revents(&self, _: &[pollfd]) -> Result<Flags>;
|
||||
|
||||
/// Wrapper around count and fill - returns an array of pollfds
|
||||
fn get(&self) -> Result<Vec<pollfd>> {
|
||||
let mut v = vec![pollfd { fd: 0, events: 0, revents: 0 }; self.count()];
|
||||
if self.fill(&mut v)? != v.len() { Err(Error::unsupported("did not fill the poll descriptors array")) }
|
||||
else { Ok(v) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Descriptors for pollfd {
|
||||
fn count(&self) -> usize { 1 }
|
||||
fn fill(&self, a: &mut [pollfd]) -> Result<usize> { a[0] = *self; Ok(1) }
|
||||
fn revents(&self, a: &[pollfd]) -> Result<Flags> { Ok(Flags::from_bits_truncate(a[0].revents)) }
|
||||
}
|
||||
|
||||
/// Wrapper around the libc poll call.
|
||||
pub fn poll(fds: &mut[pollfd], timeout: i32) -> Result<usize> {
|
||||
let r = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, timeout as libc::c_int) };
|
||||
if r >= 0 { Ok(r as usize) } else {
|
||||
from_code("poll", -io::Error::last_os_error().raw_os_error().unwrap()).map(|_| unreachable!())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a pollfd array, polls it, and returns the poll descriptors which have non-zero revents.
|
||||
pub fn poll_all<'a>(desc: &[&'a dyn Descriptors], timeout: i32) -> Result<Vec<(&'a dyn Descriptors, Flags)>> {
|
||||
|
||||
let mut pollfds: Vec<pollfd> = vec!();
|
||||
let mut indices = vec!();
|
||||
for v2 in desc.iter().map(|q| q.get()) {
|
||||
let v = v2?;
|
||||
indices.push(pollfds.len() .. pollfds.len()+v.len());
|
||||
pollfds.extend(v);
|
||||
};
|
||||
|
||||
poll(&mut pollfds, timeout)?;
|
||||
|
||||
let mut res = vec!();
|
||||
for (i, r) in indices.into_iter().enumerate() {
|
||||
let z = desc[i].revents(&pollfds[r])?;
|
||||
if !z.is_empty() { res.push((desc[i], z)); }
|
||||
}
|
||||
Ok(res)
|
||||
}
|
211
alsa/src/rawmidi.rs
Normal file
211
alsa/src/rawmidi.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
//! MIDI devices I/O and enumeration
|
||||
|
||||
use libc::{c_int, c_uint, c_void, size_t, c_short, pollfd};
|
||||
use super::ctl_int::{ctl_ptr, Ctl};
|
||||
use super::{Direction, poll};
|
||||
use super::error::*;
|
||||
use crate::alsa;
|
||||
use std::{ptr, io};
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
/// Iterator over [Rawmidi](http://www.alsa-project.org/alsa-doc/alsa-lib/group___raw_midi.html) devices and subdevices
|
||||
pub struct Iter<'a> {
|
||||
ctl: &'a Ctl,
|
||||
device: c_int,
|
||||
in_count: i32,
|
||||
out_count: i32,
|
||||
current: i32,
|
||||
}
|
||||
|
||||
/// [snd_rawmidi_info_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___raw_midi.html) wrapper
|
||||
pub struct Info(*mut alsa::snd_rawmidi_info_t);
|
||||
|
||||
impl Drop for Info {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_rawmidi_info_free(self.0) }; }
|
||||
}
|
||||
|
||||
impl Info {
|
||||
fn new() -> Result<Info> {
|
||||
let mut p = ptr::null_mut();
|
||||
acheck!(snd_rawmidi_info_malloc(&mut p)).map(|_| Info(p))
|
||||
}
|
||||
|
||||
fn from_iter(c: &Ctl, device: i32, sub: i32, dir: Direction) -> Result<Info> {
|
||||
let r = Info::new()?;
|
||||
unsafe { alsa::snd_rawmidi_info_set_device(r.0, device as c_uint) };
|
||||
let d = match dir {
|
||||
Direction::Playback => alsa::SND_RAWMIDI_STREAM_OUTPUT,
|
||||
Direction::Capture => alsa::SND_RAWMIDI_STREAM_INPUT,
|
||||
};
|
||||
unsafe { alsa::snd_rawmidi_info_set_stream(r.0, d) };
|
||||
unsafe { alsa::snd_rawmidi_info_set_subdevice(r.0, sub as c_uint) };
|
||||
acheck!(snd_ctl_rawmidi_info(ctl_ptr(c), r.0)).map(|_| r)
|
||||
}
|
||||
|
||||
fn subdev_count(c: &Ctl, device: c_int) -> Result<(i32, i32)> {
|
||||
let i = Info::from_iter(c, device, 0, Direction::Capture)?;
|
||||
let o = Info::from_iter(c, device, 0, Direction::Playback)?;
|
||||
Ok((unsafe { alsa::snd_rawmidi_info_get_subdevices_count(o.0) as i32 },
|
||||
unsafe { alsa::snd_rawmidi_info_get_subdevices_count(i.0) as i32 }))
|
||||
}
|
||||
|
||||
pub fn get_device(&self) -> i32 { unsafe { alsa::snd_rawmidi_info_get_device(self.0) as i32 }}
|
||||
pub fn get_subdevice(&self) -> i32 { unsafe { alsa::snd_rawmidi_info_get_subdevice(self.0) as i32 }}
|
||||
pub fn get_stream(&self) -> super::Direction {
|
||||
if unsafe { alsa::snd_rawmidi_info_get_stream(self.0) } == alsa::SND_RAWMIDI_STREAM_OUTPUT { super::Direction::Playback }
|
||||
else { super::Direction::Capture }
|
||||
}
|
||||
|
||||
pub fn get_subdevice_name(&self) -> Result<String> {
|
||||
let c = unsafe { alsa::snd_rawmidi_info_get_subdevice_name(self.0) };
|
||||
from_const("snd_rawmidi_info_get_subdevice_name", c).map(|s| s.to_string())
|
||||
}
|
||||
pub fn get_id(&self) -> Result<String> {
|
||||
let c = unsafe { alsa::snd_rawmidi_info_get_id(self.0) };
|
||||
from_const("snd_rawmidi_info_get_id", c).map(|s| s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// [snd_rawmidi_info_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___raw_midi.html) wrapper
|
||||
pub struct Status(*mut alsa::snd_rawmidi_status_t);
|
||||
|
||||
impl Status {
|
||||
fn new() -> Result<Self> {
|
||||
let mut p = ptr::null_mut();
|
||||
acheck!(snd_rawmidi_status_malloc(&mut p)).map(|_| Status(p))
|
||||
}
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn get_avail(&self) -> usize { unsafe { alsa::snd_rawmidi_status_get_avail(self.0 as *const _) } }
|
||||
pub fn get_xruns(&self) -> usize { unsafe { alsa::snd_rawmidi_status_get_xruns(self.0 as *const _) } }
|
||||
}
|
||||
|
||||
impl Drop for Status {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_rawmidi_status_free(self.0) }; }
|
||||
}
|
||||
|
||||
|
||||
impl<'a> Iter<'a> {
|
||||
pub fn new(c: &'a Ctl) -> Iter<'a> { Iter { ctl: c, device: -1, in_count: 0, out_count: 0, current: 0 }}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = Result<Info>;
|
||||
fn next(&mut self) -> Option<Result<Info>> {
|
||||
if self.current < self.in_count {
|
||||
self.current += 1;
|
||||
return Some(Info::from_iter(self.ctl, self.device, self.current-1, Direction::Capture));
|
||||
}
|
||||
if self.current - self.in_count < self.out_count {
|
||||
self.current += 1;
|
||||
return Some(Info::from_iter(self.ctl, self.device, self.current-1-self.in_count, Direction::Playback));
|
||||
}
|
||||
|
||||
let r = acheck!(snd_ctl_rawmidi_next_device(ctl_ptr(self.ctl), &mut self.device));
|
||||
match r {
|
||||
Err(e) => return Some(Err(e)),
|
||||
Ok(_) if self.device == -1 => return None,
|
||||
_ => {},
|
||||
}
|
||||
self.current = 0;
|
||||
match Info::subdev_count(self.ctl, self.device) {
|
||||
Err(e) => Some(Err(e)),
|
||||
Ok((oo, ii)) => {
|
||||
self.in_count = ii;
|
||||
self.out_count = oo;
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [snd_rawmidi_t](http://www.alsa-project.org/alsa-doc/alsa-lib/group___raw_midi.html) wrapper
|
||||
pub struct Rawmidi(*mut alsa::snd_rawmidi_t);
|
||||
|
||||
unsafe impl Send for Rawmidi {}
|
||||
|
||||
impl Drop for Rawmidi {
|
||||
fn drop(&mut self) { unsafe { alsa::snd_rawmidi_close(self.0) }; }
|
||||
}
|
||||
|
||||
impl Rawmidi {
|
||||
|
||||
/// Wrapper around open that takes a &str instead of a &CStr
|
||||
pub fn new(name: &str, dir: Direction, nonblock: bool) -> Result<Self> {
|
||||
Self::open(&CString::new(name).unwrap(), dir, nonblock)
|
||||
}
|
||||
|
||||
pub fn open(name: &CStr, dir: Direction, nonblock: bool) -> Result<Rawmidi> {
|
||||
let mut h = ptr::null_mut();
|
||||
let flags = if nonblock { 2 } else { 0 }; // FIXME: alsa::SND_RAWMIDI_NONBLOCK does not exist in alsa-sys
|
||||
acheck!(snd_rawmidi_open(
|
||||
if dir == Direction::Capture { &mut h } else { ptr::null_mut() },
|
||||
if dir == Direction::Playback { &mut h } else { ptr::null_mut() },
|
||||
name.as_ptr(), flags))
|
||||
.map(|_| Rawmidi(h))
|
||||
}
|
||||
|
||||
pub fn info(&self) -> Result<Info> {
|
||||
Info::new().and_then(|i| acheck!(snd_rawmidi_info(self.0, i.0)).map(|_| i))
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Result<Status> {
|
||||
Status::new().and_then(|i| acheck!(snd_rawmidi_status(self.0, i.0)).map(|_| i))
|
||||
}
|
||||
|
||||
pub fn drop(&self) -> Result<()> { acheck!(snd_rawmidi_drop(self.0)).map(|_| ()) }
|
||||
pub fn drain(&self) -> Result<()> { acheck!(snd_rawmidi_drain(self.0)).map(|_| ()) }
|
||||
pub fn name(&self) -> Result<String> {
|
||||
let c = unsafe { alsa::snd_rawmidi_name(self.0) };
|
||||
from_const("snd_rawmidi_name", c).map(|s| s.to_string())
|
||||
}
|
||||
|
||||
pub fn io(&self) -> IO { IO(self) }
|
||||
}
|
||||
|
||||
impl poll::Descriptors for Rawmidi {
|
||||
fn count(&self) -> usize {
|
||||
unsafe { alsa::snd_rawmidi_poll_descriptors_count(self.0) as usize }
|
||||
}
|
||||
fn fill(&self, p: &mut [pollfd]) -> Result<usize> {
|
||||
let z = unsafe { alsa::snd_rawmidi_poll_descriptors(self.0, p.as_mut_ptr(), p.len() as c_uint) };
|
||||
from_code("snd_rawmidi_poll_descriptors", z).map(|_| z as usize)
|
||||
}
|
||||
fn revents(&self, p: &[pollfd]) -> Result<poll::Flags> {
|
||||
let mut r = 0;
|
||||
let z = unsafe { alsa::snd_rawmidi_poll_descriptors_revents(self.0, p.as_ptr() as *mut pollfd, p.len() as c_uint, &mut r) };
|
||||
from_code("snd_rawmidi_poll_descriptors_revents", z).map(|_| poll::Flags::from_bits_truncate(r as c_short))
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `std::io::Read` and `std::io::Write` for `Rawmidi`
|
||||
pub struct IO<'a>(&'a Rawmidi);
|
||||
|
||||
impl<'a> io::Read for IO<'a> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let r = unsafe { alsa::snd_rawmidi_read((self.0).0, buf.as_mut_ptr() as *mut c_void, buf.len() as size_t) };
|
||||
if r < 0 { Err(io::Error::from_raw_os_error(r as i32)) }
|
||||
else { Ok(r as usize) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> io::Write for IO<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let r = unsafe { alsa::snd_rawmidi_write((self.0).0, buf.as_ptr() as *const c_void, buf.len() as size_t) };
|
||||
if r < 0 { Err(io::Error::from_raw_os_error(r as i32)) }
|
||||
else { Ok(r as usize) }
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> { Ok(()) }
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn print_rawmidis() {
|
||||
for a in super::card::Iter::new().map(|a| a.unwrap()) {
|
||||
for b in Iter::new(&Ctl::from_card(&a, false).unwrap()).map(|b| b.unwrap()) {
|
||||
println!("Rawmidi {:?} (hw:{},{},{}) {} - {}", b.get_stream(), a.get_index(), b.get_device(), b.get_subdevice(),
|
||||
a.get_name().unwrap(), b.get_subdevice_name().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
1567
alsa/src/seq.rs
Normal file
1567
alsa/src/seq.rs
Normal file
File diff suppressed because it is too large
Load diff
9
alsa/synth-example/Cargo.toml
Normal file
9
alsa/synth-example/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "synth-example"
|
||||
version = "0.1.0"
|
||||
authors = ["David Henningsson <coding@diwic.se>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
dasp = { version = "0.11", features = ["signal"] }
|
||||
alsa = { path = "..", version = "0.6" }
|
311
alsa/synth-example/src/main.rs
Normal file
311
alsa/synth-example/src/main.rs
Normal file
|
@ -0,0 +1,311 @@
|
|||
// A quickly made Hammond organ.
|
||||
|
||||
use std::{iter, error};
|
||||
use alsa::{seq, pcm};
|
||||
use std::ffi::CString;
|
||||
use dasp::signal;
|
||||
|
||||
type Res<T> = Result<T, Box<dyn error::Error>>;
|
||||
|
||||
fn connect_midi_source_ports(s: &alsa::Seq, our_port: i32) -> Res<()> {
|
||||
// Iterate over clients and clients' ports
|
||||
let our_id = s.client_id()?;
|
||||
let ci = seq::ClientIter::new(&s);
|
||||
for client in ci {
|
||||
if client.get_client() == our_id { continue; } // Skip ourselves
|
||||
let pi = seq::PortIter::new(&s, client.get_client());
|
||||
for port in pi {
|
||||
let caps = port.get_capability();
|
||||
|
||||
// Check that it's a normal input port
|
||||
if !caps.contains(seq::PortCap::READ) || !caps.contains(seq::PortCap::SUBS_READ) { continue; }
|
||||
if !port.get_type().contains(seq::PortType::MIDI_GENERIC) { continue; }
|
||||
|
||||
// Connect source and dest ports
|
||||
let subs = seq::PortSubscribe::empty()?;
|
||||
subs.set_sender(seq::Addr { client: port.get_client(), port: port.get_port() });
|
||||
subs.set_dest(seq::Addr { client: our_id, port: our_port });
|
||||
println!("Reading from midi input {:?}", port);
|
||||
s.subscribe_port(&subs)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_midi_dev() -> Res<alsa::Seq> {
|
||||
// Open the sequencer.
|
||||
let s = alsa::Seq::open(None, Some(alsa::Direction::Capture), true)?;
|
||||
let cstr = CString::new("rust_synth_example").unwrap();
|
||||
s.set_client_name(&cstr)?;
|
||||
|
||||
// Create a destination port we can read from
|
||||
let mut dinfo = seq::PortInfo::empty().unwrap();
|
||||
dinfo.set_capability(seq::PortCap::WRITE | seq::PortCap::SUBS_WRITE);
|
||||
dinfo.set_type(seq::PortType::MIDI_GENERIC | seq::PortType::APPLICATION);
|
||||
dinfo.set_name(&cstr);
|
||||
s.create_port(&dinfo).unwrap();
|
||||
let dport = dinfo.get_port();
|
||||
|
||||
// source ports should ideally be configurable, but right now we're just reading them all.
|
||||
connect_midi_source_ports(&s, dport)?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn open_audio_dev() -> Res<(alsa::PCM, u32)> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
if args.len() < 2 {
|
||||
println!("Usage: 'cargo run --release CARD_NAME SAMPLE_RATE BUF_SIZE'");
|
||||
Err("No card name specified")?
|
||||
}
|
||||
let req_devname = format!("hw:{}", args[1]);
|
||||
let req_samplerate = args.get(2).map(|x| x.parse()).unwrap_or(Ok(48000))?;
|
||||
let req_bufsize = args.get(3).map(|x| x.parse()).unwrap_or(Ok(256))?; // A few ms latency by default, that should be nice
|
||||
|
||||
// Open the device
|
||||
let p = alsa::PCM::new(&req_devname, alsa::Direction::Playback, false)?;
|
||||
|
||||
// Set hardware parameters
|
||||
{
|
||||
let hwp = pcm::HwParams::any(&p)?;
|
||||
hwp.set_channels(2)?;
|
||||
hwp.set_rate(req_samplerate, alsa::ValueOr::Nearest)?;
|
||||
hwp.set_format(pcm::Format::s16())?;
|
||||
hwp.set_access(pcm::Access::MMapInterleaved)?;
|
||||
hwp.set_buffer_size(req_bufsize)?;
|
||||
hwp.set_period_size(req_bufsize / 4, alsa::ValueOr::Nearest)?;
|
||||
p.hw_params(&hwp)?;
|
||||
}
|
||||
|
||||
// Set software parameters
|
||||
let rate = {
|
||||
let hwp = p.hw_params_current()?;
|
||||
let swp = p.sw_params_current()?;
|
||||
let (bufsize, periodsize) = (hwp.get_buffer_size()?, hwp.get_period_size()?);
|
||||
swp.set_start_threshold(bufsize - periodsize)?;
|
||||
swp.set_avail_min(periodsize)?;
|
||||
p.sw_params(&swp)?;
|
||||
println!("Opened audio output {:?} with parameters: {:?}, {:?}", req_devname, hwp, swp);
|
||||
hwp.get_rate()?
|
||||
};
|
||||
|
||||
Ok((p, rate))
|
||||
}
|
||||
|
||||
// Sample format
|
||||
type SF = i16;
|
||||
|
||||
type SigGen = signal::Sine<signal::ConstHz>;
|
||||
|
||||
// Standard Hammond drawbar.
|
||||
const BAR_FREQS: [f64; 9] = [16., 5.+1./3., 8., 4., 2.+2./3., 2., 1.+3./5., 1.+1./3., 1.];
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Sig {
|
||||
note: u8,
|
||||
sig: SigGen,
|
||||
targetvol: f64,
|
||||
curvol: f64,
|
||||
baridx: usize,
|
||||
}
|
||||
|
||||
|
||||
struct Synth {
|
||||
sigs: Vec<Option<Sig>>,
|
||||
sample_rate: signal::Rate,
|
||||
stored_sample: Option<SF>,
|
||||
bar_values: [f64; 9],
|
||||
}
|
||||
|
||||
impl Synth {
|
||||
fn add_note(&mut self, note: u8, vol: f64) {
|
||||
let hz = 440. * 2_f64.powf((note as f64 - 69.)/12.);
|
||||
|
||||
for (baridx, barfreq) in BAR_FREQS.iter().enumerate() {
|
||||
let idx = self.sigs.iter().position(|s| s.is_none());
|
||||
let idx = if let Some(idx) = idx { idx } else {
|
||||
println!("Voice overflow!"); return;
|
||||
};
|
||||
let hz = self.sample_rate.const_hz(hz * 8. / barfreq);
|
||||
let s = Sig { sig: hz.sine(), note, targetvol: vol, curvol: 0., baridx };
|
||||
self.sigs[idx] = Some(s);
|
||||
}
|
||||
}
|
||||
fn remove_note(&mut self, note: u8) {
|
||||
for i in self.sigs.iter_mut() {
|
||||
if let &mut Some(ref mut i) = i {
|
||||
if i.note == note { i.targetvol = 0. }
|
||||
}
|
||||
}
|
||||
}
|
||||
fn cc(&mut self, ctrl: u32, value: i32) {
|
||||
let idx = match ctrl {
|
||||
// Standard knobs on UMA25S, modify to your liking
|
||||
1 => 0,
|
||||
74 => 1,
|
||||
71 => 2,
|
||||
73 => 3,
|
||||
75 => 4,
|
||||
72 => 5,
|
||||
91 => 6,
|
||||
93 => 7,
|
||||
10 => 8,
|
||||
_ => return,
|
||||
};
|
||||
self.bar_values[idx] = f64::from(value) / 255.;
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Synth {
|
||||
type Item = SF;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use dasp::{signal::Signal, Sample};
|
||||
|
||||
// Mono -> Stereo
|
||||
if let Some(s) = self.stored_sample.take() { return Some(s) };
|
||||
|
||||
let mut z = 0f64;
|
||||
for sig in &mut self.sigs {
|
||||
let mut remove = false;
|
||||
if let &mut Some(ref mut i) = sig {
|
||||
let barvalue = self.bar_values[i.baridx];
|
||||
if barvalue > 0.0 {
|
||||
let s = i.sig.next();
|
||||
z += s.mul_amp(i.curvol * barvalue);
|
||||
}
|
||||
|
||||
// Quick and dirty volume envelope to avoid clicks.
|
||||
if i.curvol != i.targetvol {
|
||||
if i.targetvol == 0. {
|
||||
i.curvol -= 0.002;
|
||||
if i.curvol <= 0. { remove = true; }
|
||||
} else {
|
||||
i.curvol += 0.002;
|
||||
if i.curvol >= i.targetvol { i.curvol = i.targetvol; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if remove { *sig = None };
|
||||
}
|
||||
let z = z.min(0.999).max(-0.999);
|
||||
let z: Option<SF> = Some(SF::from_sample(z));
|
||||
self.stored_sample = z;
|
||||
z
|
||||
}
|
||||
}
|
||||
|
||||
fn write_samples_direct(p: &alsa::PCM, mmap: &mut alsa::direct::pcm::MmapPlayback<SF>, synth: &mut Synth)
|
||||
-> Res<bool> {
|
||||
|
||||
if mmap.avail() > 0 {
|
||||
// Write samples to DMA area from iterator
|
||||
mmap.write(synth);
|
||||
}
|
||||
use alsa::pcm::State;
|
||||
match mmap.status().state() {
|
||||
State::Running => { return Ok(false); }, // All fine
|
||||
State::Prepared => { println!("Starting audio output stream"); p.start()? },
|
||||
State::XRun => { println!("Underrun in audio output stream!"); p.prepare()? },
|
||||
State::Suspended => { println!("Resuming audio output stream"); p.resume()? },
|
||||
n @ _ => Err(format!("Unexpected pcm state {:?}", n))?,
|
||||
}
|
||||
Ok(true) // Call us again, please, there might be more data to write
|
||||
}
|
||||
|
||||
fn write_samples_io(p: &alsa::PCM, io: &mut alsa::pcm::IO<SF>, synth: &mut Synth) -> Res<bool> {
|
||||
let avail = match p.avail_update() {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
println!("Recovering from {}", e);
|
||||
p.recover(e.errno() as std::os::raw::c_int, true)?;
|
||||
p.avail_update()?
|
||||
}
|
||||
} as usize;
|
||||
|
||||
if avail > 0 {
|
||||
io.mmap(avail, |buf| {
|
||||
for sample in buf.iter_mut() {
|
||||
*sample = synth.next().unwrap()
|
||||
};
|
||||
buf.len() / 2
|
||||
})?;
|
||||
}
|
||||
use alsa::pcm::State;
|
||||
match p.state() {
|
||||
State::Running => Ok(false), // All fine
|
||||
State::Prepared => { println!("Starting audio output stream"); p.start()?; Ok(true) },
|
||||
State::Suspended | State::XRun => Ok(true), // Recover from this in next round
|
||||
n @ _ => Err(format!("Unexpected pcm state {:?}", n))?,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_midi_event(input: &mut seq::Input, synth: &mut Synth) -> Res<bool> {
|
||||
if input.event_input_pending(true)? == 0 { return Ok(false); }
|
||||
let ev = input.event_input()?;
|
||||
// println!("Received: {:?}", ev);
|
||||
match ev.get_type() {
|
||||
seq::EventType::Noteon => {
|
||||
let data: seq::EvNote = ev.get_data().unwrap();
|
||||
if data.velocity == 0 {
|
||||
synth.remove_note(data.note);
|
||||
} else {
|
||||
synth.add_note(data.note, f64::from(data.velocity + 64) / 2048.);
|
||||
}
|
||||
},
|
||||
seq::EventType::Noteoff => {
|
||||
let data: seq::EvNote = ev.get_data().unwrap();
|
||||
synth.remove_note(data.note);
|
||||
},
|
||||
seq::EventType::Controller => {
|
||||
let data: seq::EvCtrl = ev.get_data().unwrap();
|
||||
synth.cc(data.param, data.value);
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
fn run() -> Res<()> {
|
||||
let (audio_dev, rate) = open_audio_dev()?;
|
||||
let midi_dev = open_midi_dev()?;
|
||||
|
||||
let mut midi_input = midi_dev.input();
|
||||
|
||||
// 256 Voices synth
|
||||
let mut synth = Synth {
|
||||
sigs: iter::repeat(None).take(256).collect(),
|
||||
sample_rate: signal::rate(f64::from(rate)),
|
||||
stored_sample: None,
|
||||
bar_values: [1., 0.75, 1., 0.75, 0., 0., 0., 0., 0.75], // Some Gospel-ish default.
|
||||
};
|
||||
|
||||
// Create an array of fds to poll.
|
||||
use alsa::PollDescriptors;
|
||||
let mut fds = audio_dev.get()?;
|
||||
fds.append(&mut (&midi_dev, Some(alsa::Direction::Capture)).get()?);
|
||||
|
||||
// Let's use the fancy new "direct mode" for minimum overhead!
|
||||
let mut mmap = audio_dev.direct_mmap_playback::<SF>();
|
||||
|
||||
// Direct mode unavailable, use alsa-lib's mmap emulation instead
|
||||
let mut io = if mmap.is_err() {
|
||||
Some(audio_dev.io_i16()?)
|
||||
} else { None };
|
||||
|
||||
loop {
|
||||
if let Ok(ref mut mmap) = mmap {
|
||||
if write_samples_direct(&audio_dev, mmap, &mut synth)? { continue; }
|
||||
} else if let Some(ref mut io) = io {
|
||||
if write_samples_io(&audio_dev, io, &mut synth)? { continue; }
|
||||
}
|
||||
if read_midi_event(&mut midi_input, &mut synth)? { continue; }
|
||||
// Nothing to do, let's sleep until woken up by the kernel.
|
||||
alsa::poll::poll(&mut fds, 100)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() { println!("Error: {}", e); }
|
||||
}
|
59
j314.conf
Normal file
59
j314.conf
Normal file
|
@ -0,0 +1,59 @@
|
|||
[Left Tweeter]
|
||||
r_shunt = 1
|
||||
r_dc = 2
|
||||
r_amp = 3
|
||||
tau_coil = 4
|
||||
tau_magnet = 5
|
||||
tr_coil = 6
|
||||
ramp_factor = 7
|
||||
temp_limit = 100
|
||||
|
||||
[Right Tweeter]
|
||||
r_shunt = 1
|
||||
r_dc = 2
|
||||
r_amp = 3
|
||||
tau_coil = 4
|
||||
tau_magnet = 5
|
||||
tr_coil = 6
|
||||
ramp_factor = 7
|
||||
temp_limit = 100
|
||||
|
||||
[Left Woofer 1]
|
||||
r_shunt = 1
|
||||
r_dc = 2
|
||||
r_amp = 3
|
||||
tau_coil = 4
|
||||
tau_magnet = 5
|
||||
tr_coil = 6
|
||||
ramp_factor = 7
|
||||
temp_limit = 100
|
||||
|
||||
[Right Woofer 1]
|
||||
r_shunt = 1
|
||||
r_dc = 2
|
||||
r_amp = 3
|
||||
tau_coil = 4
|
||||
tau_magnet = 5
|
||||
tr_coil = 6
|
||||
ramp_factor = 7
|
||||
temp_limit = 100
|
||||
|
||||
[Left Woofer 2]
|
||||
r_shunt = 1
|
||||
r_dc = 2
|
||||
r_amp = 3
|
||||
tau_coil = 4
|
||||
tau_magnet = 5
|
||||
tr_coil = 6
|
||||
ramp_factor = 7
|
||||
temp_limit = 100
|
||||
|
||||
[Right Woofer 2]
|
||||
r_shunt = 1
|
||||
r_dc = 2
|
||||
r_amp = 3
|
||||
tau_coil = 4
|
||||
tau_magnet = 5
|
||||
tr_coil = 6
|
||||
ramp_factor = 7
|
||||
temp_limit = 100
|
128
src/helpers.rs
Normal file
128
src/helpers.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// (C) 2022 The Asahi Linux Contributors
|
||||
|
||||
use configparser::ini::Ini;
|
||||
use alsa::mixer::MilliBel;
|
||||
|
||||
/**
|
||||
Failsafe: Limit speaker volume massively and bail.
|
||||
|
||||
TODO: enable TAS safe mode with IOCTL.
|
||||
*/
|
||||
pub fn fail() {
|
||||
println!("A catastrophic error has occurred.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
pub fn open_card(card: &str) -> alsa::ctl::Ctl {
|
||||
let ctldev: alsa::ctl::Ctl = match alsa::ctl::Ctl::new(card, false) {
|
||||
Ok(ctldev) => ctldev,
|
||||
Err(e) => {
|
||||
println!("{}: Could not open sound card! Error: {}", card, e);
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
return ctldev;
|
||||
}
|
||||
|
||||
/**
|
||||
Wrapper around configparser::ini::Ini.getfloat()
|
||||
to safely unwrap the Result<Option<f64>, E> returned by
|
||||
it.
|
||||
*/
|
||||
pub fn parse_float(config: &Ini, section: &str, key: &str) -> f64 {
|
||||
let _result: Option<f64> = match config.getfloat(section, key) {
|
||||
Ok(result) => match result{
|
||||
Some(inner) => {
|
||||
let float: f64 = inner;
|
||||
return float;
|
||||
},
|
||||
None => {
|
||||
println!("{}: Failed to parse {}", section, key);
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
},
|
||||
Err(e) => {
|
||||
println!("{}: Invalid value for {}. Error: {}", section, key, e);
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Wrapper around alsa::ctl::ElemValue::new(). Lets us bail on errors and
|
||||
pass in the Bytes type for V/ISENSE
|
||||
*/
|
||||
pub fn new_elemvalue(t: alsa::ctl::ElemType) -> alsa::ctl::ElemValue {
|
||||
let val = match alsa::ctl::ElemValue::new(t) {
|
||||
Ok(val) => val,
|
||||
Err(_e) => {
|
||||
println!("Could not open a handle to an element!");
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Wrapper for alsa::ctl::Ctl::elem_read().
|
||||
*/
|
||||
pub fn read_ev(card: &alsa::ctl::Ctl, ev: &mut alsa::ctl::ElemValue, name: &str) {
|
||||
let _val = match card.elem_read(ev) { // alsa:Result<()>
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
println!("Could not read elem value {}. alsa-lib error: {:?}", name, e);
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
Wrapper for alsa::ctl::Ctl::elem_write().
|
||||
*/
|
||||
pub fn write_ev(card: &alsa::ctl::Ctl, ev: &alsa::ctl::ElemValue, name: &str) {
|
||||
let _val = match card.elem_write(ev) { // alsa:Result<()>
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
println!("Could not write elem value {}. alsa-lib error: {:?}", name, e);
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn int_to_db(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: i32) -> MilliBel {
|
||||
let db = match card.convert_to_db(id, val.into()) {
|
||||
Ok(inner) => inner,
|
||||
Err(e) => {
|
||||
println!("Could not convert val {} to dB! alsa-lib error: {:?}", val, e);
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
pub fn db_to_int(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: f32) -> i32 {
|
||||
let mb: MilliBel = MilliBel((val * 100.0) as i64);
|
||||
let new_int = match card.convert_from_db(id, mb, alsa::Round::Floor) {
|
||||
Ok(inner) => inner as i32,
|
||||
Err(e) => {
|
||||
println!("Could not convert MilliBel {:?} to int! alsa-lib error: {:?}", val, e);
|
||||
fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
return new_int;
|
||||
}
|
80
src/main.rs
Normal file
80
src/main.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// (C) 2022 The Asahi Linux Contributors
|
||||
|
||||
|
||||
/**
|
||||
Handles speaker safety on Apple Silicon machines. This code is designed to
|
||||
fail safe. The speaker should not be enabled until this daemon has successfully
|
||||
initialised. If at any time we run into an unrecoverable error (we shouldn't),
|
||||
we gracefully bail and use an IOCTL to shut off the speakers.
|
||||
*/
|
||||
|
||||
|
||||
use std::io;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
use configparser::ini::Ini;
|
||||
|
||||
mod types;
|
||||
mod helpers;
|
||||
|
||||
use crate::types::SafetyMonitor;
|
||||
|
||||
static ASAHI_DEVICE: &str = "hw:0";
|
||||
|
||||
// Will eventually be /etc/speakersafetyd/ or similar
|
||||
static CONFIG_DIR: &str = "./";
|
||||
static SUPPORTED: [&str; 2] = [
|
||||
"j314",
|
||||
"j316",
|
||||
];
|
||||
|
||||
fn get_machine() -> String {
|
||||
let _compat: io::Result<String> = match read_to_string("/proc/device-tree/compatible") {
|
||||
Ok(compat) => {
|
||||
return compat[6..10].to_string();
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Could not read devicetree compatible: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
fn get_drivers(config: &Ini) -> Vec<String> {
|
||||
|
||||
let drivers = config.sections();
|
||||
|
||||
return drivers;
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
let model: String = get_machine();
|
||||
let mut cfg: Ini = Ini::new_cs();
|
||||
let mut speakers: Vec<types::Speaker> = Vec::new();
|
||||
let card: alsa::ctl::Ctl = helpers::open_card(&ASAHI_DEVICE);
|
||||
|
||||
if SUPPORTED.contains(&model.as_str()) {
|
||||
cfg.load(CONFIG_DIR.to_owned() + &model + ".conf").unwrap();
|
||||
} else {
|
||||
println!("Unsupported machine {}", model);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let list_drivers = get_drivers(&cfg);
|
||||
|
||||
for i in list_drivers {
|
||||
let new_speaker: types::Speaker = types::SafetyMonitor::new(&i, &cfg, &card);
|
||||
speakers.push(new_speaker);
|
||||
}
|
||||
|
||||
// Temporary to check that everything works. Threaded eventually if necessary.
|
||||
for mut i in speakers {
|
||||
i.run(&card);
|
||||
}
|
||||
|
||||
|
||||
}
|
250
src/types.rs
Normal file
250
src/types.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// (C) 2022 The Asahi Linux Contributors
|
||||
|
||||
use std::ffi::{CString, CStr};
|
||||
use half::f16;
|
||||
use configparser::ini::Ini;
|
||||
use alsa::ctl::Ctl;
|
||||
|
||||
use crate::helpers;
|
||||
|
||||
/**
|
||||
Struct with fields necessary for manipulating an ALSA elem.
|
||||
|
||||
The val field is created using a wrapper so that we can handle
|
||||
any errors. This is also necessary so that we can create one of type
|
||||
Bytes for the V/ISENSE elems.
|
||||
*/
|
||||
struct Elem {
|
||||
elem_name: String,
|
||||
id: alsa::ctl::ElemId,
|
||||
val: alsa::ctl::ElemValue,
|
||||
}
|
||||
|
||||
trait ALSAElem {
|
||||
fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Self;
|
||||
}
|
||||
|
||||
impl ALSAElem for Elem {
|
||||
fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Elem {
|
||||
// CString::new() cannot borrow a String. We want name for the elem
|
||||
// for error identification though, so it can't consume name directly.
|
||||
let borrow: String = name.clone();
|
||||
|
||||
let mut new_elem: Elem = { Elem {
|
||||
elem_name: name,
|
||||
id: alsa::ctl::ElemId::new(alsa::ctl::ElemIface::Mixer),
|
||||
val: helpers::new_elemvalue(t),
|
||||
}};
|
||||
|
||||
let cname: CString = CString::new(borrow).unwrap();
|
||||
let cstr: &CStr = cname.as_c_str();
|
||||
|
||||
new_elem.id.set_name(cstr);
|
||||
new_elem.val.set_id(&new_elem.id);
|
||||
helpers::read_ev(card, &mut new_elem.val, &new_elem.elem_name);
|
||||
|
||||
return new_elem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Mixer struct representing the controls associated with a given
|
||||
Speaker. Populated with the important ALSA controls at runtime.
|
||||
|
||||
level: mixer volume control
|
||||
vsense: VSENSE as reported by the driver (V, readonly)
|
||||
isense: ISENSE as reported by the driver (A, readonly)
|
||||
|
||||
*/
|
||||
struct Mixer {
|
||||
drv: String,
|
||||
level: Elem,
|
||||
vsense: Elem,
|
||||
isense: Elem,
|
||||
}
|
||||
|
||||
trait ALSACtl {
|
||||
fn new(name: &str, card: &Ctl) -> Self;
|
||||
|
||||
fn get_vsense(&mut self, card: &Ctl) -> f16;
|
||||
fn get_isense(&mut self, card: &Ctl) -> f16;
|
||||
fn get_lvl(&mut self, card: &Ctl) -> f32;
|
||||
fn set_lvl(&mut self, card: &Ctl, lvl: f32);
|
||||
}
|
||||
|
||||
impl ALSACtl for Mixer {
|
||||
// TODO: wire up real V/ISENSE elems (pending driver support)
|
||||
fn new(name: &str, card: &Ctl) -> Mixer {
|
||||
let new_mixer: Mixer = { Mixer {
|
||||
drv: name.to_owned(),
|
||||
level: ALSAElem::new(name.to_owned() + " Speaker Volume", card,
|
||||
alsa::ctl::ElemType::Integer),
|
||||
vsense: ALSAElem::new(name.to_owned() + " VSENSE Switch", card,
|
||||
alsa::ctl::ElemType::Boolean),
|
||||
isense: ALSAElem::new(name.to_owned() + " ISENSE Switch", card,
|
||||
alsa::ctl::ElemType::Boolean),
|
||||
}};
|
||||
|
||||
return new_mixer;
|
||||
}
|
||||
|
||||
/**
|
||||
MOCK IMPLEMENTATIONS
|
||||
|
||||
V/ISENSE are 16-bit floats sent in a 32-bit TDM slot by the codec.
|
||||
This is expressed by the driver as a byte array, with rightmost 16
|
||||
bits as padding.
|
||||
|
||||
TODO: Condense into a single function and pass in a borrowed Elem
|
||||
*/
|
||||
fn get_vsense(&mut self, card: &Ctl) -> f16 {
|
||||
helpers::read_ev(card, &mut self.vsense.val, &self.vsense.elem_name);
|
||||
let val: &[u8] = match self.vsense.val.get_bytes() {
|
||||
Some(inner) => inner,
|
||||
None => {
|
||||
println!("Could not read VSENSE from {}", self.drv);
|
||||
helpers::fail();
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let vs = f16::from_ne_bytes([val[0], val[1]]);
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
fn get_isense(&mut self, card: &Ctl) -> f16 {
|
||||
helpers::read_ev(card, &mut self.isense.val, &self.isense.elem_name);
|
||||
let val: &[u8] = match self.vsense.val.get_bytes() {
|
||||
Some(inner) => inner,
|
||||
None => {
|
||||
println!("Could not read ISENSE from {}", self.drv);
|
||||
helpers::fail();
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let is = f16::from_ne_bytes([val[0], val[1]]);
|
||||
|
||||
return is;
|
||||
}
|
||||
|
||||
fn get_lvl(&mut self, card: &Ctl) -> f32 {
|
||||
helpers::read_ev(card, &mut self.level.val, &self.level.elem_name);
|
||||
|
||||
let val: i32 = match self.level.val.get_integer(0) {
|
||||
Some(inner) => inner,
|
||||
None => {
|
||||
println!("Could not read level from {}", self.drv);
|
||||
helpers::fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
let db: f32 = helpers::int_to_db(card, &self.level.id, val).to_db();
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
fn set_lvl(&mut self, card: &Ctl, lvl: f32) {
|
||||
|
||||
let new_val: i32 = helpers::db_to_int(card, &self.level.id, lvl);
|
||||
|
||||
match self.level.val.set_integer(0, new_val) {
|
||||
Some(_) => {},
|
||||
None => {
|
||||
println!("Could not set level for {}", self.drv);
|
||||
helpers::fail();
|
||||
std::process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
helpers::write_ev(card, &self.level.val, &self.level.elem_name);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Struct representing a driver. Parameters are parsed out of a config
|
||||
file, which is loaded at runtime based on the machine's DT compatible
|
||||
string.
|
||||
|
||||
name: driver name as it appears in ALSA
|
||||
alsa_iface: Mixer struct with handles to the driver's control elements
|
||||
r_dc: dc resistance of the voice coil (ohms)
|
||||
tau_coil: voice coil ramp time constant (seconds)
|
||||
tau_magnet: magnet ramp time constant (seconds)
|
||||
tr_coil: thermal resistance of voice coil (*C/W)
|
||||
temp_limit: absolute max temp of the voice coil (*C)
|
||||
|
||||
Borrows the handle to the control interface to do calculations.
|
||||
*/
|
||||
pub struct Speaker {
|
||||
name: String,
|
||||
alsa_iface: Mixer,
|
||||
tau_coil: f64,
|
||||
tr_coil: f64,
|
||||
temp_limit: f64,
|
||||
}
|
||||
|
||||
|
||||
pub trait SafetyMonitor {
|
||||
fn new(driver_name: &str, config: &Ini, card: &Ctl) -> Self;
|
||||
|
||||
fn run(&mut self, card: &Ctl);
|
||||
}
|
||||
|
||||
impl SafetyMonitor for Speaker {
|
||||
fn new(driver_name: &str, config: &Ini, card: &Ctl) -> Speaker {
|
||||
let new_speaker: Speaker = { Speaker {
|
||||
name: driver_name.to_string(),
|
||||
alsa_iface: ALSACtl::new(&driver_name, card),
|
||||
tau_coil: helpers::parse_float(config, driver_name, "tau_coil"),
|
||||
tr_coil: helpers::parse_float(config, driver_name, "tr_coil"),
|
||||
temp_limit: helpers::parse_float(config, driver_name, "temp_limit"),
|
||||
}};
|
||||
|
||||
return new_speaker;
|
||||
}
|
||||
|
||||
// I'm not sure on the maths here for determining when to start dropping the volume.
|
||||
fn run(&mut self, card: &Ctl) {
|
||||
//let v: f16 = self.alsa_iface.get_vsense(card);
|
||||
//let i: f16 = self.alsa_iface.get_isense(card);
|
||||
let lvl: f32 = self.alsa_iface.get_lvl(card);
|
||||
|
||||
// Technically, this is the temp ~tau_coil seconds in the future
|
||||
//let temp: f64 = ((v * i).to_f64()) * self.tr_coil;
|
||||
|
||||
// if temp < self.temp_limit && lvl < 0f32 {
|
||||
// println!("Voice coil for {} below temp limit, ramping back up.", self.name);
|
||||
//
|
||||
// // For every degree below temp_limit, raise level by 0.5 dB
|
||||
// let new_lvl: f32 = lvl + ((self.temp_limit - temp) as f32 * 0.5);
|
||||
// self.alsa_iface.set_lvl(card, new_lvl);
|
||||
// }
|
||||
//
|
||||
// if temp > self.temp_limit {
|
||||
// println!("Voice coil at {}*C in {} on {}! Dropping volume!", temp, self.tau_coil, self.name);
|
||||
//
|
||||
// // For every degree above temp_limit, drop the level by 1.5 dB
|
||||
// let new_lvl: f32 = lvl - ((temp - self.temp_limit) as f32 * 1.5);
|
||||
// self.alsa_iface.set_lvl(card, new_lvl);
|
||||
// }
|
||||
|
||||
// TEMPORARY PROOF THAT THIS WORKS!
|
||||
|
||||
println!("Volume on {} is currently {} dB. Setting to -18 dB.", self.name, lvl);
|
||||
|
||||
let new_lvl: f32 = -18.0;
|
||||
self.alsa_iface.set_lvl(card, new_lvl);
|
||||
|
||||
println!("Volume on {} is now {} dB", self.name, self.alsa_iface.get_lvl(card));
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue