97 Commits
0.4.0-1 ... deb

Author SHA1 Message Date
921944e64e Replace README with a project sunsetting notice 2025-09-08 15:33:10 -05:00
9e00217cec Update changelog for 0.5.1-1 release 2025-06-09 22:38:00 -05:00
04ad890f16 Change default compression to xz level 9 2025-06-09 22:31:10 -05:00
a89558d5a7 Merge in upstream version 0.5.1 to Debian pkg 2025-06-09 22:30:46 -05:00
5b8bdaca4b Bump debian/changelog to verson 0.5.0-1 2025-06-09 22:30:31 -05:00
d8b08b46fc Merge in upstream version 0.5.0 to Debian pkg 2025-06-09 21:59:44 -05:00
runiq
e6fbb4f146 More Flatpak manifest cleanup
The flow should be clearer if every module is structured this way:

1. name
2. sources
3. buildsystem
4. config-opts
5. build-commands
6. post-install
7. cleanup
2025-03-16 20:17:06 +10:30
runiq
e4dc805422 Remove superfluous files from Flatpak 2025-03-16 20:17:03 +10:30
runiq
87ee0ed66b Add alsactl utility
Allows saving and loading device state with the Flatpak version. The
Gnome 47 SDK uses alsa-lib 1.2.12 [1] via the Freedesktop.org SDK [2],
so we use that here as well.

[1] https://gitlab.gnome.org/search?search=alsa&nav_source=navbar&project_id=456&group_id=8&search_code=true&repository_ref=47.4
[2] https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/blob/release/24.08/elements/components/alsa-lib.bst?ref_type=heads
2025-03-16 20:17:01 +10:30
Geoffrey D. Bennett
adeea461fd Change alsa_get_elem_int_values() to return longs rather than ints 2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
1f7bafbfc3 Update window-hardware with big 4th Gen and Vocaster models 2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
b8420ba31c Add support for rebooting devices using the FCP socket interface 2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
a5676eeb5a Replace hwdep check in window-startup.c with driver_type check
Since alsa.c already checks the hwdep version to determine the driver
type, window-startup.c doesn't need to do the same.
2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
9a33b92392 Don't attempt to attach unused routing_mixer_in_grid 2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
97f993db7b Add support for waiting for FCP driver initialisation
When a card using the FCP driver is added at runtime, we need to wait
for fcp-server to finish creating all the controls before we attempt
to enumerate them.
2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
6f0ab1890d Add driver type detection 2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
c88f7796f4 Move card init from alsa_scan_cards() to new card_init() function 2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
0b5b47ae66 Disable the startup menu option for 1st Gen devices 2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
b6117a501f Replace 1st Gen Startup Controls info with Startup Configuration
The Startup Controls information wasn't very useful, and the Startup
Configuration information is actually important.
2025-03-16 20:08:47 +10:30
Geoffrey D. Bennett
a34df84dfa Improve "settings keep resetting" FAQ entry 2025-03-16 20:08:47 +10:30
Pro-pra
6677e5c87d Use template spec with macros 2025-03-07 23:59:50 +10:30
Geoffrey D. Bennett
91fc3bbb03 Add information about alsa-state and alsa-restore to FAQ.md 2025-02-26 03:27:21 +10:30
Geoffrey D. Bennett
460b03c668 Replace '/" with ’/“/” in *.md 2025-02-26 03:27:21 +10:30
Geoffrey D. Bennett
8a2e5f5835 Add RTFM advice to FAQ.md 2025-02-26 03:27:21 +10:30
Geoffrey D. Bennett
72fd974da1 Update startup window no-startup-controls message
Replace the message suggesting a kernel upgrade because the 1st Gen
driver has no startup controls.
2025-02-26 02:23:24 +10:30
Geoffrey D. Bennett
e6166de04b Update 1st Gen doc to mention Level Meters and Startup Controls 2025-02-26 02:23:24 +10:30
Geoffrey D. Bennett
f0213eadb1 Replace -j4 with -j$(nproc) 2025-02-26 02:23:24 +10:30
Geoffrey D. Bennett
ae23674f21 Add small deadband to dial drag to stop double-click adjustments
Sometimes 0.5 < abs(offset_y) < 1 when double-clicking without moving
the mouse, causing the intended toggling between -inf and 0dB to not
work.

Fixes: #149.
2025-02-26 02:22:59 +10:30
Geoffrey D. Bennett
68e45e58a6 Remove unused start_x, start_y from gtk_dial_drag_gesture_update() 2025-02-26 02:07:55 +10:30
Geoffrey D. Bennett
f1585a3b8c Update flatpak container image from gnome-45 to gnome-47 2025-02-21 05:00:28 +10:30
Geoffrey D. Bennett
d1c1eb5db2 Undefine _FORTIFY_SOURCE before defining so GitHub can build the deb
The GitHub build was failing with:
<command-line>: error: "_FORTIFY_SOURCE" redefined [-Werror]
2025-02-21 04:57:17 +10:30
Geoffrey D. Bennett
21cdfbbe1a Make make clean do depclean too 2025-02-21 04:34:43 +10:30
Geoffrey D. Bennett
7033f9f622 Add big 4th Gen demo files 2025-02-21 04:34:43 +10:30
Geoffrey D. Bennett
5106ed228e Update docs and such for 1st Gen and big 4th Gen support 2025-02-21 04:34:43 +10:30
Geoffrey D. Bennett
ab40037064 Bump copyright year to 2025 2025-02-21 04:08:35 +10:30
Geoffrey D. Bennett
ed4f9cbaa7 Call card_destroy_callback() when an ALSA element is removed 2025-02-21 04:08:35 +10:30
Geoffrey D. Bennett
c7357c0539 Move card_destroy_callback() before alsa_card_callback() 2025-02-21 04:08:35 +10:30
Geoffrey D. Bennett
01c947f434 Fix output control column/mute tooltip handling 2025-02-21 04:08:35 +10:30
Geoffrey D. Bennett
dc21eb52d0 Add support for Level Meter labels 2025-02-21 04:08:35 +10:30
Geoffrey D. Bennett
c4ab20f9b5 Update alsa.c to handle differing FCP mixer element names 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
b41a47587b Add support for TLVs from the FCP driver
Decode level meter labels and the FCP socket location from TLVs.
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
11dba2b42c Simplify update_levels_controls() 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
f1f085abcf Add support for new 4th Gen control names
The 4th Gen driver has renamed "Line" to "Analogue" and removed
"Input" and "Output" in cases like "Mixer Input", "DSP Input", and
"Analogue Output". Some numbers are no longer zero-padded.
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
18841b2a45 Add support for two-control speaker switching and talkback
The 4th Gen driver has two boolean controls each for the speaker
switching and talkback controls, rather than the single enum control
that the 3rd Gen driver presents.
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
64d9f8173a Make perror("fopen") messages distinct 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
78e2d9642f Update alsa interface and gain widget to support linear volume
# Conflicts:
#	src/alsa.c
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
4a40b00695 Update gtkdial to support linear-volume controls 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
0f7389dca8 Highlight mixer labels on dial hover 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
640d027502 Update routing hover to highlight corresponding source sink 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
2bc6c86a8d Handle per-channel link buttons
Older kernel versions had one link button per channel pair.
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
67ccd1d684 Handle interfaces with fixed mixer inputs 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
81bc3c77c8 Treat locked ALSA elements as read-only 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
e0083f7085 Update constants for new maximum number of mux inputs and meters
Big 4th Gen devices have more inputs and meters than previous devices.
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
5da140df1e Wrap long line, fix reopen callback comment in alsa.c 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
1b0e072237 Gen 1: Add support for 1st Gen output controls 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
8178bd298b Gen 1: Add support for 1st Gen input controls 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
845dd5c98b Gen 1: Add support for 1st Gen mixer controls 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
45287711a4 Gen 1: Add support for 1st Gen stereo elements
Move routing src/snk creation into alsa.c from window-routing.c.
Move port_category and port_num from struct routing_snk to struct
alsa_elem.
Handle ALSA elements with two values.
Handle controls labelled as 1L and 1R instead of 1 and 2.
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
b1831c137a Gen 1: Add support for elements with count > 1 in saved config 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
1cdac65c00 Gen 1: Move alsa-sim elem creation into alsa_config_to_new_elem() 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
c38bbba793 Gen 1: Parse and save config count field
Needed for 1st Gen stereo volume controls.
2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
da1f011ab4 Gen 1: Ignore control "index" value in saved configurations 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
64f0cc36cc Gen 1: Add PC_OFF port category 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
9034790c06 Gen 1: Trigger support based on "Matrix" element presence 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
91d7218a47 Gen 1: Add 1st Gen devices to window-hardware.c 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
97ced90466 Gen 1: Mute switches are backwards 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
fa3e73d52f Gen 1: Handle different names for clock source and sync status 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
602854d087 Add Scarlett 1st Gen demo state files 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
97ca9ae754 Add get_elem_by_substr() to alsa.[ch] 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
6a04e1d1fa Update logo 2025-02-21 04:08:34 +10:30
Geoffrey D. Bennett
da4be2993e Update flatpak to GNOME 47 2025-02-21 04:08:25 +10:30
Geoffrey D. Bennett
abdb7f40f5 Fix crash in window-level.c on_destroy() 2024-07-03 02:53:18 +09:30
Geoffrey D. Bennett
0187698826 Replace cairo_show_text() in gtkdial.c with Pango
Fixes: #126.
2024-07-03 02:53:18 +09:30
unhappy-ending
c5b1ff0b94 Update Makefile to use $(CC) rather than cc
Calling cc directly causes a build failure on Clang/LLVM based Gentoo
machines that use LLVM specific toolchain flags.
2024-05-17 18:30:13 +09:30
Geoffrey D. Bennett
955dd1355a Add 3rd Gen 18i8/18i20 S/PDIF/Digital I/O Mode startup controls 2024-05-10 22:27:45 +09:30
Geoffrey D. Bennett
1615580de6 Add const to get*elem*() char* function arguments 2024-05-10 22:25:43 +09:30
Geoffrey D. Bennett
5526aa2f54 Fix link from FAQ.md to INSTALL.md
Fixes: #116.
2024-04-15 13:44:59 +09:30
Geoffrey D. Bennett
4ce2565b90 Add peak value display to the level meters 2024-04-11 22:47:48 +09:30
Geoffrey D. Bennett
909d3618b3 Use snprintf() in widget-gain.c when printing floats 2024-04-11 21:24:27 +09:30
Geoffrey D. Bennett
1fa964d348 Add peak display to the level meters 2024-04-11 21:24:27 +09:30
Geoffrey D. Bennett
159b3340eb Move level meter fields out of struct alsa_card
Create a levels struct managed inside window-levels.c.
2024-04-11 21:24:27 +09:30
Geoffrey D. Bennett
5fb3191124 Fix up deb and RPM package description & add docs
# Conflicts:
#	.github/workflows/build-debian-package.yml
2024-04-11 21:24:27 +09:30
Geoffrey D. Bennett
cc6853f541 Make flatpak build faster 2024-04-11 21:24:04 +09:30
Geoffrey D. Bennett
5d77207b66 Download and include scarlett2 firmware in flatpak
Fixes: #112.
2024-04-11 21:23:49 +09:30
Geoffrey D. Bennett
a940db51c2 Add -fPIE and -pie build flags to fix flatpak build under Fedora 2024-04-11 18:13:21 +09:30
Guillaume
d47e31eaed Add missing GTK and ALSA dependencies on deb package
Fixes: #109.
2024-04-11 13:39:23 +09:30
Geoffrey D. Bennett
92f9d5db8e Switch to embedded SVG icons
Make the icons independent of the desktop theme so they always look
good.
2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
af97b72b12 Update widget-boolean to cache the icon widgets 2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
3f7a4c2063 Allow for boolean controls that are backwards
Gen 1 has playback controls (0 = off, 1 = on), not mute controls
(0 = not muted, 1 = muted) like the Gen 2+ do.
2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
111ec1154d Add support for volatile buttons to widget-boolean.c
Will be used by Gen 1 support.
2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
db0929bd08 Search $PATH and /usr/sbin for alsactl
The path to alsactl was previously hardcoded because some distros put
it in /usr/sbin but don't include that directory in $PATH.
Unfortunately other distros put alsactl elsewhere. Let's search $PATH
and /usr/sbin to cater for both.

Fixes #101.
2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
2ddede4d3f Override focus and colour CSS button styles
Set all button focus outline properties and set the colour and filter
on fixed buttons so more theme styles are overridden.
2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
0e227e1e07 Fix Sample Rate button to be insensitive 2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
1d2ac0fd5c Add Arch package dependency 2024-03-31 03:29:10 +10:30
Geoffrey D. Bennett
05e9d9e0a2 Fix widget-boolean.c to free data on button destruction 2024-03-31 03:17:35 +10:30
Giorgio Reale
fcb5028aa2 Add 4rd Gen models to window-hardware.c 2024-03-28 13:15:21 +10:30
Geoffrey D. Bennett
c57e4eb2a4 Move 4th Gen Solo 48V switch above the Air switch
Fixes #107.
2024-03-28 13:15:21 +10:30
129 changed files with 60135 additions and 901 deletions

View File

@@ -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

View File

@@ -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

142
FAQ.md
View File

@@ -1,31 +1,34 @@
# 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,
youll 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
@@ -40,49 +43,112 @@ 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 its 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 theyre 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)
dont have any software controls. All the controls are available from
the front panel, so they dont require the specialised drivers or this
GUI.
## Where are the options to set the sample rate and buffer size?
Its 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 doesnt 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.
## Why do my settings keep resetting?
The settings in the ALSA Scarlett Control Panel are automatically
saved in the interface itself (all series except 1st Gen), so they
should persist across reboots, power cycles, USB disconnect/reconnect,
and even across different computers. This includes all routing,
mixing, and other control panel settings.
If you find that your settings are reverting whenever you plug your
interface in or power it back on, the most likely cause is the
`alsa-state` and `alsa-restore` systemd services. These services save
the state of ALSA controls on system shutdown to
`/var/lib/alsa/asound.state` and then restore it each time the device
is plugged in, potentially overwriting your interfaces stored
settings.
It can be rather annoying, wondering why your device is unusable or
needs to be reconfigured every time you plug it in or turn it on.
To fix this issue, disable these services:
```sh
sudo systemctl mask alsa-state
sudo systemctl mask alsa-restore
```
You can verify if this is the cause of your issues by:
1. Change some setting that is indicated on the device (the “Inst”
setting is a good).
2. Disconnect USB and notice the state of the setting on the device
has not changed.
3. Power cycle the device and notice the state of the setting on the
device has not changed.
4. Reconnect USB and notice the state of the setting on the device has
changed.
If the setting on the device changes at step 4, then the `alsa-state`
and `alsa-restore` services are the likely cause of your issues.
## Help?!
For help with the driver:
https://github.com/geoffreybennett/scarlett-gen2/issues
Have you read the User Guide for your interface? Its available
online: https://downloads.focusrite.com/focusrite and contains a lot
of helpful/useful/important information about your device.
You can skip the “Easy Start” and “Setting up your DAW” sections, but
the rest is well worth reading. Even the information about Focusrite
Control is useful, although not directly applicable, because it will
help you understand more about the possibilities of what you can do
with your device.
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

View File

@@ -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:"

View File

@@ -1,86 +1,8 @@
# 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.
- Upstream project here: https://github.com/geoffreybennett/alsa-scarlett-gui
- Debian's packaged version here: https://salsa.debian.org/doge-tech/alsa-scarlett-gui
Supported interfaces:
- Scarlett 2nd Gen 6i6, 18i8, 18i20
- Scarlett 3rd Gen Solo, 2i2, 4i4, 8i6, 18i8, 18i20
- Scarlett 4th Gen Solo, 2i2, 4i4
- Clarett 2Pre, 4Pre, 8Pre USB
- Clarett+ 2Pre, 4Pre, 8Pre
- Vocaster One and Vocaster Two
This fork of the repo was started so I could write a Debian package manifest for the program. While I successfully made a working package, I had no intention of uploading it to the Debian archives -- I felt that I didn't have enough experience to bother the Debian maintainers with my nonsense. Someone else, however, [did upload one!](https://salsa.debian.org/doge-tech/alsa-scarlett-gui). That version has since been included with the Debian 13 release.
## About
<img src="src/img/alsa-scarlett-gui-logo.png" align="right">
The 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,
they have a bunch of proprietary functionality that required a kernel
driver to be written specifically for those devices.
Unfortunately, actually using this functionality used to be quite an
awful experience. The existing applications like `alsamixer` and
`qasmixer` become completely user-hostile with the hundreds of
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!
![Demonstration](img/demo.gif)
## Documentation
Refer to [INSTALL.md](docs/INSTALL.md) for prerequisites, how to
build, install, and run.
Refer to [USAGE.md](docs/USAGE.md) for general usage information and
known issues.
Information specific to various models:
- [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)
## Donations
This program is Free Software, developed using my personal resources,
over hundreds of hours.
If you like this software, please consider a donation to say thank
you! Any donation is appreciated.
- https://liberapay.com/gdb
- https://paypal.me/gdbau
## License
Copyright 2022-2024 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
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
## Disclaimer Third Parties
Focusrite, Scarlett, Clarett, and Vocaster are trademarks or
registered trademarks of Focusrite Audio Engineering Limited in
England, USA, and/or other countries. Use of these trademarks does not
imply any affiliation or endorsement of this software.
I'm archiving this repo because it is redundant with that package. I want to keep it around as something I made, while also making it clear that this project is dead.

View File

@@ -1,33 +1,29 @@
Summary: ALSA Scarlett Gen 2/3 Control Panel
Name: alsa-scarlett-gui
Version: VERSION
Release: 1%{?dist}
License: GPLv3+ LGPLv3+
Url: https://github.com/geoffreybennett/alsa-scarlett-gui
Source: %{name}-%{version}.tar.gz
Summary: ALSA Scarlett Control Panel
Name: alsa-scarlett-gui
Version: VERSION
Release: 1%{?dist}
License: GPLv3+ LGPLv3+
Url: https://github.com/geoffreybennett/alsa-scarlett-gui
Source0: https://github.com/geoffreybennett/alsa-scarlett-gui/archive/refs/tags/%{version}.tar.gz?/%{name}-%{version}.tar.gz
BuildRequires: pkgconfig(alsa)
BuildRequires: pkgconfig(gtk4)
BuildRequires: pkgconfig(openssl)
%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
%setup -q -n %{name}-%{version}/src
%build
make -C src -j4 VERSION=%{version} PREFIX=/usr
%make_build VERSION=%{version} PREFIX=%{_prefix}
%install
%make_install -C src PREFIX=/usr
DOCDIR=%{buildroot}/usr/share/doc/%{name}-%{version}
mkdir -p $DOCDIR/img
mkdir $DOCDIR/demo
cp *.md $DOCDIR
cp img/* $DOCDIR/img
cp demo/* $DOCDIR/demo
%make_install PREFIX=%{_prefix}
%files
%doc /usr/share/doc/%{name}-%{version}
/usr/bin/alsa-scarlett-gui
/usr/share/applications/vu.b4.alsa-scarlett-gui.desktop
/usr/share/icons/hicolor/256x256/apps/vu.b4.alsa-scarlett-gui.png
%doc ../img ../demo ../docs ../*.md
%{_bindir}/alsa-scarlett-gui
%{_datadir}/applications/vu.b4.alsa-scarlett-gui.desktop
%{_iconsdir}/hicolor/256x256/apps/vu.b4.alsa-scarlett-gui.png

124
debian/changelog vendored
View File

@@ -1,3 +1,127 @@
alsa-scarlett-gui (0.5.1-1) unstable; urgency=medium
[ Geoffrey D. Bennett ]
* Add RTFM advice to FAQ.md
* Replace '/" with /“/” in *.md
* Add information about alsa-state and alsa-restore to FAQ.md
[ Pro-pra ]
* Use template spec with macros
[ Geoffrey D. Bennett ]
* Improve "settings keep resetting" FAQ entry
* Replace 1st Gen Startup Controls info with Startup Configuration
* Disable the startup menu option for 1st Gen devices
* Move card init from alsa_scan_cards() to new card_init() function
* Add driver type detection
* Add support for waiting for FCP driver initialisation
* Don't attempt to attach unused routing_mixer_in_grid
* Replace hwdep check in window-startup.c with driver_type check
* Add support for rebooting devices using the FCP socket interface
* Update window-hardware with big 4th Gen and Vocaster models
* Change alsa_get_elem_int_values() to return longs rather than ints
[ runiq ]
* Add alsactl utility
* Remove superfluous files from Flatpak
* More Flatpak manifest cleanup
[ Robert Garrett ]
* Change default compression to xz level 9
-- Robert Garrett <robertgarrett404@gmail.com> Mon, 09 Jun 2025 22:37:54 -0500
alsa-scarlett-gui (0.5.0-1) unstable; urgency=medium
[ Geoffrey D. Bennett ]
* Move 4th Gen Solo 48V switch above the Air switch
[ Giorgio Reale ]
* Add 4rd Gen models to window-hardware.c
[ Geoffrey D. Bennett ]
* Fix widget-boolean.c to free data on button destruction
* Add Arch package dependency
* Fix Sample Rate button to be insensitive
* Override focus and colour CSS button styles
* Search $PATH and /usr/sbin for alsactl
* Add support for volatile buttons to widget-boolean.c
* Allow for boolean controls that are backwards
* Update widget-boolean to cache the icon widgets
* Switch to embedded SVG icons
[ Guillaume ]
* Add missing GTK and ALSA dependencies on deb package
[ Geoffrey D. Bennett ]
* Add -fPIE and -pie build flags to fix flatpak build under Fedora
* Download and include scarlett2 firmware in flatpak
* Make flatpak build faster
* Fix up deb and RPM package description & add docs
* Move level meter fields out of struct alsa_card
* Add peak display to the level meters
* Use snprintf() in widget-gain.c when printing floats
* Add peak value display to the level meters
* Fix link from FAQ.md to INSTALL.md
* Add const to get*elem*() char* function arguments
* Add 3rd Gen 18i8/18i20 S/PDIF/Digital I/O Mode startup controls
[ unhappy-ending ]
* Update Makefile to use $(CC) rather than cc
[ Geoffrey D. Bennett ]
* Replace cairo_show_text() in gtkdial.c with Pango
* Fix crash in window-level.c on_destroy()
* Update flatpak to GNOME 47
* Update logo
* Add get_elem_by_substr() to alsa.[ch]
* Add Scarlett 1st Gen demo state files
* Gen 1: Handle different names for clock source and sync status
* Gen 1: Mute switches are backwards
* Gen 1: Add 1st Gen devices to window-hardware.c
* Gen 1: Trigger support based on "Matrix" element presence
* Gen 1: Add PC_OFF port category
* Gen 1: Ignore control "index" value in saved configurations
* Gen 1: Parse and save config count field
* Gen 1: Move alsa-sim elem creation into alsa_config_to_new_elem()
* Gen 1: Add support for elements with count > 1 in saved config
* Gen 1: Add support for 1st Gen stereo elements
* Gen 1: Add support for 1st Gen mixer controls
* Gen 1: Add support for 1st Gen input controls
* Gen 1: Add support for 1st Gen output controls
* Wrap long line, fix reopen callback comment in alsa.c
* Update constants for new maximum number of mux inputs and meters
* Treat locked ALSA elements as read-only
* Handle interfaces with fixed mixer inputs
* Handle per-channel link buttons
* Update routing hover to highlight corresponding source sink
* Highlight mixer labels on dial hover
* Update gtkdial to support linear-volume controls
* Update alsa interface and gain widget to support linear volume
* Make perror("fopen") messages distinct
* Add support for two-control speaker switching and talkback
* Add support for new 4th Gen control names
* Simplify update_levels_controls()
* Add support for TLVs from the FCP driver
* Update alsa.c to handle differing FCP mixer element names
* Add support for Level Meter labels
* Fix output control column/mute tooltip handling
* Move card_destroy_callback() before alsa_card_callback()
* Call card_destroy_callback() when an ALSA element is removed
* Bump copyright year to 2025
* Update docs and such for 1st Gen and big 4th Gen support
* Add big 4th Gen demo files
* Make make clean do depclean too
* Undefine _FORTIFY_SOURCE before defining so GitHub can build the deb
* Update flatpak container image from gnome-45 to gnome-47
* Remove unused start_x, start_y from gtk_dial_drag_gesture_update()
* Add small deadband to dial drag to stop double-click adjustments
* Replace -j4 with -j$(nproc)
* Update 1st Gen doc to mention Level Meters and Startup Controls
* Update startup window no-startup-controls message
-- Robert Garrett <robertgarrett404@gmail.com> Mon, 09 Jun 2025 22:09:31 -0500
alsa-scarlett-gui (0.4.0-1) UNRELEASED; urgency=low
[ Guillaume ]

2
debian/gbp.conf vendored
View File

@@ -1,4 +1,6 @@
[DEFAULT]
compression = xz
compression-level = 9
upstream-branch = master
debian-branch = deb
upstream-tag = %(version)s

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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, youll 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 youve got a Vocaster, or if your distribution doesnt 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 dont have any proprietary
software controls so they dont 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 doesnt include a recent-enough kernel for your
interface, you can get the latest driver from here and build it for
your current kernel if its 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 youll 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 dont 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 @@ doesnt 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 youve already updated it using the
manufacturers 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 youve already
updated it using the manufacturers 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:

View File

@@ -1,14 +1,18 @@
# ALSA Scarlett2 Usage With Old Kernels
**This information is mostly for historical purposes. If youre
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. Its recommended
that you run 6.7 or later, or build the backported driver for your
kernel. If you do, then these instructions arent 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. Its
recommended that you run 6.7 or later, or build the backported driver
for your kernel. If you do, then these instructions arent relevant;
continue with [INSTALL.md](INSTALL.md) for prerequisites, how to
build, install, and run `alsa-scarlett-gui`.
If youve got a Scarlett Gen 2 or 3 or a Clarett+ 8Pre and dont 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.

View File

@@ -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.

164
docs/iface-1st-gen.md Normal file
View File

@@ -0,0 +1,164 @@
# 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
wont actually update the control.
3. **Level Meters**: The driver does not support reading the level
meters from the hardware.
4. **Startup Configuration**: The driver is not able to save the
current configuration to the non-volatile memory of the device, so
youll need to reapply the desired configuration each time you
restart it (or write your preferred configuration using MixControl
on Windows or Mac).
### 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 arent 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 arent 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.
![Routing Window](../img/scarlett-1st-gen-6i6-routing.png)
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
View 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 havent
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.
![Main Window](../img/iface-4th-gen-big.png)
### 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 arent 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 arent 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
signals 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:
![4th Gen 16i16 Routing](../img/scarlett-4th-gen-16i16-routing.png)
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.
![Levels](../img/window-levels-4th-gen-big.png)
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).

View File

@@ -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.
![Main Window](../img/iface-4th-gen.png)
![Main Window](../img/iface-4th-gen-small.png)
### Global Controls
@@ -204,10 +204,7 @@ Important Notes:
- The Focusrite Control 2 software cant control most of this routing,
so if you make changes here and then want to use Focusrite Control
2, youll probably need to reset the routing back to the factory
default settings. Theres currently no way to reset to factory
default settings from the Focusrite Control 2 software; youll 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.
![Levels](../img/window-levels-4th-gen.gif)
![Levels](../img/window-levels-4th-gen-small.gif)
Look at this in conjunction with the routing window to understand
which meter corresponds to which source or sink.

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
img/iface-4th-gen-big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

View File

@@ -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

View File

@@ -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
);

View File

@@ -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

View File

@@ -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>

View 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;
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -1,23 +1,42 @@
// 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.h"
#include "scarlett2-firmware.h"
#include "scarlett2-ioctls.h"
#include "stringhelper.h"
#include "window-iface.h"
#define MAX_TLV_RANGE_SIZE 256
#define MAJOR_HWDEP_VERSION_SCARLETT2 1
#define MAJOR_HWDEP_VERSION_FCP 2
#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 +64,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 +79,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 +95,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 +144,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 +201,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,
@@ -201,8 +220,8 @@ long alsa_get_elem_value(struct alsa_elem *elem) {
// for elements with multiple int values, return all the values
// the int array returned needs to be freed by the caller
int *alsa_get_elem_int_values(struct alsa_elem *elem) {
int *values = calloc(elem->count, sizeof(int));
long *alsa_get_elem_int_values(struct alsa_elem *elem) {
long *values = calloc(elem->count, sizeof(long));
if (elem->card->num == SIMULATED_CARD_NUM) {
for (int i = 0; i < elem->count; i++)
@@ -237,14 +256,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 +282,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 +290,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 +355,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 +527,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 +536,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 +771,98 @@ 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;
}
}
// Complete card initialisation after the driver is ready
static void complete_card_init(struct alsa_card *card) {
// Get full element list and create main window
alsa_get_elem_list(card);
alsa_set_lr_nums(card);
alsa_get_routing_controls(card);
card->best_firmware_version = scarlett2_get_best_firmware_version(card->pid);
if (card->serial) {
// Call the reopen callbacks for this card
struct reopen_callback *rc = g_hash_table_lookup(
reopen_callbacks, card->serial
);
if (rc)
rc->callback(rc->data);
g_hash_table_remove(reopen_callbacks, card->serial);
}
create_card_window(card);
}
// Check if the Firmware Version control has a TLV and is locked,
// indicating the driver is ready
static int check_driver_ready(snd_ctl_elem_info_t *info) {
return snd_ctl_elem_info_is_tlv_readable(info) &&
snd_ctl_elem_info_is_locked(info);
}
// Check if the FCP driver is initialised
static void check_driver_init(
struct alsa_card *card, int numid, unsigned int mask
) {
// Ignore controls going away
if (mask == SND_CTL_EVENT_MASK_REMOVE)
return;
// Get the control's info
snd_ctl_elem_id_t *id;
snd_ctl_elem_info_t *info;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_info_alloca(&info);
snd_ctl_elem_id_set_numid(id, numid);
snd_ctl_elem_info_set_id(info, id);
if (snd_ctl_elem_info(card->handle, info) < 0) {
fprintf(stderr, "error getting elem info %d\n", numid);
return;
}
const char *name = snd_ctl_elem_info_get_name(info);
// Check if it's the Firmware Version control being updated
if (strcmp(name, "Firmware Version"))
return;
// Check if the driver is ready
if (!check_driver_ready(info))
return;
// The driver is initialised; update the card's driver type
card->driver_type = DRIVER_TYPE_SOCKET;
// Complete the card initialisation
complete_card_init(card);
}
static gboolean alsa_card_callback(
GIOChannel *source,
GIOCondition condition,
@@ -444,16 +870,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 +887,34 @@ 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);
// Check if we're waiting for FCP driver to initialise and check if
// it's now ready
if (card->driver_type == DRIVER_TYPE_SOCKET_UNINIT) {
check_driver_init(card, numid, mask);
return 1;
}
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 +963,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(
@@ -732,6 +1150,94 @@ static void alsa_get_serial_number(struct alsa_card *card) {
card->serial = strdup(serial);
}
// return true if the Firmware Version control exists and is writable
// and locked (i.e. the FCP server is running)
static int check_firmware_version_locked(struct alsa_card *card) {
snd_ctl_elem_id_t *id;
snd_ctl_elem_info_t *info;
snd_ctl_elem_id_alloca(&id);
snd_ctl_elem_info_alloca(&info);
// look for the Firmware Version control
snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_CARD);
snd_ctl_elem_id_set_name(id, "Firmware Version");
snd_ctl_elem_info_set_id(info, id);
// no Firmware Version control found
int err = snd_ctl_elem_info(card->handle, info);
if (err < 0)
return 0;
return check_driver_ready(info);
}
// return the driver type for this card
// DRIVER_TYPE_NONE: no driver
// DRIVER_TYPE_HWDEP: Scarlett2 driver
// DRIVER_TYPE_SOCKET: FCP driver
// DRIVER_TYPE_SOCKET_UNINIT: FCP driver, but not initialised
static int get_driver_type(struct alsa_card *card) {
snd_hwdep_t *hwdep;
int err = scarlett2_open_card(card->device, &hwdep);
// no hwdep for this card - driver type none
if (err == -ENOENT)
return DRIVER_TYPE_NONE;
// if we get EPERM, it's FCP but no server running
if (err == -EPERM)
return DRIVER_TYPE_SOCKET_UNINIT;
// if we get EBUSY, it's FCP
if (err == -EBUSY)
// fcp-server locks the Firmware Version control when it has
// finished starting up
return check_firmware_version_locked(card) ?
DRIVER_TYPE_SOCKET : DRIVER_TYPE_SOCKET_UNINIT;
// failed to open hwdep
if (err < 0)
return DRIVER_TYPE_NONE;
// we can open hwdep, so now check the protocol version
int ver = scarlett2_get_protocol_version(hwdep);
scarlett2_close(hwdep);
// failed to get protocol version
if (ver < 0)
return DRIVER_TYPE_NONE;
// hwdep protocol version 1.x.x is Scarlett2 driver
if (SCARLETT2_HWDEP_VERSION_MAJOR(ver) == MAJOR_HWDEP_VERSION_SCARLETT2)
return DRIVER_TYPE_HWDEP;
// hwdep protocol version 2.x.x is FCP driver (but not initialised,
// because we were able to open the hwdep)
if (SCARLETT2_HWDEP_VERSION_MAJOR(ver) == MAJOR_HWDEP_VERSION_FCP)
return DRIVER_TYPE_SOCKET_UNINIT;
return DRIVER_TYPE_NONE;
}
static void card_init(struct alsa_card *card) {
alsa_get_usbid(card);
alsa_get_serial_number(card);
alsa_subscribe(card);
alsa_add_card_callback(card);
card->driver_type = get_driver_type(card);
// Driver not ready? Create the iface-waiting window
if (card->driver_type == DRIVER_TYPE_SOCKET_UNINIT) {
create_card_window(card);
return;
}
complete_card_init(card);
}
static void alsa_scan_cards(void) {
snd_ctl_card_info_t *info;
snd_ctl_t *ctl;
@@ -775,26 +1281,7 @@ static void alsa_scan_cards(void) {
card->name = strdup(snd_ctl_card_info_get_name(info));
card->handle = ctl;
alsa_get_elem_list(card);
alsa_subscribe(card);
alsa_get_usbid(card);
alsa_get_serial_number(card);
card->best_firmware_version = scarlett2_get_best_firmware_version(card->pid);
if (card->serial) {
// call the callbacks for this card
struct reopen_callback *rc = g_hash_table_lookup(
reopen_callbacks, card->serial
);
if (rc)
rc->callback(rc->data);
g_hash_table_remove(reopen_callbacks, card->serial);
}
create_card_window(card);
alsa_add_card_callback(card);
card_init(card);
continue;

View File

@@ -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,41 @@ 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
};
// driver types
// NONE is 1st Gen or Scarlett2 before hwdep support was added
// (no erase config or firmware update support)
// HWDEP is the Scarlett2 driver after hwdep support was added
// SOCKET is the FCP driver
enum {
DRIVER_TYPE_NONE,
DRIVER_TYPE_HWDEP,
DRIVER_TYPE_SOCKET,
DRIVER_TYPE_SOCKET_UNINIT,
DRIVER_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 +75,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 +84,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 +101,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 +115,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 +137,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 +175,13 @@ struct alsa_card {
uint32_t pid;
char *serial;
char *name;
int driver_type;
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 +203,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 +220,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(
@@ -216,9 +240,10 @@ void alsa_elem_add_callback(
int alsa_get_elem_type(struct alsa_elem *elem);
char *alsa_get_elem_name(struct alsa_elem *elem);
long alsa_get_elem_value(struct alsa_elem *elem);
int *alsa_get_elem_int_values(struct alsa_elem *elem);
long *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 +251,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);

View File

@@ -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
View 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
View 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
);

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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"

View File

@@ -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

19
src/fcp-shared.c Normal file
View File

@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
// Error messages
const char *fcp_socket_error_messages[] = {
"Success",
"Invalid magic",
"Invalid command",
"Invalid length",
"Invalid hash",
"Firmware PID does not match USB PID",
"Configuration error (check fcp-server log)",
"FCP communication error",
"Timeout",
"Read error",
"Write error",
"Not running leapfrog firmware",
"Invalid state"
};

80
src/fcp-shared.h Normal file
View File

@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdint.h>
// Error codes
#define FCP_SOCKET_ERR_INVALID_MAGIC 1
#define FCP_SOCKET_ERR_INVALID_COMMAND 2
#define FCP_SOCKET_ERR_INVALID_LENGTH 3
#define FCP_SOCKET_ERR_INVALID_HASH 4
#define FCP_SOCKET_ERR_INVALID_USB_ID 5
#define FCP_SOCKET_ERR_CONFIG 6
#define FCP_SOCKET_ERR_FCP 7
#define FCP_SOCKET_ERR_TIMEOUT 8
#define FCP_SOCKET_ERR_READ 9
#define FCP_SOCKET_ERR_WRITE 10
#define FCP_SOCKET_ERR_NOT_LEAPFROG 11
#define FCP_SOCKET_ERR_INVALID_STATE 12
#define FCP_SOCKET_ERR_MAX 12
// Protocol constants
#define FCP_SOCKET_PROTOCOL_VERSION 1
#define FCP_SOCKET_MAGIC_REQUEST 0x53
#define FCP_SOCKET_MAGIC_RESPONSE 0x73
// Maximum payload length (2MB)
#define MAX_PAYLOAD_LENGTH 2 * 1024 * 1024
// Request types
#define FCP_SOCKET_REQUEST_REBOOT 0x0001
#define FCP_SOCKET_REQUEST_CONFIG_ERASE 0x0002
#define FCP_SOCKET_REQUEST_APP_FIRMWARE_ERASE 0x0003
#define FCP_SOCKET_REQUEST_APP_FIRMWARE_UPDATE 0x0004
#define FCP_SOCKET_REQUEST_ESP_FIRMWARE_UPDATE 0x0005
// Response types
#define FCP_SOCKET_RESPONSE_VERSION 0x00
#define FCP_SOCKET_RESPONSE_SUCCESS 0x01
#define FCP_SOCKET_RESPONSE_ERROR 0x02
#define FCP_SOCKET_RESPONSE_PROGRESS 0x03
extern const char *fcp_socket_error_messages[];
// Message structures
#pragma pack(push, 1)
struct fcp_socket_msg_header {
uint8_t magic;
uint8_t msg_type;
uint32_t payload_length;
};
struct firmware_payload {
uint32_t size;
uint16_t usb_vid;
uint16_t usb_pid;
uint8_t sha256[32];
uint8_t md5[16];
uint8_t data[];
};
struct version_msg {
struct fcp_socket_msg_header header;
uint8_t version;
};
struct progress_msg {
struct fcp_socket_msg_header header;
uint8_t percent;
};
struct error_msg {
struct fcp_socket_msg_header header;
int16_t error_code;
};
#pragma pack(pop)

220
src/fcp-socket.c Normal file
View File

@@ -0,0 +1,220 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#include <fcntl.h>
#include "fcp-shared.h"
#include "fcp-socket.h"
#include "error.h"
// Connect to the FCP socket server for the given card
int fcp_socket_connect(struct alsa_card *card) {
if (!card || !card->fcp_socket) {
fprintf(stderr, "FCP socket path is not available");
return -1;
}
int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock_fd < 0) {
fprintf(stderr, "Cannot create socket: %s", strerror(errno));
return -1;
}
struct sockaddr_un addr = {
.sun_family = AF_UNIX
};
strncpy(addr.sun_path, card->fcp_socket, sizeof(addr.sun_path) - 1);
if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
fprintf(stderr, "Cannot connect to server at %s: %s",
addr.sun_path, strerror(errno));
close(sock_fd);
return -1;
}
return sock_fd;
}
// Send a simple command with no payload to the server
int fcp_socket_send_command(int sock_fd, uint8_t command) {
struct fcp_socket_msg_header header = {
.magic = FCP_SOCKET_MAGIC_REQUEST,
.msg_type = command,
.payload_length = 0
};
if (write(sock_fd, &header, sizeof(header)) != sizeof(header)) {
fprintf(stderr, "Error sending command: %s", strerror(errno));
return -1;
}
return 0;
}
// Handle server responses from a command
int fcp_socket_handle_response(int sock_fd, bool show_progress) {
struct fcp_socket_msg_header header;
ssize_t bytes_read;
// Read response header
bytes_read = read(sock_fd, &header, sizeof(header));
if (bytes_read != sizeof(header)) {
if (bytes_read == 0) {
// Server closed the connection
return 0;
}
fprintf(stderr, "Error reading response header: %s", strerror(errno));
return -1;
}
// Verify the magic value
if (header.magic != FCP_SOCKET_MAGIC_RESPONSE) {
fprintf(stderr, "Invalid response magic: 0x%02x", header.magic);
return -1;
}
// Handle different response types
switch (header.msg_type) {
case FCP_SOCKET_RESPONSE_VERSION: {
// Protocol version response
uint8_t version;
bytes_read = read(sock_fd, &version, sizeof(version));
if (bytes_read != sizeof(version)) {
fprintf(stderr, "Error reading version: %s", strerror(errno));
return -1;
}
// Protocol version mismatch?
if (version != FCP_SOCKET_PROTOCOL_VERSION) {
fprintf(stderr, "Protocol version mismatch: expected %d, got %d",
FCP_SOCKET_PROTOCOL_VERSION, version);
return -1;
}
break;
}
case FCP_SOCKET_RESPONSE_SUCCESS:
// Command completed successfully
return 0;
case FCP_SOCKET_RESPONSE_ERROR: {
// Error response
int16_t error_code;
bytes_read = read(sock_fd, &error_code, sizeof(error_code));
if (bytes_read != sizeof(error_code)) {
fprintf(stderr, "Error reading error code: %s", strerror(errno));
return -1;
}
if (error_code > 0 && error_code <= FCP_SOCKET_ERR_MAX) {
fprintf(stderr, "Server error: %s", fcp_socket_error_messages[error_code]);
} else {
fprintf(stderr, "Unknown server error code: %d", error_code);
}
return -1;
}
case FCP_SOCKET_RESPONSE_PROGRESS: {
// Progress update
if (show_progress) {
uint8_t percent;
bytes_read = read(sock_fd, &percent, sizeof(percent));
if (bytes_read != sizeof(percent)) {
fprintf(stderr, "Error reading progress: %s", strerror(errno));
return -1;
}
fprintf(stderr, "\rProgress: %d%%", percent);
if (percent == 100)
fprintf(stderr, "\n");
} else {
// Skip the progress byte
uint8_t dummy;
if (read(sock_fd, &dummy, sizeof(dummy)) < 0) {
fprintf(stderr, "Error reading progress: %s", strerror(errno));
return -1;
}
}
// Continue reading responses
return fcp_socket_handle_response(sock_fd, show_progress);
}
default:
fprintf(stderr, "Unknown response type: 0x%02x", header.msg_type);
return -1;
}
return 0;
}
// Wait for server to disconnect (used after reboot command)
int fcp_socket_wait_for_disconnect(int sock_fd) {
fd_set rfds;
struct timeval tv, start_time, now;
char buf[1];
const int TIMEOUT_SECS = 2;
gettimeofday(&start_time, NULL);
while (1) {
FD_ZERO(&rfds);
FD_SET(sock_fd, &rfds);
gettimeofday(&now, NULL);
int elapsed = now.tv_sec - start_time.tv_sec;
if (elapsed >= TIMEOUT_SECS) {
fprintf(stderr, "Timeout waiting for server disconnect\n");
return -1;
}
tv.tv_sec = TIMEOUT_SECS - elapsed;
tv.tv_usec = 0;
int ret = select(sock_fd + 1, &rfds, NULL, NULL, &tv);
if (ret < 0) {
if (errno == EINTR)
continue;
fprintf(stderr, "Select error: %s\n", strerror(errno));
return -1;
}
if (ret > 0) {
// Try to read one byte
ssize_t n = read(sock_fd, buf, 1);
if (n < 0) {
if (errno == EINTR || errno == EAGAIN)
continue;
fprintf(stderr, "Read error: %s\n", strerror(errno));
return -1;
}
if (n == 0) {
// EOF received - server has disconnected
return 0;
}
// Ignore any data received, just keep waiting for EOF
}
}
}
// Reboot a device using the FCP socket interface
int fcp_socket_reboot_device(struct alsa_card *card) {
int sock_fd, ret = -1;
sock_fd = fcp_socket_connect(card);
if (sock_fd < 0)
return -1;
// Send reboot command and wait for server to disconnect
if (fcp_socket_send_command(sock_fd, FCP_SOCKET_REQUEST_REBOOT) == 0)
ret = fcp_socket_wait_for_disconnect(sock_fd);
close(sock_fd);
return ret;
}

27
src/fcp-socket.h Normal file
View File

@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdbool.h>
#include "alsa.h"
// Connect to the FCP socket server for the given card
// Returns socket file descriptor on success, -1 on error
int fcp_socket_connect(struct alsa_card *card);
// Send a simple command with no payload to the server
// Returns 0 on success, -1 on error
int fcp_socket_send_command(int sock_fd, uint8_t command);
// Handle server responses from a command
// Returns 0 on success, -1 on error
int fcp_socket_handle_response(int sock_fd, bool show_progress);
// Wait for server to disconnect (used after reboot command)
// Returns 0 if disconnected, -1 on timeout or error
int fcp_socket_wait_for_disconnect(int sock_fd);
// Reboot a device using the FCP socket interface
// Returns 0 on success, -1 on error
int fcp_socket_reboot_device(struct alsa_card *card);

View File

@@ -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)

View File

@@ -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>

View File

@@ -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);

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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, &current_row,
"Level Capture Enum", create_input_level_control
);
create_input_controls_by_type(
elems, input_grid, &current_row,
"Phantom Power Capture Switch", create_input_phantom_control
);
create_input_controls_by_type(
elems, input_grid, &current_row,
"Air Capture Enum", create_input_air_enum_control
);
(*x)++;
return;
}
create_input_select_control(elems, input_grid, &current_row);
create_input_controls_by_type(
@@ -548,6 +669,10 @@ static void create_input_controls(
elems, input_grid, &current_row,
"Level Capture Enum", create_input_level_control
);
create_input_controls_by_type(
elems, input_grid, &current_row,
"Impedance Switch", create_input_level_control
);
create_input_controls_by_type(
elems, input_grid, &current_row,
"Air Capture Switch", create_input_air_switch_control
@@ -572,6 +697,14 @@ static void create_input_controls(
elems, input_grid, &current_row,
"Pad Capture Switch", create_input_pad_control
);
create_input_controls_by_type(
elems, input_grid, &current_row,
"Pad Switch", create_input_pad_control
);
create_input_controls_by_type(
elems, input_grid, &current_row,
"Gain Switch", create_input_gain_switch_control
);
create_input_controls_by_type(
elems, input_grid, &current_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");

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 dont 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);

View File

@@ -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

View File

@@ -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>

View File

@@ -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

117
src/iface-waiting.c Normal file
View File

@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2025 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <gtk/gtk.h>
#include "alsa.h"
#include "iface-waiting.h"
#include "scarlett2-ioctls.h"
#include "window-iface.h"
// Structure to hold timeout-related widgets
struct timeout_data {
GtkWidget *box;
GtkWidget *spinner;
GtkWidget *message_label;
guint timeout_id;
};
// Timeout callback function
static gboolean on_timeout(gpointer user_data) {
struct timeout_data *data = (struct timeout_data *)user_data;
// Remove spinner
gtk_box_remove(GTK_BOX(data->box), data->spinner);
// Update message with clickable link
if (data->message_label && GTK_IS_WIDGET(data->message_label))
gtk_label_set_markup(
GTK_LABEL(data->message_label),
"Driver not detected. Please ensure "
"<span font='monospace'>fcp-server</span> from "
"<a href=\"https://github.com/geoffreybennett/fcp-support\">"
"https://github.com/geoffreybennett/fcp-support</a> "
"has been installed."
);
// Reset the timeout ID since it won't be called again
data->timeout_id = 0;
// Return FALSE to prevent the timeout from repeating
return FALSE;
}
// Weak reference callback for cleanup
static void on_widget_dispose(gpointer data, GObject *where_the_object_was) {
struct timeout_data *timeout_data = (struct timeout_data *)data;
// Cancel the timeout if it's still active
if (timeout_data->timeout_id > 0)
g_source_remove(timeout_data->timeout_id);
// Free the data structure
g_free(timeout_data);
}
GtkWidget *create_iface_waiting_main(struct alsa_card *card) {
struct timeout_data *data;
// Main vertical box
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20);
gtk_widget_set_margin_start(box, 40);
gtk_widget_set_margin_end(box, 40);
gtk_widget_set_margin_top(box, 40);
gtk_widget_set_margin_bottom(box, 40);
// Heading
GtkWidget *label = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label),
"<span weight='bold' size='large'>Waiting for FCP Server</span>");
gtk_box_append(GTK_BOX(box), label);
// Add picture (scaled down properly)
GtkWidget *picture_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_hexpand(picture_box, TRUE);
gtk_widget_set_halign(picture_box, GTK_ALIGN_CENTER);
GtkWidget *picture = gtk_picture_new_for_resource(
"/vu/b4/alsa-scarlett-gui/icons/vu.b4.alsa-scarlett-gui.png"
);
gtk_picture_set_can_shrink(GTK_PICTURE(picture), TRUE);
gtk_widget_set_size_request(picture, 128, 128);
gtk_box_append(GTK_BOX(picture_box), picture);
gtk_box_append(GTK_BOX(box), picture_box);
// Add spinner
GtkWidget *spinner = gtk_spinner_new();
gtk_spinner_start(GTK_SPINNER(spinner));
gtk_widget_set_size_request(spinner, 48, 48);
gtk_box_append(GTK_BOX(box), spinner);
// Description
label = gtk_label_new(
"Waiting for the user-space FCP driver to initialise..."
);
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
gtk_label_set_max_width_chars(GTK_LABEL(label), 1);
gtk_widget_set_hexpand(label, TRUE);
gtk_widget_set_halign(label, GTK_ALIGN_FILL);
gtk_box_append(GTK_BOX(box), label);
// Setup timeout
data = g_new(struct timeout_data, 1);
data->box = box;
data->spinner = spinner;
data->message_label = label;
// Set timeout
data->timeout_id = g_timeout_add_seconds(5, on_timeout, data);
// Ensure data is freed when the box is destroyed
g_object_weak_ref(G_OBJECT(box), on_widget_dispose, data);
return box;
}

8
src/iface-waiting.h Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2025 Geoffrey D. Bennett <g@b4.vu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "alsa.h"
GtkWidget *create_iface_waiting_main(struct alsa_card *card);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View 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

View 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

View 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

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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
);

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More