Merge in upstream version 0.5.0 to Debian pkg
7
.github/workflows/build-debian-package.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Build from sources
|
||||
run: |
|
||||
make -C src -j4 PREFIX=/usr
|
||||
make -C src -j$(nproc) PREFIX=/usr
|
||||
|
||||
- name: Prepare package workspace
|
||||
run: |
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
cp src/alsa-scarlett-gui ${{ github.workspace }}/deb-workspace/usr/bin/
|
||||
cp src/vu.b4.alsa-scarlett-gui.desktop ${{ github.workspace }}/deb-workspace/usr/share/applications/
|
||||
cp src/img/vu.b4.alsa-scarlett-gui.png ${{ github.workspace }}/deb-workspace/usr/share/icons/hicolor/256x256/apps/
|
||||
cp -r *.md img demo ${{ github.workspace }}/deb-workspace/usr/share/doc/${{ env.APP_NAME }}-${{ env.APP_VERSION }}/
|
||||
cp -r *.md demo docs img ${{ github.workspace }}/deb-workspace/usr/share/doc/${{ env.APP_NAME }}-${{ env.APP_VERSION }}/
|
||||
|
||||
- name: Build debian package
|
||||
uses: jiro4989/build-deb-action@v2
|
||||
@@ -42,8 +42,9 @@ jobs:
|
||||
package: ${{ env.APP_NAME }}
|
||||
package_root: ${{ github.workspace }}/deb-workspace
|
||||
maintainer: geoffreybennett
|
||||
depends: 'libgtk-4-1, libasound2, alsa-utils'
|
||||
version: ${{ env.APP_VERSION }}
|
||||
desc: ${{ env.APP_NAME }} is a Gtk4 GUI for the ALSA controls presented by the Linux kernel Focusrite Scarlett Gen 2/3 Mixer Driver.
|
||||
desc: ${{ env.APP_NAME }} is a Gtk4 GUI for the ALSA controls presented by the Linux kernel Focusrite USB drivers.
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
|
||||
2
.github/workflows/build-flatpak-package.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
name: "Flatpak"
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: bilelmoussaoui/flatpak-github-actions:gnome-45
|
||||
image: bilelmoussaoui/flatpak-github-actions:gnome-47
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
94
FAQ.md
@@ -1,37 +1,40 @@
|
||||
# FAQ for the Scarlett2 Mixer Driver and `alsa-scarlett-gui`
|
||||
# FAQ for the ALSA Scarlett Control Panel (`alsa-scarlett-gui`)
|
||||
|
||||
## What is this?
|
||||
|
||||
The Scarlett2 Protocol Driver (also known as the Scarlett2 Mixer
|
||||
Driver) is a part of the Linux kernel, enhancing the ALSA kernel
|
||||
driver with additional controls for Focusrite Scarlett, Clarett, and
|
||||
Vocaster interfaces.
|
||||
The ALSA Scarlett Control Panel (`alsa-scarlett-gui`) is an
|
||||
easy-to-use application for adjusting the ALSA controls provided by
|
||||
three Linux kernel drivers for Focusrite USB interfaces:
|
||||
|
||||
1. The Scarlett 1st Gen Mixer Driver (for 1st Gen 6i6, 8i6, 18i6, 18i8, 18i20)
|
||||
2. The Scarlett2 Protocol Driver (for 2nd/3rd Gen interfaces, small 4th Gen, Clarett, and Vocaster)
|
||||
3. The FCP (Focusrite Control Protocol) Driver (for big 4th Gen interfaces: 16i16, 18i16, 18i20)
|
||||
|
||||
To check if your kernel is already up-to-date, and how to upgrade if
|
||||
not, see the [Control Panel Installation Prerequisites — Linux
|
||||
Kernel](https://github.com/geoffreybennett/alsa-scarlett-gui/blob/master/INSTALL.md).
|
||||
Kernel](docs/INSTALL.md).
|
||||
|
||||
`alsa-scarlett-gui` is an easy-to-use application to adjust those
|
||||
controls.
|
||||
## Do I need these drivers for my Focusrite interface?
|
||||
|
||||
## Do I need the driver for my Focusrite interface?
|
||||
|
||||
In order to get audio working? No. Focusrite USB interfaces are
|
||||
For basic audio functionality? No. Focusrite USB interfaces are
|
||||
“plug-and-play” — they are USB Audio Class Compliant, meaning they
|
||||
work out-of-the-box with the standard ALSA USB audio driver (to get
|
||||
full functionality on Scarlett 3rd/4th Gen/Vocaster interfaces, first
|
||||
deactivate MSD mode by holding down the 48V button while powering it
|
||||
on).
|
||||
|
||||
However, to access the mixer, routing, and hardware-specific features,
|
||||
you'll need the appropriate driver for your interface model.
|
||||
|
||||
## MSD Mode?
|
||||
|
||||
“MSD Mode” is the “Mass Storage Device Mode” that the Scarlett 3rd and
|
||||
"MSD Mode" is the "Mass Storage Device Mode" that the Scarlett 3rd and
|
||||
4th Gen interfaces ship in.
|
||||
|
||||
If MSD Mode is enabled, you need to disable it and restart your
|
||||
interface to get access to its full functionality.
|
||||
|
||||
When you plug the interface in, there’ll be a tiny read-only virtual
|
||||
When you plug the interface in, there'll be a tiny read-only virtual
|
||||
disk that has a link to the Focusrite product registration page; until
|
||||
you turn off MSD Mode not all features of the interface will be
|
||||
available.
|
||||
@@ -40,49 +43,62 @@ You can turn off MSD Mode by holding down the 48V button while
|
||||
powering on the interface, or by clicking the button in
|
||||
`alsa-scarlett-gui` and rebooting it.
|
||||
|
||||
## What is the purpose of the driver if it’s not needed for audio?
|
||||
If you do the recommended/required (depending on the model) firmware
|
||||
update, MSD Mode will automatically be turned off.
|
||||
|
||||
This driver is for users who want more control over their interface.
|
||||
It allows for detailed manipulation of internal audio routing and
|
||||
settings specific to Scarlett, Clarett, and Vocaster devices, beyond
|
||||
the basic audio I/O functionality. Also, being able to monitor the
|
||||
audio levels seen by the interface is really useful.
|
||||
## What is the purpose of these drivers if they're not needed for basic audio?
|
||||
|
||||
These drivers are for users who want more control over their
|
||||
interface. They allow for detailed manipulation of:
|
||||
|
||||
- Internal audio routing
|
||||
- Hardware-specific settings
|
||||
- Mixer functionality
|
||||
- Level monitoring
|
||||
- Input/output configuration
|
||||
|
||||
These controls go beyond the basic audio I/O functionality provided by
|
||||
the generic ALSA USB audio driver.
|
||||
|
||||
## What interfaces are supported?
|
||||
|
||||
- All Scarlett 2nd Gen interfaces with software controls (there are no
|
||||
software controls on the 2nd Gen Solo and 2i2, so the mixer driver
|
||||
is irrelevant).
|
||||
The ALSA Scarlett Control Panel supports:
|
||||
|
||||
- All Scarlett 3rd Gen interfaces.
|
||||
- **Scarlett 1st Gen**: 6i6, 8i6, 18i6, 18i8, 18i20
|
||||
- **Scarlett 2nd Gen**: 6i6, 18i8, 18i20
|
||||
- **Scarlett 3rd Gen**: Solo, 2i2, 4i4, 8i6, 18i8, 18i20
|
||||
- **Scarlett 4th Gen**: Solo, 2i2, 4i4, 16i16, 18i16, 18i20
|
||||
- **Clarett USB and Clarett+**: 2Pre, 4Pre, 8Pre
|
||||
- **Vocaster**: One, Two
|
||||
|
||||
- Scarlett 4th Gen Solo, 2i2, and 4i4.
|
||||
|
||||
- All Clarett USB and Clarett+ interfaces.
|
||||
|
||||
- Vocaster One and Vocaster Two.
|
||||
Note: The Scarlett 1st and 2nd Gen small interfaces (Solo, 2i2, 2i4)
|
||||
don't have any software controls. All the controls are available from
|
||||
the front panel, so they don't require the specialised drivers or this
|
||||
GUI.
|
||||
|
||||
## Where are the options to set the sample rate and buffer size?
|
||||
|
||||
It’s important to note that the Scarlett2 driver and
|
||||
`alsa-scarlett-gui` have nothing to do with audio input/output to and
|
||||
from the device. This task is managed by the generic part of the ALSA
|
||||
USB soundcard driver.
|
||||
The ALSA Scarlett Control Panel doesn't handle audio input/output
|
||||
settings like sample rate and buffer size. These settings are managed
|
||||
by the application using the soundcard, typically a sound server such
|
||||
as PulseAudio, JACK, or PipeWire.
|
||||
|
||||
Audio settings like the sample rate and buffer size are chosen by the
|
||||
application which is using the soundcard. In most cases, that is a
|
||||
sound server such as PulseAudio, JACK, or PipeWire.
|
||||
The sample rate shown in the control panel is informative only and
|
||||
displays the current rate being used by applications. If it shows
|
||||
“N/A” then no application is using the interface.
|
||||
|
||||
Note that not all features are available at higher sample rates; refer
|
||||
to the user manual of your interface for more information.
|
||||
|
||||
## Help?!
|
||||
|
||||
For help with the driver:
|
||||
https://github.com/geoffreybennett/scarlett-gen2/issues
|
||||
For help with the Scarlett2 and FCP kernel drivers:
|
||||
https://github.com/geoffreybennett/linux-fcp/issues
|
||||
|
||||
For help with the FCP user-space side:
|
||||
https://github.com/geoffreybennett/fcp-support/issues
|
||||
|
||||
For help with `alsa-scarlett-gui`:
|
||||
https://github.com/geoffreybennett/alsa-scarlett-gui/issues
|
||||
|
||||
For general Linux audio help:
|
||||
https://linuxmusicians.com
|
||||
For general Linux audio help: https://linuxmusicians.com
|
||||
|
||||
2
Makefile
@@ -10,7 +10,7 @@ default:
|
||||
@echo
|
||||
@echo "If you want to build and install from source, please try:"
|
||||
@echo " cd src"
|
||||
@echo " make -j4"
|
||||
@echo " make -j$(shell nproc)"
|
||||
@echo " sudo make install"
|
||||
@echo
|
||||
@echo "This Makefile knows about packaging:"
|
||||
|
||||
27
README.md
@@ -1,21 +1,26 @@
|
||||
# ALSA Scarlett2 Control Panel (`alsa-scarlett-gui`)
|
||||
# ALSA Scarlett Control Panel (`alsa-scarlett-gui`)
|
||||
|
||||
`alsa-scarlett-gui` is a Gtk4 GUI for the ALSA controls presented by
|
||||
the Linux kernel Focusrite Scarlett2 USB Protocol Mixer Driver.
|
||||
the three Linux kernel Focusrite USB drivers:
|
||||
|
||||
- Scarlett 1st Gen Driver for ALSA
|
||||
- Scarlett2 USB Protocol Mixer Driver
|
||||
- FCP (Focusrite Control Protocol) Driver
|
||||
|
||||
Supported interfaces:
|
||||
- Scarlett 1st Gen 6i6, 8i6, 18i6, 18i8, 18i20
|
||||
- Scarlett 2nd Gen 6i6, 18i8, 18i20
|
||||
- Scarlett 3rd Gen Solo, 2i2, 4i4, 8i6, 18i8, 18i20
|
||||
- Scarlett 4th Gen Solo, 2i2, 4i4
|
||||
- Scarlett 4th Gen Solo, 2i2, 4i4, 16i16, 18i16, 18i20
|
||||
- Clarett 2Pre, 4Pre, 8Pre USB
|
||||
- Clarett+ 2Pre, 4Pre, 8Pre
|
||||
- Vocaster One and Vocaster Two
|
||||
|
||||
## About
|
||||
|
||||
<img src="src/img/alsa-scarlett-gui-logo.png" align="right">
|
||||
<img src="img/alsa-scarlett-gui.png" align="right">
|
||||
|
||||
The Focusrite USB audio interfaces are class compliant meaning that
|
||||
All Focusrite USB audio interfaces are class compliant meaning that
|
||||
they work “out of the box” on Linux as audio and MIDI interfaces
|
||||
(although on Gen 3/4/Vocaster you need to disable MSD mode first for
|
||||
full functionality). However, except for some of the smallest models,
|
||||
@@ -28,8 +33,8 @@ awful experience. The existing applications like `alsamixer` and
|
||||
controls presented for the Gen 3 18i20. Even the smallest Gen 3 4i4
|
||||
interface at last count had 84 ALSA controls.
|
||||
|
||||
Announcing the ALSA Scarlett2 Control Panel, now supporting Scarlett
|
||||
Gen 2, 3, 4, Clarett, and Vocaster!
|
||||
Announcing the ALSA Scarlett Control Panel, now supporting all
|
||||
Scarlett Gen 1, 2, 3, 4, Clarett, and Vocaster USB interfaces!
|
||||
|
||||

|
||||
|
||||
@@ -43,12 +48,16 @@ known issues.
|
||||
|
||||
Information specific to various models:
|
||||
|
||||
- [Scarlett 1st Gen](docs/iface-1st-gen.md)
|
||||
|
||||
- [Scarlett 3rd Gen Solo and 2i2](docs/iface-small.md)
|
||||
|
||||
- [Scarlett 2nd Gen 6i6+, 3rd Gen 4i4+, Clarett USB, and
|
||||
Clarett+](docs/iface-large.md)
|
||||
|
||||
- [Scarlett 4th Gen](docs/iface-4th-gen.md)
|
||||
- [Scarlett Small 4th Gen](docs/iface-4th-gen-small.md)
|
||||
|
||||
- [Scarlett Big 4th Gen](docs/iface-4th-gen-big.md)
|
||||
|
||||
## Donations
|
||||
|
||||
@@ -63,7 +72,7 @@ you! Any donation is appreciated.
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2022-2024 Geoffrey D. Bennett
|
||||
Copyright 2022-2025 Geoffrey D. Bennett
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Summary: ALSA Scarlett Gen 2/3 Control Panel
|
||||
Summary: ALSA Scarlett Control Panel
|
||||
Name: alsa-scarlett-gui
|
||||
Version: VERSION
|
||||
Release: 1%{?dist}
|
||||
@@ -9,22 +9,24 @@ Source: %{name}-%{version}.tar.gz
|
||||
%description
|
||||
|
||||
alsa-scarlett-gui is a Gtk4 GUI for the ALSA controls presented by the
|
||||
Linux kernel Focusrite Scarlett Gen 2/3 Mixer Driver.
|
||||
Linux kernel Focusrite USB drivers.
|
||||
|
||||
%prep
|
||||
%setup
|
||||
|
||||
%build
|
||||
make -C src -j4 VERSION=%{version} PREFIX=/usr
|
||||
make -C src %{?_smp_mflags} VERSION=%{version} PREFIX=/usr
|
||||
|
||||
%install
|
||||
%make_install -C src PREFIX=/usr
|
||||
DOCDIR=%{buildroot}/usr/share/doc/%{name}-%{version}
|
||||
mkdir -p $DOCDIR/img
|
||||
mkdir $DOCDIR/demo
|
||||
mkdir $DOCDIR/docs
|
||||
cp *.md $DOCDIR
|
||||
cp img/* $DOCDIR/img
|
||||
cp demo/* $DOCDIR/demo
|
||||
cp docs/* $DOCDIR/docs
|
||||
|
||||
%files
|
||||
%doc /usr/share/doc/%{name}-%{version}
|
||||
|
||||
5449
demo/Scarlett Gen 1 18i20.state
Normal file
3362
demo/Scarlett Gen 1 18i6.state
Normal file
4165
demo/Scarlett Gen 1 18i8.state
Normal file
3277
demo/Scarlett Gen 1 6i6.state
Normal file
2797
demo/Scarlett Gen 1 8i6.state
Normal file
10648
demo/Scarlett Gen 4 16i16.state
Normal file
11560
demo/Scarlett Gen 4 18i16.state
Normal file
14810
demo/Scarlett Gen 4 18i20.state
Normal file
@@ -1,29 +1,46 @@
|
||||
# ALSA Scarlett2 Control Panel Installation
|
||||
# ALSA Scarlett Control Panel Installation
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Linux Kernel
|
||||
|
||||
You need to be running a Linux Kernel that has the ALSA Scarlett2
|
||||
Protocol Driver. Use `uname -r` to check what kernel version you are
|
||||
running.
|
||||
You need to be running a Linux Kernel that contains the appropriate
|
||||
driver for your interface. Use `uname -r` to check what kernel version
|
||||
you are running.
|
||||
|
||||
- For reasonable functionality of Scarlett 2nd and 3rd Gen and Clarett
|
||||
interfaces, you need at least Linux kernel version 6.7
|
||||
- For Scarlett 4th Gen support and firmware updates from Linux, you
|
||||
need at least 6.8
|
||||
- For Vocaster support, you’ll need to build an updated
|
||||
`snd-usb-audio` driver (or wait for 6.10)
|
||||
Check the following table to see which driver your interface uses and
|
||||
the first kernel version that the driver was included in:
|
||||
|
||||
If you’ve got a Vocaster, or if your distribution doesn’t include a
|
||||
recent-enough kernel for your interface, you can get the latest driver
|
||||
from here and build it for your current kernel:
|
||||
| Series | Models | Driver | Kernel Version |
|
||||
|-----------|--------|--------|:----------------------:|
|
||||
| Scarlett 1st Gen | Solo, 2i2, 2i4 | N/A* | Any |
|
||||
| Scarlett 1st Gen | 6i6, 8i6, 18i6, 18i8, 18i20 | Scarlett 1st Gen Mixer Driver | 3.19+ |
|
||||
| Scarlett 2nd Gen | Solo, 2i2, 2i4 | N/A* | Any |
|
||||
| Scarlett 2nd Gen | 6i6, 18i8, 18i20 | Scarlett2 Mixer Driver | 6.7+ |
|
||||
| Scarlett 3rd Gen | Solo, 2i2, 4i4, 8i6, 18i8, 18i20 | Scarlett2 Mixer Driver | 6.7+ |
|
||||
| Scarlett 4th Gen | Solo, 2i2, 4i4 | Scarlett2 Mixer Driver | 6.8+ |
|
||||
| Scarlett 4th Gen | 16i16, 18i16, 18i20 | FCP (Focusrite Control Protocol) Driver | 6.14+ |
|
||||
| Clarett USB and Clarett+ | 2Pre, 4Pre, 8Pre | Scarlett2 Mixer Driver | 6.7+ |
|
||||
| Vocaster | One, Two | Scarlett2 Mixer Driver | 6.10+ |
|
||||
|
||||
https://github.com/geoffreybennett/scarlett-gen2/releases
|
||||
\* The small 1st Gen and 2nd Gen models don’t have any proprietary
|
||||
software controls so they don’t need a driver beyond the standard ALSA
|
||||
USB Audio driver. This means that this application (alsa-scarlett-gui)
|
||||
is not needed, useful, or supported for these models.
|
||||
|
||||
#### Enabling the Driver
|
||||
If your distribution doesn’t include a recent-enough kernel for your
|
||||
interface, you can get the latest driver from here and build it for
|
||||
your current kernel if it's not too old (the Scarlett2 and FCP drivers
|
||||
are both maintained in the same tree here):
|
||||
https://github.com/geoffreybennett/linux-fcp/releases
|
||||
|
||||
As of Linux 6.7 the driver is enabled by default. Check the driver
|
||||
Kernel 6.7 and later have the Scarlett2 driver enabled by default. The
|
||||
Scarlett 1st Gen driver and the FCP drivers are always enabled.
|
||||
|
||||
#### Enabling the Scarlett2 Driver
|
||||
|
||||
Some kernels before 6.7 have an earlier version of the Scarlett2
|
||||
driver which is disabled by default. If this is you, check the driver
|
||||
status (after plugging your interface in) with this command:
|
||||
|
||||
```
|
||||
@@ -35,12 +52,12 @@ If all is good you’ll see messages like this:
|
||||
```
|
||||
New USB device found, idVendor=1235, idProduct=8215, bcdDevice= 6.0b
|
||||
Product: Scarlett 18i20 USB
|
||||
Focusrite Scarlett Gen 3 Mixer Driver enabled (pid=0x8215); report
|
||||
any issues to https://github.com/geoffreybennett/scarlett-gen2/issues
|
||||
Focusrite Scarlett Gen 3 Mixer Driver enabled (pid=0x8215); ...
|
||||
```
|
||||
|
||||
If you don’t see the “Mixer Driver” message or if it shows “disabled”
|
||||
then check the [OLDKERNEL.md](OLDKERNEL.md) instructions.
|
||||
then check the [OLDKERNEL.md](OLDKERNEL.md) instructions (or,
|
||||
preferably, upgrade your distro/kernel!).
|
||||
|
||||
### Gtk4
|
||||
|
||||
@@ -49,16 +66,27 @@ doesn’t have them natively, try the Flatpak instructions below.
|
||||
|
||||
### Firmware
|
||||
|
||||
As of Linux 6.8, firmware updates of all the supported interfaces can
|
||||
be done through Linux. This is mandatory for Scarlett 4th Gen and
|
||||
Vocaster interfaces (unless you’ve already updated it using the
|
||||
manufacturer’s software), and optional for Scarlett 2nd and 3rd Gen,
|
||||
Clarett USB, and Clarett+ interfaces.
|
||||
#### Scarlett2 Driver
|
||||
|
||||
As of Linux 6.8, firmware updates of all supported interfaces from the
|
||||
2nd Gen onwards can be done through Linux. This is mandatory for
|
||||
Scarlett 4th Gen and Vocaster interfaces (unless you’ve already
|
||||
updated it using the manufacturer’s software), and optional but
|
||||
recommended for Scarlett 2nd and 3rd Gen, Clarett USB, and Clarett+
|
||||
interfaces.
|
||||
|
||||
Download the firmware from
|
||||
https://github.com/geoffreybennett/scarlett2-firmware and place it in
|
||||
`/usr/lib/firmware/scarlett2` or use the RPM/deb package.
|
||||
|
||||
#### FCP Driver
|
||||
|
||||
Firmware updates for the big Scarlett 4th Gen interfaces is currently
|
||||
only possible through the CLI `fcp-tool` utility available in the
|
||||
[fcp-support](https://github.com/geoffreybennett/fcp-support). You
|
||||
need to install this package and update the firmware before
|
||||
alsa-scarlett-gui will work.
|
||||
|
||||
## Building and Running
|
||||
|
||||
On Fedora, these packages need to be installed:
|
||||
@@ -79,6 +107,12 @@ On Ubuntu:
|
||||
sudo apt -y install git make gcc libgtk-4-dev libasound2-dev libssl-dev
|
||||
```
|
||||
|
||||
On Arch:
|
||||
|
||||
```
|
||||
sudo pacman -S gtk4
|
||||
```
|
||||
|
||||
To download from github:
|
||||
|
||||
```
|
||||
@@ -90,7 +124,7 @@ To build:
|
||||
|
||||
```
|
||||
cd src
|
||||
make -j4
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
# ALSA Scarlett2 Usage With Old Kernels
|
||||
|
||||
**This information is mostly for historical purposes. If you’re
|
||||
running a kernel before 6.7, you should upgrade to a newer kernel.**
|
||||
|
||||
Linux kernel 6.7 (check your version with `uname -r`) was the first
|
||||
kernel version with this driver enabled by default. It’s recommended
|
||||
that you run 6.7 or later, or build the backported driver for your
|
||||
kernel. If you do, then these instructions aren’t relevant; continue
|
||||
with [INSTALL.md](INSTALL.md) for prerequisites, how to build,
|
||||
install, and run `alsa-scarlett-gui`.
|
||||
kernel version with the Scarlett2 driver enabled by default. It’s
|
||||
recommended that you run 6.7 or later, or build the backported driver
|
||||
for your kernel. If you do, then these instructions aren’t relevant;
|
||||
continue with [INSTALL.md](INSTALL.md) for prerequisites, how to
|
||||
build, install, and run `alsa-scarlett-gui`.
|
||||
|
||||
If you’ve got a Scarlett Gen 2 or 3 or a Clarett+ 8Pre and don’t mind
|
||||
the level meters not working, then the minimum kernel versions are:
|
||||
the level meters not working, then the first kernel support was added
|
||||
in:
|
||||
|
||||
- **Scarlett Gen 2**: Linux 5.4 (bugs fixed in Linux 5.14)
|
||||
- **Scarlett Gen 3**: Linux 5.14
|
||||
@@ -18,7 +22,7 @@ the level meters not working, then the minimum kernel versions are:
|
||||
|
||||
Install the latest version of the backported driver from here:
|
||||
|
||||
https://github.com/geoffreybennett/scarlett-gen2/releases
|
||||
https://github.com/geoffreybennett/linux-fcp/releases
|
||||
|
||||
then you can ignore the instructions below.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ALSA Scarlett2 Control Panel Usage
|
||||
# ALSA Scarlett Control Panel Usage
|
||||
|
||||
Refer to [INSTALL.md](INSTALL.md) for prerequisites, how to build,
|
||||
install, and run.
|
||||
@@ -59,7 +59,7 @@ restart the interface, and in a moment the main window will appear.
|
||||
The View → Startup menu option opens a window to configure settings
|
||||
that only take effect when the interface is powered on.
|
||||
|
||||
The options common to all interfaces are:
|
||||
The options common to most interfaces are:
|
||||
|
||||
- **Reset Configuration**: this will reset the configuration to the
|
||||
factory defaults. This is particularly useful with the 4th Gen and
|
||||
@@ -105,9 +105,13 @@ menu option File → Interface Simulation to load.
|
||||
The controls and menu items which are available vary widely, depending
|
||||
on your specific interface.
|
||||
|
||||
There are three broad categories of interfaces with different
|
||||
There are five broad categories of interfaces with different
|
||||
capabilities; each category of interface is described in a separate
|
||||
ocument:
|
||||
document:
|
||||
|
||||
- [Scarlett 1st Gen 6i6+](iface-1st-gen.md)
|
||||
|
||||
Full routing and mixing capabilities, but some significant caveats.
|
||||
|
||||
- [Scarlett 3rd Gen Solo and 2i2](iface-small.md)
|
||||
|
||||
@@ -119,13 +123,21 @@ ocument:
|
||||
|
||||
Full routing and mixing capabilities.
|
||||
|
||||
- [Scarlett 4th Gen](iface-4th-gen.md)
|
||||
- [Scarlett Small 4th Gen](iface-4th-gen-small.md)
|
||||
|
||||
Full routing and mixing capabilities, remote-controlled input gain,
|
||||
but no output controls.
|
||||
|
||||
- [Scarlett Big 4th Gen](iface-4th-gen-big.md)
|
||||
|
||||
Full routing and mixing capabilities, remote-controlled input gain
|
||||
and output volume controls.
|
||||
|
||||
## Known Bugs/Issues
|
||||
|
||||
- For interfaces using the FCP driver, alsa-scarlett-gui needs to be
|
||||
started after the interface is connected and fcp-server has started.
|
||||
|
||||
- Load/Save uses `alsactl` which will be confused if the ALSA
|
||||
interface name (e.g. `USB`) changes.
|
||||
|
||||
|
||||
160
docs/iface-1st-gen.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# ALSA Scarlett Control Panel
|
||||
|
||||
## Scarlett 1st Gen Interfaces
|
||||
|
||||
This document describes how to use the ALSA Scarlett Control Panel
|
||||
with the Scarlett 1st Gen interfaces:
|
||||
|
||||
- Scarlett 1st Gen 6i6, 8i6, 18i6, 18i8, 18i20
|
||||
|
||||
Note: The 1st Gen Scarlett Solo, 2i2, and 2i4 have all their controls
|
||||
accessible from the front panel of the device, and there are no
|
||||
proprietary software controls, so they do not require this control
|
||||
panel software.
|
||||
|
||||
## Important Driver Limitations
|
||||
|
||||
The 1st Gen Scarlett devices have some important limitations in the
|
||||
ALSA driver implementation that you should be aware of:
|
||||
|
||||
1. **Initial State Detection**: The driver cannot read the current
|
||||
state of hardware controls (this appears to be a limitation of the
|
||||
device firmware). When alsa-scarlett-gui starts, what you see will
|
||||
not reflect the actual state of your device unless the controls
|
||||
have previously been set since startup.
|
||||
|
||||
2. **State Update Issues**: The driver only updates the hardware state
|
||||
when it thinks a setting needs to be changed. If the driver
|
||||
incorrectly believes a control is already in the desired state, it
|
||||
won't actually update the control.
|
||||
|
||||
3. **Level Meters**: The driver does not support reading the level
|
||||
meters from the hardware.
|
||||
|
||||
4. **Startup Controls**: The driver has no startup controls.
|
||||
|
||||
### Recommended Workaround
|
||||
|
||||
To ensure your settings are properly applied:
|
||||
|
||||
1. Apply a "zero" configuration that sets all controls to values that
|
||||
are *not* what you desire.
|
||||
2. Then apply your desired configuration
|
||||
|
||||
This two-step process helps ensure that the driver actually sends all
|
||||
commands to the hardware. You may want to create a script using
|
||||
`alsactl` for this purpose.
|
||||
|
||||
## Main Window
|
||||
|
||||
The main window is divided into three sections:
|
||||
|
||||
- Global Controls
|
||||
- Analogue Input Controls
|
||||
- Analogue Output Controls
|
||||
|
||||
The particular controls available depend on the interface model.
|
||||
|
||||
Note that the View menu option lets you open two other windows which
|
||||
contain additional controls, described in the following sections:
|
||||
- [Routing](#routing)
|
||||
- [Mixer](#mixer)
|
||||
|
||||
The Levels and Startup windows that are available for later-generation
|
||||
interfaces are not available for 1st Gen interfaces due to driver limitations.
|
||||
|
||||
### Global Controls
|
||||
|
||||
Global controls relate to the operation of the interface as a whole.
|
||||
|
||||
#### Clock Source
|
||||
|
||||
Clock Source selects where the interface receives its digital clock
|
||||
from. If you aren't using S/PDIF or ADAT inputs, set this to Internal.
|
||||
|
||||
#### Sync Status
|
||||
|
||||
Sync Status indicates if the interface is locked to a valid digital
|
||||
clock. If you aren't using S/PDIF or ADAT inputs and the status is
|
||||
Unlocked, change the Clock Source to Internal.
|
||||
|
||||
### Analogue Input Controls
|
||||
|
||||
#### Inst
|
||||
|
||||
The Inst buttons are used to select between Mic/Line and Instrument
|
||||
level/impedance. When plugging in microphones or line-level equipment
|
||||
(such as a synthesizer, external preamp, or effects processor) to the
|
||||
input, set it to “Line”. The “Inst” setting is for instruments with
|
||||
pickups such as guitars.
|
||||
|
||||
#### Pad
|
||||
|
||||
Enabling Pad engages a 10dB attenuator in the channel, giving you more
|
||||
headroom for very hot signals.
|
||||
|
||||
#### Gain
|
||||
|
||||
The Gain switch selects Low or High gain for the input channel.
|
||||
|
||||
### Analogue Output Controls
|
||||
|
||||
The analogue output controls let you set the output volume (gain) on
|
||||
the analogue line outputs.
|
||||
|
||||
Click and drag up/down on the volume dial to change the volume, use
|
||||
your arrow keys, Home/End/PgUp/PgDn keys, or use your mouse scroll
|
||||
wheel to adjust. You can also double-click on it to quickly toggle the
|
||||
volume between off and 0dB.
|
||||
|
||||
## Routing
|
||||
|
||||
The routing window allows complete control of signal routing between
|
||||
the hardware inputs/outputs, internal mixer, and PCM (USB)
|
||||
inputs/outputs.
|
||||
|
||||

|
||||
|
||||
To manage the routing connections:
|
||||
|
||||
- Click and drag from a source to a sink or a sink to a source to
|
||||
connect them. Audio from the source will then be sent to that sink.
|
||||
|
||||
- Click on a source or a sink to clear the links connected to that
|
||||
source/sink.
|
||||
|
||||
Note that a sink can only be connected to one source, but one source
|
||||
can be connected to many sinks. If you want a sink to receive input
|
||||
from more than one source, use the mixer inputs and outputs:
|
||||
|
||||
- Connect the sources that you want to mix together to mixer inputs
|
||||
- Connect mixer outputs to the sinks that you want to receive the
|
||||
mixed audio
|
||||
- Use the Mixer window to set the amount of each mixer input that is
|
||||
sent to each mixer output
|
||||
|
||||
The Presets menu can be used to clear all connections, or to set up
|
||||
common configurations:
|
||||
|
||||
- The "Direct" preset sets up the usual configuration using the
|
||||
interface as a regular audio interface by connecting:
|
||||
|
||||
- all Hardware Inputs to PCM Inputs
|
||||
- all PCM Outputs to Hardware Outputs
|
||||
|
||||
- The "Preamp" preset connects all Hardware Inputs to Hardware
|
||||
Outputs.
|
||||
|
||||
- The "Stereo Out" preset connects PCM 1 and 2 Outputs to pairs of
|
||||
Hardware Outputs.
|
||||
|
||||
## Mixer
|
||||
|
||||
If you use the Routing window to connect Sources to Mixer Inputs and
|
||||
Mixer Outputs to Sinks, then you can use the Mixer window to set the
|
||||
amount of each Mixer Input that is sent to each Mixer Output using a
|
||||
matrix of controls.
|
||||
|
||||
Click and drag up/down on the gain controls to adjust, or use your
|
||||
mouse scroll wheel. You can also double-click on the control to
|
||||
quickly toggle between off and 0dB.
|
||||
210
docs/iface-4th-gen-big.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# ALSA Scarlett Control Panel
|
||||
|
||||
## Scarlett Big 4th Gen Interfaces
|
||||
|
||||
This document describes how to use the ALSA Scarlett Control Panel
|
||||
with the big Scarlett 4th Gen interfaces:
|
||||
|
||||
- Scarlett 4th Gen 16i16, 18i16, 18i20
|
||||
|
||||
### FCP Driver
|
||||
|
||||
The big 4th Gen interfaces are supported by a new “FCP” (Focusrite
|
||||
Control Protocol) driver introduced in Linux 6.14. If you haven't
|
||||
installed
|
||||
[fcp-support](https://github.com/geoffreybennett/fcp-support) yet, you
|
||||
need to do that (and update the firmware) before you can use
|
||||
alsa-scarlett-gui.
|
||||
|
||||
## Main Window
|
||||
|
||||
The main window is divided into three sections:
|
||||
- Global Controls
|
||||
- Analogue Input Controls
|
||||
- Analogue Output Controls
|
||||
|
||||
The main window for the 16i16 interface is shown below. The 18i16 and
|
||||
18i20 interfaces are similar, but with more controls.
|
||||
|
||||

|
||||
|
||||
### Global Controls
|
||||
|
||||
#### Clock Source (interfaces with S/PDIF or ADAT inputs only)
|
||||
|
||||
Clock Source selects where the interface receives its digital clock
|
||||
from. If you aren’t using S/PDIF or ADAT inputs, set this to Internal.
|
||||
|
||||
#### Sync Status
|
||||
|
||||
Sync Status indicates if the interface is locked to a valid digital
|
||||
clock. If you aren’t using S/PDIF or ADAT inputs and the Sync Status
|
||||
is Unlocked, change the Clock Source to Internal.
|
||||
|
||||
#### Sample Rate
|
||||
|
||||
Sample Rate is informative only, and displays the current sample rate
|
||||
if the interface is currently in use. In ALSA, the sample rate is set
|
||||
by the application using the interface, which is usually a sound
|
||||
server such as PulseAudio, JACK, or PipeWire.
|
||||
|
||||
#### Speaker Switching
|
||||
|
||||
Speaker Switching lets you swap between two pairs of monitoring
|
||||
speakers very easily.
|
||||
|
||||
### Analogue Input Controls
|
||||
|
||||
#### Input Select
|
||||
|
||||
The “Input Select” control allows you to choose which channel the
|
||||
hardware 48V, Inst, Air, Auto, and Safe buttons control.
|
||||
|
||||
#### Link
|
||||
|
||||
The “Link” control links the 48V, Inst, Air, Auto, and Safe controls
|
||||
together so that they control a stereo pair of channels
|
||||
simultaneously.
|
||||
|
||||
#### Gain
|
||||
|
||||
The “Gain” controls adjust the input gain for the selected channel.
|
||||
Click and drag up/down on the control to adjust the gain, use your
|
||||
mouse scroll wheel, or click the control to select it and use the
|
||||
arrow keys, Page Up, Page Down, Home, and End keys.
|
||||
|
||||
#### Autogain
|
||||
|
||||
When the “Autogain” control is enabled, the interface will listen to
|
||||
the input signal for ten seconds and automatically adjust the gain to
|
||||
get the best signal level. When autogain is not running, the
|
||||
most-recent autogain exit status is shown below the “Autogain”
|
||||
control.
|
||||
|
||||
#### Safe
|
||||
|
||||
“Safe” mode is a feature that automatically reduces the gain if the
|
||||
signal is too loud. This can be useful to prevent clipping.
|
||||
|
||||
#### Instrument
|
||||
|
||||
The Inst button(s) are used to select between Mic/Line and Instrument
|
||||
level/impedance. When plugging in microphones or line-level equipment
|
||||
(such as a synthesizer, external preamp, or effects processor) to the
|
||||
input, set it to “Line”. The “Inst” setting is for instruments with
|
||||
pickups such as guitars.
|
||||
|
||||
#### Air
|
||||
|
||||
The Scarlett 3rd Gen introduced Air mode which transformed your
|
||||
recordings and inspired you while making music by boosting the
|
||||
signal’s high-end. The 4th Gen interfaces now call that “Air Presence”
|
||||
and add a new mode “Air Presence+Drive” which boosts mid-range
|
||||
harmonics in your sound.
|
||||
|
||||
#### Phantom Power (48V)
|
||||
|
||||
Turning the “48V” switch on sends “Phantom Power” to the XLR
|
||||
microphone input. This is required for some microphones (such as
|
||||
condensor microphones), and damaging to some microphones (particularly
|
||||
vintage ribbon microphones).
|
||||
|
||||
### Analogue Output Controls
|
||||
|
||||
The analogue output controls are a bit sparse. More controls are
|
||||
coming soon.
|
||||
|
||||
#### Volume Knobs
|
||||
|
||||
The volume knobs control the volume of the analogue outputs. The two
|
||||
channels of the stereo pairs are shown separately, but are internally
|
||||
linked together.
|
||||
|
||||
#### Mute and Dim
|
||||
|
||||
The speaker icon buttons are “mute” and “dim” (reduce volume) buttons,
|
||||
corresponding to the front-panel buttons on the interface (although
|
||||
only the 18i20 has a physical dim button).
|
||||
|
||||
## Routing and Mixing
|
||||
|
||||
The routing window allows (almost) complete control of signal routing
|
||||
between the hardware inputs/outputs, internal mixer, and PCM (USB)
|
||||
inputs/outputs.
|
||||
|
||||
The routing and mixing capabilities of the big 4th Gen interfaces are
|
||||
the same in concept as the older interfaces, but the mixer inputs are
|
||||
fixed and not shown in the routing window as there are too many to
|
||||
sensibly display.
|
||||
|
||||
From the main window, open the Routing window with the View → Routing
|
||||
menu option or pressing Ctrl-R:
|
||||
|
||||

|
||||
|
||||
To manage the routing connections:
|
||||
|
||||
- Click and drag from a source to a sink or a sink to a source to
|
||||
connect them. Audio from the source will then be sent to that sink.
|
||||
|
||||
- Click on a source or a sink to clear the links connected to that
|
||||
source/sink.
|
||||
|
||||
Note that a sink can only be connected to one source, but one source
|
||||
can be connected to many sinks. If you want a sink to receive input
|
||||
from more than one source, connect the sinks to mixer outputs:
|
||||
|
||||
- Connect mixer outputs to the sinks that you want to receive the
|
||||
mixed audio
|
||||
- Use the Mixer window to set the amount of each mixer input that is
|
||||
sent to each mixer output
|
||||
|
||||
The Presets menu can be used to clear all connections, or to set up
|
||||
common configurations:
|
||||
|
||||
- The “Direct” preset sets up the usual configuration using the
|
||||
interface as a regular audio interface by connecting:
|
||||
|
||||
- all Hardware Inputs to PCM Inputs
|
||||
- all PCM Outputs to Hardware Outputs
|
||||
|
||||
- The “Preamp” preset connects all Hardware Inputs to Hardware
|
||||
Outputs.
|
||||
|
||||
- The “Stereo Out” preset connects PCM 1 and 2 Outputs to pairs of
|
||||
Hardware Outputs.
|
||||
|
||||
To adjust the routing:
|
||||
|
||||
- Click and drag from a source to a sink or a sink to a source to
|
||||
connect them. Audio from the source will then be sent to that sink.
|
||||
|
||||
- Click on a source or a sink to clear the links connected to that
|
||||
source/sink.
|
||||
|
||||
Note that a sink can only be connected to one source, but one source
|
||||
can be connected to many sinks.
|
||||
|
||||
To adjust the mixer output levels:
|
||||
|
||||
1) Open the mixer window with the main window View → Mixer menu
|
||||
option, or press Ctrl-M.
|
||||
|
||||
2) Mixer levels can be adjusted with your keyboard or mouse in the
|
||||
same way as the [Gain Controls](#gain).
|
||||
|
||||
## Levels
|
||||
|
||||
The meters show the levels seen by the interface at every routing
|
||||
source as well as the analogue outputs. Open this window by selecting
|
||||
the View → Levels menu option or pressing Ctrl-L.
|
||||
|
||||

|
||||
|
||||
Look at this in conjunction with the routing window to understand
|
||||
which meter corresponds to which source or sink.
|
||||
|
||||
Thanks for reading this far! If you appreciate the hundreds of hours
|
||||
of work that went into the kernel driver, the control panel, and this
|
||||
documentation, please consider supporting the author with a
|
||||
[donation](../README.md#donations).
|
||||
@@ -1,9 +1,9 @@
|
||||
# ALSA Scarlett2 Control Panel
|
||||
# ALSA Scarlett Control Panel
|
||||
|
||||
## Scarlett 4th Gen Interfaces
|
||||
## Scarlett Small 4th Gen Interfaces
|
||||
|
||||
This document describes how to use the ALSA Scarlett2 Control Panel
|
||||
with the Scarlett 4th Gen interfaces:
|
||||
This document describes how to use the ALSA Scarlett Control Panel
|
||||
with the small Scarlett 4th Gen interfaces:
|
||||
|
||||
- Scarlett 4th Gen Solo, 2i2, and 4i4
|
||||
|
||||
@@ -39,7 +39,7 @@ The main window for the Solo and 2i2 interfaces is shown below; the
|
||||
Monitor control, and can show the position of the front panel volume
|
||||
knobs.
|
||||
|
||||

|
||||

|
||||
|
||||
### Global Controls
|
||||
|
||||
@@ -204,10 +204,7 @@ Important Notes:
|
||||
- The Focusrite Control 2 software can’t control most of this routing,
|
||||
so if you make changes here and then want to use Focusrite Control
|
||||
2, you’ll probably need to reset the routing back to the factory
|
||||
default settings. There’s currently no way to reset to factory
|
||||
default settings from the Focusrite Control 2 software; you’ll need
|
||||
to use the [Reset Configuration](USAGE.md#startup-controls) option
|
||||
in this software, or the `scarlett2` utility.
|
||||
default settings.
|
||||
|
||||
To adjust the routing:
|
||||
|
||||
@@ -338,7 +335,7 @@ sink: Hardware Outputs, Mixer Inputs, DSP Inputs, and PCM Inputs. Open
|
||||
this window by selecting the View → Levels menu option or pressing
|
||||
Ctrl-L.
|
||||
|
||||

|
||||

|
||||
|
||||
Look at this in conjunction with the routing window to understand
|
||||
which meter corresponds to which source or sink.
|
||||
@@ -1,8 +1,8 @@
|
||||
# ALSA Scarlett2 Control Panel
|
||||
# ALSA Scarlett Control Panel
|
||||
|
||||
## Large Scarlett 2nd and 3rd Gen and Clarett Interfaces
|
||||
|
||||
This document describes how to use the ALSA Scarlett2 Control Panel
|
||||
This document describes how to use the ALSA Scarlett Control Panel
|
||||
with the larger Scarlett 2nd Gen, 3rd Gen, and Clarett USB interfaces:
|
||||
|
||||
- Scarlett 2nd Gen 6i6, 18i8, 18i20
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ALSA Scarlett2 Control Panel
|
||||
# ALSA Scarlett Control Panel
|
||||
|
||||
## Small Scarlett 3rd Gen Interfaces
|
||||
|
||||
|
||||
BIN
img/alsa-scarlett-gui.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
img/iface-4th-gen-big.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 76 KiB |
BIN
img/scarlett-1st-gen-6i6-routing.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
img/scarlett-4th-gen-16i16-routing.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
img/window-levels-4th-gen-big.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
img/window-levels-4th-gen-small.gif
Normal file
|
After Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 306 KiB |
13
src/Makefile
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
# SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Credit to Tom Tromey and Paul D. Smith:
|
||||
@@ -12,8 +12,9 @@ VERSION := $(shell \
|
||||
DEPDIR := .deps
|
||||
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
|
||||
|
||||
CFLAGS ?= -ggdb -fno-omit-frame-pointer -O2
|
||||
CFLAGS += -Wall -Werror -D_FORTIFY_SOURCE=2
|
||||
CFLAGS ?= -ggdb -fno-omit-frame-pointer -fPIE -O2
|
||||
CFLAGS += -Wall -Werror
|
||||
CFLAGS += -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3
|
||||
CFLAGS += -DVERSION=\"$(VERSION)\"
|
||||
CFLAGS += -Wno-error=deprecated-declarations
|
||||
|
||||
@@ -26,7 +27,7 @@ CFLAGS += $(shell $(PKG_CONFIG) --cflags alsa)
|
||||
LDFLAGS += $(shell $(PKG_CONFIG) --libs glib-2.0)
|
||||
LDFLAGS += $(shell $(PKG_CONFIG) --libs gtk4)
|
||||
LDFLAGS += $(shell $(PKG_CONFIG) --libs alsa)
|
||||
LDFLAGS += -lm -lcrypto
|
||||
LDFLAGS += -lm -lcrypto -pie
|
||||
|
||||
COMPILE.c = $(CC) $(DEPFLAGS) $(CFLAGS) -c
|
||||
|
||||
@@ -52,7 +53,7 @@ GLIB_COMPILE_RESOURCES := $(shell $(PKG_CONFIG) --variable=glib_compile_resource
|
||||
|
||||
all: $(TARGET) $(DESKTOP_FILE)
|
||||
|
||||
clean:
|
||||
clean: depclean
|
||||
rm -f $(TARGET) $(DESKTOP_FILE) $(OBJS) $(XML_OBJ)
|
||||
|
||||
depclean:
|
||||
@@ -66,7 +67,7 @@ $(DEPFILES):
|
||||
include $(wildcard $(DEPFILES))
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
cc -o $(TARGET) $(OBJS) ${LDFLAGS}
|
||||
$(CC) -o $(TARGET) $(OBJS) ${LDFLAGS}
|
||||
|
||||
ifeq ($(PREFIX),)
|
||||
PREFIX := /usr/local
|
||||
|
||||
19
src/about.c
@@ -1,8 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "about.h"
|
||||
|
||||
static GdkTexture *logo = NULL;
|
||||
|
||||
void activate_about(
|
||||
GSimpleAction *action,
|
||||
GVariant *parameter,
|
||||
@@ -15,18 +17,23 @@ void activate_about(
|
||||
NULL
|
||||
};
|
||||
|
||||
if (!logo)
|
||||
logo = gdk_texture_new_from_resource(
|
||||
"/vu/b4/alsa-scarlett-gui/icons/vu.b4.alsa-scarlett-gui.png"
|
||||
);
|
||||
|
||||
gtk_show_about_dialog(
|
||||
w,
|
||||
"program-name", "ALSA Scarlett2 Control Panel",
|
||||
"program-name", "ALSA Scarlett Control Panel",
|
||||
"version", "Version " VERSION,
|
||||
"comments",
|
||||
"Gtk4 GUI for the ALSA controls presented by the\n"
|
||||
"Linux kernel Focusrite Scarlett2 Mixer Driver",
|
||||
"Linux kernel Focusrite USB drivers",
|
||||
"website", "https://github.com/geoffreybennett/alsa-scarlett-gui",
|
||||
"copyright", "Copyright 2022-2024 Geoffrey D. Bennett",
|
||||
"copyright", "Copyright 2022-2025 Geoffrey D. Bennett",
|
||||
"license-type", GTK_LICENSE_GPL_3_0,
|
||||
"logo-icon-name", "alsa-scarlett-gui-logo",
|
||||
"title", "About ALSA Scarlett2 Mixer Interface",
|
||||
"logo", logo,
|
||||
"title", "About ALSA Scarlett Mixer Interface",
|
||||
"authors", authors,
|
||||
NULL
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/vu/b4/alsa-scarlett-gui/icons">
|
||||
<file alias="alsa-scarlett-gui-logo.png">img/alsa-scarlett-gui-logo.png</file>
|
||||
<file alias="vu.b4.alsa-scarlett-gui.png">img/vu.b4.alsa-scarlett-gui.png</file>
|
||||
<file alias="socket.svg">img/socket.svg</file>
|
||||
<file alias="audio-volume-high.svg">img/audio-volume-high.svg</file>
|
||||
<file alias="audio-volume-low.svg">img/audio-volume-low.svg</file>
|
||||
<file alias="audio-volume-medium.svg">img/audio-volume-medium.svg</file>
|
||||
<file alias="audio-volume-muted.svg">img/audio-volume-muted.svg</file>
|
||||
</gresource>
|
||||
<gresource prefix="/vu/b4/alsa-scarlett-gui">
|
||||
<file>alsa-scarlett-gui.css</file>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.route-label:hover {
|
||||
.route-label-hover {
|
||||
background: #801010;
|
||||
outline: 2px solid #801010;
|
||||
}
|
||||
@@ -65,6 +65,14 @@
|
||||
background: #801010;
|
||||
}
|
||||
|
||||
.mixer-label {
|
||||
}
|
||||
|
||||
.mixer-label-hover {
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px #00c000, 0 0 15px #00c000;
|
||||
}
|
||||
|
||||
label.gain {
|
||||
font-size: smaller;
|
||||
}
|
||||
@@ -83,7 +91,7 @@ label.gain {
|
||||
}
|
||||
|
||||
.window-frame button:focus:focus-visible {
|
||||
outline-color: #801010;
|
||||
outline: 2px solid #801010;
|
||||
}
|
||||
|
||||
/* padding doesn't work when selected with .window-frame, so use
|
||||
@@ -118,6 +126,11 @@ button.toggle {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.window-frame button.fixed label {
|
||||
color: #ffffff;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* Combobox controls that are always disabled because they indicate status */
|
||||
.window-frame combobox.fixed > box > button {
|
||||
color: #ffffff;
|
||||
@@ -146,6 +159,27 @@ button.toggle {
|
||||
text-shadow: 0 0 5px #0000ff, 0 0 15px #0000ff;
|
||||
}
|
||||
|
||||
.window-frame button.speaker-switching-enable:checked {
|
||||
text-shadow: 0 0 5px #00c000, 0 0 15px #00c000;
|
||||
}
|
||||
|
||||
.window-frame button.speaker-switching-alt {
|
||||
color: #ffffff;
|
||||
text-shadow: 0 0 5px #00ff00, 0 0 15px #00c000;
|
||||
}
|
||||
|
||||
.window-frame button.speaker-switching-alt:checked {
|
||||
text-shadow: 0 0 5px #ff0000, 0 0 15px #c00000;
|
||||
}
|
||||
|
||||
.window-frame button.talkback-enable:checked {
|
||||
text-shadow: 0 0 5px #00c000, 0 0 15px #00c000;
|
||||
}
|
||||
|
||||
.window-frame button.talk:checked {
|
||||
text-shadow: 0 0 5px #00c000, 0 0 15px #00c000;
|
||||
}
|
||||
|
||||
/* orange */
|
||||
.window-frame .vocaster button.autogain:checked {
|
||||
text-shadow: 0 0 5px #ffc000, 0 0 15px #ffc000;
|
||||
@@ -205,6 +239,10 @@ button.toggle {
|
||||
text-shadow: 0 0 5px #00c000, 0 0 15px #00c000;
|
||||
}
|
||||
|
||||
.window-frame button.gain-switch:checked {
|
||||
text-shadow: 0 0 5px #00c000, 0 0 15px #00c000;
|
||||
}
|
||||
|
||||
.window-frame button.phantom:checked {
|
||||
text-shadow: 0 0 5px #ff0000, 0 0 15px #c00000;
|
||||
}
|
||||
|
||||
134
src/alsa-sim.c
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "alsa.h"
|
||||
@@ -99,6 +99,53 @@ static void alsa_parse_enum_items(
|
||||
}
|
||||
}
|
||||
|
||||
static void alsa_parse_int_array(
|
||||
snd_config_t *node,
|
||||
long **int_values
|
||||
) {
|
||||
int count = snd_config_is_array(node);
|
||||
if (count < 0) {
|
||||
printf("error: parse int array value %d\n", count);
|
||||
return;
|
||||
}
|
||||
|
||||
*int_values = calloc(count, sizeof(long));
|
||||
|
||||
int item_num = 0;
|
||||
|
||||
snd_config_iterator_t i, next;
|
||||
snd_config_for_each(i, next, node) {
|
||||
snd_config_t *node = snd_config_iterator_entry(i);
|
||||
|
||||
const char *key;
|
||||
|
||||
int err = snd_config_get_id(node, &key);
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_id error", err);
|
||||
|
||||
int type = snd_config_get_type(node);
|
||||
|
||||
if (type == SND_CONFIG_TYPE_STRING) {
|
||||
const char *string_value;
|
||||
|
||||
err = snd_config_get_string(node, &string_value);
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_string error", err);
|
||||
|
||||
if (strcmp(string_value, "true") == 0)
|
||||
(*int_values)[item_num++] = 1;
|
||||
|
||||
} else if (type == SND_CONFIG_TYPE_INTEGER) {
|
||||
long int_value;
|
||||
err = snd_config_get_integer(node, &int_value);
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_integer error", err);
|
||||
|
||||
(*int_values)[item_num++] = int_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse a comment node and update elem, e.g.:
|
||||
//
|
||||
// comment {
|
||||
@@ -133,7 +180,7 @@ static void alsa_parse_comment_node(
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_string error", err);
|
||||
if (strstr(access, "write"))
|
||||
elem->writable = 1;
|
||||
elem->is_writable = 1;
|
||||
} else if (strcmp(key, "type") == 0) {
|
||||
if (type != SND_CONFIG_TYPE_STRING) {
|
||||
printf("type type not string\n");
|
||||
@@ -149,6 +196,14 @@ static void alsa_parse_comment_node(
|
||||
elem->type = SND_CTL_ELEM_TYPE_ENUMERATED;
|
||||
else if (strcmp(type, "INTEGER") == 0)
|
||||
elem->type = SND_CTL_ELEM_TYPE_INTEGER;
|
||||
} else if (strcmp(key, "count") == 0) {
|
||||
long count;
|
||||
|
||||
err = snd_config_get_integer(node, &count);
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_integer error", err);
|
||||
|
||||
elem->count = count;
|
||||
} else if (strcmp(key, "item") == 0) {
|
||||
alsa_parse_enum_items(node, elem);
|
||||
} else if (strcmp(key, "range") == 0) {
|
||||
@@ -176,7 +231,7 @@ static void alsa_parse_comment_node(
|
||||
err = snd_config_get_integer(node, &dbmin);
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_integer error", err);
|
||||
elem->min_dB = dbmin / 100;
|
||||
elem->min_cdB = dbmin;
|
||||
} else if (strcmp(key, "dbmax") == 0) {
|
||||
if (type != SND_CONFIG_TYPE_INTEGER) {
|
||||
printf("dbmax type not integer\n");
|
||||
@@ -186,14 +241,14 @@ static void alsa_parse_comment_node(
|
||||
err = snd_config_get_integer(node, &dbmax);
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_integer error", err);
|
||||
elem->max_dB = dbmax / 100;
|
||||
elem->max_cdB = dbmax;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int alsa_config_to_new_elem(
|
||||
snd_config_t *config,
|
||||
struct alsa_elem *elem
|
||||
struct alsa_card *card,
|
||||
snd_config_t *config
|
||||
) {
|
||||
const char *s;
|
||||
int id;
|
||||
@@ -202,8 +257,11 @@ static int alsa_config_to_new_elem(
|
||||
int value_type = -1;
|
||||
char *string_value = NULL;
|
||||
long int_value;
|
||||
long *int_values = NULL;
|
||||
int err;
|
||||
|
||||
struct alsa_elem elem = {};
|
||||
|
||||
err = snd_config_get_id(config, &s);
|
||||
if (err < 0)
|
||||
fatal_alsa_error("snd_config_get_id error", err);
|
||||
@@ -260,11 +318,14 @@ static int alsa_config_to_new_elem(
|
||||
fatal_alsa_error("snd_config_get_string error", err);
|
||||
string_value = strdup(s);
|
||||
} else if (type == SND_CONFIG_TYPE_COMPOUND) {
|
||||
elem->count = snd_config_is_array(node);
|
||||
elem.count = snd_config_is_array(node);
|
||||
|
||||
if (strcmp(name, "Level Meter") == 0) {
|
||||
seen_value = 1;
|
||||
value_type = SND_CONFIG_TYPE_INTEGER;
|
||||
int_value = 0;
|
||||
} else if (elem.count == 2 && strncmp(name, "Master", 6) == 0) {
|
||||
alsa_parse_int_array(node, &int_values);
|
||||
} else {
|
||||
goto fail;
|
||||
}
|
||||
@@ -278,7 +339,11 @@ static int alsa_config_to_new_elem(
|
||||
|
||||
// comment node?
|
||||
} else if (strcmp(key, "comment") == 0) {
|
||||
alsa_parse_comment_node(node, elem);
|
||||
alsa_parse_comment_node(node, &elem);
|
||||
|
||||
// this isn't needed
|
||||
} else if (strcmp(key, "index") == 0) {
|
||||
|
||||
} else {
|
||||
printf("skipping unknown node %s for %d\n", key, id);
|
||||
goto fail;
|
||||
@@ -309,21 +374,21 @@ static int alsa_config_to_new_elem(
|
||||
|
||||
// integer in config
|
||||
if (value_type == SND_CONFIG_TYPE_INTEGER) {
|
||||
elem->value = int_value;
|
||||
elem.value = int_value;
|
||||
|
||||
// string in config
|
||||
} else if (value_type == SND_CONFIG_TYPE_STRING) {
|
||||
|
||||
// translate boolean true/false
|
||||
if (elem->type == SND_CTL_ELEM_TYPE_BOOLEAN) {
|
||||
if (elem.type == SND_CTL_ELEM_TYPE_BOOLEAN) {
|
||||
if (strcmp(string_value, "true") == 0)
|
||||
elem->value = 1;
|
||||
elem.value = 1;
|
||||
|
||||
// translate enum string value to integer
|
||||
} else if (elem->type == SND_CTL_ELEM_TYPE_ENUMERATED) {
|
||||
for (int i = 0; i < elem->item_count; i++) {
|
||||
if (strcmp(string_value, elem->item_names[i]) == 0) {
|
||||
elem->value = i;
|
||||
} else if (elem.type == SND_CTL_ELEM_TYPE_ENUMERATED) {
|
||||
for (int i = 0; i < elem.item_count; i++) {
|
||||
if (strcmp(string_value, elem.item_names[i]) == 0) {
|
||||
elem.value = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -334,11 +399,30 @@ static int alsa_config_to_new_elem(
|
||||
}
|
||||
}
|
||||
|
||||
elem->numid = id;
|
||||
elem->name = name;
|
||||
elem.card = card;
|
||||
elem.numid = id;
|
||||
elem.name = name;
|
||||
|
||||
// duplicate the element for each channel except for the Level Meter
|
||||
int count = elem.count;
|
||||
|
||||
if (strcmp(elem.name, "Level Meter") == 0)
|
||||
count = 1;
|
||||
|
||||
// for each channel, create a new element and add it to the card
|
||||
// incrementing the index each time
|
||||
for (int i = 0; i < count; i++, elem.index++) {
|
||||
if (count > 1)
|
||||
elem.value = int_values[i];
|
||||
|
||||
int array_len = card->elems->len;
|
||||
g_array_set_size(card->elems, array_len + 1);
|
||||
g_array_index(card->elems, struct alsa_elem, array_len) = elem;
|
||||
}
|
||||
|
||||
free(iface);
|
||||
free(string_value);
|
||||
free(int_values);
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -346,6 +430,7 @@ fail:
|
||||
free(iface);
|
||||
free(name);
|
||||
free(string_value);
|
||||
free(int_values);
|
||||
|
||||
return -1;
|
||||
}
|
||||
@@ -370,18 +455,8 @@ static void alsa_config_to_new_card(
|
||||
if (snd_config_get_type(config) != SND_CONFIG_TYPE_COMPOUND)
|
||||
continue;
|
||||
|
||||
struct alsa_elem elem = {};
|
||||
elem.card = card;
|
||||
|
||||
// create the element
|
||||
int err = alsa_config_to_new_elem(node, &elem);
|
||||
|
||||
if (err)
|
||||
continue;
|
||||
|
||||
if (card->elems->len <= elem.numid)
|
||||
g_array_set_size(card->elems, elem.numid + 1);
|
||||
g_array_index(card->elems, struct alsa_elem, elem.numid) = elem;
|
||||
alsa_config_to_new_elem(card, node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,5 +508,8 @@ void create_sim_from_file(GtkWindow *w, char *fn) {
|
||||
|
||||
snd_config_delete(config);
|
||||
|
||||
alsa_set_lr_nums(card);
|
||||
alsa_get_routing_controls(card);
|
||||
|
||||
create_card_window(card);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
633
src/alsa.c
@@ -1,23 +1,37 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <sys/inotify.h>
|
||||
#include <alsa/sound/uapi/tlv.h>
|
||||
|
||||
#include "alsa.h"
|
||||
#include "scarlett2-firmware.h"
|
||||
#include "stringhelper.h"
|
||||
#include "window-iface.h"
|
||||
|
||||
#define MAX_TLV_RANGE_SIZE 256
|
||||
#define MAX_TLV_RANGE_SIZE 1024
|
||||
|
||||
// TLV type for channel labels
|
||||
#ifndef SNDRV_CTL_TLVT_FCP_CHANNEL_LABELS
|
||||
#define SNDRV_CTL_TLVT_FCP_CHANNEL_LABELS 0x110
|
||||
#endif
|
||||
|
||||
// names for the port categories
|
||||
const char *port_category_names[PC_COUNT] = {
|
||||
NULL,
|
||||
"Hardware Outputs",
|
||||
"Mixer Inputs",
|
||||
"DSP Inputs",
|
||||
"PCM Inputs"
|
||||
};
|
||||
|
||||
// names for the hardware types
|
||||
const char *hw_type_names[HW_TYPE_COUNT] = {
|
||||
"Analogue",
|
||||
"S/PDIF",
|
||||
"ADAT"
|
||||
};
|
||||
|
||||
// global array of cards
|
||||
static GArray *alsa_cards;
|
||||
|
||||
@@ -45,7 +59,7 @@ void fatal_alsa_error(const char *msg, int err) {
|
||||
//
|
||||
|
||||
// return the element with the exact matching name
|
||||
struct alsa_elem *get_elem_by_name(GArray *elems, char *name) {
|
||||
struct alsa_elem *get_elem_by_name(GArray *elems, const char *name) {
|
||||
for (int i = 0; i < elems->len; i++) {
|
||||
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
|
||||
|
||||
@@ -60,7 +74,7 @@ struct alsa_elem *get_elem_by_name(GArray *elems, char *name) {
|
||||
}
|
||||
|
||||
// return the first element with a name starting with the given prefix
|
||||
struct alsa_elem *get_elem_by_prefix(GArray *elems, char *prefix) {
|
||||
struct alsa_elem *get_elem_by_prefix(GArray *elems, const char *prefix) {
|
||||
int prefix_len = strlen(prefix);
|
||||
|
||||
for (int i = 0; i < elems->len; i++) {
|
||||
@@ -76,12 +90,31 @@ struct alsa_elem *get_elem_by_prefix(GArray *elems, char *prefix) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// return the first element with a name containing the given substring
|
||||
struct alsa_elem *get_elem_by_substr(GArray *elems, const char *substr) {
|
||||
for (int i = 0; i < elems->len; i++) {
|
||||
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
|
||||
|
||||
if (!elem->card)
|
||||
continue;
|
||||
|
||||
if (strstr(elem->name, substr))
|
||||
return elem;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// find the maximum number in the matching elements
|
||||
// search by element name prefix and substring
|
||||
// e.g. get_max_elem_by_name(elems, "Line", "Pad Capture Switch")
|
||||
// will return 8 when the last pad capture switch is
|
||||
// "Line In 8 Pad Capture Switch"
|
||||
int get_max_elem_by_name(GArray *elems, char *prefix, char *needle) {
|
||||
int get_max_elem_by_name(
|
||||
GArray *elems,
|
||||
const char *prefix,
|
||||
const char *needle
|
||||
) {
|
||||
int max = 0;
|
||||
int l = strlen(prefix);
|
||||
|
||||
@@ -106,25 +139,6 @@ int get_max_elem_by_name(GArray *elems, char *prefix, char *needle) {
|
||||
return max;
|
||||
}
|
||||
|
||||
// return true if the element is an routing sink enum, e.g.:
|
||||
// PCM xx Capture Enum
|
||||
// Mixer Input xx Capture Enum
|
||||
// Analogue Output xx Playback Enum
|
||||
// S/PDIF Output xx Playback Enum
|
||||
// ADAT Output xx Playback Enum
|
||||
int is_elem_routing_snk(struct alsa_elem *elem) {
|
||||
if (strstr(elem->name, "Capture Enum") && (
|
||||
strncmp(elem->name, "PCM ", 4) == 0 ||
|
||||
strncmp(elem->name, "Mixer Input ", 12) == 0 ||
|
||||
strncmp(elem->name, "DSP Input ", 10) == 0
|
||||
))
|
||||
return 1;
|
||||
if (strstr(elem->name, "Output") &&
|
||||
strstr(elem->name, "Playback Enum"))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// add a callback to the list of callbacks for this element
|
||||
void alsa_elem_add_callback(
|
||||
struct alsa_elem *elem,
|
||||
@@ -182,11 +196,11 @@ long alsa_get_elem_value(struct alsa_elem *elem) {
|
||||
|
||||
int type = elem->type;
|
||||
if (type == SND_CTL_ELEM_TYPE_BOOLEAN) {
|
||||
return snd_ctl_elem_value_get_boolean(elem_value, 0);
|
||||
return snd_ctl_elem_value_get_boolean(elem_value, elem->index);
|
||||
} else if (type == SND_CTL_ELEM_TYPE_ENUMERATED) {
|
||||
return snd_ctl_elem_value_get_enumerated(elem_value, 0);
|
||||
return snd_ctl_elem_value_get_enumerated(elem_value, elem->index);
|
||||
} else if (type == SND_CTL_ELEM_TYPE_INTEGER) {
|
||||
return snd_ctl_elem_value_get_integer(elem_value, 0);
|
||||
return snd_ctl_elem_value_get_integer(elem_value, elem->index);
|
||||
} else {
|
||||
fprintf(
|
||||
stderr,
|
||||
@@ -237,14 +251,15 @@ void alsa_set_elem_value(struct alsa_elem *elem, long value) {
|
||||
|
||||
snd_ctl_elem_value_alloca(&elem_value);
|
||||
snd_ctl_elem_value_set_numid(elem_value, elem->numid);
|
||||
snd_ctl_elem_read(elem->card->handle, elem_value);
|
||||
|
||||
int type = elem->type;
|
||||
if (type == SND_CTL_ELEM_TYPE_BOOLEAN) {
|
||||
snd_ctl_elem_value_set_boolean(elem_value, 0, value);
|
||||
snd_ctl_elem_value_set_boolean(elem_value, elem->index, value);
|
||||
} else if (type == SND_CTL_ELEM_TYPE_ENUMERATED) {
|
||||
snd_ctl_elem_value_set_enumerated(elem_value, 0, value);
|
||||
snd_ctl_elem_value_set_enumerated(elem_value, elem->index, value);
|
||||
} else if (type == SND_CTL_ELEM_TYPE_INTEGER) {
|
||||
snd_ctl_elem_value_set_integer(elem_value, 0, value);
|
||||
snd_ctl_elem_value_set_integer(elem_value, elem->index, value);
|
||||
} else {
|
||||
fprintf(
|
||||
stderr,
|
||||
@@ -262,7 +277,7 @@ void alsa_set_elem_value(struct alsa_elem *elem, long value) {
|
||||
// return whether the element can be modified (is writable)
|
||||
int alsa_get_elem_writable(struct alsa_elem *elem) {
|
||||
if (elem->card->num == SIMULATED_CARD_NUM)
|
||||
return elem->writable;
|
||||
return elem->is_writable;
|
||||
|
||||
snd_ctl_elem_info_t *elem_info;
|
||||
|
||||
@@ -270,7 +285,23 @@ int alsa_get_elem_writable(struct alsa_elem *elem) {
|
||||
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
|
||||
snd_ctl_elem_info(elem->card->handle, elem_info);
|
||||
|
||||
return snd_ctl_elem_info_is_writable(elem_info);
|
||||
return snd_ctl_elem_info_is_writable(elem_info) &&
|
||||
!snd_ctl_elem_info_is_locked(elem_info);
|
||||
}
|
||||
|
||||
// return whether the element is volatile (can change without
|
||||
// notification)
|
||||
int alsa_get_elem_volatile(struct alsa_elem *elem) {
|
||||
if (elem->card->num == SIMULATED_CARD_NUM)
|
||||
return elem->is_volatile;
|
||||
|
||||
snd_ctl_elem_info_t *elem_info;
|
||||
|
||||
snd_ctl_elem_info_alloca(&elem_info);
|
||||
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
|
||||
snd_ctl_elem_info(elem->card->handle, elem_info);
|
||||
|
||||
return snd_ctl_elem_info_is_volatile(elem_info);
|
||||
}
|
||||
|
||||
// get the number of values this element has
|
||||
@@ -319,6 +350,163 @@ char *alsa_get_item_name(struct alsa_elem *elem, int i) {
|
||||
// create/destroy alsa cards
|
||||
//
|
||||
|
||||
static void alsa_get_elem_tlv(struct alsa_elem *elem) {
|
||||
struct alsa_card *card = elem->card;
|
||||
|
||||
if (elem->type != SND_CTL_ELEM_TYPE_INTEGER)
|
||||
return;
|
||||
|
||||
snd_ctl_elem_info_t *elem_info;
|
||||
|
||||
snd_ctl_elem_info_alloca(&elem_info);
|
||||
snd_ctl_elem_info_set_numid(elem_info, elem->numid);
|
||||
snd_ctl_elem_info(card->handle, elem_info);
|
||||
|
||||
if (!snd_ctl_elem_info_is_tlv_readable(elem_info))
|
||||
return;
|
||||
|
||||
snd_ctl_elem_id_t *elem_id;
|
||||
unsigned int tlv[MAX_TLV_RANGE_SIZE];
|
||||
unsigned int *dbrec;
|
||||
int ret;
|
||||
long min_cdB, max_cdB;
|
||||
|
||||
snd_ctl_elem_id_alloca(&elem_id);
|
||||
snd_ctl_elem_id_set_numid(elem_id, elem->numid);
|
||||
|
||||
ret = snd_ctl_elem_tlv_read(
|
||||
card->handle, elem_id, tlv, sizeof(tlv)
|
||||
);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "TLV read error: %s\n", snd_strerror(ret));
|
||||
return;
|
||||
}
|
||||
|
||||
// meter map
|
||||
if (tlv[SNDRV_CTL_TLVO_TYPE] == SNDRV_CTL_TLVT_FCP_CHANNEL_LABELS) {
|
||||
int label_data_size = tlv[SNDRV_CTL_TLVO_LEN];
|
||||
char *label_data = (char *)&tlv[SNDRV_CTL_TLVO_LEN + 1];
|
||||
|
||||
// check that there are at least elem->count labels in the data
|
||||
int label_count = 0;
|
||||
for (int i = 0; i < label_data_size; i++) {
|
||||
if (!label_data[i])
|
||||
label_count++;
|
||||
}
|
||||
|
||||
if (label_count < elem->count) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"TLV label count %d < %d\n",
|
||||
label_count,
|
||||
elem->count
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (elem->count < 0 || elem->count > 255) {
|
||||
fprintf(stderr, "TLV label count %d out of range\n", elem->count);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
elem->meter_labels = calloc(elem->count, sizeof(char *));
|
||||
|
||||
char *cur_label = label_data;
|
||||
for (int i = 0; i < elem->count; i++) {
|
||||
elem->meter_labels[i] = strdup(cur_label);
|
||||
if (!elem->meter_labels[i]) {
|
||||
fprintf(stderr, "strdup failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
cur_label += strlen(cur_label) + 1;
|
||||
}
|
||||
|
||||
/* firmware version TLV contains socket location */
|
||||
} else if (tlv[SNDRV_CTL_TLVO_TYPE] == 0x53434B54) {
|
||||
card->fcp_socket = strdup((char *)&tlv[SNDRV_CTL_TLVO_LEN + 1]);
|
||||
|
||||
/* dB range */
|
||||
} else {
|
||||
ret = snd_tlv_parse_dB_info(tlv, sizeof(tlv), &dbrec);
|
||||
if (ret <= 0) {
|
||||
fprintf(stderr, "TLV parse error: %s\n", snd_strerror(ret));
|
||||
return;
|
||||
}
|
||||
|
||||
int min_val = snd_ctl_elem_info_get_min(elem_info);
|
||||
int max_val = snd_ctl_elem_info_get_max(elem_info);
|
||||
|
||||
ret = snd_tlv_get_dB_range(
|
||||
dbrec, min_val, max_val, &min_cdB, &max_cdB
|
||||
);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "TLV range error: %s\n", snd_strerror(ret));
|
||||
return;
|
||||
}
|
||||
|
||||
elem->min_val = min_val;
|
||||
elem->max_val = max_val;
|
||||
elem->dB_type = dbrec[SNDRV_CTL_TLVO_TYPE];
|
||||
elem->min_cdB = min_cdB;
|
||||
elem->max_cdB = max_cdB;
|
||||
}
|
||||
}
|
||||
|
||||
static void alsa_get_elem(struct alsa_card *card, int numid) {
|
||||
// allocate a temporary struct alsa_elem (will be copied later if
|
||||
// we want to keep it)
|
||||
struct alsa_elem alsa_elem = {};
|
||||
|
||||
// keep a reference to the card in the element
|
||||
alsa_elem.card = card;
|
||||
|
||||
// get the control's numeric identifier (different to the index
|
||||
// into this array)
|
||||
alsa_elem.numid = numid;
|
||||
|
||||
// get the control's info
|
||||
alsa_elem.type = alsa_get_elem_type(&alsa_elem);
|
||||
alsa_elem.name = alsa_get_elem_name(&alsa_elem);
|
||||
alsa_elem.count = alsa_get_elem_count(&alsa_elem);
|
||||
|
||||
switch (alsa_elem.type) {
|
||||
case SND_CTL_ELEM_TYPE_BOOLEAN:
|
||||
case SND_CTL_ELEM_TYPE_ENUMERATED:
|
||||
case SND_CTL_ELEM_TYPE_INTEGER:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (strstr(alsa_elem.name, "Validity"))
|
||||
return;
|
||||
if (strstr(alsa_elem.name, "Channel Map"))
|
||||
return;
|
||||
|
||||
alsa_get_elem_tlv(&alsa_elem);
|
||||
|
||||
// Scarlett 1st Gen driver puts two volume controls/mutes in the
|
||||
// same element, so split them out to match the other series
|
||||
int count = alsa_elem.count;
|
||||
|
||||
if (strcmp(alsa_elem.name, "Level Meter") == 0)
|
||||
count = 1;
|
||||
|
||||
if (count > 2) {
|
||||
fprintf(stderr, "element %s has count %d\n", alsa_elem.name, count);
|
||||
count = 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++, alsa_elem.lr_num++) {
|
||||
alsa_elem.index = i;
|
||||
|
||||
int array_len = card->elems->len;
|
||||
g_array_set_size(card->elems, array_len + 1);
|
||||
g_array_index(card->elems, struct alsa_elem, array_len) = alsa_elem;
|
||||
}
|
||||
}
|
||||
|
||||
// scan the ALSA ctl element list container and put the useful
|
||||
// elements into the cards->elems array of struct alsa_elem
|
||||
static void alsa_get_elem_list(struct alsa_card *card) {
|
||||
@@ -334,88 +522,8 @@ static void alsa_get_elem_list(struct alsa_card *card) {
|
||||
|
||||
// for each element in the list
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
||||
// allocate a temporary struct alsa_elem (will be copied later if
|
||||
// we want to keep it)
|
||||
struct alsa_elem alsa_elem = {};
|
||||
|
||||
// keep a reference to the card in the element
|
||||
alsa_elem.card = card;
|
||||
|
||||
// get the control's numeric identifier (different to the index
|
||||
// into this array)
|
||||
alsa_elem.numid = snd_ctl_elem_list_get_numid(list, i);
|
||||
|
||||
// get the control's info
|
||||
alsa_elem.type = alsa_get_elem_type(&alsa_elem);
|
||||
alsa_elem.name = alsa_get_elem_name(&alsa_elem);
|
||||
alsa_elem.count = alsa_get_elem_count(&alsa_elem);
|
||||
|
||||
switch (alsa_elem.type) {
|
||||
case SND_CTL_ELEM_TYPE_BOOLEAN:
|
||||
case SND_CTL_ELEM_TYPE_ENUMERATED:
|
||||
case SND_CTL_ELEM_TYPE_INTEGER:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strstr(alsa_elem.name, "Validity"))
|
||||
continue;
|
||||
if (strstr(alsa_elem.name, "Channel Map"))
|
||||
continue;
|
||||
|
||||
// get TLV info if it's a volume control
|
||||
if (alsa_elem.type == SND_CTL_ELEM_TYPE_INTEGER) {
|
||||
snd_ctl_elem_info_t *elem_info;
|
||||
|
||||
snd_ctl_elem_info_alloca(&elem_info);
|
||||
snd_ctl_elem_info_set_numid(elem_info, alsa_elem.numid);
|
||||
snd_ctl_elem_info(card->handle, elem_info);
|
||||
|
||||
if (snd_ctl_elem_info_is_tlv_readable(elem_info)) {
|
||||
snd_ctl_elem_id_t *elem_id;
|
||||
unsigned int tlv[MAX_TLV_RANGE_SIZE];
|
||||
unsigned int *dbrec;
|
||||
int ret;
|
||||
long min_dB, max_dB;
|
||||
|
||||
snd_ctl_elem_id_alloca(&elem_id);
|
||||
snd_ctl_elem_id_set_numid(elem_id, alsa_elem.numid);
|
||||
|
||||
ret = snd_ctl_elem_tlv_read(
|
||||
card->handle, elem_id, tlv, sizeof(tlv)
|
||||
);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "TLV read error %d\n", ret);
|
||||
continue;
|
||||
}
|
||||
|
||||
ret = snd_tlv_parse_dB_info(tlv, sizeof(tlv), &dbrec);
|
||||
if (ret <= 0) {
|
||||
fprintf(stderr, "TLV parse error %d\n", ret);
|
||||
continue;
|
||||
}
|
||||
|
||||
int min_val = snd_ctl_elem_info_get_min(elem_info);
|
||||
int max_val = snd_ctl_elem_info_get_max(elem_info);
|
||||
|
||||
ret = snd_tlv_get_dB_range(tlv, min_val, max_val, &min_dB, &max_dB);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "TLV range error %d\n", ret);
|
||||
continue;
|
||||
}
|
||||
|
||||
alsa_elem.min_val = min_val;
|
||||
alsa_elem.max_val = max_val;
|
||||
alsa_elem.min_dB = min_dB / 100;
|
||||
alsa_elem.max_dB = max_dB / 100;
|
||||
}
|
||||
}
|
||||
|
||||
if (card->elems->len <= alsa_elem.numid)
|
||||
g_array_set_size(card->elems, alsa_elem.numid + 1);
|
||||
g_array_index(card->elems, struct alsa_elem, alsa_elem.numid) = alsa_elem;
|
||||
int numid = snd_ctl_elem_list_get_numid(list, i);
|
||||
alsa_get_elem(card, numid);
|
||||
}
|
||||
|
||||
// free the ALSA list
|
||||
@@ -423,6 +531,227 @@ static void alsa_get_elem_list(struct alsa_card *card) {
|
||||
snd_ctl_elem_list_free(list);
|
||||
}
|
||||
|
||||
static void alsa_set_elem_lr_num(struct alsa_elem *elem) {
|
||||
const char *name = elem->name;
|
||||
char side;
|
||||
|
||||
if (strncmp(name, "Master Playback", 15) == 0 ||
|
||||
strncmp(name, "Master HW Playback", 18) == 0)
|
||||
elem->lr_num = 0;
|
||||
|
||||
else if (strncmp(name, "Master", 6) == 0)
|
||||
if (sscanf(name, "Master %d%c", &elem->lr_num, &side) != 2)
|
||||
printf("can't parse Master '%s'\n", name);
|
||||
else
|
||||
elem->lr_num = elem->lr_num * 2
|
||||
- (side == 'L' || side == ' ')
|
||||
+ elem->index;
|
||||
|
||||
else
|
||||
elem->lr_num = get_num_from_string(name);
|
||||
}
|
||||
|
||||
void alsa_set_lr_nums(struct alsa_card *card) {
|
||||
for (int i = 0; i < card->elems->len; i++) {
|
||||
struct alsa_elem *elem = &g_array_index(card->elems, struct alsa_elem, i);
|
||||
|
||||
alsa_set_elem_lr_num(elem);
|
||||
}
|
||||
}
|
||||
|
||||
static void get_routing_srcs(struct alsa_card *card) {
|
||||
struct alsa_elem *elem = card->sample_capture_elem;
|
||||
|
||||
int count = alsa_get_item_count(elem);
|
||||
card->routing_srcs = g_array_new(
|
||||
FALSE, TRUE, sizeof(struct routing_src)
|
||||
);
|
||||
g_array_set_size(card->routing_srcs, count);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
char *name = alsa_get_item_name(elem, i);
|
||||
|
||||
struct routing_src *r = &g_array_index(
|
||||
card->routing_srcs, struct routing_src, i
|
||||
);
|
||||
r->card = card;
|
||||
r->id = i;
|
||||
|
||||
if (strcmp(name, "Off") == 0)
|
||||
r->port_category = PC_OFF;
|
||||
else if (strncmp(name, "Mix", 3) == 0)
|
||||
r->port_category = PC_MIX;
|
||||
else if (strncmp(name, "DSP", 3) == 0)
|
||||
r->port_category = PC_DSP;
|
||||
else if (strncmp(name, "PCM", 3) == 0)
|
||||
r->port_category = PC_PCM;
|
||||
else {
|
||||
r->port_category = PC_HW;
|
||||
|
||||
if (strncmp(name, "Analog", 6) == 0)
|
||||
r->hw_type = HW_TYPE_ANALOGUE;
|
||||
else if (strncmp(name, "S/PDIF", 6) == 0)
|
||||
r->hw_type = HW_TYPE_SPDIF;
|
||||
else if (strncmp(name, "SPDIF", 5) == 0)
|
||||
r->hw_type = HW_TYPE_SPDIF;
|
||||
else if (strncmp(name, "ADAT", 4) == 0)
|
||||
r->hw_type = HW_TYPE_ADAT;
|
||||
}
|
||||
|
||||
r->name = name;
|
||||
r->lr_num =
|
||||
r->port_category == PC_MIX
|
||||
? name[4] - 'A' + 1
|
||||
: get_num_from_string(name);
|
||||
|
||||
r->port_num = card->routing_in_count[r->port_category]++;
|
||||
}
|
||||
|
||||
assert(card->routing_in_count[PC_MIX] <= MAX_MIX_OUT);
|
||||
}
|
||||
|
||||
// return true if the element is an routing sink enum, e.g.:
|
||||
// PCM xx Capture Enum
|
||||
// Mixer Input xx Capture Enum
|
||||
// Analogue Output xx Playback Enum
|
||||
// S/PDIF Output xx Playback Enum
|
||||
// ADAT Output xx Playback Enum
|
||||
//
|
||||
// or new style:
|
||||
// PCM xx Capture Enum
|
||||
// Mixer xx Capture Enum
|
||||
// Analogue xx Playback Enum
|
||||
// S/PDIF xx Playback Enum
|
||||
// ADAT xx Playback Enum
|
||||
|
||||
static int is_elem_routing_snk(struct alsa_elem *elem) {
|
||||
if (strstr(elem->name, "Capture Route") ||
|
||||
strstr(elem->name, "Input Playback Route") ||
|
||||
strstr(elem->name, "Source Playback Enu"))
|
||||
return 1;
|
||||
|
||||
if (strstr(elem->name, "Capture Enum") && (
|
||||
strncmp(elem->name, "PCM ", 4) == 0 ||
|
||||
strncmp(elem->name, "Mixer ", 6) == 0 ||
|
||||
strncmp(elem->name, "DSP ", 4) == 0
|
||||
))
|
||||
return 1;
|
||||
|
||||
if (strstr(elem->name, "Playback Enum") && (
|
||||
strncmp(elem->name, "Analogue ", 9) == 0 ||
|
||||
strncmp(elem->name, "S/PDIF ", 7) == 0 ||
|
||||
strncmp(elem->name, "ADAT ", 5) == 0
|
||||
))
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void get_routing_snks(struct alsa_card *card) {
|
||||
GArray *elems = card->elems;
|
||||
|
||||
int count = 0;
|
||||
|
||||
// count and label routing snks
|
||||
for (int i = 0; i < elems->len; i++) {
|
||||
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
|
||||
|
||||
if (!elem->card)
|
||||
continue;
|
||||
|
||||
if (!is_elem_routing_snk(elem))
|
||||
continue;
|
||||
|
||||
elem->is_routing_snk = 1;
|
||||
|
||||
if (strncmp(elem->name, "Mixer", 5) == 0 ||
|
||||
strncmp(elem->name, "Matrix", 6) == 0) {
|
||||
elem->port_category = PC_MIX;
|
||||
|
||||
if (!alsa_get_elem_writable(elem))
|
||||
card->has_fixed_mixer_inputs = 1;
|
||||
|
||||
} else if (strncmp(elem->name, "DSP", 3) == 0) {
|
||||
elem->port_category = PC_DSP;
|
||||
} else if (strncmp(elem->name, "PCM", 3) == 0 ||
|
||||
strncmp(elem->name, "Input Source", 12) == 0) {
|
||||
elem->port_category = PC_PCM;
|
||||
} else if (strstr(elem->name, "Playback Enu")) {
|
||||
elem->port_category = PC_HW;
|
||||
|
||||
if (strncmp(elem->name, "Analog", 6) == 0)
|
||||
elem->hw_type = HW_TYPE_ANALOGUE;
|
||||
else if (strncmp(elem->name, "S/PDIF", 6) == 0 ||
|
||||
strstr(elem->name, "SPDIF"))
|
||||
elem->hw_type = HW_TYPE_SPDIF;
|
||||
else if (strstr(elem->name, "ADAT"))
|
||||
elem->hw_type = HW_TYPE_ADAT;
|
||||
} else {
|
||||
printf("unknown mixer routing elem %s\n", elem->name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (elem->lr_num <= 0) {
|
||||
fprintf(stderr, "routing sink %s had no number\n", elem->name);
|
||||
continue;
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
// create an array of routing snks pointing to those elements
|
||||
card->routing_snks = g_array_new(
|
||||
FALSE, TRUE, sizeof(struct routing_snk)
|
||||
);
|
||||
g_array_set_size(card->routing_snks, count);
|
||||
|
||||
// count through card->routing_snks
|
||||
int j = 0;
|
||||
|
||||
for (int i = 0; i < elems->len; i++) {
|
||||
struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
|
||||
|
||||
if (!elem->is_routing_snk)
|
||||
continue;
|
||||
|
||||
struct routing_snk *r = &g_array_index(
|
||||
card->routing_snks, struct routing_snk, j
|
||||
);
|
||||
r->idx = j;
|
||||
j++;
|
||||
r->elem = elem;
|
||||
elem->port_num = card->routing_out_count[elem->port_category]++;
|
||||
}
|
||||
|
||||
assert(j == count);
|
||||
}
|
||||
|
||||
void alsa_get_routing_controls(struct alsa_card *card) {
|
||||
|
||||
// check that we can find a routing control
|
||||
card->sample_capture_elem =
|
||||
get_elem_by_name(card->elems, "PCM 01 Capture Enum");
|
||||
if (!card->sample_capture_elem)
|
||||
card->sample_capture_elem =
|
||||
get_elem_by_name(card->elems, "PCM 1 Capture Enum");
|
||||
if (!card->sample_capture_elem)
|
||||
card->sample_capture_elem =
|
||||
get_elem_by_name(card->elems, "Input Source 01 Capture Route");
|
||||
|
||||
if (!card->sample_capture_elem) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"can't find routing control PCM 01 Capture Enum or "
|
||||
"Input Source 01 Capture Route\n"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
get_routing_srcs(card);
|
||||
get_routing_snks(card);
|
||||
}
|
||||
|
||||
static void alsa_elem_change(struct alsa_elem *elem) {
|
||||
if (!elem || !elem->callbacks)
|
||||
return;
|
||||
@@ -437,6 +766,27 @@ static void alsa_elem_change(struct alsa_elem *elem) {
|
||||
}
|
||||
}
|
||||
|
||||
static void card_destroy_callback(void *data) {
|
||||
struct alsa_card *card = data;
|
||||
|
||||
// close the windows associated with this card
|
||||
destroy_card_window(card);
|
||||
|
||||
// TODO: there is more to free
|
||||
free(card->device);
|
||||
free(card->serial);
|
||||
free(card->name);
|
||||
free(card);
|
||||
|
||||
// go through the alsa_cards array and clear the entry for this card
|
||||
for (int i = 0; i < alsa_cards->len; i++) {
|
||||
struct alsa_card **card_ptr =
|
||||
&g_array_index(alsa_cards, struct alsa_card *, i);
|
||||
if (*card_ptr == card)
|
||||
*card_ptr = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean alsa_card_callback(
|
||||
GIOChannel *source,
|
||||
GIOCondition condition,
|
||||
@@ -444,16 +794,13 @@ static gboolean alsa_card_callback(
|
||||
) {
|
||||
struct alsa_card *card = data;
|
||||
snd_ctl_event_t *event;
|
||||
unsigned int mask;
|
||||
int err, numid;
|
||||
struct alsa_elem *elem;
|
||||
|
||||
snd_ctl_event_alloca(&event);
|
||||
if (!card->handle) {
|
||||
printf("oops, no card handle??\n");
|
||||
return 0;
|
||||
}
|
||||
err = snd_ctl_read(card->handle, event);
|
||||
int err = snd_ctl_read(card->handle, event);
|
||||
if (err == 0) {
|
||||
printf("alsa_card_callback nothing to read??\n");
|
||||
return 0;
|
||||
@@ -464,18 +811,27 @@ static gboolean alsa_card_callback(
|
||||
printf("card_callback_error %d\n", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM)
|
||||
return 1;
|
||||
|
||||
numid = snd_ctl_event_elem_get_numid(event);
|
||||
elem = &g_array_index(card->elems, struct alsa_elem, numid);
|
||||
if (elem->numid != numid)
|
||||
int numid = snd_ctl_event_elem_get_numid(event);
|
||||
unsigned int mask = snd_ctl_event_elem_get_mask(event);
|
||||
|
||||
if (mask == SND_CTL_EVENT_MASK_REMOVE) {
|
||||
card_destroy_callback(card);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!(mask & (SND_CTL_EVENT_MASK_VALUE | SND_CTL_EVENT_MASK_INFO)))
|
||||
return 1;
|
||||
|
||||
mask = snd_ctl_event_elem_get_mask(event);
|
||||
for (int i = 0; i < card->elems->len; i++) {
|
||||
struct alsa_elem *elem = &g_array_index(card->elems, struct alsa_elem, i);
|
||||
|
||||
if (mask & (SND_CTL_EVENT_MASK_VALUE | SND_CTL_EVENT_MASK_INFO))
|
||||
alsa_elem_change(elem);
|
||||
if (elem->numid == numid)
|
||||
alsa_elem_change(elem);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -524,27 +880,6 @@ struct alsa_card *card_create(int card_num) {
|
||||
return card;
|
||||
}
|
||||
|
||||
static void card_destroy_callback(void *data) {
|
||||
struct alsa_card *card = data;
|
||||
|
||||
// close the windows associated with this card
|
||||
destroy_card_window(card);
|
||||
|
||||
// TODO: there is more to free
|
||||
free(card->device);
|
||||
free(card->serial);
|
||||
free(card->name);
|
||||
free(card);
|
||||
|
||||
// go through the alsa_cards array and clear the entry for this card
|
||||
for (int i = 0; i < alsa_cards->len; i++) {
|
||||
struct alsa_card **card_ptr =
|
||||
&g_array_index(alsa_cards, struct alsa_card *, i);
|
||||
if (*card_ptr == card)
|
||||
*card_ptr = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void alsa_add_card_callback(struct alsa_card *card) {
|
||||
card->io_channel = g_io_channel_unix_new(card->pfd.fd);
|
||||
card->event_source_id = g_io_add_watch_full(
|
||||
@@ -776,14 +1111,18 @@ static void alsa_scan_cards(void) {
|
||||
card->handle = ctl;
|
||||
|
||||
alsa_get_elem_list(card);
|
||||
alsa_set_lr_nums(card);
|
||||
alsa_get_routing_controls(card);
|
||||
|
||||
alsa_subscribe(card);
|
||||
alsa_get_usbid(card);
|
||||
alsa_get_serial_number(card);
|
||||
card->best_firmware_version = scarlett2_get_best_firmware_version(card->pid);
|
||||
card->best_firmware_version =
|
||||
scarlett2_get_best_firmware_version(card->pid);
|
||||
|
||||
if (card->serial) {
|
||||
|
||||
// call the callbacks for this card
|
||||
// call the reopen callbacks for this card
|
||||
struct reopen_callback *rc = g_hash_table_lookup(
|
||||
reopen_callbacks, card->serial
|
||||
);
|
||||
|
||||
89
src/alsa.h
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -22,25 +22,28 @@ typedef void (AlsaElemCallback)(struct alsa_elem *, void *);
|
||||
// port categories for routing_src and routing_snk entries
|
||||
// must match the level meter ordering from the driver
|
||||
enum {
|
||||
// Hardware inputs/outputs
|
||||
PC_HW = 0,
|
||||
|
||||
// Mixer inputs/outputs
|
||||
PC_MIX = 1,
|
||||
|
||||
// DSP inputs/outputs
|
||||
PC_DSP = 2,
|
||||
|
||||
// PCM inputs/outputs
|
||||
PC_PCM = 3,
|
||||
|
||||
// number of port categories
|
||||
PC_COUNT = 4
|
||||
PC_OFF, // Off (the source when a sink is not connected)
|
||||
PC_HW, // Hardware inputs/outputs
|
||||
PC_MIX, // Mixer inputs/outputs
|
||||
PC_DSP, // DSP inputs/outputs
|
||||
PC_PCM, // PCM inputs/outputs
|
||||
PC_COUNT // number of port categories
|
||||
};
|
||||
|
||||
// names for the port categories
|
||||
extern const char *port_category_names[PC_COUNT];
|
||||
|
||||
// hardware types
|
||||
enum {
|
||||
HW_TYPE_ANALOGUE,
|
||||
HW_TYPE_SPDIF,
|
||||
HW_TYPE_ADAT,
|
||||
HW_TYPE_COUNT
|
||||
};
|
||||
|
||||
// names for the hardware types
|
||||
extern const char *hw_type_names[HW_TYPE_COUNT];
|
||||
|
||||
// is a drag active, and whether dragging from a routing source or a
|
||||
// routing sink
|
||||
enum {
|
||||
@@ -59,7 +62,7 @@ struct routing_src {
|
||||
// the enum id of the alsa item
|
||||
int id;
|
||||
|
||||
// PC_DSP, PC_MIX, PC_PCM, or PC_HW
|
||||
// PC_OFF, PC_DSP, PC_MIX, PC_PCM, or PC_HW
|
||||
int port_category;
|
||||
|
||||
// 0-based count within port_category
|
||||
@@ -68,6 +71,9 @@ struct routing_src {
|
||||
// the alsa item name
|
||||
char *name;
|
||||
|
||||
// for PC_HW, the hardware type
|
||||
int hw_type;
|
||||
|
||||
// the number (or translated letter; A = 1) in the item name
|
||||
int lr_num;
|
||||
|
||||
@@ -82,8 +88,6 @@ struct routing_src {
|
||||
// entry in alsa_card routing_snks (routing sinks) array for alsa
|
||||
// elements that are routing sinks like Analogue Output 01 Playback
|
||||
// Enum
|
||||
// port_category is set to PC_DSP, PC_MIX, PC_PCM, PC_HW
|
||||
// port_num is a count (0-based) within that category
|
||||
struct routing_snk {
|
||||
|
||||
// location within the array
|
||||
@@ -98,12 +102,6 @@ struct routing_snk {
|
||||
// socket widget on the routing page
|
||||
GtkWidget *socket_widget;
|
||||
|
||||
// PC_DSP, PC_MIX, PC_PCM, or PC_HW
|
||||
int port_category;
|
||||
|
||||
// 0-based count within port_category
|
||||
int port_num;
|
||||
|
||||
// the mixer label widgets for this sink
|
||||
GtkWidget *mixer_label_top;
|
||||
GtkWidget *mixer_label_bottom;
|
||||
@@ -126,22 +124,31 @@ struct alsa_elem {
|
||||
const char *name;
|
||||
int type;
|
||||
int count;
|
||||
int index;
|
||||
|
||||
// for gain/volume elements, the dB range and step
|
||||
// for gain/volume elements, the value range, dB type, and dB range
|
||||
int min_val;
|
||||
int max_val;
|
||||
int min_dB;
|
||||
int max_dB;
|
||||
int dB_type;
|
||||
int min_cdB;
|
||||
int max_cdB;
|
||||
|
||||
// for the number (or translated letter; A = 1) in the item name
|
||||
// TODO: move this to struct routing_snk?
|
||||
// level meter labels
|
||||
char **meter_labels;
|
||||
|
||||
// for routing sinks
|
||||
int is_routing_snk;
|
||||
int port_category;
|
||||
int port_num;
|
||||
int hw_type;
|
||||
int lr_num;
|
||||
|
||||
// the callback functions for this ALSA control element
|
||||
GList *callbacks;
|
||||
|
||||
// for simulated elements, the current state
|
||||
int writable;
|
||||
int is_writable;
|
||||
int is_volatile;
|
||||
long value;
|
||||
|
||||
// for simulated enumerated elements, the items
|
||||
@@ -155,12 +162,12 @@ struct alsa_card {
|
||||
uint32_t pid;
|
||||
char *serial;
|
||||
char *name;
|
||||
char *fcp_socket;
|
||||
int best_firmware_version;
|
||||
snd_ctl_t *handle;
|
||||
struct pollfd pfd;
|
||||
GArray *elems;
|
||||
struct alsa_elem *sample_capture_elem;
|
||||
struct alsa_elem *level_meter_elem;
|
||||
GArray *routing_srcs;
|
||||
GArray *routing_snks;
|
||||
GIOChannel *io_channel;
|
||||
@@ -182,10 +189,9 @@ struct alsa_card {
|
||||
GtkWidget *routing_dsp_out_grid;
|
||||
GtkWidget *routing_mixer_in_grid;
|
||||
GtkWidget *routing_mixer_out_grid;
|
||||
GtkWidget *meters[MAX_METERS];
|
||||
guint meter_gsource_timer;
|
||||
int has_speaker_switching;
|
||||
int has_talkback;
|
||||
int has_fixed_mixer_inputs;
|
||||
int routing_out_count[PC_COUNT];
|
||||
int routing_in_count[PC_COUNT];
|
||||
GMenu *routing_src_menu;
|
||||
@@ -200,10 +206,14 @@ struct alsa_card {
|
||||
void fatal_alsa_error(const char *msg, int err);
|
||||
|
||||
// locate elements or get information about them
|
||||
struct alsa_elem *get_elem_by_name(GArray *elems, char *name);
|
||||
struct alsa_elem *get_elem_by_prefix(GArray *elems, char *prefix);
|
||||
int get_max_elem_by_name(GArray *elems, char *prefix, char *needle);
|
||||
int is_elem_routing_snk(struct alsa_elem *elem);
|
||||
struct alsa_elem *get_elem_by_name(GArray *elems, const char *name);
|
||||
struct alsa_elem *get_elem_by_prefix(GArray *elems, const char *prefix);
|
||||
struct alsa_elem *get_elem_by_substr(GArray *elems, const char *substr);
|
||||
int get_max_elem_by_name(
|
||||
GArray *elems,
|
||||
const char *prefix,
|
||||
const char *needle
|
||||
);
|
||||
|
||||
// add callback to alsa_elem callback list
|
||||
void alsa_elem_add_callback(
|
||||
@@ -219,6 +229,7 @@ long alsa_get_elem_value(struct alsa_elem *elem);
|
||||
int *alsa_get_elem_int_values(struct alsa_elem *elem);
|
||||
void alsa_set_elem_value(struct alsa_elem *elem, long value);
|
||||
int alsa_get_elem_writable(struct alsa_elem *elem);
|
||||
int alsa_get_elem_volatile(struct alsa_elem *elem);
|
||||
int alsa_get_elem_count(struct alsa_elem *elem);
|
||||
int alsa_get_item_count(struct alsa_elem *elem);
|
||||
char *alsa_get_item_name(struct alsa_elem *elem, int i);
|
||||
@@ -226,6 +237,10 @@ char *alsa_get_item_name(struct alsa_elem *elem, int i);
|
||||
// add to alsa_cards array
|
||||
struct alsa_card *card_create(int card_num);
|
||||
|
||||
// parse elements (used by alsa-sim.c)
|
||||
void alsa_set_lr_nums(struct alsa_card *card);
|
||||
void alsa_get_routing_controls(struct alsa_card *card);
|
||||
|
||||
// init
|
||||
void alsa_init(void);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -7,7 +7,7 @@
|
||||
#define MAX_MIX_OUT 12
|
||||
|
||||
// maximum number of mux inputs
|
||||
#define MAX_MUX_IN 25
|
||||
#define MAX_MUX_IN 42
|
||||
|
||||
// maximum number of meters
|
||||
#define MAX_METERS 65
|
||||
#define MAX_METERS 92
|
||||
|
||||
83
src/db.c
Normal file
@@ -0,0 +1,83 @@
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <math.h>
|
||||
|
||||
static double db_to_linear(double db) {
|
||||
if (db <= SND_CTL_TLV_DB_GAIN_MUTE)
|
||||
return 0.0;
|
||||
return pow(10.0, db / 20.0);
|
||||
}
|
||||
|
||||
static double linear_to_db(double linear) {
|
||||
if (linear <= 0.0)
|
||||
return SND_CTL_TLV_DB_GAIN_MUTE;
|
||||
return 20.0 * log10(linear);
|
||||
}
|
||||
|
||||
int cdb_to_linear_value(
|
||||
int cdb, int min_val, int max_val, int min_cdb, int max_cdb
|
||||
) {
|
||||
if (cdb <= min_cdb)
|
||||
return min_val;
|
||||
if (cdb >= max_cdb)
|
||||
return max_val;
|
||||
|
||||
// Convert centidB to dB
|
||||
double db = (double)cdb / 100.0;
|
||||
double max_db = (double)max_cdb / 100.0;
|
||||
|
||||
// Convert dB relative to max_db to linear scale 0.0-1.0
|
||||
double linear = db_to_linear(db - max_db);
|
||||
|
||||
// Scale to full ALSA range
|
||||
double scaled = linear * (double)max_val;
|
||||
int value = (int)round(scaled);
|
||||
if (value < min_val)
|
||||
return min_val;
|
||||
if (value > max_val)
|
||||
return max_val;
|
||||
return value;
|
||||
}
|
||||
|
||||
int linear_value_to_cdb(
|
||||
int value, int min_val, int max_val, int min_cdb, int max_cdb
|
||||
) {
|
||||
if (value <= min_val)
|
||||
return min_cdb;
|
||||
if (value >= max_val)
|
||||
return max_cdb;
|
||||
|
||||
// Convert to 0.0-1.0 linear scale
|
||||
double linear = (double)value / (double)max_val;
|
||||
double max_db = (double)max_cdb / 100.0;
|
||||
|
||||
// Convert to dB relative to max_db and back to centidB
|
||||
int cdb = (int)round((linear_to_db(linear) + max_db) * 100.0);
|
||||
if (cdb < min_cdb)
|
||||
return min_cdb;
|
||||
if (cdb > max_cdb)
|
||||
return max_cdb;
|
||||
return cdb;
|
||||
}
|
||||
|
||||
double linear_value_to_db(
|
||||
int value, int min_val, int max_val, int min_db, int max_db
|
||||
) {
|
||||
if (value <= min_val)
|
||||
return min_db;
|
||||
if (value >= max_val)
|
||||
return max_db;
|
||||
|
||||
// Convert to 0.0-1.0 linear scale
|
||||
double linear = (double)value / (double)max_val;
|
||||
|
||||
// Convert to dB relative to max_db
|
||||
double db = linear_to_db(linear) + max_db;
|
||||
if (db < min_db)
|
||||
return min_db;
|
||||
if (db > max_db)
|
||||
return max_db;
|
||||
return db;
|
||||
}
|
||||
16
src/db.h
Normal file
@@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
int cdb_to_linear_value(
|
||||
int cdb, int min_val, int max_val, int min_cdb, int max_cdb
|
||||
);
|
||||
|
||||
int linear_value_to_cdb(
|
||||
int value, int min_val, int max_val, int min_cdb, int max_cdb
|
||||
);
|
||||
|
||||
double linear_value_to_db(
|
||||
int value, int min_val, int max_val, int min_db, int max_db
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "error.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
11
src/file.c
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "alsa.h"
|
||||
@@ -14,9 +14,15 @@ static void run_alsactl(
|
||||
) {
|
||||
GtkWindow *w = GTK_WINDOW(card->window_main);
|
||||
|
||||
gchar *alsactl_path = g_find_program_in_path("alsactl");
|
||||
|
||||
if (!alsactl_path)
|
||||
alsactl_path = g_strdup("/usr/sbin/alsactl");
|
||||
|
||||
gchar *argv[] = {
|
||||
"/usr/sbin/alsactl", cmd, card->device, "-f", fn, NULL
|
||||
alsactl_path, cmd, card->device, "-f", fn, NULL
|
||||
};
|
||||
|
||||
gchar *stdout;
|
||||
gchar *stderr;
|
||||
gint exit_status;
|
||||
@@ -52,6 +58,7 @@ static void run_alsactl(
|
||||
g_free(error_message);
|
||||
|
||||
done:
|
||||
g_free(alsactl_path);
|
||||
g_free(stdout);
|
||||
g_free(stderr);
|
||||
if (error)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
345
src/gtkdial.c
@@ -1,5 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2021 Stiliyan Varbanov <https://www.fiverr.com/stilvar>
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
@@ -17,10 +17,13 @@
|
||||
#include <math.h>
|
||||
|
||||
#include "gtkdial.h"
|
||||
#include "db.h"
|
||||
|
||||
#define DIAL_MIN_WIDTH 50
|
||||
#define DIAL_MAX_WIDTH 70
|
||||
|
||||
#define HISTORY_COUNT 50
|
||||
|
||||
static int set_value(GtkDial *dial, double newval);
|
||||
|
||||
static void gtk_dial_set_property(
|
||||
@@ -88,8 +91,10 @@ enum {
|
||||
PROP_ROUND_DIGITS,
|
||||
PROP_ZERO_DB,
|
||||
PROP_OFF_DB,
|
||||
PROP_IS_LINEAR,
|
||||
PROP_TAPER,
|
||||
PROP_CAN_CONTROL,
|
||||
PROP_PEAK_HOLD,
|
||||
LAST_PROP
|
||||
};
|
||||
|
||||
@@ -115,8 +120,10 @@ struct _GtkDial {
|
||||
int round_digits;
|
||||
double zero_db;
|
||||
double off_db;
|
||||
gboolean is_linear;
|
||||
int taper;
|
||||
gboolean can_control;
|
||||
int peak_hold;
|
||||
|
||||
int properties_updated;
|
||||
|
||||
@@ -150,11 +157,24 @@ struct _GtkDial {
|
||||
cairo_pattern_t *fill_pattern[2][2];
|
||||
cairo_pattern_t *outline_pattern[2];
|
||||
|
||||
// pango resources for displaying the peak value
|
||||
PangoLayout *peak_layout;
|
||||
PangoFontDescription *peak_font_desc;
|
||||
|
||||
// variables derived from the dial value
|
||||
double valp;
|
||||
double angle;
|
||||
double slider_cx;
|
||||
double slider_cy;
|
||||
|
||||
// same for the peak angle
|
||||
double peak_angle;
|
||||
|
||||
// value history for displaying peak
|
||||
double hist_values[HISTORY_COUNT];
|
||||
long long hist_time[HISTORY_COUNT];
|
||||
double current_peak;
|
||||
int hist_head, hist_tail, hist_count;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(GtkDial, gtk_dial, GTK_TYPE_WIDGET)
|
||||
@@ -176,6 +196,17 @@ static void dial_measure(
|
||||
"move-slider", \
|
||||
"(i)", scroll)
|
||||
|
||||
long long current_time = 0;
|
||||
|
||||
void gtk_dial_peak_tick(void) {
|
||||
struct timespec ts;
|
||||
|
||||
if (clock_gettime(CLOCK_BOOTTIME, &ts) < 0)
|
||||
return;
|
||||
|
||||
current_time = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
// BEGIN SECTION HELPERS
|
||||
|
||||
#define TOTAL_ROTATION_DEGREES 290
|
||||
@@ -226,6 +257,13 @@ static double calc_taper(GtkDial *dial, double val) {
|
||||
double mn = gtk_adjustment_get_lower(dial->adj);
|
||||
double mx = gtk_adjustment_get_upper(dial->adj);
|
||||
double off_db = gtk_dial_get_off_db(dial);
|
||||
gboolean is_linear = gtk_dial_get_is_linear(dial);
|
||||
|
||||
if (is_linear) {
|
||||
val = linear_value_to_cdb(val, mn, mx, -8000, 1200) / 100.0;
|
||||
mn = -60;
|
||||
mx = 12;
|
||||
}
|
||||
|
||||
// if off_db is set, then values below it are considered as
|
||||
// almost-silence, so we clamp them to 0.01
|
||||
@@ -381,6 +419,21 @@ static int update_dial_properties(GtkDial *dial) {
|
||||
dial->outline_pattern[dim] = pat;
|
||||
}
|
||||
|
||||
// init pango layout for peak value
|
||||
if (dial->peak_layout)
|
||||
g_object_unref(dial->peak_layout);
|
||||
if (dial->peak_font_desc)
|
||||
pango_font_description_free(dial->peak_font_desc);
|
||||
|
||||
PangoContext *context = gtk_widget_create_pango_context(GTK_WIDGET(dial));
|
||||
dial->peak_layout = pango_layout_new(context);
|
||||
dial->peak_font_desc = pango_context_get_font_description(context);
|
||||
int size = pango_font_description_get_size(dial->peak_font_desc) * 0.6;
|
||||
dial->peak_font_desc = pango_font_description_copy(dial->peak_font_desc);
|
||||
pango_font_description_set_size(dial->peak_font_desc, size);
|
||||
pango_layout_set_font_description(dial->peak_layout, dial->peak_font_desc);
|
||||
g_object_unref(context);
|
||||
|
||||
// calculate level meter breakpoint angles
|
||||
if (dial->level_breakpoint_angles)
|
||||
free(dial->level_breakpoint_angles);
|
||||
@@ -404,6 +457,12 @@ static void update_dial_values(GtkDial *dial) {
|
||||
dial->angle = calc_val(dial->valp, ANGLE_START, ANGLE_END);
|
||||
dial->slider_cx = cos(dial->angle) * dial->slider_radius + dial->cx;
|
||||
dial->slider_cy = sin(dial->angle) * dial->slider_radius + dial->cy;
|
||||
|
||||
if (!dial->peak_hold)
|
||||
return;
|
||||
|
||||
double peak_valp = calc_taper(dial, dial->current_peak);
|
||||
dial->peak_angle = calc_val(peak_valp, ANGLE_START, ANGLE_END);
|
||||
}
|
||||
|
||||
static double pdist2(double x1, double y1, double x2, double y2) {
|
||||
@@ -502,6 +561,19 @@ static void gtk_dial_class_init(GtkDialClass *klass) {
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT
|
||||
);
|
||||
|
||||
/**
|
||||
* GtkDial:is_linear: (attributes org.gtk.Method.get=gtk_dial_get_is_linear org.gtk.Method.set=gtk_dial_set_is_linear)
|
||||
*
|
||||
* Whether the dial values are linear or dB.
|
||||
*/
|
||||
properties[PROP_IS_LINEAR] = g_param_spec_boolean(
|
||||
"is_linear",
|
||||
"IsLinear",
|
||||
"Whether the dial values are linear or dB",
|
||||
FALSE,
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT
|
||||
);
|
||||
|
||||
/**
|
||||
* GtkDial:taper: (attributes org.gtk.Method.get=gtk_dial_get_taper org.gtk.Method.set=gtk_dial_set_taper)
|
||||
*
|
||||
@@ -530,6 +602,20 @@ static void gtk_dial_class_init(GtkDialClass *klass) {
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT
|
||||
);
|
||||
|
||||
/**
|
||||
* GtkDial:peak-hold: (attributes org.gtk.Method.get=gtk_dial_get_peak_hold org.gtk.Method.set=gtk_dial_set_peak_hold)
|
||||
*
|
||||
* The number of milliseconds to hold the peak value.
|
||||
*/
|
||||
properties[PROP_PEAK_HOLD] = g_param_spec_int(
|
||||
"peak-hold",
|
||||
"PeakHold",
|
||||
"The number of milliseconds to hold the peak value",
|
||||
0, 1000,
|
||||
0,
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT
|
||||
);
|
||||
|
||||
g_object_class_install_properties(g_class, LAST_PROP, properties);
|
||||
|
||||
/**
|
||||
@@ -648,6 +734,11 @@ static void gtk_dial_init(GtkDial *dial) {
|
||||
g_signal_connect(
|
||||
dial, "notify::sensitive", G_CALLBACK(gtk_dial_notify_sensitive_cb), dial
|
||||
);
|
||||
|
||||
dial->current_peak = -INFINITY;
|
||||
dial->hist_head = 0;
|
||||
dial->hist_tail = 0;
|
||||
dial->hist_count = 0;
|
||||
}
|
||||
|
||||
static void dial_measure(
|
||||
@@ -686,6 +777,70 @@ static void cairo_set_source_rgba_dim(
|
||||
cairo_set_source_rgba(cr, r, g, b, a);
|
||||
}
|
||||
|
||||
static void draw_peak(GtkDial *dial, cairo_t *cr, double radius) {
|
||||
|
||||
double angle_start = dial->peak_angle - M_PI / 180;
|
||||
if (angle_start < ANGLE_START)
|
||||
return;
|
||||
|
||||
// determine the colour of the peak
|
||||
int count = dial->level_breakpoints_count;
|
||||
|
||||
// if there are no colours, don't draw the peak
|
||||
if (!count)
|
||||
return;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < count - 1; i++)
|
||||
if (dial->current_peak < dial->level_breakpoints[i + 1])
|
||||
break;
|
||||
|
||||
const double *colours = &dial->level_colours[i * 3];
|
||||
|
||||
cairo_set_source_rgba_dim(
|
||||
cr, colours[0], colours[1], colours[2], 0.5, dial->dim
|
||||
);
|
||||
cairo_set_line_width(cr, 2);
|
||||
cairo_arc(cr, dial->cx, dial->cy, radius, ANGLE_START, dial->peak_angle);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_set_source_rgba_dim(
|
||||
cr, colours[0], colours[1], colours[2], 1, dial->dim
|
||||
);
|
||||
cairo_set_line_width(cr, 4);
|
||||
cairo_arc(cr, dial->cx, dial->cy, radius, angle_start, dial->peak_angle);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
static void show_peak_value(GtkDial *dial, cairo_t *cr) {
|
||||
double value = round(dial->current_peak);
|
||||
|
||||
if (value <= gtk_adjustment_get_lower(dial->adj))
|
||||
return;
|
||||
|
||||
char s[20];
|
||||
char *p = s;
|
||||
if (value < 0)
|
||||
p += sprintf(p, "−");
|
||||
snprintf(p, 10, "%.0f", fabs(value));
|
||||
|
||||
pango_layout_set_text(dial->peak_layout, s, -1);
|
||||
|
||||
int width, height;
|
||||
pango_layout_get_pixel_size(dial->peak_layout, &width, &height);
|
||||
|
||||
cairo_set_source_rgba_dim(cr, 1, 1, 1, 0.5, dial->dim);
|
||||
|
||||
cairo_move_to(
|
||||
cr,
|
||||
dial->cx - width / 2 - 1,
|
||||
dial->cy - height / 2
|
||||
);
|
||||
|
||||
pango_cairo_show_layout(cr, dial->peak_layout);
|
||||
}
|
||||
|
||||
static void draw_slider(
|
||||
GtkDial *dial,
|
||||
cairo_t *cr,
|
||||
@@ -781,6 +936,10 @@ static void dial_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) {
|
||||
draw_slider(dial, cr, dial->slider_radius, 6, 0.3);
|
||||
}
|
||||
|
||||
// peak hold
|
||||
if (dial->peak_hold)
|
||||
draw_peak(dial, cr, dial->slider_radius);
|
||||
|
||||
// draw line to zero db
|
||||
double zero_db = gtk_dial_get_zero_db(dial);
|
||||
if (zero_db != -G_MAXDOUBLE) {
|
||||
@@ -821,6 +980,10 @@ static void dial_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) {
|
||||
cairo_set_line_width(cr, 2);
|
||||
cairo_stroke(cr);
|
||||
|
||||
// show the peak value
|
||||
if (dial->peak_hold)
|
||||
show_peak_value(dial, cr);
|
||||
|
||||
// if focussed
|
||||
if (has_focus) {
|
||||
cairo_set_source_rgba(cr, 1, 0.125, 0.125, 0.5);
|
||||
@@ -897,12 +1060,18 @@ static void gtk_dial_set_property(
|
||||
case PROP_OFF_DB:
|
||||
gtk_dial_set_off_db(dial, g_value_get_double(value));
|
||||
break;
|
||||
case PROP_IS_LINEAR:
|
||||
gtk_dial_set_is_linear(dial, g_value_get_boolean(value));
|
||||
break;
|
||||
case PROP_TAPER:
|
||||
gtk_dial_set_taper(dial, g_value_get_int(value));
|
||||
break;
|
||||
case PROP_CAN_CONTROL:
|
||||
gtk_dial_set_can_control(dial, g_value_get_boolean(value));
|
||||
break;
|
||||
case PROP_PEAK_HOLD:
|
||||
gtk_dial_set_peak_hold(dial, g_value_get_int(value));
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
@@ -930,12 +1099,18 @@ static void gtk_dial_get_property(
|
||||
case PROP_OFF_DB:
|
||||
g_value_set_double(value, dial->off_db);
|
||||
break;
|
||||
case PROP_IS_LINEAR:
|
||||
g_value_set_boolean(value, dial->is_linear);
|
||||
break;
|
||||
case PROP_TAPER:
|
||||
g_value_set_int(value, dial->taper);
|
||||
break;
|
||||
case PROP_CAN_CONTROL:
|
||||
g_value_set_boolean(value, dial->can_control);
|
||||
break;
|
||||
case PROP_PEAK_HOLD:
|
||||
g_value_set_int(value, dial->peak_hold);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
@@ -978,6 +1153,15 @@ double gtk_dial_get_off_db(GtkDial *dial) {
|
||||
return dial->off_db;
|
||||
}
|
||||
|
||||
void gtk_dial_set_is_linear(GtkDial *dial, gboolean is_linear) {
|
||||
dial->is_linear = is_linear;
|
||||
dial->properties_updated = 1;
|
||||
}
|
||||
|
||||
gboolean gtk_dial_get_is_linear(GtkDial *dial) {
|
||||
return dial->is_linear;
|
||||
}
|
||||
|
||||
void gtk_dial_set_taper(GtkDial *dial, int taper) {
|
||||
dial->taper = taper;
|
||||
dial->properties_updated = 1;
|
||||
@@ -1044,6 +1228,14 @@ void gtk_dial_set_level_meter_colours(
|
||||
dial->properties_updated = 1;
|
||||
}
|
||||
|
||||
void gtk_dial_set_peak_hold(GtkDial *dial, int peak_hold) {
|
||||
dial->peak_hold = peak_hold;
|
||||
}
|
||||
|
||||
int gtk_dial_get_peak_hold(GtkDial *dial) {
|
||||
return dial->peak_hold;
|
||||
}
|
||||
|
||||
void gtk_dial_set_adjustment(GtkDial *dial, GtkAdjustment *adj) {
|
||||
if (!(adj == NULL || GTK_IS_ADJUSTMENT(adj)))
|
||||
return;
|
||||
@@ -1059,6 +1251,46 @@ GtkAdjustment *gtk_dial_get_adjustment(GtkDial *dial) {
|
||||
return dial->adj;
|
||||
}
|
||||
|
||||
static void gtk_dial_add_hist_value(GtkDial *dial, double value) {
|
||||
|
||||
int need_peak_update = 0;
|
||||
|
||||
// remove the oldest value(s) if they are too old or if the history
|
||||
// is full
|
||||
while (dial->hist_count > 0 &&
|
||||
(dial->hist_time[dial->hist_head] < current_time - dial->peak_hold ||
|
||||
dial->hist_count == HISTORY_COUNT)) {
|
||||
|
||||
// check if the value removed is the current peak
|
||||
if (dial->hist_values[dial->hist_head] >= dial->current_peak)
|
||||
need_peak_update = 1;
|
||||
|
||||
// move the head forward
|
||||
dial->hist_head = (dial->hist_head + 1) % HISTORY_COUNT;
|
||||
dial->hist_count--;
|
||||
}
|
||||
|
||||
// recalculate the peak if needed
|
||||
if (need_peak_update) {
|
||||
dial->current_peak = -INFINITY;
|
||||
for (int i = dial->hist_head;
|
||||
i != dial->hist_tail;
|
||||
i = (i + 1) % HISTORY_COUNT)
|
||||
if (dial->hist_values[i] > dial->current_peak)
|
||||
dial->current_peak = dial->hist_values[i];
|
||||
}
|
||||
|
||||
// add the new value
|
||||
dial->hist_values[dial->hist_tail] = value;
|
||||
dial->hist_time[dial->hist_tail] = current_time;
|
||||
dial->hist_tail = (dial->hist_tail + 1) % HISTORY_COUNT;
|
||||
dial->hist_count++;
|
||||
|
||||
// update the peak if needed
|
||||
if (value > dial->current_peak)
|
||||
dial->current_peak = value;
|
||||
}
|
||||
|
||||
static int set_value(GtkDial *dial, double newval) {
|
||||
if (dial->round_digits >= 0) {
|
||||
double power;
|
||||
@@ -1079,7 +1311,10 @@ static int set_value(GtkDial *dial, double newval) {
|
||||
|
||||
double oldval = gtk_adjustment_get_value(dial->adj);
|
||||
|
||||
if (oldval == newval)
|
||||
double old_peak = dial->current_peak;
|
||||
gtk_dial_add_hist_value(dial, newval);
|
||||
|
||||
if (oldval == newval && old_peak == dial->current_peak)
|
||||
return 0;
|
||||
|
||||
gtk_adjustment_set_value(dial->adj, newval);
|
||||
@@ -1088,35 +1323,44 @@ static int set_value(GtkDial *dial, double newval) {
|
||||
double old_valp = dial->valp;
|
||||
update_dial_values(dial);
|
||||
|
||||
return old_valp != dial->valp;
|
||||
return old_valp != dial->valp || old_peak != dial->current_peak;
|
||||
}
|
||||
|
||||
static double do_step(GtkDial *dial, double step_amount) {
|
||||
double mn = gtk_adjustment_get_lower(dial->adj);
|
||||
double mx = gtk_adjustment_get_upper(dial->adj);
|
||||
double newval = gtk_adjustment_get_value(dial->adj);
|
||||
double step = gtk_adjustment_get_step_increment(dial->adj);
|
||||
|
||||
if (gtk_dial_get_is_linear(dial)) {
|
||||
double db_val = linear_value_to_cdb(newval, mn, mx, -8000, 1200) / 100.0;
|
||||
db_val = round(db_val / step) * step + step_amount;
|
||||
|
||||
newval = cdb_to_linear_value(db_val * 100.0, mn, mx, -8000, 1200);
|
||||
if (newval == gtk_adjustment_get_value(dial->adj)) {
|
||||
newval = CLAMP(newval + (step_amount > 0 ? 1 : -1), mn, mx);
|
||||
}
|
||||
} else {
|
||||
newval += step_amount;
|
||||
}
|
||||
|
||||
return newval;
|
||||
}
|
||||
|
||||
static void step_back(GtkDial *dial) {
|
||||
double newval;
|
||||
|
||||
newval = gtk_adjustment_get_value(dial->adj) - gtk_adjustment_get_step_increment(dial->adj);
|
||||
set_value(dial, newval);
|
||||
set_value(dial, do_step(dial, -gtk_adjustment_get_step_increment(dial->adj)));
|
||||
}
|
||||
|
||||
static void step_forward(GtkDial *dial) {
|
||||
double newval;
|
||||
|
||||
newval = gtk_adjustment_get_value(dial->adj) + gtk_adjustment_get_step_increment(dial->adj);
|
||||
set_value(dial, newval);
|
||||
set_value(dial, do_step(dial, gtk_adjustment_get_step_increment(dial->adj)));
|
||||
}
|
||||
|
||||
static void page_back(GtkDial *dial) {
|
||||
double newval;
|
||||
|
||||
newval = gtk_adjustment_get_value(dial->adj) - gtk_adjustment_get_page_increment(dial->adj);
|
||||
set_value(dial, newval);
|
||||
set_value(dial, do_step(dial, -gtk_adjustment_get_page_increment(dial->adj)));
|
||||
}
|
||||
|
||||
static void page_forward(GtkDial *dial) {
|
||||
double newval;
|
||||
|
||||
newval = gtk_adjustment_get_value(dial->adj) + gtk_adjustment_get_page_increment(dial->adj);
|
||||
set_value(dial, newval);
|
||||
set_value(dial, do_step(dial, gtk_adjustment_get_page_increment(dial->adj)));
|
||||
}
|
||||
|
||||
static void scroll_begin(GtkDial *dial) {
|
||||
@@ -1246,20 +1490,56 @@ static void gtk_dial_drag_gesture_update(
|
||||
double offset_y,
|
||||
GtkDial *dial
|
||||
) {
|
||||
double start_x, start_y;
|
||||
double mn = gtk_adjustment_get_lower(dial->adj);
|
||||
double mx = gtk_adjustment_get_upper(dial->adj);
|
||||
gboolean is_linear = gtk_dial_get_is_linear(dial);
|
||||
|
||||
gtk_gesture_drag_get_start_point(gesture, &start_x, &start_y);
|
||||
double valp;
|
||||
|
||||
double valp = dial->dvalp - DRAG_FACTOR * (offset_y / dial->h);
|
||||
valp = CLAMP(valp, 0.0, 1.0);
|
||||
// add a 1px deadband to prevent double-click with zero mouse
|
||||
// movement from changing the value from the toggled -inf/0dB value
|
||||
// (sometimes we see an offset_y value that rounds to +/- 1 which
|
||||
// causes the value to change after the double-click has set the
|
||||
// value)
|
||||
if (offset_y < -1) {
|
||||
offset_y += 1;
|
||||
} else if (offset_y < 1) {
|
||||
offset_y = 0;
|
||||
} else {
|
||||
offset_y -= 1;
|
||||
}
|
||||
|
||||
double val = calc_val(
|
||||
valp,
|
||||
gtk_adjustment_get_lower(dial->adj),
|
||||
gtk_adjustment_get_upper(dial->adj)
|
||||
);
|
||||
if (is_linear) {
|
||||
double step = gtk_adjustment_get_step_increment(dial->adj);
|
||||
|
||||
// Convert initial value from linear to dB space
|
||||
double db_val = linear_value_to_cdb(
|
||||
calc_val(dial->dvalp, mn, mx),
|
||||
mn, mx,
|
||||
-8000, 1200
|
||||
) / 100.0;
|
||||
|
||||
// Adjust in dB space
|
||||
db_val -= 30.0 * DRAG_FACTOR * (offset_y / dial->h);
|
||||
|
||||
// Round
|
||||
db_val = round(db_val / step) * step;
|
||||
|
||||
// Convert back to linear space and normalise
|
||||
double val = cdb_to_linear_value(
|
||||
db_val * 100.0,
|
||||
mn, mx,
|
||||
-8000, 1200
|
||||
);
|
||||
|
||||
valp = calc_valp(val, mn, mx);
|
||||
} else {
|
||||
valp = dial->dvalp - DRAG_FACTOR * (offset_y / dial->h);
|
||||
valp = CLAMP(valp, 0.0, 1.0);
|
||||
}
|
||||
|
||||
set_value(dial, calc_val(valp, mn, mx));
|
||||
|
||||
set_value(dial, val);
|
||||
gtk_widget_queue_draw(GTK_WIDGET(dial));
|
||||
}
|
||||
|
||||
@@ -1322,7 +1602,7 @@ static gboolean gtk_dial_scroll_controller_scroll(
|
||||
|
||||
double step = -gtk_adjustment_get_step_increment(dial->adj) * delta;
|
||||
|
||||
set_value(dial, gtk_adjustment_get_value(dial->adj) + step);
|
||||
set_value(dial, do_step(dial, step));
|
||||
gtk_widget_queue_draw(GTK_WIDGET(dial));
|
||||
|
||||
return GDK_EVENT_STOP;
|
||||
@@ -1348,6 +1628,11 @@ void gtk_dial_dispose(GObject *o) {
|
||||
if (dial->outline_pattern[dim])
|
||||
cairo_pattern_destroy(dial->outline_pattern[dim]);
|
||||
|
||||
if (dial->peak_layout)
|
||||
g_object_unref(dial->peak_layout);
|
||||
if (dial->peak_font_desc)
|
||||
pango_font_description_free(dial->peak_font_desc);
|
||||
|
||||
g_object_unref(dial->adj);
|
||||
dial->adj = NULL;
|
||||
G_OBJECT_CLASS(gtk_dial_parent_class)->dispose(o);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2021 Stiliyan Varbanov <https://www.fiverr.com/stilvar>
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
@@ -68,6 +68,9 @@ double gtk_dial_get_zero_db(GtkDial *dial);
|
||||
void gtk_dial_set_off_db(GtkDial *dial, double off_db);
|
||||
double gtk_dial_get_off_db(GtkDial *dial);
|
||||
|
||||
void gtk_dial_set_is_linear(GtkDial *dial, gboolean is_linear);
|
||||
gboolean gtk_dial_get_is_linear(GtkDial *dial);
|
||||
|
||||
// taper functions
|
||||
enum {
|
||||
GTK_DIAL_TAPER_LINEAR,
|
||||
@@ -94,6 +97,17 @@ void gtk_dial_set_level_meter_colours(
|
||||
int count
|
||||
);
|
||||
|
||||
void gtk_dial_set_peak_hold(GtkDial *dial, int peak_hold);
|
||||
int gtk_dial_get_peak_hold(GtkDial *dial);
|
||||
void gtk_dial_peak_tick(void);
|
||||
|
||||
int cdb_to_linear_value(
|
||||
int db, int min_val, int max_val, int min_db, int max_db
|
||||
);
|
||||
int linear_value_to_cdb(
|
||||
int value, int min_val, int max_val, int min_db, int max_db
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "gtkhelper.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// Supported devices
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "gtkhelper.h"
|
||||
@@ -26,8 +26,12 @@ static void add_clock_source_control(
|
||||
|
||||
struct alsa_elem *clock_source = get_elem_by_prefix(elems, "Clock Source");
|
||||
|
||||
if (!clock_source)
|
||||
return;
|
||||
if (!clock_source) {
|
||||
clock_source = get_elem_by_substr(elems, "Sync Clock Source");
|
||||
|
||||
if (!clock_source)
|
||||
return;
|
||||
}
|
||||
|
||||
GtkWidget *b = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
gtk_widget_set_tooltip_text(
|
||||
@@ -55,8 +59,11 @@ static void add_sync_status_control(
|
||||
|
||||
struct alsa_elem *sync_status = get_elem_by_name(elems, "Sync Status");
|
||||
|
||||
if (!sync_status)
|
||||
return;
|
||||
if (!sync_status) {
|
||||
sync_status = get_elem_by_name(elems, "Sample Clock Sync Status");
|
||||
if (!sync_status)
|
||||
return;
|
||||
}
|
||||
|
||||
GtkWidget *b = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
if (get_elem_by_prefix(elems, "Clock Source")) {
|
||||
@@ -139,7 +146,7 @@ static void add_sample_rate_control(
|
||||
gtk_box_append(GTK_BOX(b), w);
|
||||
}
|
||||
|
||||
static void add_speaker_switching_controls(
|
||||
static void add_speaker_switching_controls_enum(
|
||||
struct alsa_card *card,
|
||||
GtkWidget *global_controls
|
||||
) {
|
||||
@@ -167,7 +174,43 @@ static void add_speaker_switching_controls(
|
||||
gtk_box_append(GTK_BOX(global_controls), w);
|
||||
}
|
||||
|
||||
static void add_talkback_controls(
|
||||
static void add_speaker_switching_controls_switches(
|
||||
struct alsa_card *card,
|
||||
GtkWidget *global_controls
|
||||
) {
|
||||
GArray *elems = card->elems;
|
||||
|
||||
struct alsa_elem *enable = get_elem_by_name(
|
||||
elems, "Speaker Switching Playback Switch"
|
||||
);
|
||||
struct alsa_elem *alt = get_elem_by_name(
|
||||
elems, "Speaker Switching Alt Playback Switch"
|
||||
);
|
||||
|
||||
if (!enable || !alt)
|
||||
return;
|
||||
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
gtk_widget_set_tooltip_text(
|
||||
box,
|
||||
"Speaker Switching lets you swap between two pairs of "
|
||||
"monitoring speakers very easily."
|
||||
);
|
||||
|
||||
GtkWidget *l = gtk_label_new("Speaker Switching");
|
||||
GtkWidget *w1 = make_boolean_alsa_elem(enable, "Off", "On");
|
||||
GtkWidget *w2 = make_boolean_alsa_elem(alt, "Main", "Alt");
|
||||
|
||||
gtk_widget_add_css_class(w1, "speaker-switching-enable");
|
||||
gtk_widget_add_css_class(w2, "speaker-switching-alt");
|
||||
|
||||
gtk_box_append(GTK_BOX(box), l);
|
||||
gtk_box_append(GTK_BOX(box), w1);
|
||||
gtk_box_append(GTK_BOX(box), w2);
|
||||
gtk_box_append(GTK_BOX(global_controls), box);
|
||||
}
|
||||
|
||||
static void add_talkback_controls_enum(
|
||||
struct alsa_card *card,
|
||||
GtkWidget *global_controls
|
||||
) {
|
||||
@@ -196,6 +239,43 @@ static void add_talkback_controls(
|
||||
gtk_box_append(GTK_BOX(global_controls), w);
|
||||
}
|
||||
|
||||
static void add_talkback_controls_switches(
|
||||
struct alsa_card *card,
|
||||
GtkWidget *global_controls
|
||||
) {
|
||||
GArray *elems = card->elems;
|
||||
|
||||
struct alsa_elem *enable = get_elem_by_name(
|
||||
elems, "Talkback Enable Playback Switch"
|
||||
);
|
||||
struct alsa_elem *talk = get_elem_by_name(
|
||||
elems, "Talk Playback Switch"
|
||||
);
|
||||
|
||||
if (!enable || !talk)
|
||||
return;
|
||||
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
gtk_widget_set_tooltip_text(
|
||||
box,
|
||||
"Talkback lets you add another channel (usually the talkback "
|
||||
"mic) to a mix with a button push, usually to talk to "
|
||||
"musicians, and without using an additional mic channel."
|
||||
);
|
||||
|
||||
GtkWidget *l = gtk_label_new("Talkback");
|
||||
GtkWidget *w1 = make_boolean_alsa_elem(enable, "Disabled", "Enabled");
|
||||
GtkWidget *w2 = make_boolean_alsa_elem(talk, "Talk", "Talk");
|
||||
|
||||
gtk_widget_add_css_class(w1, "talkback-enable");
|
||||
gtk_widget_add_css_class(w2, "talk");
|
||||
|
||||
gtk_box_append(GTK_BOX(box), l);
|
||||
gtk_box_append(GTK_BOX(box), w1);
|
||||
gtk_box_append(GTK_BOX(box), w2);
|
||||
gtk_box_append(GTK_BOX(global_controls), box);
|
||||
}
|
||||
|
||||
static GtkWidget *create_global_box(GtkWidget *grid, int *x, int orient) {
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
gtk_widget_set_vexpand(box, TRUE);
|
||||
@@ -253,8 +333,13 @@ static void create_input_link_control(
|
||||
|
||||
int from, to;
|
||||
get_two_num_from_string(elem->name, &from, &to);
|
||||
|
||||
// skip even numbers
|
||||
if (!(from % 2))
|
||||
return;
|
||||
|
||||
if (to == -1)
|
||||
to = from;
|
||||
to = from + 1;
|
||||
|
||||
gtk_grid_attach(GTK_GRID(grid), w, from - 1, current_row, to - from + 1, 1);
|
||||
}
|
||||
@@ -420,6 +505,25 @@ static void create_input_pad_control(
|
||||
gtk_grid_attach(GTK_GRID(grid), w, column_num, current_row, 1, 1);
|
||||
}
|
||||
|
||||
static void create_input_gain_switch_control(
|
||||
struct alsa_elem *elem,
|
||||
GtkWidget *grid,
|
||||
int current_row,
|
||||
int column_num
|
||||
) {
|
||||
GtkWidget *w = make_boolean_alsa_elem(elem, "Gain", NULL);
|
||||
gtk_widget_add_css_class(w, "gain-switch");
|
||||
gtk_widget_set_hexpand(w, TRUE);
|
||||
gtk_widget_set_tooltip_text(
|
||||
w,
|
||||
"Enabling Gain switches from Low gain input (0dBFS = +16dBu)\n"
|
||||
"to High gain input (0dBFS = −10dBV, approx −6dBu)."
|
||||
);
|
||||
|
||||
// ignore current_row, always put it in the first row
|
||||
gtk_grid_attach(GTK_GRID(grid), w, column_num, 1, 1, 1);
|
||||
}
|
||||
|
||||
static void create_input_phantom_control(
|
||||
struct alsa_elem *elem,
|
||||
GtkWidget *grid,
|
||||
@@ -476,13 +580,11 @@ static void create_input_controls_by_type(
|
||||
static void create_input_controls(
|
||||
struct alsa_card *card,
|
||||
GtkWidget *top,
|
||||
int *x
|
||||
int *x,
|
||||
int input_count
|
||||
) {
|
||||
GArray *elems = card->elems;
|
||||
|
||||
// find how many inputs have switches
|
||||
int input_count = get_max_elem_by_name(elems, "Line", "Capture Switch");
|
||||
|
||||
// Only the 18i20 Gen 2 has no input controls
|
||||
if (!input_count)
|
||||
return;
|
||||
@@ -522,6 +624,25 @@ static void create_input_controls(
|
||||
|
||||
int current_row = 1;
|
||||
|
||||
// 4th Gen Solo, put the Phantom Power control above the Air control
|
||||
if (get_elem_by_name(elems, "Direct Monitor Playback Switch")) {
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Level Capture Enum", create_input_level_control
|
||||
);
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Phantom Power Capture Switch", create_input_phantom_control
|
||||
);
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Air Capture Enum", create_input_air_enum_control
|
||||
);
|
||||
|
||||
(*x)++;
|
||||
return;
|
||||
}
|
||||
|
||||
create_input_select_control(elems, input_grid, ¤t_row);
|
||||
|
||||
create_input_controls_by_type(
|
||||
@@ -548,6 +669,10 @@ static void create_input_controls(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Level Capture Enum", create_input_level_control
|
||||
);
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Impedance Switch", create_input_level_control
|
||||
);
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Air Capture Switch", create_input_air_switch_control
|
||||
@@ -572,6 +697,14 @@ static void create_input_controls(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Pad Capture Switch", create_input_pad_control
|
||||
);
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Pad Switch", create_input_pad_control
|
||||
);
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Gain Switch", create_input_gain_switch_control
|
||||
);
|
||||
create_input_controls_by_type(
|
||||
elems, input_grid, ¤t_row,
|
||||
"Phantom Power Capture Switch", create_input_phantom_control
|
||||
@@ -585,7 +718,8 @@ static void create_output_controls(
|
||||
GtkWidget *top,
|
||||
int *x,
|
||||
int y,
|
||||
int x_span
|
||||
int x_span,
|
||||
int output_count
|
||||
) {
|
||||
GArray *elems = card->elems;
|
||||
|
||||
@@ -605,8 +739,6 @@ static void create_output_controls(
|
||||
|
||||
gtk_grid_attach(GTK_GRID(top), box, *x, y, x_span, 1);
|
||||
|
||||
int output_count = get_max_elem_by_name(elems, "Line", "Playback Volume");
|
||||
|
||||
/* 4th Gen Solo/2i2 */
|
||||
if (get_elem_by_prefix(elems, "Direct Monitor Playback")) {
|
||||
struct alsa_elem *elem;
|
||||
@@ -651,8 +783,12 @@ static void create_output_controls(
|
||||
return;
|
||||
}
|
||||
|
||||
int has_hw_vol = !!get_elem_by_name(elems, "Master HW Playback Volume");
|
||||
int line_1_col = has_hw_vol;
|
||||
int has_sw_hw_ctrls =
|
||||
!!get_elem_by_substr(elems, "Volume Control Playback Enum");
|
||||
int line_1_col =
|
||||
has_sw_hw_ctrls ||
|
||||
get_elem_by_name(elems, "Mute Playback Switch") ||
|
||||
get_elem_by_name(elems, "Master Playback Switch");
|
||||
|
||||
for (int i = 0; i < output_count; i++) {
|
||||
char s[20];
|
||||
@@ -669,21 +805,31 @@ static void create_output_controls(
|
||||
if (!elem->card)
|
||||
continue;
|
||||
|
||||
int line_num = get_num_from_string(elem->name);
|
||||
|
||||
// output controls
|
||||
if (strncmp(elem->name, "Line", 4) == 0) {
|
||||
|
||||
// Gen 1 master output control
|
||||
if (strcmp(elem->name, "Master Playback Volume") == 0) {
|
||||
GtkWidget *l = gtk_label_new("Master");
|
||||
gtk_grid_attach(GTK_GRID(output_grid), l, 0, 0, 1, 1);
|
||||
w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_LOG, 0);
|
||||
gtk_widget_set_tooltip_text(w, "Master Volume Control");
|
||||
gtk_grid_attach(GTK_GRID(output_grid), w, 0, 1, 1, 1);
|
||||
|
||||
} else if (strncmp(elem->name, "Line", 4) == 0 ||
|
||||
strncmp(elem->name, "Master", 4) == 0 ||
|
||||
strncmp(elem->name, "Analogue", 8) == 0) {
|
||||
|
||||
if (strstr(elem->name, "Playback Volume")) {
|
||||
w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_LOG, 1);
|
||||
gtk_grid_attach(
|
||||
GTK_GRID(output_grid), w, line_num - 1 + line_1_col, 1, 1, 1
|
||||
GTK_GRID(output_grid), w, elem->lr_num - 1 + line_1_col, 1, 1, 1
|
||||
);
|
||||
} else if (strstr(elem->name, "Mute Playback Switch")) {
|
||||
} else if (strstr(elem->name, "Playback Switch")) {
|
||||
w = make_boolean_alsa_elem(
|
||||
elem, "*audio-volume-high", "*audio-volume-muted"
|
||||
);
|
||||
gtk_widget_add_css_class(w, "mute");
|
||||
if (has_hw_vol) {
|
||||
if (has_sw_hw_ctrls) {
|
||||
gtk_widget_set_tooltip_text(
|
||||
w,
|
||||
"Mute (only available when under software control)"
|
||||
@@ -692,7 +838,7 @@ static void create_output_controls(
|
||||
gtk_widget_set_tooltip_text(w, "Mute");
|
||||
}
|
||||
gtk_grid_attach(
|
||||
GTK_GRID(output_grid), w, line_num - 1 + line_1_col, 2, 1, 1
|
||||
GTK_GRID(output_grid), w, elem->lr_num - 1 + line_1_col, 2, 1, 1
|
||||
);
|
||||
} else if (strstr(elem->name, "Volume Control Playback Enum")) {
|
||||
w = make_boolean_alsa_elem(elem, "SW", "HW");
|
||||
@@ -703,7 +849,7 @@ static void create_output_controls(
|
||||
"volume for this analogue output."
|
||||
);
|
||||
gtk_grid_attach(
|
||||
GTK_GRID(output_grid), w, line_num - 1 + line_1_col, 3, 1, 1
|
||||
GTK_GRID(output_grid), w, elem->lr_num - 1 + line_1_col, 3, 1, 1
|
||||
);
|
||||
}
|
||||
|
||||
@@ -785,8 +931,10 @@ static void create_global_controls(
|
||||
add_sync_status_control(card, column[1]);
|
||||
add_power_status_control(card, column[1]);
|
||||
add_sample_rate_control(card, column[2]);
|
||||
add_speaker_switching_controls(card, column[0]);
|
||||
add_talkback_controls(card, column[1]);
|
||||
add_speaker_switching_controls_enum(card, column[0]);
|
||||
add_speaker_switching_controls_switches(card, column[0]);
|
||||
add_talkback_controls_enum(card, column[1]);
|
||||
add_talkback_controls_switches(card, column[1]);
|
||||
}
|
||||
|
||||
static GtkWidget *create_main_window_controls(struct alsa_card *card) {
|
||||
@@ -812,18 +960,28 @@ static GtkWidget *create_main_window_controls(struct alsa_card *card) {
|
||||
int input_count = get_max_elem_by_name(
|
||||
card->elems, "Line", "Capture Switch"
|
||||
);
|
||||
if (!input_count)
|
||||
input_count =
|
||||
get_max_elem_by_name(card->elems, "Input", "Switch");
|
||||
|
||||
int output_count = get_max_elem_by_name(
|
||||
card->elems, "Line", "Playback Volume"
|
||||
);
|
||||
if (!output_count)
|
||||
output_count =
|
||||
get_max_elem_by_name(card->elems, "Master", "Playback Volume") * 2;
|
||||
if (!output_count)
|
||||
output_count =
|
||||
get_max_elem_by_name(card->elems, "Analogue", "Playback Volume");
|
||||
|
||||
create_global_controls(card, top, &x);
|
||||
create_input_controls(card, top, &x);
|
||||
create_input_controls(card, top, &x, input_count);
|
||||
|
||||
if (input_count + output_count >= 12) {
|
||||
x = 0;
|
||||
create_output_controls(card, top, &x, 1, 2);
|
||||
create_output_controls(card, top, &x, 1, 2, output_count);
|
||||
} else {
|
||||
create_output_controls(card, top, &x, 0, 1);
|
||||
create_output_controls(card, top, &x, 0, 1, output_count);
|
||||
}
|
||||
|
||||
return top;
|
||||
@@ -880,9 +1038,12 @@ static void create_scrollable_window(GtkWidget *window, GtkWidget *controls) {
|
||||
|
||||
GtkWidget *create_iface_mixer_main(struct alsa_card *card) {
|
||||
card->has_speaker_switching =
|
||||
!!get_elem_by_name(card->elems, "Speaker Switching Playback Enum");
|
||||
get_elem_by_name(card->elems, "Speaker Switching Playback Enum") ||
|
||||
get_elem_by_name(card->elems, "Speaker Switching Playback Switch");
|
||||
|
||||
card->has_talkback =
|
||||
!!get_elem_by_name(card->elems, "Talkback Playback Enum");
|
||||
get_elem_by_name(card->elems, "Talkback Playback Enum") ||
|
||||
get_elem_by_name(card->elems, "Talkback Enable Playback Switch");
|
||||
|
||||
GtkWidget *top = gtk_frame_new(NULL);
|
||||
gtk_widget_add_css_class(top, "window-frame");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "gtkhelper.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "alsa.h"
|
||||
@@ -10,7 +10,7 @@ GtkWidget *create_window_iface_none(GtkApplication *app) {
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 50);
|
||||
gtk_widget_set_margin(box, 50);
|
||||
GtkWidget *picture = gtk_picture_new_for_resource(
|
||||
"/vu/b4/alsa-scarlett-gui/icons/alsa-scarlett-gui-logo.png"
|
||||
"/vu/b4/alsa-scarlett-gui/icons/vu.b4.alsa-scarlett-gui.png"
|
||||
);
|
||||
GtkWidget *label = gtk_label_new("No Scarlett/Clarett/Vocaster interface found.");
|
||||
|
||||
@@ -19,7 +19,7 @@ GtkWidget *create_window_iface_none(GtkApplication *app) {
|
||||
|
||||
GtkWidget *w = gtk_application_window_new(app);
|
||||
gtk_window_set_resizable(GTK_WINDOW(w), FALSE);
|
||||
gtk_window_set_title(GTK_WINDOW(w), "ALSA Scarlett2 Control Panel");
|
||||
gtk_window_set_title(GTK_WINDOW(w), "ALSA Scarlett Control Panel");
|
||||
gtk_window_set_child(GTK_WINDOW(w), box);
|
||||
gtk_application_window_set_show_menubar(
|
||||
GTK_APPLICATION_WINDOW(w), TRUE
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "gtkhelper.h"
|
||||
@@ -9,21 +9,15 @@ GtkWidget *create_iface_unknown_main(void) {
|
||||
"Sorry, I don’t recognise the controls on this card.\n\n"
|
||||
|
||||
"These Focusrite models should be supported:\n"
|
||||
"– Gen 1: 6i6/8i6/18i6/18i8/18i20\n"
|
||||
"– Gen 2: 6i6/18i8/18i20\n"
|
||||
"– Gen 3: Solo/2i2/4i4/8i6/18i8/18i20\n"
|
||||
"– Gen 4: Solo/2i2/4i4\n"
|
||||
"– Gen 4: Solo/2i2/4i4/16i16/18i16/18i20\n"
|
||||
"– Vocaster One and Two\n"
|
||||
"– Clarett USB and Clarett+ 2Pre/4Pre/8Pre\n\n"
|
||||
|
||||
"Are you running a recent kernel with Scarlett2 support "
|
||||
"enabled?\n\n"
|
||||
|
||||
"Check dmesg output for “Focusrite ... Mixer Driver”:\n\n"
|
||||
|
||||
"dmesg | grep -A 5 -B 5 -i focusrite\n\n"
|
||||
|
||||
"For kernels before 6.7 you may need to create a file\n"
|
||||
"/etc/modprobe.d/scarlett.conf\n"
|
||||
"with an “options snd_usb_audio ...” line and reboot."
|
||||
"Please check the prerequisites at:\n"
|
||||
"https://github.com/geoffreybennett/alsa-scarlett-gui/"
|
||||
);
|
||||
gtk_widget_set_margin(label, 30);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 61 KiB |
4
src/img/audio-volume-high.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 7 1.007812 c -0.296875 -0.003906 -0.578125 0.125 -0.769531 0.351563 l -3.230469 3.640625 h -1 c -1.09375 0 -2 0.84375 -2 2 v 2 c 0 1.089844 0.910156 2 2 2 h 1 l 3.230469 3.640625 c 0.210937 0.253906 0.492187 0.363281 0.769531 0.359375 z m 6.460938 0.960938 c -0.191407 -0.003906 -0.386719 0.054688 -0.558594 0.167969 c -0.457032 0.3125 -0.578125 0.933593 -0.269532 1.390625 c 1.824219 2.707031 1.824219 6.238281 0 8.945312 c -0.308593 0.457032 -0.1875 1.078125 0.269532 1.390625 c 0.457031 0.308594 1.078125 0.1875 1.390625 -0.269531 c 1.136719 -1.691406 1.707031 -3.640625 1.707031 -5.59375 s -0.570312 -3.902344 -1.707031 -5.59375 c -0.195313 -0.285156 -0.511719 -0.4375 -0.832031 -0.4375 z m -3.421876 2.019531 c -0.222656 -0.007812 -0.453124 0.058594 -0.644531 0.203125 c -0.261719 0.199219 -0.394531 0.5 -0.394531 0.804688 v 0.058594 c 0.011719 0.191406 0.074219 0.375 0.199219 0.535156 c 1.074219 1.429687 1.074219 3.390625 0 4.816406 c -0.125 0.164062 -0.1875 0.347656 -0.199219 0.535156 v 0.0625 c 0 0.304688 0.132812 0.605469 0.394531 0.804688 c 0.441407 0.332031 1.066407 0.242187 1.398438 -0.199219 c 0.804687 -1.066406 1.207031 -2.335937 1.207031 -3.609375 s -0.402344 -2.542969 -1.207031 -3.613281 c -0.183594 -0.246094 -0.464844 -0.382813 -0.753907 -0.398438 z m 0 0" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
4
src/img/audio-volume-low.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 7 1.007812 c -0.296875 -0.003906 -0.578125 0.125 -0.769531 0.351563 l -3.230469 3.640625 h -1 c -1.09375 0 -2 0.84375 -2 2 v 2 c 0 1.089844 0.910156 2 2 2 h 1 l 3.230469 3.640625 c 0.210937 0.253906 0.492187 0.363281 0.769531 0.359375 z m 2.957031 2.980469 c -0.199219 0.011719 -0.394531 0.074219 -0.5625 0.203125 c -0.441406 0.332032 -0.53125 0.960938 -0.195312 1.402344 c 1.074219 1.425781 1.074219 3.386719 0 4.8125 c -0.335938 0.441406 -0.246094 1.070312 0.195312 1.402344 c 0.441407 0.332031 1.066407 0.242187 1.398438 -0.195313 c 0.804687 -1.070312 1.207031 -2.339843 1.207031 -3.613281 s -0.402344 -2.542969 -1.207031 -3.613281 c -0.183594 -0.246094 -0.464844 -0.382813 -0.753907 -0.398438 c -0.027343 0 -0.054687 0 -0.085937 0 z m 0 0" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 910 B |
7
src/img/audio-volume-medium.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#ffffff">
|
||||
<path d="m 7 1.007812 c -0.296875 -0.003906 -0.578125 0.125 -0.769531 0.351563 l -3.230469 3.640625 h -1 c -1.09375 0 -2 0.84375 -2 2 v 2 c 0 1.089844 0.910156 2 2 2 h 1 l 3.230469 3.640625 c 0.210937 0.253906 0.492187 0.363281 0.769531 0.359375 z m 3.039062 2.980469 c -0.222656 -0.007812 -0.453124 0.058594 -0.644531 0.203125 c -0.261719 0.199219 -0.394531 0.5 -0.394531 0.804688 v 0.066406 c 0.011719 0.1875 0.078125 0.371094 0.199219 0.527344 c 1.074219 1.429687 1.074219 3.390625 0 4.816406 c -0.121094 0.160156 -0.1875 0.34375 -0.199219 0.53125 v 0.066406 c 0 0.304688 0.132812 0.605469 0.394531 0.804688 c 0.441407 0.332031 1.066407 0.242187 1.398438 -0.199219 c 0.804687 -1.066406 1.207031 -2.335937 1.207031 -3.609375 s -0.402344 -2.542969 -1.207031 -3.613281 c -0.183594 -0.246094 -0.464844 -0.382813 -0.753907 -0.398438 z m 0 0"/>
|
||||
<path d="m 13.460938 1.96875 c -0.191407 -0.003906 -0.386719 0.054688 -0.558594 0.167969 c -0.457032 0.3125 -0.578125 0.933593 -0.269532 1.390625 c 1.824219 2.707031 1.824219 6.238281 0 8.945312 c -0.308593 0.457032 -0.1875 1.078125 0.269532 1.390625 c 0.457031 0.308594 1.078125 0.1875 1.390625 -0.269531 c 1.136719 -1.691406 1.707031 -3.640625 1.707031 -5.59375 s -0.570312 -3.902344 -1.707031 -5.59375 c -0.195313 -0.285156 -0.511719 -0.4375 -0.832031 -0.4375 z m 0 0" fill-opacity="0.34902"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
7
src/img/audio-volume-muted.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#ffffff">
|
||||
<path d="m 7 1.007812 c -0.296875 -0.003906 -0.578125 0.125 -0.769531 0.351563 l -3.230469 3.640625 h -1 c -1.09375 0 -2 0.84375 -2 2 v 2 c 0 1.089844 0.910156 2 2 2 h 1 l 3.230469 3.640625 c 0.210937 0.253906 0.492187 0.363281 0.769531 0.359375 z m 0 0"/>
|
||||
<path d="m 10 5 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 1.292969 1.292969 l -1.292969 1.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 1.292969 -1.292969 l 1.292969 1.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -1.292969 -1.292969 l 1.292969 -1.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -1.292969 1.292969 l -1.292969 -1.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 47 KiB |
520
src/img/vu.b4.alsa-scarlett-gui.svg
Normal file
|
After Width: | Height: | Size: 91 KiB |
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "alsa.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "about.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "routing-drag-line.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "alsa.h"
|
||||
@@ -237,7 +237,7 @@ static void get_snk_center(
|
||||
double *y
|
||||
) {
|
||||
get_widget_center(r_snk->socket_widget, parent, x, y);
|
||||
if (IS_MIXER(r_snk->port_category))
|
||||
if (IS_MIXER(r_snk->elem->port_category))
|
||||
(*y)++;
|
||||
}
|
||||
|
||||
@@ -261,6 +261,12 @@ void draw_routing_lines(
|
||||
struct routing_snk *r_snk = &g_array_index(
|
||||
card->routing_snks, struct routing_snk, i
|
||||
);
|
||||
struct alsa_elem *elem = r_snk->elem;
|
||||
|
||||
// don't draw lines to read-only mixer sinks
|
||||
if (elem->port_category == PC_MIX &&
|
||||
card->has_fixed_mixer_inputs)
|
||||
continue;
|
||||
|
||||
// if dragging and a routing sink is being reconnected then draw
|
||||
// it with dots
|
||||
@@ -271,7 +277,7 @@ void draw_routing_lines(
|
||||
cairo_set_dash(cr, NULL, 0, 0);
|
||||
|
||||
// get the sink and skip if it's "Off"
|
||||
int r_src_idx = alsa_get_elem_value(r_snk->elem);
|
||||
int r_src_idx = alsa_get_elem_value(elem);
|
||||
if (!r_src_idx)
|
||||
continue;
|
||||
|
||||
@@ -300,7 +306,7 @@ void draw_routing_lines(
|
||||
draw_connection(
|
||||
cr,
|
||||
x1, y1, r_src->port_category,
|
||||
x2, y2, r_snk->port_category,
|
||||
x2, y2, elem->port_category,
|
||||
r, g, b, 2
|
||||
);
|
||||
}
|
||||
@@ -362,7 +368,7 @@ void draw_drag_line(
|
||||
draw_connection(
|
||||
cr,
|
||||
x1, y1, card->src_drag->port_category,
|
||||
x2, y2, card->snk_drag->port_category,
|
||||
x2, y2, card->snk_drag->elem->port_category,
|
||||
1, 1, 1, 2
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <glib.h>
|
||||
@@ -72,7 +72,7 @@ struct scarlett2_firmware_header *scarlett2_read_firmware_header(
|
||||
) {
|
||||
FILE *file = fopen(fn, "rb");
|
||||
if (!file) {
|
||||
perror("fopen");
|
||||
perror("fopen firmware header");
|
||||
fprintf(stderr, "Unable to open %s\n", fn);
|
||||
return NULL;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ struct scarlett2_firmware_header *scarlett2_read_firmware_header(
|
||||
struct scarlett2_firmware_file *scarlett2_read_firmware_file(const char *fn) {
|
||||
FILE *file = fopen(fn, "rb");
|
||||
if (!file) {
|
||||
perror("fopen");
|
||||
perror("fopen firmware file");
|
||||
fprintf(stderr, "Unable to open %s\n", fn);
|
||||
return NULL;
|
||||
}
|
||||
@@ -265,7 +265,12 @@ static void enum_firmware_dir(const char *dir_name) {
|
||||
|
||||
void scarlett2_enum_firmware(void) {
|
||||
init_best_firmware();
|
||||
enum_firmware_dir(SCARLETT2_FIRMWARE_DIR);
|
||||
|
||||
const char *fw_dir = getenv("SCARLETT2_FIRMWARE_DIR");
|
||||
|
||||
if (!fw_dir)
|
||||
fw_dir = SCARLETT2_FIRMWARE_DIR;
|
||||
enum_firmware_dir(fw_dir);
|
||||
}
|
||||
|
||||
uint32_t scarlett2_get_best_firmware_version(uint32_t pid) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "tooltips.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ALSA Scarlett2 Control Panel
|
||||
Name=ALSA Scarlett Control Panel
|
||||
Icon=vu.b4.alsa-scarlett-gui
|
||||
Exec=PREFIX/bin/alsa-scarlett-gui
|
||||
Categories=GTK;AudioVideo;Audio;Mixer;
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "gtkhelper.h"
|
||||
#include "widget-boolean.h"
|
||||
|
||||
struct boolean {
|
||||
struct alsa_elem *elem;
|
||||
int backwards;
|
||||
GtkWidget *button;
|
||||
guint source;
|
||||
const char *text[2];
|
||||
GtkWidget *icons[2];
|
||||
};
|
||||
|
||||
static void button_clicked(GtkWidget *widget, struct alsa_elem *elem) {
|
||||
static void button_clicked(GtkWidget *widget, struct boolean *data) {
|
||||
int value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
|
||||
|
||||
alsa_set_elem_value(elem, value);
|
||||
alsa_set_elem_value(data->elem, value ^ data->backwards);
|
||||
}
|
||||
|
||||
static void toggle_button_set_text(GtkWidget *button, const char *text) {
|
||||
static void toggle_button_set_text(struct boolean *data, int value) {
|
||||
const char *text = data->text[value];
|
||||
|
||||
if (!text)
|
||||
return;
|
||||
|
||||
if (*text == '*') {
|
||||
GtkWidget *icon = gtk_image_new_from_icon_name(text + 1);
|
||||
gtk_button_set_child(GTK_BUTTON(button), icon);
|
||||
} else {
|
||||
gtk_button_set_label(GTK_BUTTON(button), text);
|
||||
}
|
||||
if (*text == '*')
|
||||
gtk_button_set_child(GTK_BUTTON(data->button), data->icons[value]);
|
||||
else
|
||||
gtk_button_set_label(GTK_BUTTON(data->button), text);
|
||||
}
|
||||
|
||||
static void toggle_button_updated(
|
||||
@@ -36,10 +40,40 @@ static void toggle_button_updated(
|
||||
int is_writable = alsa_get_elem_writable(elem);
|
||||
gtk_widget_set_sensitive(data->button, is_writable);
|
||||
|
||||
int value = !!alsa_get_elem_value(elem);
|
||||
int value = !!alsa_get_elem_value(elem) ^ data->backwards;
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->button), value);
|
||||
|
||||
toggle_button_set_text(data->button, data->text[value]);
|
||||
toggle_button_set_text(data, value);
|
||||
}
|
||||
|
||||
static gboolean update_toggle_button(struct boolean *data) {
|
||||
toggle_button_updated(data->elem, data);
|
||||
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void on_destroy(struct boolean *data) {
|
||||
if (data->source)
|
||||
g_source_remove(data->source);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
if (data->icons[i])
|
||||
g_object_unref(data->icons[i]);
|
||||
|
||||
g_free(data);
|
||||
}
|
||||
|
||||
static void load_icons(struct boolean *data) {
|
||||
for (int i = 0; i < 2; i++)
|
||||
if (data->text[i] && *data->text[i] == '*') {
|
||||
char *path = g_strdup_printf(
|
||||
"/vu/b4/alsa-scarlett-gui/icons/%s.svg", data->text[i] + 1
|
||||
);
|
||||
data->icons[i] = gtk_image_new_from_resource(path);
|
||||
gtk_widget_set_align(data->icons[i], GTK_ALIGN_CENTER, GTK_ALIGN_CENTER);
|
||||
g_object_ref(data->icons[i]);
|
||||
g_free(path);
|
||||
}
|
||||
}
|
||||
|
||||
GtkWidget *make_boolean_alsa_elem(
|
||||
@@ -47,21 +81,26 @@ GtkWidget *make_boolean_alsa_elem(
|
||||
const char *disabled_text,
|
||||
const char *enabled_text
|
||||
) {
|
||||
struct boolean *data = g_malloc(sizeof(struct boolean));
|
||||
struct boolean *data = g_malloc0(sizeof(struct boolean));
|
||||
data->elem = elem;
|
||||
data->button = gtk_toggle_button_new();
|
||||
|
||||
if (strncmp(elem->name, "Master", 6) == 0 &&
|
||||
strstr(elem->name, "Playback Switch"))
|
||||
data->backwards = 1;
|
||||
|
||||
g_signal_connect(
|
||||
data->button, "clicked", G_CALLBACK(button_clicked), elem
|
||||
data->button, "clicked", G_CALLBACK(button_clicked), data
|
||||
);
|
||||
alsa_elem_add_callback(elem, toggle_button_updated, data);
|
||||
data->text[0] = disabled_text;
|
||||
data->text[1] = enabled_text;
|
||||
load_icons(data);
|
||||
|
||||
// find the maximum width and height of both possible labels
|
||||
int max_width = 0, max_height = 0;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
toggle_button_set_text(data->button, data->text[i]);
|
||||
toggle_button_set_text(data, i);
|
||||
|
||||
GtkRequisition *size = gtk_requisition_new();
|
||||
gtk_widget_get_preferred_size(data->button, size, NULL);
|
||||
@@ -78,5 +117,12 @@ GtkWidget *make_boolean_alsa_elem(
|
||||
|
||||
toggle_button_updated(elem, data);
|
||||
|
||||
// periodically update volatile controls
|
||||
if (alsa_get_elem_volatile(elem))
|
||||
data->source =
|
||||
g_timeout_add_seconds(1, (GSourceFunc)update_toggle_button, data);
|
||||
|
||||
g_object_weak_ref(G_OBJECT(data->button), (GWeakNotify)on_destroy, data);
|
||||
|
||||
return data->button;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "widget-dual.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "gtkdial.h"
|
||||
#include "stringhelper.h"
|
||||
#include "widget-gain.h"
|
||||
#include "db.h"
|
||||
|
||||
struct gain {
|
||||
struct alsa_elem *elem;
|
||||
@@ -56,24 +57,39 @@ static void gain_updated(
|
||||
|
||||
char s[20];
|
||||
char *p = s;
|
||||
float value = (float)alsa_value * data->scale + elem->min_dB;
|
||||
float value;
|
||||
int min_db = round(elem->min_cdB / 100.0);
|
||||
int max_db = round(elem->max_cdB / 100.0);
|
||||
|
||||
if (value > elem->max_dB)
|
||||
value = elem->max_dB;
|
||||
else if (value < elem->min_dB)
|
||||
value = elem->min_dB;
|
||||
if (elem->dB_type == SND_CTL_TLVT_DB_LINEAR) {
|
||||
value = linear_value_to_db(
|
||||
alsa_value,
|
||||
elem->min_val,
|
||||
elem->max_val,
|
||||
min_db,
|
||||
max_db
|
||||
);
|
||||
} else {
|
||||
value = ((float)(alsa_value - elem->min_val)) * data->scale + (elem->min_cdB / 100.0);
|
||||
if (value > max_db)
|
||||
value = max_db;
|
||||
else if (value < min_db)
|
||||
value = min_db;
|
||||
}
|
||||
|
||||
if (data->zero_is_off && alsa_value == 0) {
|
||||
if (data->zero_is_off && value == min_db) {
|
||||
p += sprintf(p, "−∞");
|
||||
} else {
|
||||
if (data->scale <= 0.5)
|
||||
value = round(value * 10) / 10;
|
||||
if (value < 0)
|
||||
p += sprintf(p, "−");
|
||||
else if (value > 0)
|
||||
p += sprintf(p, "+");
|
||||
if (data->scale <= 0.5)
|
||||
p += sprintf(p, "%.1f", fabs(value));
|
||||
p += snprintf(p, 10, "%.1f", fabs(value));
|
||||
else
|
||||
p += sprintf(p, "%.0f", fabs(value));
|
||||
p += snprintf(p, 10, "%.0f", fabs(value));
|
||||
}
|
||||
if (data->scale > 0.5)
|
||||
p += sprintf(p, "dB");
|
||||
@@ -159,20 +175,42 @@ GtkWidget *make_gain_alsa_elem(
|
||||
gtk_widget_set_valign(data->vbox, GTK_ALIGN_START);
|
||||
gtk_widget_set_vexpand(data->vbox, TRUE);
|
||||
|
||||
data->scale = (float)(elem->max_dB - elem->min_dB) /
|
||||
(elem->max_val - elem->min_val);
|
||||
gboolean is_linear = elem->dB_type == SND_CTL_TLVT_DB_LINEAR;
|
||||
double step;
|
||||
|
||||
if (is_linear) {
|
||||
data->scale = 0.5;
|
||||
step = 0.5;
|
||||
} else {
|
||||
data->scale = (float)(elem->max_cdB - elem->min_cdB) / 100.0 /
|
||||
(elem->max_val - elem->min_val);
|
||||
step = 1;
|
||||
}
|
||||
data->dial = gtk_dial_new_with_range(
|
||||
elem->min_val,
|
||||
elem->max_val,
|
||||
1,
|
||||
step,
|
||||
3 / data->scale
|
||||
);
|
||||
|
||||
// calculate 0dB value
|
||||
int zero_db_value = (int)((0 - elem->min_dB) / data->scale + elem->min_val);
|
||||
int zero_db_value;
|
||||
|
||||
if (is_linear) {
|
||||
zero_db_value = cdb_to_linear_value(
|
||||
0,
|
||||
elem->min_val,
|
||||
elem->max_val,
|
||||
elem->min_cdB,
|
||||
elem->max_cdB
|
||||
);
|
||||
} else {
|
||||
zero_db_value =
|
||||
(int)((0 - elem->min_cdB) / 100.0 / data->scale + elem->min_val);
|
||||
}
|
||||
|
||||
gtk_dial_set_zero_db(GTK_DIAL(data->dial), zero_db_value);
|
||||
gtk_dial_set_is_linear(GTK_DIAL(data->dial), is_linear);
|
||||
|
||||
// convert from widget_taper to gtk_dial_taper
|
||||
int gtk_dial_taper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "stringhelper.h"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett <g@b4.vu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||