├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ └── issue.md
└── workflows
│ ├── build-debian-package.yml
│ └── build-flatpak-package.yml
├── .gitignore
├── FAQ.md
├── LICENSES
├── GPL-3.0-or-later.txt
└── LGPL-3.0-or-later.txt
├── Makefile
├── README.md
├── alsa-scarlett-gui.spec.template
├── demo
├── Clarett Plus 2Pre.state
├── Clarett Plus 4Pre.state
├── Clarett Plus 8Pre.state
├── Scarlett Gen 1 18i20.state
├── Scarlett Gen 1 18i6.state
├── Scarlett Gen 1 18i8.state
├── Scarlett Gen 1 6i6.state
├── Scarlett Gen 1 8i6.state
├── Scarlett Gen 2 18i20.state
├── Scarlett Gen 2 18i8.state
├── Scarlett Gen 2 6i6.state
├── Scarlett Gen 3 18i20.state
├── Scarlett Gen 3 18i8.state
├── Scarlett Gen 3 2i2.state
├── Scarlett Gen 3 4i4.state
├── Scarlett Gen 3 8i6.state
├── Scarlett Gen 3 Solo.state
├── Scarlett Gen 4 16i16.state
├── Scarlett Gen 4 18i16.state
├── Scarlett Gen 4 18i20.state
├── Scarlett Gen 4 2i2.state
├── Scarlett Gen 4 4i4.state
├── Scarlett Gen 4 Solo.state
├── Vocaster One.state
└── Vocaster Two.state
├── docs
├── INSTALL.md
├── OLDKERNEL.md
├── USAGE.md
├── iface-1st-gen.md
├── iface-4th-gen-big.md
├── iface-4th-gen-small.md
├── iface-large.md
└── iface-small.md
├── img
├── alsa-scarlett-gui.png
├── demo.gif
├── firmware-missing.png
├── firmware-update-required.png
├── firmware-updating.png
├── iface-4th-gen-big.png
├── iface-4th-gen-small.png
├── iface-msd.png
├── iface-none.png
├── iface-small-gen3.png
├── main-global.png
├── main-inputs.png
├── main-outputs.png
├── routing-direct.png
├── scarlett-1st-gen-6i6-routing.png
├── scarlett-4th-gen-16i16-routing.png
├── scarlett-4th-gen-2i2-monitor.gif
├── scarlett-4th-gen-2i2-routing.png
├── scarlett-4th-gen-4i4-routing.png
├── scarlett-4th-gen-solo-mix-e-f.png
├── scarlett-4th-gen-solo-mix.gif
├── scarlett-4th-gen-solo-monitor.gif
├── window-levels-3rd-gen.png
├── window-levels-4th-gen-big.png
├── window-levels-4th-gen-small.gif
├── window-main.png
├── window-mixer.png
├── window-routing.png
└── window-startup.png
├── src
├── Makefile
├── about.c
├── about.h
├── alsa-scarlett-gui-resources.xml
├── alsa-scarlett-gui.css
├── alsa-sim.c
├── alsa-sim.h
├── alsa.c
├── alsa.h
├── const.h
├── db.c
├── db.h
├── device-reset-config.c
├── device-reset-config.h
├── device-update-firmware.c
├── device-update-firmware.h
├── error.c
├── error.h
├── fcp-shared.c
├── fcp-shared.h
├── fcp-socket.c
├── fcp-socket.h
├── file.c
├── file.h
├── gtkdial.c
├── gtkdial.h
├── gtkhelper.c
├── gtkhelper.h
├── hardware.c
├── hardware.h
├── iface-mixer.c
├── iface-mixer.h
├── iface-no-mixer.c
├── iface-no-mixer.h
├── iface-none.c
├── iface-none.h
├── iface-unknown.c
├── iface-unknown.h
├── iface-update.c
├── iface-update.h
├── iface-waiting.c
├── iface-waiting.h
├── img
│ ├── audio-volume-high.svg
│ ├── audio-volume-low.svg
│ ├── audio-volume-medium.svg
│ ├── audio-volume-muted.svg
│ ├── socket.svg
│ ├── vu.b4.alsa-scarlett-gui.png
│ └── vu.b4.alsa-scarlett-gui.svg
├── main.c
├── main.h
├── menu.c
├── menu.h
├── routing-drag-line.c
├── routing-drag-line.h
├── routing-lines.c
├── routing-lines.h
├── scarlett2-firmware.c
├── scarlett2-firmware.h
├── scarlett2-ioctls.c
├── scarlett2-ioctls.h
├── scarlett2.h
├── stringhelper.c
├── stringhelper.h
├── tooltips.c
├── tooltips.h
├── vu.b4.alsa-scarlett-gui.desktop.template
├── widget-boolean.c
├── widget-boolean.h
├── widget-drop-down.c
├── widget-drop-down.h
├── widget-dual.c
├── widget-dual.h
├── widget-gain.c
├── widget-gain.h
├── widget-input-select.c
├── widget-input-select.h
├── widget-label.c
├── widget-label.h
├── widget-sample-rate.c
├── widget-sample-rate.h
├── window-hardware.c
├── window-hardware.h
├── window-helper.c
├── window-helper.h
├── window-iface.c
├── window-iface.h
├── window-levels.c
├── window-levels.h
├── window-mixer.c
├── window-mixer.h
├── window-modal.c
├── window-modal.h
├── window-routing.c
├── window-routing.h
├── window-startup.c
└── window-startup.h
└── vu.b4.alsa-scarlett-gui.yml
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | liberapay: gdb
2 | custom: 'https://www.paypal.me/gdbau'
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue
3 | about: Open an issue for help, to report a bug, or request a feature
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | # `alsa-scarlett-gui` Issue Template
11 |
12 | Thank you for taking the time to contribute to the `alsa-scarlett-gui` project. Before you submit your issue, please ensure you have checked the FAQ and provide the necessary information below.
13 |
14 | ## Confirmation
15 | - [ ] I confirm that I have read the [FAQ](https://github.com/geoffreybennett/alsa-scarlett-gui/blob/master/FAQ.md).
16 |
17 | ## Issue Category
18 | Please select the category that best describes your issue:
19 | - [ ] Help Request
20 | - [ ] Bug Report
21 | - [ ] Feature Request
22 |
23 | ## Environment Details
24 | Please provide the following details about your environment.
25 |
26 | ### Linux Distribution and Version
27 | (paste output from `cat /etc/redhat-release` or `cat /etc/lsb_release` here)
28 | - Distribution:
29 | - Version:
30 |
31 | ### Kernel Version
32 | (paste output from `uname -r` here)
33 | - Kernel version:
34 |
35 | ### Kernel Messages
36 | (paste output from `dmesg | grep -A 5 -B 5 -i focusrite` here)
37 |
38 | ### Focusrite Interface Series and Model
39 | (maybe shown in kernel messages, or paste output from `lsusb -d1235:` if unsure)
40 | - Series (e.g., Scarlett 2nd/3rd/4th Gen, Clarett USB, Clarett+):
41 | - Model (e.g., Solo, 2i2, 4i4, etc.):
42 |
43 | ### Audio System
44 | (use `ps aux | grep -E "pulseaudio|jackd|pipewire"` to check)
45 | - [ ] PulseAudio
46 | - [ ] JACK
47 | - [ ] PipeWire
48 |
49 | ## Issue Description
50 | Please provide a detailed description of the issue or feature request, including steps to reproduce (if applicable), expected behavior, and actual behavior:
51 |
52 | ---
53 |
54 | Thank you for helping improve `alsa-scarlett-gui`!
55 |
--------------------------------------------------------------------------------
/.github/workflows/build-debian-package.yml:
--------------------------------------------------------------------------------
1 | name: Build debian package
2 |
3 | on:
4 | release:
5 | branches: '*'
6 | types: [published]
7 |
8 | env:
9 | APP_NAME: alsa-scarlett-gui
10 | APP_VERSION: ${{ github.event.release.tag_name }}
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-22.04
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Install build dependencies
20 | run: |
21 | sudo apt -y update
22 | sudo apt -y install git make gcc libgtk-4-dev libasound2-dev libssl-dev
23 |
24 | - name: Build from sources
25 | run: |
26 | make -C src -j$(nproc) PREFIX=/usr
27 |
28 | - name: Prepare package workspace
29 | run: |
30 | mkdir -p ${{ github.workspace }}/deb-workspace/usr/bin \
31 | ${{ github.workspace }}/deb-workspace/usr/share/applications \
32 | ${{ github.workspace }}/deb-workspace/usr/share/icons/hicolor/256x256/apps \
33 | ${{ github.workspace }}/deb-workspace/usr/share/doc/${{ env.APP_NAME }}-${{ env.APP_VERSION }}
34 | cp src/alsa-scarlett-gui ${{ github.workspace }}/deb-workspace/usr/bin/
35 | cp src/vu.b4.alsa-scarlett-gui.desktop ${{ github.workspace }}/deb-workspace/usr/share/applications/
36 | cp src/img/vu.b4.alsa-scarlett-gui.png ${{ github.workspace }}/deb-workspace/usr/share/icons/hicolor/256x256/apps/
37 | cp -r *.md demo docs img ${{ github.workspace }}/deb-workspace/usr/share/doc/${{ env.APP_NAME }}-${{ env.APP_VERSION }}/
38 |
39 | - name: Build debian package
40 | uses: jiro4989/build-deb-action@v2
41 | with:
42 | package: ${{ env.APP_NAME }}
43 | package_root: ${{ github.workspace }}/deb-workspace
44 | maintainer: geoffreybennett
45 | depends: 'libgtk-4-1, libasound2, alsa-utils'
46 | version: ${{ env.APP_VERSION }}
47 | desc: ${{ env.APP_NAME }} is a Gtk4 GUI for the ALSA controls presented by the Linux kernel Focusrite USB drivers.
48 |
49 | - name: Upload Release Asset
50 | uses: actions/upload-release-asset@v1
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 | with:
54 | upload_url: ${{ github.event.release.upload_url }}
55 | asset_path: ./${{ env.APP_NAME }}_${{ env.APP_VERSION }}_amd64.deb
56 | asset_name: ${{ env.APP_NAME }}_${{ env.APP_VERSION }}_amd64.deb
57 | asset_content_type: application/vnd.debian.binary-package
58 |
--------------------------------------------------------------------------------
/.github/workflows/build-flatpak-package.yml:
--------------------------------------------------------------------------------
1 | name: Build flatpak package
2 |
3 | on:
4 | release:
5 | branches: '*'
6 | types: [published]
7 |
8 | env:
9 | APP_NAME: alsa-scarlett-gui
10 | APP_VERSION: ${{ github.event.release.tag_name }}
11 |
12 | jobs:
13 | flatpak:
14 | name: "Flatpak"
15 | runs-on: ubuntu-latest
16 | container:
17 | image: bilelmoussaoui/flatpak-github-actions:gnome-47
18 | options: --privileged
19 | steps:
20 | - uses: actions/checkout@v4
21 |
22 | - name: Build flatpak package
23 | uses: flatpak/flatpak-github-actions/flatpak-builder@v6
24 | with:
25 | bundle: ${{ env.APP_NAME }}.flatpak
26 | manifest-path: vu.b4.alsa-scarlett-gui.yml
27 | cache-key: flatpak-builder-${{ github.sha }}
28 |
29 | - name: Upload Release Asset
30 | uses: actions/upload-release-asset@v1
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 | with:
34 | upload_url: ${{ github.event.release.upload_url }}
35 | asset_path: ./${{ env.APP_NAME }}.flatpak
36 | asset_name: ${{ env.APP_NAME }}_${{ env.APP_VERSION }}.flatpak
37 | asset_content_type: application/octet-stream
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.tar.gz
2 | *.o
3 | .deps
4 | .gdb_history
5 | alsa-scarlett-gui
6 | alsa-scarlett-gui-resources.c
7 | vu.b4.alsa-scarlett-gui.desktop
8 | .flatpak-builder/
9 | flatpak-build/
10 |
--------------------------------------------------------------------------------
/FAQ.md:
--------------------------------------------------------------------------------
1 | # FAQ for the ALSA Scarlett Control Panel (`alsa-scarlett-gui`)
2 |
3 | ## What is this?
4 |
5 | The ALSA Scarlett Control Panel (`alsa-scarlett-gui`) is an
6 | easy-to-use application for adjusting the ALSA controls provided by
7 | three Linux kernel drivers for Focusrite USB interfaces:
8 |
9 | 1. The Scarlett 1st Gen Mixer Driver (for 1st Gen 6i6, 8i6, 18i6, 18i8, 18i20)
10 | 2. The Scarlett2 Protocol Driver (for 2nd/3rd Gen interfaces, small 4th Gen, Clarett, and Vocaster)
11 | 3. The FCP (Focusrite Control Protocol) Driver (for big 4th Gen interfaces: 16i16, 18i16, 18i20)
12 |
13 | To check if your kernel is already up-to-date, and how to upgrade if
14 | not, see the [Control Panel Installation Prerequisites — Linux
15 | Kernel](docs/INSTALL.md).
16 |
17 | ## Do I need these drivers for my Focusrite interface?
18 |
19 | For basic audio functionality? No. Focusrite USB interfaces are
20 | “plug-and-play” — they are USB Audio Class Compliant, meaning they
21 | work out-of-the-box with the standard ALSA USB audio driver (to get
22 | full functionality on Scarlett 3rd/4th Gen/Vocaster interfaces, first
23 | deactivate MSD mode by holding down the 48V button while powering it
24 | on).
25 |
26 | However, to access the mixer, routing, and hardware-specific features,
27 | you’ll need the appropriate driver for your interface model.
28 |
29 | ## MSD Mode?
30 |
31 | "MSD Mode" is the "Mass Storage Device Mode" that the Scarlett 3rd and
32 | 4th Gen interfaces ship in.
33 |
34 | If MSD Mode is enabled, you need to disable it and restart your
35 | interface to get access to its full functionality.
36 |
37 | When you plug the interface in, there’ll be a tiny read-only virtual
38 | disk that has a link to the Focusrite product registration page; until
39 | you turn off MSD Mode not all features of the interface will be
40 | available.
41 |
42 | You can turn off MSD Mode by holding down the 48V button while
43 | powering on the interface, or by clicking the button in
44 | `alsa-scarlett-gui` and rebooting it.
45 |
46 | If you do the recommended/required (depending on the model) firmware
47 | update, MSD Mode will automatically be turned off.
48 |
49 | ## What is the purpose of these drivers if they’re not needed for basic audio?
50 |
51 | These drivers are for users who want more control over their
52 | interface. They allow for detailed manipulation of:
53 |
54 | - Internal audio routing
55 | - Hardware-specific settings
56 | - Mixer functionality
57 | - Level monitoring
58 | - Input/output configuration
59 |
60 | These controls go beyond the basic audio I/O functionality provided by
61 | the generic ALSA USB audio driver.
62 |
63 | ## What interfaces are supported?
64 |
65 | The ALSA Scarlett Control Panel supports:
66 |
67 | - **Scarlett 1st Gen**: 6i6, 8i6, 18i6, 18i8, 18i20
68 | - **Scarlett 2nd Gen**: 6i6, 18i8, 18i20
69 | - **Scarlett 3rd Gen**: Solo, 2i2, 4i4, 8i6, 18i8, 18i20
70 | - **Scarlett 4th Gen**: Solo, 2i2, 4i4, 16i16, 18i16, 18i20
71 | - **Clarett USB and Clarett+**: 2Pre, 4Pre, 8Pre
72 | - **Vocaster**: One, Two
73 |
74 | Note: The Scarlett 1st and 2nd Gen small interfaces (Solo, 2i2, 2i4)
75 | don’t have any software controls. All the controls are available from
76 | the front panel, so they don’t require the specialised drivers or this
77 | GUI.
78 |
79 | ## Where are the options to set the sample rate and buffer size?
80 |
81 | The ALSA Scarlett Control Panel doesn’t handle audio input/output
82 | settings like sample rate and buffer size. These settings are managed
83 | by the application using the soundcard, typically a sound server such
84 | as PulseAudio, JACK, or PipeWire.
85 |
86 | The sample rate shown in the control panel is informative only and
87 | displays the current rate being used by applications. If it shows
88 | “N/A” then no application is using the interface.
89 |
90 | Note that not all features are available at higher sample rates; refer
91 | to the user manual of your interface for more information.
92 |
93 | ## Why do my settings keep resetting?
94 |
95 | The settings in the ALSA Scarlett Control Panel are automatically
96 | saved in the interface itself (all series except 1st Gen), so they
97 | should persist across reboots, power cycles, USB disconnect/reconnect,
98 | and even across different computers. This includes all routing,
99 | mixing, and other control panel settings.
100 |
101 | If you find that your settings are reverting whenever you plug your
102 | interface in, power it back on, or even if you reset to factory
103 | defaults, the most likely cause is the `alsa-state` and `alsa-restore`
104 | systemd services. These services save the state of ALSA controls on
105 | system shutdown to `/var/lib/alsa/asound.state` and then restore it
106 | each time the device is plugged in, potentially overwriting your
107 | interface’s stored settings.
108 |
109 | It can be rather annoying, wondering why your device is unusable or
110 | needs to be reconfigured every time you plug it in or turn it on.
111 |
112 | Presuming that you have no other sound card that needs this ALSA
113 | service, then disable and stop these two services and remove the
114 | `asound.state` file:
115 |
116 | ```sh
117 | sudo systemctl mask alsa-state
118 | sudo systemctl mask alsa-restore
119 | sudo systemctl stop alsa-state
120 | sudo systemctl stop alsa-restore
121 | sudo rm /var/lib/alsa/asound.state
122 | ```
123 |
124 | You can verify if this is the cause of your issues by:
125 |
126 | 1. Change some setting that is indicated on the device (the “Inst”
127 | setting is a good one to test with).
128 | 2. Disconnect USB and notice the state of the setting on the device
129 | has not changed.
130 | 3. Power cycle the device and notice the state of the setting on the
131 | device has not changed.
132 | 4. Reconnect USB and notice the state of the setting on the device has
133 | changed.
134 |
135 | If the setting on the device changes at step 4, then the `alsa-state`
136 | and `alsa-restore` services are the likely cause of your issues and
137 | you should disable them as above.
138 |
139 | ## Help?!
140 |
141 | Have you read the User Guide for your interface? It’s available
142 | online: https://downloads.focusrite.com/focusrite and contains a lot
143 | of helpful/useful/important information about your device.
144 |
145 | You can skip the “Easy Start” and “Setting up your DAW” sections, but
146 | the rest is well worth reading. Even the information about Focusrite
147 | Control is useful, although not directly applicable, because it will
148 | help you understand more about the possibilities of what you can do
149 | with your device.
150 |
151 | For help with the Scarlett2 and FCP kernel drivers:
152 | https://github.com/geoffreybennett/linux-fcp/issues
153 |
154 | For help with the FCP user-space side:
155 | https://github.com/geoffreybennett/fcp-support/issues
156 |
157 | For help with `alsa-scarlett-gui`:
158 | https://github.com/geoffreybennett/alsa-scarlett-gui/issues
159 |
160 | For general Linux audio help: https://linuxmusicians.com
161 |
--------------------------------------------------------------------------------
/LICENSES/LGPL-3.0-or-later.txt:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 |
6 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
7 |
8 | This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below.
9 |
10 | 0. Additional Definitions.
11 |
12 | As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License.
13 |
14 | "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below.
15 |
16 | An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library.
17 |
18 | A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version".
19 |
20 | The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version.
21 |
22 | The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work.
23 |
24 | 1. Exception to Section 3 of the GNU GPL.
25 | You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL.
26 |
27 | 2. Conveying Modified Versions.
28 | If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version:
29 |
30 | a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or
31 |
32 | b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy.
33 |
34 | 3. Object Code Incorporating Material from Library Header Files.
35 | The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following:
36 |
37 | a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License.
38 |
39 | b) Accompany the object code with a copy of the GNU GPL and this license document.
40 |
41 | 4. Combined Works.
42 | You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following:
43 |
44 | a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License.
45 |
46 | b) Accompany the Combined Work with a copy of the GNU GPL and this license document.
47 |
48 | c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document.
49 |
50 | d) Do one of the following:
51 |
52 | 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.
53 |
54 | 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version.
55 |
56 | e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.)
57 |
58 | 5. Combined Libraries.
59 | You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following:
60 |
61 | a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License.
62 |
63 | b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
64 |
65 | 6. Revised Versions of the GNU Lesser General Public License.
66 | The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
67 |
68 | Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation.
69 |
70 | If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall
71 | apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.
72 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NAME := alsa-scarlett-gui
2 | VERSION := $(shell git describe --abbrev=4 --always --tags | sed 's/-/./g')
3 | NAMEVER := $(NAME)-$(VERSION)
4 | TAR_FILE := $(NAMEVER).tar
5 | TARGZ_FILE := $(TAR_FILE).gz
6 | SPEC_FILE := $(NAME).spec
7 |
8 | default:
9 | @echo "alsa-scarlett-gui"
10 | @echo
11 | @echo "If you want to build and install from source, please try:"
12 | @echo " cd src"
13 | @echo " make -j$(shell nproc)"
14 | @echo " sudo make install"
15 | @echo
16 | @echo "This Makefile knows about packaging:"
17 | @echo " make tar"
18 | @echo " make rpm"
19 |
20 | tar: $(TARGZ_FILE)
21 |
22 | $(TARGZ_FILE):
23 | git archive --format=tar --prefix=$(NAMEVER)/ HEAD > $(TAR_FILE)
24 | sed 's_VERSION$$_$(VERSION)_' < $(SPEC_FILE).template > $(SPEC_FILE)
25 | tar --append -f $(TAR_FILE) \
26 | --transform s_^_$(NAMEVER)/_ \
27 | --owner=root --group=root \
28 | $(SPEC_FILE)
29 | rm -f $(SPEC_FILE)
30 | gzip < $(TAR_FILE) > $(TARGZ_FILE)
31 | rm -f $(TAR_FILE)
32 |
33 | rpm: $(TARGZ_FILE)
34 | rpmbuild -tb $(TARGZ_FILE)
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ALSA Scarlett Control Panel (`alsa-scarlett-gui`)
2 |
3 | `alsa-scarlett-gui` is a Gtk4 GUI for the ALSA controls presented by
4 | the three Linux kernel Focusrite USB drivers:
5 |
6 | - Scarlett 1st Gen Driver for ALSA
7 | - Scarlett2 USB Protocol Mixer Driver
8 | - FCP (Focusrite Control Protocol) Driver
9 |
10 | Supported interfaces:
11 | - Scarlett 1st Gen 6i6, 8i6, 18i6, 18i8, 18i20
12 | - Scarlett 2nd Gen 6i6, 18i8, 18i20
13 | - Scarlett 3rd Gen Solo, 2i2, 4i4, 8i6, 18i8, 18i20
14 | - Scarlett 4th Gen Solo, 2i2, 4i4, 16i16, 18i16, 18i20
15 | - Clarett 2Pre, 4Pre, 8Pre USB
16 | - Clarett+ 2Pre, 4Pre, 8Pre
17 | - Vocaster One and Vocaster Two
18 |
19 | ## About
20 |
21 |
22 |
23 | All Focusrite USB audio interfaces are class compliant meaning that
24 | they work “out of the box” on Linux as audio and MIDI interfaces
25 | (although on Gen 3/4/Vocaster you need to disable MSD mode first for
26 | full functionality). However, except for some of the smallest models,
27 | they have a bunch of proprietary functionality that required a kernel
28 | driver to be written specifically for those devices.
29 |
30 | Unfortunately, actually using this functionality used to be quite an
31 | awful experience. The existing applications like `alsamixer` and
32 | `qasmixer` become completely user-hostile with the hundreds of
33 | controls presented for the Gen 3 18i20. Even the smallest Gen 3 4i4
34 | interface at last count had 84 ALSA controls.
35 |
36 | Announcing the ALSA Scarlett Control Panel, now supporting all
37 | Scarlett Gen 1, 2, 3, 4, Clarett, and Vocaster USB interfaces!
38 |
39 | 
40 |
41 | ## Documentation
42 |
43 | Refer to [INSTALL.md](docs/INSTALL.md) for prerequisites, how to
44 | build, install, and run.
45 |
46 | Refer to [USAGE.md](docs/USAGE.md) for general usage information and
47 | known issues.
48 |
49 | Information specific to various models:
50 |
51 | - [Scarlett 1st Gen](docs/iface-1st-gen.md)
52 |
53 | - [Scarlett 3rd Gen Solo and 2i2](docs/iface-small.md)
54 |
55 | - [Scarlett 2nd Gen 6i6+, 3rd Gen 4i4+, Clarett USB, and
56 | Clarett+](docs/iface-large.md)
57 |
58 | - [Scarlett Small 4th Gen](docs/iface-4th-gen-small.md)
59 |
60 | - [Scarlett Big 4th Gen](docs/iface-4th-gen-big.md)
61 |
62 | ## Donations
63 |
64 | This program is Free Software, developed using my personal resources,
65 | over hundreds of hours.
66 |
67 | If you like this software, please consider a donation to say thank
68 | you! Any donation is appreciated.
69 |
70 | - https://liberapay.com/gdb
71 | - https://paypal.me/gdbau
72 |
73 | ## License
74 |
75 | Copyright 2022-2025 Geoffrey D. Bennett
76 |
77 | This program is free software: you can redistribute it and/or modify
78 | it under the terms of the GNU General Public License as published by
79 | the Free Software Foundation, either version 3 of the License, or (at
80 | your option) any later version.
81 |
82 | This program is distributed in the hope that it will be useful, but
83 | WITHOUT ANY WARRANTY; without even the implied warranty of
84 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
85 | General Public License for more details.
86 |
87 | You should have received a copy of the GNU General Public License
88 | along with this program. If not, see .
89 |
90 | ## Disclaimer Third Parties
91 |
92 | Focusrite, Scarlett, Clarett, and Vocaster are trademarks or
93 | registered trademarks of Focusrite Audio Engineering Limited in
94 | England, USA, and/or other countries. Use of these trademarks does not
95 | imply any affiliation or endorsement of this software.
96 |
--------------------------------------------------------------------------------
/alsa-scarlett-gui.spec.template:
--------------------------------------------------------------------------------
1 | Summary: ALSA Scarlett Control Panel
2 | Name: alsa-scarlett-gui
3 | Version: VERSION
4 | Release: 1%{?dist}
5 | License: GPLv3+ LGPLv3+
6 | Url: https://github.com/geoffreybennett/alsa-scarlett-gui
7 | Source0: https://github.com/geoffreybennett/alsa-scarlett-gui/archive/refs/tags/%{version}.tar.gz?/%{name}-%{version}.tar.gz
8 | BuildRequires: pkgconfig(alsa)
9 | BuildRequires: pkgconfig(gtk4)
10 | BuildRequires: pkgconfig(openssl)
11 |
12 | %description
13 | alsa-scarlett-gui is a Gtk4 GUI for the ALSA controls presented by the
14 | Linux kernel Focusrite USB drivers.
15 |
16 | %prep
17 | %setup -q -n %{name}-%{version}/src
18 |
19 | %build
20 | %make_build VERSION=%{version} PREFIX=%{_prefix}
21 |
22 | %install
23 | %make_install PREFIX=%{_prefix}
24 |
25 | %files
26 | %doc ../img ../demo ../docs ../*.md
27 | %{_bindir}/alsa-scarlett-gui
28 | %{_datadir}/applications/vu.b4.alsa-scarlett-gui.desktop
29 | %{_iconsdir}/hicolor/256x256/apps/vu.b4.alsa-scarlett-gui.png
30 |
--------------------------------------------------------------------------------
/demo/Scarlett Gen 3 2i2.state:
--------------------------------------------------------------------------------
1 | state.USB {
2 | control.1 {
3 | iface PCM
4 | name 'Playback Channel Map'
5 | value.0 0
6 | value.1 0
7 | comment {
8 | access read
9 | type INTEGER
10 | count 2
11 | range '0 - 36'
12 | }
13 | }
14 | control.2 {
15 | iface PCM
16 | name 'Capture Channel Map'
17 | value.0 0
18 | value.1 0
19 | comment {
20 | access read
21 | type INTEGER
22 | count 2
23 | range '0 - 36'
24 | }
25 | }
26 | control.3 {
27 | iface CARD
28 | name 'USB Internal Validity'
29 | value true
30 | comment {
31 | access read
32 | type BOOLEAN
33 | count 1
34 | }
35 | }
36 | control.4 {
37 | iface MIXER
38 | name 'Line In 1 Level Capture Enum'
39 | value Line
40 | comment {
41 | access 'read write'
42 | type ENUMERATED
43 | count 1
44 | item.0 Line
45 | item.1 Inst
46 | }
47 | }
48 | control.5 {
49 | iface MIXER
50 | name 'Line In 2 Level Capture Enum'
51 | value Line
52 | comment {
53 | access 'read write'
54 | type ENUMERATED
55 | count 1
56 | item.0 Line
57 | item.1 Inst
58 | }
59 | }
60 | control.6 {
61 | iface MIXER
62 | name 'Line In 1 Air Capture Switch'
63 | value false
64 | comment {
65 | access 'read write'
66 | type BOOLEAN
67 | count 1
68 | }
69 | }
70 | control.7 {
71 | iface MIXER
72 | name 'Line In 2 Air Capture Switch'
73 | value false
74 | comment {
75 | access 'read write'
76 | type BOOLEAN
77 | count 1
78 | }
79 | }
80 | control.8 {
81 | iface MIXER
82 | name 'Line In 1-2 Phantom Power Capture Switch'
83 | value true
84 | comment {
85 | access 'read write'
86 | type BOOLEAN
87 | count 1
88 | }
89 | }
90 | control.9 {
91 | iface MIXER
92 | name 'Phantom Power Persistence Capture Switch'
93 | value true
94 | comment {
95 | access 'read write'
96 | type BOOLEAN
97 | count 1
98 | }
99 | }
100 | control.10 {
101 | iface MIXER
102 | name 'Direct Monitor Playback Enum'
103 | value Mono
104 | comment {
105 | access 'read write'
106 | type ENUMERATED
107 | count 1
108 | item.0 Off
109 | item.1 Mono
110 | item.2 Stereo
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/demo/Scarlett Gen 3 Solo.state:
--------------------------------------------------------------------------------
1 | state.USB {
2 | control.1 {
3 | iface PCM
4 | name 'Playback Channel Map'
5 | value.0 0
6 | value.1 0
7 | comment {
8 | access read
9 | type INTEGER
10 | count 2
11 | range '0 - 36'
12 | }
13 | }
14 | control.2 {
15 | iface PCM
16 | name 'Capture Channel Map'
17 | value.0 0
18 | value.1 0
19 | comment {
20 | access read
21 | type INTEGER
22 | count 2
23 | range '0 - 36'
24 | }
25 | }
26 | control.3 {
27 | iface CARD
28 | name 'USB Internal Validity'
29 | value true
30 | comment {
31 | access read
32 | type BOOLEAN
33 | count 1
34 | }
35 | }
36 | control.4 {
37 | iface MIXER
38 | name 'Line In 2 Level Capture Enum'
39 | value Line
40 | comment {
41 | access 'read write'
42 | type ENUMERATED
43 | count 1
44 | item.0 Line
45 | item.1 Inst
46 | }
47 | }
48 | control.5 {
49 | iface MIXER
50 | name 'Line In 1 Air Capture Switch'
51 | value false
52 | comment {
53 | access 'read write'
54 | type BOOLEAN
55 | count 1
56 | }
57 | }
58 | control.6 {
59 | iface MIXER
60 | name 'Line In 1 Phantom Power Capture Switch'
61 | value true
62 | comment {
63 | access 'read write'
64 | type BOOLEAN
65 | count 1
66 | }
67 | }
68 | control.7 {
69 | iface MIXER
70 | name 'Phantom Power Persistence Capture Switch'
71 | value true
72 | comment {
73 | access 'read write'
74 | type BOOLEAN
75 | count 1
76 | }
77 | }
78 | control.8 {
79 | iface MIXER
80 | name 'Direct Monitor Playback Switch'
81 | value true
82 | comment {
83 | access 'read write'
84 | type BOOLEAN
85 | count 1
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/docs/INSTALL.md:
--------------------------------------------------------------------------------
1 | # ALSA Scarlett Control Panel Installation
2 |
3 | ## Prerequisites
4 |
5 | ### Linux Kernel
6 |
7 | You need to be running a Linux Kernel that contains the appropriate
8 | driver for your interface. Use `uname -r` to check what kernel version
9 | you are running.
10 |
11 | Check the following table to see which driver your interface uses and
12 | the first kernel version that the driver was included in:
13 |
14 | | Series | Models | Driver | Kernel Version |
15 | |-----------|--------|--------|:----------------------:|
16 | | Scarlett 1st Gen | Solo, 2i2, 2i4 | N/A* | Any |
17 | | Scarlett 1st Gen | 6i6, 8i6, 18i6, 18i8, 18i20 | Scarlett 1st Gen Mixer Driver | 3.19+ |
18 | | Scarlett 2nd Gen | Solo, 2i2, 2i4 | N/A* | Any |
19 | | Scarlett 2nd Gen | 6i6, 18i8, 18i20 | Scarlett2 Mixer Driver | 6.7+ |
20 | | Scarlett 3rd Gen | Solo, 2i2, 4i4, 8i6, 18i8, 18i20 | Scarlett2 Mixer Driver | 6.7+ |
21 | | Scarlett 4th Gen | Solo, 2i2, 4i4 | Scarlett2 Mixer Driver | 6.8+ |
22 | | Scarlett 4th Gen | 16i16, 18i16, 18i20 | FCP (Focusrite Control Protocol) Driver | 6.14+ |
23 | | Clarett USB and Clarett+ | 2Pre, 4Pre, 8Pre | Scarlett2 Mixer Driver | 6.7+ |
24 | | Vocaster | One, Two | Scarlett2 Mixer Driver | 6.10+ |
25 |
26 | \* The small 1st Gen and 2nd Gen models don’t have any proprietary
27 | software controls so they don’t need a driver beyond the standard ALSA
28 | USB Audio driver. This means that this application (alsa-scarlett-gui)
29 | is not needed, useful, or supported for these models.
30 |
31 | If your distribution doesn’t include a recent-enough kernel for your
32 | interface, you can get the latest driver from here and build it for
33 | your current kernel, if it’s not too old (the Scarlett2 and FCP
34 | drivers are both maintained in the same tree here):
35 | https://github.com/geoffreybennett/linux-fcp/releases
36 |
37 | Kernel 6.7 and later have the Scarlett2 driver enabled by default. The
38 | Scarlett 1st Gen driver and the FCP drivers are always enabled.
39 |
40 | #### Enabling the Scarlett2 Driver
41 |
42 | Some kernels before 6.7 have an earlier version of the Scarlett2
43 | driver which is disabled by default. If this is you, check the driver
44 | status (after plugging your interface in) with this command:
45 |
46 | ```
47 | dmesg | grep -i -A 5 -B 5 focusrite
48 | ```
49 |
50 | If all is good you’ll see messages like this:
51 |
52 | ```
53 | New USB device found, idVendor=1235, idProduct=8215, bcdDevice= 6.0b
54 | Product: Scarlett 18i20 USB
55 | Focusrite Scarlett Gen 3 Mixer Driver enabled (pid=0x8215); ...
56 | ```
57 |
58 | If you don’t see the “Mixer Driver” message or if it shows “disabled”
59 | then check the [OLDKERNEL.md](OLDKERNEL.md) instructions (or,
60 | preferably, upgrade your distro/kernel!).
61 |
62 | ### FCP User-Space Driver
63 |
64 | The FCP kernel driver requires the user-space driver `fcp-server` in
65 | order to do anything useful. This is available from the
66 | [fcp-support](https://github.com/geoffreybennett/fcp-support) repo.
67 |
68 | ### Gtk4
69 |
70 | You need a Linux distribution with Gtk4 development libraries. If it
71 | doesn’t have them natively, try the Flatpak instructions below.
72 |
73 | ### Firmware
74 |
75 | #### Scarlett2 Driver
76 |
77 | As of Linux 6.8, firmware updates of all supported interfaces from the
78 | 2nd Gen onwards can be done through Linux. This is mandatory for
79 | Scarlett 4th Gen and Vocaster interfaces (unless you’ve already
80 | updated it using the manufacturer’s software), and optional but
81 | recommended for Scarlett 2nd and 3rd Gen, Clarett USB, and Clarett+
82 | interfaces.
83 |
84 | Download the firmware from
85 | https://github.com/geoffreybennett/scarlett2-firmware and place it in
86 | `/usr/lib/firmware/scarlett2` or use the RPM/deb package.
87 |
88 | #### FCP Driver
89 |
90 | Firmware updates for the big Scarlett 4th Gen interfaces are currently
91 | only possible through the CLI `fcp-tool` utility available in the
92 | [fcp-support](https://github.com/geoffreybennett/fcp-support) repo.
93 | Updating the firmware is mandatory for these interfaces.
94 |
95 | Download the firmware from
96 | https://github.com/geoffreybennett/scarlett4-firmware and place it in
97 | `/usr/lib/firmware/scarlett4` or use the RPM/deb package.
98 |
99 | ## Building and Running
100 |
101 | On Fedora, these packages need to be installed:
102 |
103 | ```
104 | sudo dnf -y install alsa-lib-devel gtk4-devel openssl-devel
105 | ```
106 |
107 | On OpenSUSE:
108 |
109 | ```
110 | sudo zypper in git alsa-devel gtk4-devel libopenssl-devel
111 | ```
112 |
113 | On Ubuntu:
114 |
115 | ```
116 | sudo apt -y install git make gcc libgtk-4-dev libasound2-dev libssl-dev
117 | ```
118 |
119 | On Arch:
120 |
121 | ```
122 | sudo pacman -S gtk4
123 | ```
124 |
125 | To download from github:
126 |
127 | ```
128 | git clone https://github.com/geoffreybennett/alsa-scarlett-gui
129 | cd alsa-scarlett-gui
130 | ```
131 |
132 | To build:
133 |
134 | ```
135 | cd src
136 | make -j$(nproc)
137 | ```
138 |
139 | To run:
140 |
141 | ```
142 | ./alsa-scarlett-gui
143 | ```
144 |
145 | You can install it into `/usr/local` (binary, desktop file, and icon)
146 | with:
147 |
148 | ```
149 | sudo make install
150 | ```
151 |
152 | And uninstall with:
153 |
154 | ```
155 | sudo make uninstall
156 | ```
157 |
158 | Continue on to reading [USAGE.md](USAGE.md) for how to use the GUI.
159 |
160 | ## Flatpak
161 |
162 | With Flatpak, in any distro:
163 |
164 | ```
165 | flatpak-builder --user --install --force-clean flatpak-build \
166 | vu.b4.alsa-scarlett-gui.yml
167 | ```
168 |
169 | Be sure to use `flatpak-build` as the directory where the flatpak is
170 | built or hence you risk bundling the artifacts when committing!
171 |
172 | If you get messages like these:
173 |
174 | ```
175 | Failed to init: Unable to find sdk org.gnome.Sdk version 45
176 | Failed to init: Unable to find runtime org.gnome.Platform version 45
177 | ```
178 |
179 | Then install them:
180 |
181 | ```
182 | flatpak install org.gnome.Sdk
183 | flatpak install org.gnome.Platform
184 | ```
185 |
186 | If you get:
187 |
188 | ```
189 | Looking for matches…
190 | error: No remote refs found for ‘org.gnome.Sdk’
191 | ```
192 |
193 | Then:
194 |
195 | ```
196 | flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
197 | ```
198 |
--------------------------------------------------------------------------------
/docs/OLDKERNEL.md:
--------------------------------------------------------------------------------
1 | # ALSA Scarlett2 Usage With Old Kernels
2 |
3 | **This information is mostly for historical purposes. If you’re
4 | running a kernel before 6.7, you should upgrade to a newer kernel.**
5 |
6 | Linux kernel 6.7 (check your version with `uname -r`) was the first
7 | kernel version with the Scarlett2 driver enabled by default. It’s
8 | recommended that you run 6.7 or later, or build the backported driver
9 | for your kernel. If you do, then these instructions aren’t relevant;
10 | continue with [INSTALL.md](INSTALL.md) for prerequisites, how to
11 | build, install, and run `alsa-scarlett-gui`.
12 |
13 | If you’ve got a Scarlett Gen 2 or 3 or a Clarett+ 8Pre and don’t mind
14 | the level meters not working, then the first kernel support was added
15 | in:
16 |
17 | - **Scarlett Gen 2**: Linux 5.4 (bugs fixed in Linux 5.14)
18 | - **Scarlett Gen 3**: Linux 5.14
19 | - **Clarett+ 8Pre**: Linux 6.1
20 |
21 | ## Linux Kernel with Backported Driver (recommended)
22 |
23 | Install the latest version of the backported driver from here:
24 |
25 | https://github.com/geoffreybennett/linux-fcp/releases
26 |
27 | then you can ignore the instructions below.
28 |
29 | ## Linux Kernel before 6.7 without Backported Driver
30 |
31 | If you’re running a kernel before 6.7 without the backported driver,
32 | you need to enable it at module load time with the `device_setup=1`
33 | option to insmod/modprobe. Create a file
34 | `/etc/modprobe.d/scarlett.conf` containing the appropriate line for
35 | your device:
36 |
37 | Scarlett Gen 2:
38 |
39 | - **6i6**: `options snd_usb_audio vid=0x1235 pid=0x8203 device_setup=1`
40 | - **18i8**: `options snd_usb_audio vid=0x1235 pid=0x8204 device_setup=1`
41 | - **18i20**: `options snd_usb_audio vid=0x1235 pid=0x8201 device_setup=1`
42 |
43 | Scarlett Gen 3:
44 |
45 | - **Solo**: `options snd_usb_audio vid=0x1235 pid=0x8211 device_setup=1`
46 | - **2i2**: `options snd_usb_audio vid=0x1235 pid=0x8210 device_setup=1`
47 | - **4i4**: `options snd_usb_audio vid=0x1235 pid=0x8212 device_setup=1`
48 | - **8i6**: `options snd_usb_audio vid=0x1235 pid=0x8213 device_setup=1`
49 | - **18i8**: `options snd_usb_audio vid=0x1235 pid=0x8214 device_setup=1`
50 | - **18i20**: `options snd_usb_audio vid=0x1235 pid=0x8215 device_setup=1`
51 |
52 | Clarett+:
53 |
54 | - **8Pre**: `options snd_usb_audio vid=0x1235 pid=0x820c device_setup=1`
55 |
56 | Or you can use a sledgehammer:
57 | ```
58 | options snd_usb_audio device_setup=1,1,1,1
59 | ```
60 | to pass that option to the first 4 USB audio devices.
61 |
62 | To see if the driver is present and enabled: `dmesg | grep -i -A 5 -B
63 | 5 focusrite` should display information like:
64 |
65 | ```
66 | New USB device found, idVendor=1235, idProduct=8215, bcdDevice= 6.0b
67 | Product: Scarlett 18i20 USB
68 | Focusrite Scarlett Gen 2/3 Mixer Driver enabled pid=0x8215
69 | ```
70 |
71 | If the driver is disabled you’ll see a message like:
72 |
73 | ```
74 | Focusrite Scarlett Gen 2/3 Mixer Driver disabled; use options
75 | snd_usb_audio vid=0x1235 pid=0x8215 device_setup=1 to enable and
76 | report any issues to g@b4.vu
77 | ```
78 |
--------------------------------------------------------------------------------
/docs/USAGE.md:
--------------------------------------------------------------------------------
1 | # ALSA Scarlett Control Panel Usage
2 |
3 | Refer to [INSTALL.md](INSTALL.md) for prerequisites, how to build,
4 | install, and run.
5 |
6 | ## No interface connected
7 |
8 | If no interface is detected (usually because there isn’t one
9 | connected!) you’ll see this window:
10 |
11 | 
12 |
13 | Plug in an interface or select the menu option File → Interface
14 | Simulation and load a demo file to make more interesting things
15 | happen.
16 |
17 | ## First Time Usage
18 |
19 | If your interface is fresh out of the box (or you haven’t updated it
20 | using the manufacturer’s software), you may need to update the
21 | firmware and/or disable MSD Mode first.
22 |
23 | ### Firmware Update Required
24 |
25 | Some interfaces require a firmware update before all their
26 | functionality is available. If the firmware is not available on your
27 | system, you’ll see this window:
28 |
29 | 
31 |
32 | In this case, click on the link, download and install the firmware
33 | package, then restart `alsa-scarlett-gui`.
34 |
35 | If a firmware update is required and the firmware is available, you’ll
36 | see this window:
37 |
38 | 
39 |
40 | Click “Update”, then “Yes” to update the firmware.
41 |
42 | 
43 |
44 | The update will take about 15 seconds, and then your interface will
45 | restart, showing the main window.
46 |
47 | ### MSD (Mass Storage Device/Quick Start/Easy Start) Mode
48 |
49 | If MSD Mode is enabled (as it is from the factory) and a firmware
50 | update is not available or required, then you’ll see this window:
51 |
52 | 
53 |
54 | Click the “Enabled” button to disable MSD Mode, then click “Reboot” to
55 | restart the interface, and in a moment the main window will appear.
56 |
57 | ## Startup Controls
58 |
59 | The View → Startup menu option opens a window to configure settings
60 | that only take effect when the interface is powered on.
61 |
62 | The options common to most interfaces are:
63 |
64 | - **Reset Configuration**: this will reset the configuration to the
65 | factory defaults. This is particularly useful with the 4th Gen and
66 | Vocaster interfaces if you’ve made a mess of the configuration and
67 | want to start again.
68 |
69 | - **Update Firmware**: if a firmware update is found in the
70 | `/usr/share/firmware/scarlett2` directory, then an option to update
71 | the firmware will be available here.
72 |
73 | ## File Menu
74 |
75 | The File menu contains options to load and save the configuration,
76 | load a configuration in simulation mode, and to exit the application.
77 |
78 | ### Load/Save Configuration
79 |
80 | The entire state of the interface can be loaded and saved using the
81 | File → Load Configuration and File → Save Configuration menu options.
82 |
83 | Internally, this uses `alsactl`:
84 |
85 | - **Load**: `alsactl restore USB -f `
86 | - **Save**: `alsactl store USB -f `
87 |
88 | The saved state files can be used to simulate an interface if you
89 | don’t have one attached. The `demo` directory in the distribution
90 | contains a sample file for every supported model.
91 |
92 | ### Interface Simulation Mode
93 |
94 | The GUI can load an `alsactl` state file saved from a real interface
95 | and display a GUI as if the corresponding interface was connected.
96 |
97 | This is useful if you don’t have an interface connected and want to
98 | try, develop, or debug the GUI.
99 |
100 | Either specify the `.state` filename on the command line or select the
101 | menu option File → Interface Simulation to load.
102 |
103 | ## Interface Controls
104 |
105 | The controls and menu items which are available vary widely, depending
106 | on your specific interface.
107 |
108 | There are five broad categories of interfaces with different
109 | capabilities; each category of interface is described in a separate
110 | document:
111 |
112 | - [Scarlett 1st Gen 6i6+](iface-1st-gen.md)
113 |
114 | Full routing and mixing capabilities, but some significant caveats.
115 |
116 | - [Scarlett 3rd Gen Solo and 2i2](iface-small.md)
117 |
118 | Minimal number of controls, and they mostly accessible through
119 | hardware buttons anyway. Not very interesting.
120 |
121 | - [Scarlett 2nd Gen 6i6+, 3rd Gen 4i4+, Clarett USB, and
122 | Clarett+](iface-large.md)
123 |
124 | Full routing and mixing capabilities.
125 |
126 | - [Scarlett Small 4th Gen](iface-4th-gen-small.md)
127 |
128 | Full routing and mixing capabilities, remote-controlled input gain,
129 | but no output controls.
130 |
131 | - [Scarlett Big 4th Gen](iface-4th-gen-big.md)
132 |
133 | Full routing and mixing capabilities, remote-controlled input gain
134 | and output volume controls.
135 |
136 | ## Known Bugs/Issues
137 |
138 | - For interfaces using the FCP driver, alsa-scarlett-gui needs to be
139 | started after the interface is connected and fcp-server has started.
140 |
141 | - Load/Save uses `alsactl` which will be confused if the ALSA
142 | interface name (e.g. `USB`) changes.
143 |
144 | - Load/Save is not implemented for simulated interfaces.
145 |
146 | - The read-only status of controls in interface simulation mode does
147 | not change when the HW/SW button is clicked.
148 |
149 | - When there’s more than one main window open, closing one of them
150 | doesn’t free and close everything related to that card.
151 |
152 | - There is no facility to group channels into stereo pairs (needs
153 | kernel support to save this information in the interface).
154 |
155 | - There is no facility to give channels custom names (needs kernel
156 | support to save this information in the interface).
157 |
--------------------------------------------------------------------------------
/docs/iface-1st-gen.md:
--------------------------------------------------------------------------------
1 | # ALSA Scarlett Control Panel
2 |
3 | ## Scarlett 1st Gen Interfaces
4 |
5 | This document describes how to use the ALSA Scarlett Control Panel
6 | with the Scarlett 1st Gen interfaces:
7 |
8 | - Scarlett 1st Gen 6i6, 8i6, 18i6, 18i8, 18i20
9 |
10 | Note: The 1st Gen Scarlett Solo, 2i2, and 2i4 have all their controls
11 | accessible from the front panel of the device, and there are no
12 | proprietary software controls, so they do not require this control
13 | panel software.
14 |
15 | ## Important Driver Limitations
16 |
17 | The 1st Gen Scarlett devices have some important limitations in the
18 | ALSA driver implementation that you should be aware of:
19 |
20 | 1. **Initial State Detection**: The driver cannot read the current
21 | state of hardware controls (this appears to be a limitation of the
22 | device firmware). When alsa-scarlett-gui starts, what you see will
23 | not reflect the actual state of your device unless the controls
24 | have previously been set since startup.
25 |
26 | 2. **State Update Issues**: The driver only updates the hardware state
27 | when it thinks a setting needs to be changed. If the driver
28 | incorrectly believes a control is already in the desired state, it
29 | won’t actually update the control.
30 |
31 | 3. **Level Meters**: The driver does not support reading the level
32 | meters from the hardware.
33 |
34 | 4. **Startup Configuration**: The driver is not able to save the
35 | current configuration to the non-volatile memory of the device, so
36 | you’ll need to reapply the desired configuration each time you
37 | restart it (or write your preferred configuration using MixControl
38 | on Windows or Mac).
39 |
40 | ### Recommended Workaround
41 |
42 | To ensure your settings are properly applied:
43 |
44 | 1. Apply a “zero” configuration that sets all controls to values that
45 | are *not* what you desire.
46 | 2. Then apply your desired configuration
47 |
48 | This two-step process helps ensure that the driver actually sends all
49 | commands to the hardware. You may want to create a script using
50 | `alsactl` for this purpose.
51 |
52 | ## Main Window
53 |
54 | The main window is divided into three sections:
55 |
56 | - Global Controls
57 | - Analogue Input Controls
58 | - Analogue Output Controls
59 |
60 | The particular controls available depend on the interface model.
61 |
62 | Note that the View menu option lets you open two other windows which
63 | contain additional controls, described in the following sections:
64 | - [Routing](#routing)
65 | - [Mixer](#mixer)
66 |
67 | The Levels and Startup windows that are available for later-generation
68 | interfaces are not available for 1st Gen interfaces due to driver limitations.
69 |
70 | ### Global Controls
71 |
72 | Global controls relate to the operation of the interface as a whole.
73 |
74 | #### Clock Source
75 |
76 | Clock Source selects where the interface receives its digital clock
77 | from. If you aren’t using S/PDIF or ADAT inputs, set this to Internal.
78 |
79 | #### Sync Status
80 |
81 | Sync Status indicates if the interface is locked to a valid digital
82 | clock. If you aren’t using S/PDIF or ADAT inputs and the status is
83 | Unlocked, change the Clock Source to Internal.
84 |
85 | ### Analogue Input Controls
86 |
87 | #### Inst
88 |
89 | The Inst buttons are used to select between Mic/Line and Instrument
90 | level/impedance. When plugging in microphones or line-level equipment
91 | (such as a synthesizer, external preamp, or effects processor) to the
92 | input, set it to “Line”. The “Inst” setting is for instruments with
93 | pickups such as guitars.
94 |
95 | #### Pad
96 |
97 | Enabling Pad engages a 10dB attenuator in the channel, giving you more
98 | headroom for very hot signals.
99 |
100 | #### Gain
101 |
102 | The Gain switch selects Low or High gain for the input channel.
103 |
104 | ### Analogue Output Controls
105 |
106 | The analogue output controls let you set the output volume (gain) on
107 | the analogue line outputs.
108 |
109 | Click and drag up/down on the volume dial to change the volume, use
110 | your arrow keys, Home/End/PgUp/PgDn keys, or use your mouse scroll
111 | wheel to adjust. You can also double-click on it to quickly toggle the
112 | volume between off and 0dB.
113 |
114 | ## Routing
115 |
116 | The routing window allows complete control of signal routing between
117 | the hardware inputs/outputs, internal mixer, and PCM (USB)
118 | inputs/outputs.
119 |
120 | 
121 |
122 | To manage the routing connections:
123 |
124 | - Click and drag from a source to a sink or a sink to a source to
125 | connect them. Audio from the source will then be sent to that sink.
126 |
127 | - Click on a source or a sink to clear the links connected to that
128 | source/sink.
129 |
130 | Note that a sink can only be connected to one source, but one source
131 | can be connected to many sinks. If you want a sink to receive input
132 | from more than one source, use the mixer inputs and outputs:
133 |
134 | - Connect the sources that you want to mix together to mixer inputs
135 | - Connect mixer outputs to the sinks that you want to receive the
136 | mixed audio
137 | - Use the Mixer window to set the amount of each mixer input that is
138 | sent to each mixer output
139 |
140 | The Presets menu can be used to clear all connections, or to set up
141 | common configurations:
142 |
143 | - The “Direct” preset sets up the usual configuration using the
144 | interface as a regular audio interface by connecting:
145 |
146 | - all Hardware Inputs to PCM Inputs
147 | - all PCM Outputs to Hardware Outputs
148 |
149 | - The “Preamp” preset connects all Hardware Inputs to Hardware
150 | Outputs.
151 |
152 | - The “Stereo Out” preset connects PCM 1 and 2 Outputs to pairs of
153 | Hardware Outputs.
154 |
155 | ## Mixer
156 |
157 | If you use the Routing window to connect Sources to Mixer Inputs and
158 | Mixer Outputs to Sinks, then you can use the Mixer window to set the
159 | amount of each Mixer Input that is sent to each Mixer Output using a
160 | matrix of controls.
161 |
162 | Click and drag up/down on the gain controls to adjust, or use your
163 | mouse scroll wheel. You can also double-click on the control to
164 | quickly toggle between off and 0dB.
165 |
--------------------------------------------------------------------------------
/docs/iface-4th-gen-big.md:
--------------------------------------------------------------------------------
1 | # ALSA Scarlett Control Panel
2 |
3 | ## Scarlett Big 4th Gen Interfaces
4 |
5 | This document describes how to use the ALSA Scarlett Control Panel
6 | with the big Scarlett 4th Gen interfaces:
7 |
8 | - Scarlett 4th Gen 16i16, 18i16, 18i20
9 |
10 | ### FCP Driver
11 |
12 | The big 4th Gen interfaces are supported by a new “FCP” (Focusrite
13 | Control Protocol) driver introduced in Linux 6.14. If you haven’t
14 | installed
15 | [fcp-support](https://github.com/geoffreybennett/fcp-support) yet, you
16 | need to do that (and update the firmware) before you can use
17 | alsa-scarlett-gui.
18 |
19 | ## Main Window
20 |
21 | The main window is divided into three sections:
22 | - Global Controls
23 | - Analogue Input Controls
24 | - Analogue Output Controls
25 |
26 | The main window for the 16i16 interface is shown below. The 18i16 and
27 | 18i20 interfaces are similar, but with more controls.
28 |
29 | 
30 |
31 | ### Global Controls
32 |
33 | #### Clock Source (interfaces with S/PDIF or ADAT inputs only)
34 |
35 | Clock Source selects where the interface receives its digital clock
36 | from. If you aren’t using S/PDIF or ADAT inputs, set this to Internal.
37 |
38 | #### Sync Status
39 |
40 | Sync Status indicates if the interface is locked to a valid digital
41 | clock. If you aren’t using S/PDIF or ADAT inputs and the Sync Status
42 | is Unlocked, change the Clock Source to Internal.
43 |
44 | #### Sample Rate
45 |
46 | Sample Rate is informative only, and displays the current sample rate
47 | if the interface is currently in use. In ALSA, the sample rate is set
48 | by the application using the interface, which is usually a sound
49 | server such as PulseAudio, JACK, or PipeWire.
50 |
51 | #### Speaker Switching
52 |
53 | Speaker Switching lets you swap between two pairs of monitoring
54 | speakers very easily.
55 |
56 | ### Analogue Input Controls
57 |
58 | #### Input Select
59 |
60 | The “Input Select” control allows you to choose which channel the
61 | hardware 48V, Inst, Air, Auto, and Safe buttons control.
62 |
63 | #### Link
64 |
65 | The “Link” control links the 48V, Inst, Air, Auto, and Safe controls
66 | together so that they control a stereo pair of channels
67 | simultaneously.
68 |
69 | #### Gain
70 |
71 | The “Gain” controls adjust the input gain for the selected channel.
72 | Click and drag up/down on the control to adjust the gain, use your
73 | mouse scroll wheel, or click the control to select it and use the
74 | arrow keys, Page Up, Page Down, Home, and End keys.
75 |
76 | #### Autogain
77 |
78 | When the “Autogain” control is enabled, the interface will listen to
79 | the input signal for ten seconds and automatically adjust the gain to
80 | get the best signal level. When autogain is not running, the
81 | most-recent autogain exit status is shown below the “Autogain”
82 | control.
83 |
84 | #### Safe
85 |
86 | “Safe” mode is a feature that automatically reduces the gain if the
87 | signal is too loud. This can be useful to prevent clipping.
88 |
89 | #### Instrument
90 |
91 | The Inst button(s) are used to select between Mic/Line and Instrument
92 | level/impedance. When plugging in microphones or line-level equipment
93 | (such as a synthesizer, external preamp, or effects processor) to the
94 | input, set it to “Line”. The “Inst” setting is for instruments with
95 | pickups such as guitars.
96 |
97 | #### Air
98 |
99 | The Scarlett 3rd Gen introduced Air mode which transformed your
100 | recordings and inspired you while making music by boosting the
101 | signal’s high-end. The 4th Gen interfaces now call that “Air Presence”
102 | and add a new mode “Air Presence+Drive” which boosts mid-range
103 | harmonics in your sound.
104 |
105 | #### Phantom Power (48V)
106 |
107 | Turning the “48V” switch on sends “Phantom Power” to the XLR
108 | microphone input. This is required for some microphones (such as
109 | condensor microphones), and damaging to some microphones (particularly
110 | vintage ribbon microphones).
111 |
112 | ### Analogue Output Controls
113 |
114 | The analogue output controls are a bit sparse. More controls are
115 | coming soon.
116 |
117 | #### Volume Knobs
118 |
119 | The volume knobs control the volume of the analogue outputs. The two
120 | channels of the stereo pairs are shown separately, but are internally
121 | linked together.
122 |
123 | #### Mute and Dim
124 |
125 | The speaker icon buttons are “mute” and “dim” (reduce volume) buttons,
126 | corresponding to the front-panel buttons on the interface (although
127 | only the 18i20 has a physical dim button).
128 |
129 | ## Routing and Mixing
130 |
131 | The routing window allows (almost) complete control of signal routing
132 | between the hardware inputs/outputs, internal mixer, and PCM (USB)
133 | inputs/outputs.
134 |
135 | The routing and mixing capabilities of the big 4th Gen interfaces are
136 | the same in concept as the older interfaces, but the mixer inputs are
137 | fixed and not shown in the routing window as there are too many to
138 | sensibly display.
139 |
140 | From the main window, open the Routing window with the View → Routing
141 | menu option or pressing Ctrl-R:
142 |
143 | 
144 |
145 | To manage the routing connections:
146 |
147 | - Click and drag from a source to a sink or a sink to a source to
148 | connect them. Audio from the source will then be sent to that sink.
149 |
150 | - Click on a source or a sink to clear the links connected to that
151 | source/sink.
152 |
153 | Note that a sink can only be connected to one source, but one source
154 | can be connected to many sinks. If you want a sink to receive input
155 | from more than one source, connect the sinks to mixer outputs:
156 |
157 | - Connect mixer outputs to the sinks that you want to receive the
158 | mixed audio
159 | - Use the Mixer window to set the amount of each mixer input that is
160 | sent to each mixer output
161 |
162 | The Presets menu can be used to clear all connections, or to set up
163 | common configurations:
164 |
165 | - The “Direct” preset sets up the usual configuration using the
166 | interface as a regular audio interface by connecting:
167 |
168 | - all Hardware Inputs to PCM Inputs
169 | - all PCM Outputs to Hardware Outputs
170 |
171 | - The “Preamp” preset connects all Hardware Inputs to Hardware
172 | Outputs.
173 |
174 | - The “Stereo Out” preset connects PCM 1 and 2 Outputs to pairs of
175 | Hardware Outputs.
176 |
177 | To adjust the routing:
178 |
179 | - Click and drag from a source to a sink or a sink to a source to
180 | connect them. Audio from the source will then be sent to that sink.
181 |
182 | - Click on a source or a sink to clear the links connected to that
183 | source/sink.
184 |
185 | Note that a sink can only be connected to one source, but one source
186 | can be connected to many sinks.
187 |
188 | To adjust the mixer output levels:
189 |
190 | 1) Open the mixer window with the main window View → Mixer menu
191 | option, or press Ctrl-M.
192 |
193 | 2) Mixer levels can be adjusted with your keyboard or mouse in the
194 | same way as the [Gain Controls](#gain).
195 |
196 | ## Levels
197 |
198 | The meters show the levels seen by the interface at every routing
199 | source as well as the analogue outputs. Open this window by selecting
200 | the View → Levels menu option or pressing Ctrl-L.
201 |
202 | 
203 |
204 | Look at this in conjunction with the routing window to understand
205 | which meter corresponds to which source or sink.
206 |
207 | Thanks for reading this far! If you appreciate the hundreds of hours
208 | of work that went into the kernel driver, the control panel, and this
209 | documentation, please consider supporting the author with a
210 | [donation](../README.md#donations).
211 |
--------------------------------------------------------------------------------
/docs/iface-small.md:
--------------------------------------------------------------------------------
1 | # ALSA Scarlett Control Panel
2 |
3 | ## Small Scarlett 3rd Gen Interfaces
4 |
5 | The Scarlett 3rd Gen Solo and 2i2 interfaces have just a few buttons to control
6 | the Air, Line, Phantom Power, and Direct Monitor settings. Mostly
7 | nothing that you can’t access from the front panel anyway.
8 |
9 | 
10 |
11 | ## Input Controls
12 |
13 | ### Air
14 |
15 | Enabling Air will transform your recordings and inspire you while
16 | making music.
17 |
18 | ### Inst
19 |
20 | The Inst buttons are used to select between Mic/Line and Instrument
21 | level/impedance. When plugging in microphones or line-level equipment
22 | (such as a synthesizer, external preamp, or effects processor) to the
23 | input, set it to “Line”. The “Inst” setting is for instruments with
24 | pickups such as guitars.
25 |
26 | ### 48V (Phantom Power)
27 |
28 | Turning the “48V” switch on sends “Phantom Power” to the XLR
29 | microphone input(s). This is required for some microphones (such as
30 | condensor microphones), and damaging to some microphones (particularly
31 | vintage ribbon microphones).
32 |
33 | ## Output Controls
34 |
35 | ### Direct Monitor
36 |
37 | Direct Monitor sends the analogue input signals to the analogue
38 | outputs for zero-latency monitoring.
39 |
40 | On the 2i2, you have the choice of Mono or Stereo monitoring when you
41 | click the button. Mono sends both inputs to the left and right
42 | outputs. Stereo sends input 1 to the left, and input 2 to the right
43 | output.
44 |
45 | ## Startup Controls
46 |
47 | #### Phantom Power Persistence
48 |
49 | By default, phantom power is turned off when the interface is turned
50 | on. This can be changed in the startup configuration (menu option View
51 | → Startup).
52 |
53 | The one control not accessible from the front panel is “Phantom Power
54 | Persistence” (menu option View → Startup) which controls the Phantom
55 | Power state when the interface is powered on.
56 |
57 |
--------------------------------------------------------------------------------
/img/alsa-scarlett-gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/alsa-scarlett-gui.png
--------------------------------------------------------------------------------
/img/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/demo.gif
--------------------------------------------------------------------------------
/img/firmware-missing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/firmware-missing.png
--------------------------------------------------------------------------------
/img/firmware-update-required.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/firmware-update-required.png
--------------------------------------------------------------------------------
/img/firmware-updating.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/firmware-updating.png
--------------------------------------------------------------------------------
/img/iface-4th-gen-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/iface-4th-gen-big.png
--------------------------------------------------------------------------------
/img/iface-4th-gen-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/iface-4th-gen-small.png
--------------------------------------------------------------------------------
/img/iface-msd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/iface-msd.png
--------------------------------------------------------------------------------
/img/iface-none.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/iface-none.png
--------------------------------------------------------------------------------
/img/iface-small-gen3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/iface-small-gen3.png
--------------------------------------------------------------------------------
/img/main-global.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/main-global.png
--------------------------------------------------------------------------------
/img/main-inputs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/main-inputs.png
--------------------------------------------------------------------------------
/img/main-outputs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/main-outputs.png
--------------------------------------------------------------------------------
/img/routing-direct.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/routing-direct.png
--------------------------------------------------------------------------------
/img/scarlett-1st-gen-6i6-routing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-1st-gen-6i6-routing.png
--------------------------------------------------------------------------------
/img/scarlett-4th-gen-16i16-routing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-4th-gen-16i16-routing.png
--------------------------------------------------------------------------------
/img/scarlett-4th-gen-2i2-monitor.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-4th-gen-2i2-monitor.gif
--------------------------------------------------------------------------------
/img/scarlett-4th-gen-2i2-routing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-4th-gen-2i2-routing.png
--------------------------------------------------------------------------------
/img/scarlett-4th-gen-4i4-routing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-4th-gen-4i4-routing.png
--------------------------------------------------------------------------------
/img/scarlett-4th-gen-solo-mix-e-f.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-4th-gen-solo-mix-e-f.png
--------------------------------------------------------------------------------
/img/scarlett-4th-gen-solo-mix.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-4th-gen-solo-mix.gif
--------------------------------------------------------------------------------
/img/scarlett-4th-gen-solo-monitor.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/scarlett-4th-gen-solo-monitor.gif
--------------------------------------------------------------------------------
/img/window-levels-3rd-gen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/window-levels-3rd-gen.png
--------------------------------------------------------------------------------
/img/window-levels-4th-gen-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/window-levels-4th-gen-big.png
--------------------------------------------------------------------------------
/img/window-levels-4th-gen-small.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/window-levels-4th-gen-small.gif
--------------------------------------------------------------------------------
/img/window-main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/window-main.png
--------------------------------------------------------------------------------
/img/window-mixer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/window-mixer.png
--------------------------------------------------------------------------------
/img/window-routing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/window-routing.png
--------------------------------------------------------------------------------
/img/window-startup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/img/window-startup.png
--------------------------------------------------------------------------------
/src/Makefile:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | # Credit to Tom Tromey and Paul D. Smith:
5 | # http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/
6 |
7 | VERSION := $(shell \
8 | git describe --abbrev=4 --dirty --always --tags 2>/dev/null || \
9 | echo $${APP_VERSION:-Unknown} \
10 | )
11 |
12 | DEPDIR := .deps
13 | DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
14 |
15 | CFLAGS ?= -ggdb -fno-omit-frame-pointer -fPIE -O2
16 | CFLAGS += -Wall -Werror
17 | CFLAGS += -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3
18 | CFLAGS += -DVERSION=\"$(VERSION)\"
19 | CFLAGS += -Wno-error=deprecated-declarations
20 |
21 | PKG_CONFIG=pkg-config
22 |
23 | CFLAGS += $(shell $(PKG_CONFIG) --cflags glib-2.0)
24 | CFLAGS += $(shell $(PKG_CONFIG) --cflags gtk4)
25 | CFLAGS += $(shell $(PKG_CONFIG) --cflags alsa)
26 |
27 | LDFLAGS += $(shell $(PKG_CONFIG) --libs glib-2.0)
28 | LDFLAGS += $(shell $(PKG_CONFIG) --libs gtk4)
29 | LDFLAGS += $(shell $(PKG_CONFIG) --libs alsa)
30 | LDFLAGS += -lm -lcrypto -pie
31 |
32 | COMPILE.c = $(CC) $(DEPFLAGS) $(CFLAGS) -c
33 |
34 | %.c: %.xml $(DEPDIR)/%-xml.d | $(DEPDIR)
35 | echo $@: $(shell $(GLIB_COMPILE_RESOURCES) $< --generate-dependencies) > $(DEPDIR)/$*-xml.d
36 | $(GLIB_COMPILE_RESOURCES) $< --target=$@ --generate-source
37 |
38 | XML_SRC := $(wildcard *.xml)
39 | XML_OBJ := $(patsubst %.xml,%.c,$(XML_SRC))
40 |
41 | %.o: %.c
42 | %.o: %.c Makefile $(DEPDIR)/%.d | $(DEPDIR)
43 | $(COMPILE.c) $(OUTPUT_OPTION) $<
44 |
45 | SRCS := $(sort $(wildcard *.c) $(XML_OBJ))
46 | OBJS := $(patsubst %.c,%.o,$(SRCS))
47 | TARGET := alsa-scarlett-gui
48 | DOMAIN_PREFIX := vu.b4
49 | DESKTOP_FILE := $(DOMAIN_PREFIX).$(TARGET).desktop
50 | ICON_FILE := $(DOMAIN_PREFIX).$(TARGET).png
51 |
52 | GLIB_COMPILE_RESOURCES := $(shell $(PKG_CONFIG) --variable=glib_compile_resources gio-2.0)
53 |
54 | all: $(TARGET) $(DESKTOP_FILE)
55 |
56 | clean: depclean
57 | rm -f $(TARGET) $(DESKTOP_FILE) $(OBJS) $(XML_OBJ)
58 |
59 | depclean:
60 | rm -rf $(DEPDIR)
61 |
62 | $(DEPDIR): ; @mkdir -p $@
63 |
64 | DEPFILES := $(SRCS:%.c=$(DEPDIR)/%.d) $(XML_SRC:%.xml=$(DEPDIR)/%-xml.d)
65 | $(DEPFILES):
66 |
67 | include $(wildcard $(DEPFILES))
68 |
69 | $(TARGET): $(OBJS)
70 | $(CC) -o $(TARGET) $(OBJS) ${LDFLAGS}
71 |
72 | ifeq ($(PREFIX),)
73 | PREFIX := /usr/local
74 | endif
75 |
76 | BINDIR := $(DESTDIR)$(PREFIX)/bin
77 | ICONTOP := $(DESTDIR)$(PREFIX)/share/icons/hicolor
78 | ICONDIR := $(ICONTOP)/256x256/apps
79 | DESKTOPDIR := $(DESTDIR)$(PREFIX)/share/applications
80 |
81 | $(DESKTOP_FILE): $(DESKTOP_FILE).template
82 | sed 's_PREFIX_$(PREFIX)_' < $< > $@
83 |
84 | install: all
85 | install -d $(BINDIR)
86 | install -m 755 $(TARGET) $(BINDIR)
87 | install -d $(ICONDIR)
88 | install -m 644 img/$(ICON_FILE) $(ICONDIR)
89 | install -d $(DESKTOPDIR)
90 | install -m 644 $(DESKTOP_FILE) $(DESKTOPDIR)
91 |
92 | uninstall:
93 | rm -f $(BINDIR)/$(TARGET)
94 | rm -f $(ICONDIR)/$(ICON_FILE)
95 | rm -f $(DESKTOPDIR)/$(DESKTOP_FILE)
96 |
97 | help:
98 | @echo "alsa-scarlett-gui"
99 | @echo
100 | @echo "This Makefile knows about:"
101 | @echo " make"
102 | @echo " make install"
103 | @echo " make uninstall"
104 |
--------------------------------------------------------------------------------
/src/about.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "about.h"
5 |
6 | static GdkTexture *logo = NULL;
7 |
8 | void activate_about(
9 | GSimpleAction *action,
10 | GVariant *parameter,
11 | gpointer data
12 | ) {
13 | GtkWindow *w = GTK_WINDOW(data);
14 |
15 | const char *authors[] = {
16 | "Geoffrey D. Bennett ",
17 | NULL
18 | };
19 |
20 | if (!logo)
21 | logo = gdk_texture_new_from_resource(
22 | "/vu/b4/alsa-scarlett-gui/icons/vu.b4.alsa-scarlett-gui.png"
23 | );
24 |
25 | gtk_show_about_dialog(
26 | w,
27 | "program-name", "ALSA Scarlett Control Panel",
28 | "version", "Version " VERSION,
29 | "comments",
30 | "Gtk4 GUI for the ALSA controls presented by the\n"
31 | "Linux kernel Focusrite USB drivers",
32 | "website", "https://github.com/geoffreybennett/alsa-scarlett-gui",
33 | "copyright", "Copyright 2022-2025 Geoffrey D. Bennett",
34 | "license-type", GTK_LICENSE_GPL_3_0,
35 | "logo", logo,
36 | "title", "About ALSA Scarlett Mixer Interface",
37 | "authors", authors,
38 | NULL
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/about.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | void activate_about(
9 | GSimpleAction *action,
10 | GVariant *parameter,
11 | gpointer data
12 | );
13 |
--------------------------------------------------------------------------------
/src/alsa-scarlett-gui-resources.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | img/vu.b4.alsa-scarlett-gui.png
5 | img/socket.svg
6 | img/audio-volume-high.svg
7 | img/audio-volume-low.svg
8 | img/audio-volume-medium.svg
9 | img/audio-volume-muted.svg
10 |
11 |
12 | alsa-scarlett-gui.css
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/alsa-sim.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | void create_sim_from_file(GtkWindow *w, char *fn);
9 |
--------------------------------------------------------------------------------
/src/const.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | // maximum number of mix outputs
7 | #define MAX_MIX_OUT 12
8 |
9 | // maximum number of mux inputs
10 | #define MAX_MUX_IN 42
11 |
12 | // maximum number of meters
13 | #define MAX_METERS 92
14 |
--------------------------------------------------------------------------------
/src/db.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 | #include
6 |
7 | static double db_to_linear(double db) {
8 | if (db <= SND_CTL_TLV_DB_GAIN_MUTE)
9 | return 0.0;
10 | return pow(10.0, db / 20.0);
11 | }
12 |
13 | static double linear_to_db(double linear) {
14 | if (linear <= 0.0)
15 | return SND_CTL_TLV_DB_GAIN_MUTE;
16 | return 20.0 * log10(linear);
17 | }
18 |
19 | int cdb_to_linear_value(
20 | int cdb, int min_val, int max_val, int min_cdb, int max_cdb
21 | ) {
22 | if (cdb <= min_cdb)
23 | return min_val;
24 | if (cdb >= max_cdb)
25 | return max_val;
26 |
27 | // Convert centidB to dB
28 | double db = (double)cdb / 100.0;
29 | double max_db = (double)max_cdb / 100.0;
30 |
31 | // Convert dB relative to max_db to linear scale 0.0-1.0
32 | double linear = db_to_linear(db - max_db);
33 |
34 | // Scale to full ALSA range
35 | double scaled = linear * (double)max_val;
36 | int value = (int)round(scaled);
37 | if (value < min_val)
38 | return min_val;
39 | if (value > max_val)
40 | return max_val;
41 | return value;
42 | }
43 |
44 | int linear_value_to_cdb(
45 | int value, int min_val, int max_val, int min_cdb, int max_cdb
46 | ) {
47 | if (value <= min_val)
48 | return min_cdb;
49 | if (value >= max_val)
50 | return max_cdb;
51 |
52 | // Convert to 0.0-1.0 linear scale
53 | double linear = (double)value / (double)max_val;
54 | double max_db = (double)max_cdb / 100.0;
55 |
56 | // Convert to dB relative to max_db and back to centidB
57 | int cdb = (int)round((linear_to_db(linear) + max_db) * 100.0);
58 | if (cdb < min_cdb)
59 | return min_cdb;
60 | if (cdb > max_cdb)
61 | return max_cdb;
62 | return cdb;
63 | }
64 |
65 | double linear_value_to_db(
66 | int value, int min_val, int max_val, int min_db, int max_db
67 | ) {
68 | if (value <= min_val)
69 | return min_db;
70 | if (value >= max_val)
71 | return max_db;
72 |
73 | // Convert to 0.0-1.0 linear scale
74 | double linear = (double)value / (double)max_val;
75 |
76 | // Convert to dB relative to max_db
77 | double db = linear_to_db(linear) + max_db;
78 | if (db < min_db)
79 | return min_db;
80 | if (db > max_db)
81 | return max_db;
82 | return db;
83 | }
84 |
--------------------------------------------------------------------------------
/src/db.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | int cdb_to_linear_value(
7 | int cdb, int min_val, int max_val, int min_cdb, int max_cdb
8 | );
9 |
10 | int linear_value_to_cdb(
11 | int value, int min_val, int max_val, int min_cdb, int max_cdb
12 | );
13 |
14 | double linear_value_to_db(
15 | int value, int min_val, int max_val, int min_db, int max_db
16 | );
17 |
--------------------------------------------------------------------------------
/src/device-reset-config.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 | #include "device-reset-config.h"
6 | #include "scarlett2.h"
7 | #include "scarlett2-ioctls.h"
8 | #include "window-modal.h"
9 |
10 | static gpointer update_progress(
11 | struct modal_data *modal_data,
12 | char *text,
13 | int progress
14 | ) {
15 | struct progress_data *progress_data = g_new0(struct progress_data, 1);
16 | progress_data->modal_data = modal_data;
17 | progress_data->text = text;
18 | progress_data->progress = progress;
19 |
20 | g_main_context_invoke(NULL, modal_update_progress, progress_data);
21 | return NULL;
22 | }
23 |
24 | #define fail(msg) { \
25 | if (hwdep) \
26 | scarlett2_close(hwdep); \
27 | return update_progress(modal_data, msg, -1); \
28 | }
29 |
30 | #define failsndmsg(msg) g_strdup_printf(msg, snd_strerror(err))
31 |
32 | gpointer reset_config_thread(gpointer user_data) {
33 | struct modal_data *modal_data = user_data;
34 |
35 | update_progress(modal_data, g_strdup("Resetting configuration..."), 0);
36 |
37 | snd_hwdep_t *hwdep;
38 |
39 | int err = scarlett2_open_card(modal_data->card->device, &hwdep);
40 | if (err < 0)
41 | fail(failsndmsg("Unable to open hwdep interface: %s"));
42 |
43 | err = scarlett2_erase_config(hwdep);
44 | if (err < 0)
45 | fail(failsndmsg("Unable to reset configuration: %s"));
46 |
47 | while (1) {
48 | g_usleep(50000);
49 |
50 | err = scarlett2_get_erase_progress(hwdep);
51 | if (err < 0)
52 | fail(failsndmsg("Unable to get erase progress: %s"));
53 | if (err == 255)
54 | break;
55 |
56 | update_progress(modal_data, NULL, err);
57 | }
58 |
59 | g_main_context_invoke(NULL, modal_start_reboot_progress, modal_data);
60 | scarlett2_reboot(hwdep);
61 | scarlett2_close(hwdep);
62 |
63 | return NULL;
64 | }
65 |
66 | static void join_thread(gpointer thread) {
67 | g_thread_join(thread);
68 | }
69 |
70 | static void reset_config_yes_callback(struct modal_data *modal_data) {
71 | GThread *thread = g_thread_new(
72 | "reset_config_thread", reset_config_thread, modal_data
73 | );
74 | g_object_set_data_full(
75 | G_OBJECT(modal_data->button_box), "thread", thread, join_thread
76 | );
77 | }
78 |
79 | void create_reset_config_window(GtkWidget *w, struct alsa_card *card) {
80 | create_modal_window(
81 | w, card,
82 | "Confirm Reset Configuration",
83 | "Resetting Configuration",
84 | "Are you sure you want to reset the configuration?",
85 | reset_config_yes_callback
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/src/device-reset-config.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 | #include "alsa.h"
8 |
9 | void create_reset_config_window(GtkWidget *w, struct alsa_card *card);
10 |
--------------------------------------------------------------------------------
/src/device-update-firmware.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 | #include "device-reset-config.h"
6 | #include "scarlett2.h"
7 | #include "scarlett2-firmware.h"
8 | #include "scarlett2-ioctls.h"
9 | #include "window-modal.h"
10 |
11 | static gpointer update_progress(
12 | struct modal_data *modal_data,
13 | char *text,
14 | int progress
15 | ) {
16 | struct progress_data *progress_data = g_new0(struct progress_data, 1);
17 | progress_data->modal_data = modal_data;
18 | progress_data->text = text;
19 | progress_data->progress = progress;
20 |
21 | g_main_context_invoke(NULL, modal_update_progress, progress_data);
22 | return NULL;
23 | }
24 |
25 | #define fail(msg) { \
26 | if (hwdep) \
27 | scarlett2_close(hwdep); \
28 | if (firmware) \
29 | scarlett2_free_firmware_file(firmware); \
30 | return update_progress(modal_data, msg, -1); \
31 | }
32 |
33 | #define failsndmsg(msg) g_strdup_printf(msg, snd_strerror(err))
34 |
35 | gpointer update_firmware_thread(gpointer user_data) {
36 | struct modal_data *modal_data = user_data;
37 | struct alsa_card *card = modal_data->card;
38 |
39 | int err = 0;
40 | snd_hwdep_t *hwdep = NULL;
41 |
42 | // read the firmware file
43 | update_progress(modal_data, g_strdup("Checking firmware..."), 0);
44 | struct scarlett2_firmware_file *firmware =
45 | scarlett2_get_best_firmware(card->pid);
46 |
47 | // if no firmware, fail
48 | if (!firmware)
49 | fail(failsndmsg("No update firmware found for device: %s"));
50 |
51 | if (firmware->header.usb_pid != card->pid)
52 | fail(g_strdup("Firmware file does not match device"));
53 |
54 | update_progress(modal_data, g_strdup("Resetting configuration..."), 0);
55 |
56 | err = scarlett2_open_card(card->device, &hwdep);
57 | if (err < 0)
58 | fail(failsndmsg("Unable to open hwdep interface: %s"));
59 |
60 | err = scarlett2_erase_config(hwdep);
61 | if (err < 0)
62 | fail(failsndmsg("Unable to reset configuration: %s"));
63 |
64 | while (1) {
65 | g_usleep(50000);
66 |
67 | err = scarlett2_get_erase_progress(hwdep);
68 | if (err < 0)
69 | fail(failsndmsg("Unable to get erase progress: %s"));
70 | if (err == 255)
71 | break;
72 |
73 | update_progress(modal_data, NULL, err);
74 | }
75 |
76 | update_progress(modal_data, g_strdup("Erasing flash..."), 0);
77 |
78 | err = scarlett2_erase_firmware(hwdep);
79 | if (err < 0)
80 | fail(failsndmsg("Unable to erase upgrade firmware: %s"));
81 |
82 | while (1) {
83 | g_usleep(50000);
84 |
85 | err = scarlett2_get_erase_progress(hwdep);
86 | if (err < 0)
87 | fail(failsndmsg("Unable to get erase progress: %s"));
88 | if (err == 255)
89 | break;
90 |
91 | update_progress(modal_data, NULL, err);
92 | }
93 |
94 | update_progress(modal_data, g_strdup("Writing firmware..."), 0);
95 |
96 | size_t offset = 0;
97 | size_t len = firmware->header.firmware_length;
98 | unsigned char *buf = firmware->firmware_data;
99 |
100 | while (offset < len) {
101 | err = snd_hwdep_write(hwdep, buf + offset, len - offset);
102 | if (err < 0)
103 | fail(failsndmsg("Unable to write firmware: %s"));
104 |
105 | offset += err;
106 |
107 | update_progress(modal_data, NULL, (offset * 100) / len);
108 | }
109 |
110 | g_main_context_invoke(NULL, modal_start_reboot_progress, modal_data);
111 | scarlett2_reboot(hwdep);
112 | scarlett2_close(hwdep);
113 |
114 | return NULL;
115 | }
116 |
117 | static void join_thread(gpointer thread) {
118 | g_thread_join(thread);
119 | }
120 |
121 | static void update_firmware_yes_callback(struct modal_data *modal_data) {
122 | GThread *thread = g_thread_new(
123 | "update_firmware_thread", update_firmware_thread, modal_data
124 | );
125 | g_object_set_data_full(
126 | G_OBJECT(modal_data->button_box), "thread", thread, join_thread
127 | );
128 | }
129 |
130 | void create_update_firmware_window(GtkWidget *w, struct alsa_card *card) {
131 | create_modal_window(
132 | w, card,
133 | "Confirm Update Firmware",
134 | "Updating Firmware",
135 | "The firmware update process will take about 15 seconds.\n"
136 | "Please do not disconnect the device while updating.\n"
137 | "Ready to proceed?",
138 | update_firmware_yes_callback
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/src/device-update-firmware.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 | #include "alsa.h"
8 |
9 | void create_update_firmware_window(GtkWidget *w, struct alsa_card *card);
10 |
--------------------------------------------------------------------------------
/src/error.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "error.h"
5 |
6 | void show_error(GtkWindow *w, char *s) {
7 | if (!w) {
8 | printf("%s\n", s);
9 | return;
10 | }
11 |
12 | GtkWidget *dialog = gtk_message_dialog_new(
13 | w,
14 | GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
15 | GTK_MESSAGE_ERROR,
16 | GTK_BUTTONS_CLOSE,
17 | "%s",
18 | s
19 | );
20 | gtk_widget_set_visible(dialog, TRUE);
21 |
22 | g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL);
23 | }
24 |
--------------------------------------------------------------------------------
/src/error.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | void show_error(GtkWindow *w, char *s);
9 |
--------------------------------------------------------------------------------
/src/fcp-shared.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | // Error messages
5 | const char *fcp_socket_error_messages[] = {
6 | "Success",
7 | "Invalid magic",
8 | "Invalid command",
9 | "Invalid length",
10 | "Invalid hash",
11 | "Firmware PID does not match USB PID",
12 | "Configuration error (check fcp-server log)",
13 | "FCP communication error",
14 | "Timeout",
15 | "Read error",
16 | "Write error",
17 | "Not running leapfrog firmware",
18 | "Invalid state"
19 | };
20 |
--------------------------------------------------------------------------------
/src/fcp-shared.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | // Error codes
9 | #define FCP_SOCKET_ERR_INVALID_MAGIC 1
10 | #define FCP_SOCKET_ERR_INVALID_COMMAND 2
11 | #define FCP_SOCKET_ERR_INVALID_LENGTH 3
12 | #define FCP_SOCKET_ERR_INVALID_HASH 4
13 | #define FCP_SOCKET_ERR_INVALID_USB_ID 5
14 | #define FCP_SOCKET_ERR_CONFIG 6
15 | #define FCP_SOCKET_ERR_FCP 7
16 | #define FCP_SOCKET_ERR_TIMEOUT 8
17 | #define FCP_SOCKET_ERR_READ 9
18 | #define FCP_SOCKET_ERR_WRITE 10
19 | #define FCP_SOCKET_ERR_NOT_LEAPFROG 11
20 | #define FCP_SOCKET_ERR_INVALID_STATE 12
21 | #define FCP_SOCKET_ERR_MAX 12
22 |
23 | // Protocol constants
24 | #define FCP_SOCKET_PROTOCOL_VERSION 1
25 | #define FCP_SOCKET_MAGIC_REQUEST 0x53
26 | #define FCP_SOCKET_MAGIC_RESPONSE 0x73
27 |
28 | // Maximum payload length (2MB)
29 | #define MAX_PAYLOAD_LENGTH 2 * 1024 * 1024
30 |
31 | // Request types
32 | #define FCP_SOCKET_REQUEST_REBOOT 0x0001
33 | #define FCP_SOCKET_REQUEST_CONFIG_ERASE 0x0002
34 | #define FCP_SOCKET_REQUEST_APP_FIRMWARE_ERASE 0x0003
35 | #define FCP_SOCKET_REQUEST_APP_FIRMWARE_UPDATE 0x0004
36 | #define FCP_SOCKET_REQUEST_ESP_FIRMWARE_UPDATE 0x0005
37 |
38 | // Response types
39 | #define FCP_SOCKET_RESPONSE_VERSION 0x00
40 | #define FCP_SOCKET_RESPONSE_SUCCESS 0x01
41 | #define FCP_SOCKET_RESPONSE_ERROR 0x02
42 | #define FCP_SOCKET_RESPONSE_PROGRESS 0x03
43 |
44 | extern const char *fcp_socket_error_messages[];
45 |
46 | // Message structures
47 | #pragma pack(push, 1)
48 |
49 | struct fcp_socket_msg_header {
50 | uint8_t magic;
51 | uint8_t msg_type;
52 | uint32_t payload_length;
53 | };
54 |
55 | struct firmware_payload {
56 | uint32_t size;
57 | uint16_t usb_vid;
58 | uint16_t usb_pid;
59 | uint8_t sha256[32];
60 | uint8_t md5[16];
61 | uint8_t data[];
62 | };
63 |
64 | struct version_msg {
65 | struct fcp_socket_msg_header header;
66 | uint8_t version;
67 | };
68 |
69 | struct progress_msg {
70 | struct fcp_socket_msg_header header;
71 | uint8_t percent;
72 | };
73 |
74 | struct error_msg {
75 | struct fcp_socket_msg_header header;
76 | int16_t error_code;
77 | };
78 |
79 | #pragma pack(pop)
80 |
81 |
--------------------------------------------------------------------------------
/src/fcp-socket.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #include "fcp-shared.h"
15 | #include "fcp-socket.h"
16 | #include "error.h"
17 |
18 | // Connect to the FCP socket server for the given card
19 | int fcp_socket_connect(struct alsa_card *card) {
20 | if (!card || !card->fcp_socket) {
21 | fprintf(stderr, "FCP socket path is not available");
22 | return -1;
23 | }
24 |
25 | int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
26 | if (sock_fd < 0) {
27 | fprintf(stderr, "Cannot create socket: %s", strerror(errno));
28 | return -1;
29 | }
30 |
31 | struct sockaddr_un addr = {
32 | .sun_family = AF_UNIX
33 | };
34 | strncpy(addr.sun_path, card->fcp_socket, sizeof(addr.sun_path) - 1);
35 |
36 | if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
37 | fprintf(stderr, "Cannot connect to server at %s: %s",
38 | addr.sun_path, strerror(errno));
39 | close(sock_fd);
40 | return -1;
41 | }
42 |
43 | return sock_fd;
44 | }
45 |
46 | // Send a simple command with no payload to the server
47 | int fcp_socket_send_command(int sock_fd, uint8_t command) {
48 | struct fcp_socket_msg_header header = {
49 | .magic = FCP_SOCKET_MAGIC_REQUEST,
50 | .msg_type = command,
51 | .payload_length = 0
52 | };
53 |
54 | if (write(sock_fd, &header, sizeof(header)) != sizeof(header)) {
55 | fprintf(stderr, "Error sending command: %s", strerror(errno));
56 | return -1;
57 | }
58 |
59 | return 0;
60 | }
61 |
62 | // Handle server responses from a command
63 | int fcp_socket_handle_response(int sock_fd, bool show_progress) {
64 | struct fcp_socket_msg_header header;
65 | ssize_t bytes_read;
66 |
67 | // Read response header
68 | bytes_read = read(sock_fd, &header, sizeof(header));
69 | if (bytes_read != sizeof(header)) {
70 | if (bytes_read == 0) {
71 | // Server closed the connection
72 | return 0;
73 | }
74 | fprintf(stderr, "Error reading response header: %s", strerror(errno));
75 | return -1;
76 | }
77 |
78 | // Verify the magic value
79 | if (header.magic != FCP_SOCKET_MAGIC_RESPONSE) {
80 | fprintf(stderr, "Invalid response magic: 0x%02x", header.magic);
81 | return -1;
82 | }
83 |
84 | // Handle different response types
85 | switch (header.msg_type) {
86 | case FCP_SOCKET_RESPONSE_VERSION: {
87 | // Protocol version response
88 | uint8_t version;
89 | bytes_read = read(sock_fd, &version, sizeof(version));
90 | if (bytes_read != sizeof(version)) {
91 | fprintf(stderr, "Error reading version: %s", strerror(errno));
92 | return -1;
93 | }
94 | // Protocol version mismatch?
95 | if (version != FCP_SOCKET_PROTOCOL_VERSION) {
96 | fprintf(stderr, "Protocol version mismatch: expected %d, got %d",
97 | FCP_SOCKET_PROTOCOL_VERSION, version);
98 | return -1;
99 | }
100 | break;
101 | }
102 |
103 | case FCP_SOCKET_RESPONSE_SUCCESS:
104 | // Command completed successfully
105 | return 0;
106 |
107 | case FCP_SOCKET_RESPONSE_ERROR: {
108 | // Error response
109 | int16_t error_code;
110 | bytes_read = read(sock_fd, &error_code, sizeof(error_code));
111 | if (bytes_read != sizeof(error_code)) {
112 | fprintf(stderr, "Error reading error code: %s", strerror(errno));
113 | return -1;
114 | }
115 |
116 | if (error_code > 0 && error_code <= FCP_SOCKET_ERR_MAX) {
117 | fprintf(stderr, "Server error: %s", fcp_socket_error_messages[error_code]);
118 | } else {
119 | fprintf(stderr, "Unknown server error code: %d", error_code);
120 | }
121 | return -1;
122 | }
123 |
124 | case FCP_SOCKET_RESPONSE_PROGRESS: {
125 | // Progress update
126 | if (show_progress) {
127 | uint8_t percent;
128 | bytes_read = read(sock_fd, &percent, sizeof(percent));
129 | if (bytes_read != sizeof(percent)) {
130 | fprintf(stderr, "Error reading progress: %s", strerror(errno));
131 | return -1;
132 | }
133 | fprintf(stderr, "\rProgress: %d%%", percent);
134 | if (percent == 100)
135 | fprintf(stderr, "\n");
136 | } else {
137 | // Skip the progress byte
138 | uint8_t dummy;
139 | if (read(sock_fd, &dummy, sizeof(dummy)) < 0) {
140 | fprintf(stderr, "Error reading progress: %s", strerror(errno));
141 | return -1;
142 | }
143 | }
144 |
145 | // Continue reading responses
146 | return fcp_socket_handle_response(sock_fd, show_progress);
147 | }
148 |
149 | default:
150 | fprintf(stderr, "Unknown response type: 0x%02x", header.msg_type);
151 | return -1;
152 | }
153 |
154 | return 0;
155 | }
156 |
157 | // Wait for server to disconnect (used after reboot command)
158 | int fcp_socket_wait_for_disconnect(int sock_fd) {
159 | fd_set rfds;
160 | struct timeval tv, start_time, now;
161 | char buf[1];
162 | const int TIMEOUT_SECS = 2;
163 |
164 | gettimeofday(&start_time, NULL);
165 |
166 | while (1) {
167 | FD_ZERO(&rfds);
168 | FD_SET(sock_fd, &rfds);
169 |
170 | gettimeofday(&now, NULL);
171 | int elapsed = now.tv_sec - start_time.tv_sec;
172 | if (elapsed >= TIMEOUT_SECS) {
173 | fprintf(stderr, "Timeout waiting for server disconnect\n");
174 | return -1;
175 | }
176 |
177 | tv.tv_sec = TIMEOUT_SECS - elapsed;
178 | tv.tv_usec = 0;
179 |
180 | int ret = select(sock_fd + 1, &rfds, NULL, NULL, &tv);
181 | if (ret < 0) {
182 | if (errno == EINTR)
183 | continue;
184 | fprintf(stderr, "Select error: %s\n", strerror(errno));
185 | return -1;
186 | }
187 |
188 | if (ret > 0) {
189 | // Try to read one byte
190 | ssize_t n = read(sock_fd, buf, 1);
191 | if (n < 0) {
192 | if (errno == EINTR || errno == EAGAIN)
193 | continue;
194 | fprintf(stderr, "Read error: %s\n", strerror(errno));
195 | return -1;
196 | }
197 | if (n == 0) {
198 | // EOF received - server has disconnected
199 | return 0;
200 | }
201 | // Ignore any data received, just keep waiting for EOF
202 | }
203 | }
204 | }
205 |
206 | // Reboot a device using the FCP socket interface
207 | int fcp_socket_reboot_device(struct alsa_card *card) {
208 | int sock_fd, ret = -1;
209 |
210 | sock_fd = fcp_socket_connect(card);
211 | if (sock_fd < 0)
212 | return -1;
213 |
214 | // Send reboot command and wait for server to disconnect
215 | if (fcp_socket_send_command(sock_fd, FCP_SOCKET_REQUEST_REBOOT) == 0)
216 | ret = fcp_socket_wait_for_disconnect(sock_fd);
217 |
218 | close(sock_fd);
219 | return ret;
220 | }
221 |
--------------------------------------------------------------------------------
/src/fcp-socket.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 | #include "alsa.h"
8 |
9 | // Connect to the FCP socket server for the given card
10 | // Returns socket file descriptor on success, -1 on error
11 | int fcp_socket_connect(struct alsa_card *card);
12 |
13 | // Send a simple command with no payload to the server
14 | // Returns 0 on success, -1 on error
15 | int fcp_socket_send_command(int sock_fd, uint8_t command);
16 |
17 | // Handle server responses from a command
18 | // Returns 0 on success, -1 on error
19 | int fcp_socket_handle_response(int sock_fd, bool show_progress);
20 |
21 | // Wait for server to disconnect (used after reboot command)
22 | // Returns 0 if disconnected, -1 on timeout or error
23 | int fcp_socket_wait_for_disconnect(int sock_fd);
24 |
25 | // Reboot a device using the FCP socket interface
26 | // Returns 0 on success, -1 on error
27 | int fcp_socket_reboot_device(struct alsa_card *card);
--------------------------------------------------------------------------------
/src/file.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "alsa.h"
5 | #include "alsa-sim.h"
6 | #include "error.h"
7 | #include "file.h"
8 | #include "stringhelper.h"
9 |
10 | static void run_alsactl(
11 | struct alsa_card *card,
12 | char *cmd,
13 | char *fn
14 | ) {
15 | GtkWindow *w = GTK_WINDOW(card->window_main);
16 |
17 | gchar *alsactl_path = g_find_program_in_path("alsactl");
18 |
19 | if (!alsactl_path)
20 | alsactl_path = g_strdup("/usr/sbin/alsactl");
21 |
22 | gchar *argv[] = {
23 | alsactl_path, cmd, card->device, "-f", fn, NULL
24 | };
25 |
26 | gchar *stdout;
27 | gchar *stderr;
28 | gint exit_status;
29 | GError *error = NULL;
30 |
31 | gboolean result = g_spawn_sync(
32 | NULL,
33 | argv,
34 | NULL,
35 | G_SPAWN_SEARCH_PATH,
36 | NULL,
37 | NULL,
38 | &stdout,
39 | &stderr,
40 | &exit_status,
41 | &error
42 | );
43 |
44 | if (result && WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0)
45 | goto done;
46 |
47 | char *error_message =
48 | result
49 | ? g_strdup_printf("%s\n%s", stdout, stderr)
50 | : g_strdup_printf("%s", error->message);
51 |
52 | char *msg = g_strdup_printf(
53 | "Error running “alsactl %s %s -f %s”: %s",
54 | cmd, card->device, fn, error_message
55 | );
56 | show_error(w, msg);
57 | g_free(msg);
58 | g_free(error_message);
59 |
60 | done:
61 | g_free(alsactl_path);
62 | g_free(stdout);
63 | g_free(stderr);
64 | if (error)
65 | g_error_free(error);
66 | }
67 |
68 | static void add_state_filter(GtkFileChooserNative *native) {
69 | GtkFileFilter *filter = gtk_file_filter_new();
70 | gtk_file_filter_set_name(filter, "alsactl state file (.state)");
71 | gtk_file_filter_add_pattern(filter, "*.state");
72 | gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(native), filter);
73 | }
74 |
75 | static void load_response(
76 | GtkNativeDialog *native,
77 | int response,
78 | gpointer data
79 | ) {
80 | struct alsa_card *card = data;
81 |
82 | if (response != GTK_RESPONSE_ACCEPT)
83 | goto done;
84 |
85 | GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native));
86 | char *fn = g_file_get_path(file);
87 |
88 | run_alsactl(card, "restore", fn);
89 |
90 | g_free(fn);
91 | g_object_unref(file);
92 |
93 | done:
94 | g_object_unref(native);
95 | }
96 |
97 | void activate_load(
98 | GSimpleAction *action,
99 | GVariant *parameter,
100 | gpointer data
101 | ) {
102 | struct alsa_card *card = data;
103 |
104 | GtkFileChooserNative *native = gtk_file_chooser_native_new(
105 | "Load Configuration",
106 | GTK_WINDOW(card->window_main),
107 | GTK_FILE_CHOOSER_ACTION_OPEN,
108 | "_Load",
109 | "_Cancel"
110 | );
111 |
112 | add_state_filter(native);
113 |
114 | g_signal_connect(native, "response", G_CALLBACK(load_response), card);
115 | gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
116 | }
117 |
118 | static void save_response(
119 | GtkNativeDialog *native,
120 | int response,
121 | gpointer data
122 | ) {
123 | struct alsa_card *card = data;
124 |
125 | if (response != GTK_RESPONSE_ACCEPT)
126 | goto done;
127 |
128 | GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native));
129 | char *fn = g_file_get_path(file);
130 |
131 | // append .state if not present
132 | char *fn_with_ext;
133 | if (string_ends_with(fn, ".state"))
134 | fn_with_ext = g_strdup_printf("%s", fn);
135 | else
136 | fn_with_ext = g_strdup_printf("%s.state", fn);
137 |
138 | run_alsactl(card, "store", fn_with_ext);
139 |
140 | g_free(fn);
141 | g_free(fn_with_ext);
142 | g_object_unref(file);
143 |
144 | done:
145 | g_object_unref(native);
146 | }
147 |
148 | void activate_save(
149 | GSimpleAction *action,
150 | GVariant *parameter,
151 | gpointer data
152 | ) {
153 | struct alsa_card *card = data;
154 |
155 | GtkFileChooserNative *native = gtk_file_chooser_native_new(
156 | "Save Configuration",
157 | GTK_WINDOW(card->window_main),
158 | GTK_FILE_CHOOSER_ACTION_SAVE,
159 | "_Save",
160 | "_Cancel"
161 | );
162 |
163 | add_state_filter(native);
164 |
165 | g_signal_connect(native, "response", G_CALLBACK(save_response), card);
166 | gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
167 | }
168 |
169 | static void sim_response(
170 | GtkNativeDialog *native,
171 | int response,
172 | gpointer data
173 | ) {
174 | GtkWindow *w = data;
175 |
176 | if (response != GTK_RESPONSE_ACCEPT)
177 | goto done;
178 |
179 | GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native));
180 | char *fn = g_file_get_path(file);
181 |
182 | create_sim_from_file(w, fn);
183 |
184 | g_free(fn);
185 | g_object_unref(file);
186 |
187 | done:
188 | g_object_unref(native);
189 | }
190 |
191 | void activate_sim(
192 | GSimpleAction *action,
193 | GVariant *parameter,
194 | gpointer data
195 | ) {
196 | GtkWidget *w = data;
197 |
198 | GtkFileChooserNative *native = gtk_file_chooser_native_new(
199 | "Load Configuration File for Interface Simulation",
200 | GTK_WINDOW(w),
201 | GTK_FILE_CHOOSER_ACTION_OPEN,
202 | "_Load",
203 | "_Cancel"
204 | );
205 |
206 | add_state_filter(native);
207 |
208 | g_signal_connect(native, "response", G_CALLBACK(sim_response), w);
209 | gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
210 | }
211 |
--------------------------------------------------------------------------------
/src/file.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 |
6 | void activate_load(GSimpleAction *action, GVariant *parameter, gpointer data);
7 | void activate_save(GSimpleAction *action, GVariant *parameter, gpointer data);
8 | void activate_sim(GSimpleAction *action, GVariant *parameter, gpointer data);
9 |
--------------------------------------------------------------------------------
/src/gtkdial.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2021 Stiliyan Varbanov
2 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
3 | // SPDX-License-Identifier: LGPL-3.0-or-later
4 |
5 | /*
6 | * A Dial widget for GTK-4 similar to GtkScale.
7 | * 2021 Stiliyan Varbanov www.fiverr.com/stilvar
8 | */
9 |
10 | #ifndef __GTK_DIAL_H__
11 | #define __GTK_DIAL_H__
12 |
13 | #include
14 |
15 | G_BEGIN_DECLS
16 |
17 | #define GTK_TYPE_DIAL (gtk_dial_get_type())
18 | #define GTK_DIAL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_DIAL, GtkDial))
19 | #define GTK_DIAL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_DIAL, GtkDialClass))
20 | #define GTK_IS_DIAL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_DIAL))
21 | #define GTK_IS_DIAL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTK_TYPE_DIAL))
22 | #define GTK_DIAL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTK_TYPE_DIAL, GtkDialClass))
23 |
24 | typedef struct _GtkDial GtkDial;
25 | typedef struct _GtkDialClass GtkDialClass;
26 |
27 | struct _GtkDialClass {
28 | GtkWidgetClass parent_class;
29 |
30 | void (*value_changed)(GtkDial *dial);
31 |
32 | /* action signals for keybindings */
33 | void (*move_slider)(GtkDial *dial, GtkScrollType scroll);
34 |
35 | gboolean (*change_value)(
36 | GtkDial *dial,
37 | GtkScrollType scroll,
38 | double new_value
39 | );
40 | };
41 |
42 | GType gtk_dial_get_type(void) G_GNUC_CONST;
43 |
44 | GtkWidget *gtk_dial_new(GtkAdjustment *adjustment);
45 |
46 | GtkWidget *gtk_dial_new_with_range(
47 | double min,
48 | double max,
49 | double step,
50 | double page
51 | );
52 |
53 | void gtk_dial_set_has_origin(GtkDial *dial, gboolean has_origin);
54 | gboolean gtk_dial_get_has_origin(GtkDial *dial);
55 |
56 | void gtk_dial_set_adjustment(GtkDial *dial, GtkAdjustment *adj);
57 | GtkAdjustment *gtk_dial_get_adjustment(GtkDial *dial);
58 |
59 | double gtk_dial_get_value(GtkDial *dial);
60 | void gtk_dial_set_value(GtkDial *dial, double value);
61 |
62 | void gtk_dial_set_round_digits(GtkDial *dial, int round_digits);
63 | int gtk_dial_get_round_digits(GtkDial *dial);
64 |
65 | void gtk_dial_set_zero_db(GtkDial *dial, double zero_db);
66 | double gtk_dial_get_zero_db(GtkDial *dial);
67 |
68 | void gtk_dial_set_off_db(GtkDial *dial, double off_db);
69 | double gtk_dial_get_off_db(GtkDial *dial);
70 |
71 | void gtk_dial_set_is_linear(GtkDial *dial, gboolean is_linear);
72 | gboolean gtk_dial_get_is_linear(GtkDial *dial);
73 |
74 | // taper functions
75 | enum {
76 | GTK_DIAL_TAPER_LINEAR,
77 | GTK_DIAL_TAPER_LOG
78 | };
79 |
80 | void gtk_dial_set_taper(GtkDial *dial, int taper);
81 | int gtk_dial_get_taper(GtkDial *dial);
82 |
83 | void gtk_dial_set_taper_linear_breakpoints(
84 | GtkDial *dial,
85 | const double *breakpoints,
86 | const double *outputs,
87 | int count
88 | );
89 |
90 | void gtk_dial_set_can_control(GtkDial *dial, gboolean can_control);
91 | gboolean gtk_dial_get_can_control(GtkDial *dial);
92 |
93 | void gtk_dial_set_level_meter_colours(
94 | GtkDial *dial,
95 | const int *breakpoints,
96 | const double *colours,
97 | int count
98 | );
99 |
100 | void gtk_dial_set_peak_hold(GtkDial *dial, int peak_hold);
101 | int gtk_dial_get_peak_hold(GtkDial *dial);
102 | void gtk_dial_peak_tick(void);
103 |
104 | int cdb_to_linear_value(
105 | int db, int min_val, int max_val, int min_db, int max_db
106 | );
107 | int linear_value_to_cdb(
108 | int value, int min_val, int max_val, int min_db, int max_db
109 | );
110 |
111 | G_END_DECLS
112 |
113 | #endif
114 |
--------------------------------------------------------------------------------
/src/gtkhelper.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "gtkhelper.h"
5 |
6 | void gtk_widget_set_margin(GtkWidget *w, int margin) {
7 | gtk_widget_set_margin_top(w, margin);
8 | gtk_widget_set_margin_bottom(w, margin);
9 | gtk_widget_set_margin_start(w, margin);
10 | gtk_widget_set_margin_end(w, margin);
11 | }
12 |
13 | void gtk_widget_set_expand(GtkWidget *w, gboolean expand) {
14 | gtk_widget_set_hexpand(w, expand);
15 | gtk_widget_set_vexpand(w, expand);
16 | }
17 |
18 | void gtk_widget_set_align(GtkWidget *w, GtkAlign x, GtkAlign y) {
19 | gtk_widget_set_halign(w, x);
20 | gtk_widget_set_valign(w, y);
21 | }
22 |
23 | void gtk_grid_set_spacing(GtkGrid *grid, int spacing) {
24 | gtk_grid_set_row_spacing(grid, spacing);
25 | gtk_grid_set_column_spacing(grid, spacing);
26 | }
27 |
28 | void gtk_widget_remove_css_classes_by_prefix(
29 | GtkWidget *w,
30 | const char *prefix
31 | ) {
32 | char **classes = gtk_widget_get_css_classes(w);
33 |
34 | for (char **i = classes; *i != NULL; i++)
35 | if (strncmp(*i, prefix, strlen(prefix)) == 0)
36 | gtk_widget_remove_css_class(w, *i);
37 |
38 | g_strfreev(classes);
39 | }
40 |
--------------------------------------------------------------------------------
/src/gtkhelper.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | void gtk_widget_set_margin(GtkWidget *w, int margin);
9 | void gtk_widget_set_expand(GtkWidget *w, gboolean expand);
10 | void gtk_widget_set_align(GtkWidget *w, GtkAlign x, GtkAlign y);
11 | void gtk_grid_set_spacing(GtkGrid *grid, int spacing);
12 | void gtk_widget_remove_css_classes_by_prefix(GtkWidget *w, const char *prefix);
13 |
--------------------------------------------------------------------------------
/src/hardware.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 |
6 | #include "hardware.h"
7 |
8 | struct scarlett2_device scarlett2_supported[] = {
9 | { 0x8203, "Scarlett 2nd Gen 6i6" },
10 | { 0x8204, "Scarlett 2nd Gen 18i8" },
11 | { 0x8201, "Scarlett 2nd Gen 18i20" },
12 | { 0x8211, "Scarlett 3rd Gen Solo" },
13 | { 0x8210, "Scarlett 3rd Gen 2i2" },
14 | { 0x8212, "Scarlett 3rd Gen 4i4" },
15 | { 0x8213, "Scarlett 3rd Gen 8i6" },
16 | { 0x8214, "Scarlett 3rd Gen 18i8" },
17 | { 0x8215, "Scarlett 3rd Gen 18i20" },
18 | { 0x8216, "Vocaster One" },
19 | { 0x8217, "Vocaster Two" },
20 | { 0x8218, "Scarlett 4th Gen Solo" },
21 | { 0x8219, "Scarlett 4th Gen 2i2" },
22 | { 0x821a, "Scarlett 4th Gen 4i4" },
23 | { 0x8206, "Clarett USB 2Pre" },
24 | { 0x8207, "Clarett USB 4Pre" },
25 | { 0x8208, "Clarett USB 8Pre" },
26 | { 0x820a, "Clarett+ 2Pre" },
27 | { 0x820b, "Clarett+ 4Pre" },
28 | { 0x820c, "Clarett+ 8Pre" },
29 | { 0, NULL }
30 | };
31 |
32 | struct scarlett2_device *get_device_for_pid(int pid) {
33 | for (int i = 0; scarlett2_supported[i].name; i++)
34 | if (scarlett2_supported[i].pid == pid)
35 | return &scarlett2_supported[i];
36 |
37 | return NULL;
38 | }
39 |
--------------------------------------------------------------------------------
/src/hardware.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | // Supported devices
5 | struct scarlett2_device {
6 | int pid;
7 | const char *name;
8 | };
9 |
10 | struct scarlett2_device *get_device_for_pid(int pid);
11 |
--------------------------------------------------------------------------------
/src/iface-mixer.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | GtkWidget *create_iface_mixer_main(struct alsa_card *card);
9 |
--------------------------------------------------------------------------------
/src/iface-no-mixer.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "gtkhelper.h"
5 | #include "iface-no-mixer.h"
6 | #include "stringhelper.h"
7 | #include "tooltips.h"
8 | #include "widget-boolean.h"
9 | #include "widget-drop-down.h"
10 | #include "window-helper.h"
11 | #include "window-startup.h"
12 |
13 | GtkWidget *create_iface_no_mixer_main(struct alsa_card *card) {
14 | GArray *elems = card->elems;
15 |
16 | GtkWidget *top = gtk_frame_new(NULL);
17 | gtk_widget_add_css_class(top, "window-frame");
18 |
19 | GtkWidget *content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 15);
20 | gtk_widget_add_css_class(content, "window-content");
21 | gtk_widget_add_css_class(content, "iface-no-mixer");
22 | gtk_frame_set_child(GTK_FRAME(top), content);
23 |
24 | GtkWidget *input_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
25 | GtkWidget *output_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
26 | gtk_box_append(GTK_BOX(content), input_box);
27 | gtk_box_append(GTK_BOX(content), output_box);
28 |
29 | GtkWidget *label_ic = gtk_label_new("Input Controls");
30 | GtkWidget *label_oc = gtk_label_new("Output Controls");
31 |
32 | gtk_widget_add_css_class(label_ic, "controls-label");
33 | gtk_widget_add_css_class(label_oc, "controls-label");
34 |
35 | gtk_widget_set_halign(label_ic, GTK_ALIGN_START);
36 | gtk_widget_set_halign(label_oc, GTK_ALIGN_START);
37 |
38 | gtk_box_append(GTK_BOX(input_box), label_ic);
39 | gtk_box_append(GTK_BOX(output_box), label_oc);
40 |
41 | GtkWidget *input_grid = gtk_grid_new();
42 | gtk_grid_set_spacing(GTK_GRID(input_grid), 10);
43 | gtk_widget_add_css_class(input_grid, "controls-content");
44 | gtk_widget_set_vexpand(input_grid, TRUE);
45 | gtk_box_append(GTK_BOX(input_box), input_grid);
46 |
47 | GtkWidget *output_grid = gtk_grid_new();
48 | gtk_grid_set_spacing(GTK_GRID(output_grid), 10);
49 | gtk_widget_add_css_class(output_grid, "controls-content");
50 | gtk_widget_set_vexpand(output_grid, TRUE);
51 | gtk_box_append(GTK_BOX(output_box), output_grid);
52 |
53 | // Solo or 2i2?
54 | // Solo Phantom Power is Line 1 only
55 | // 2i2 Phantom Power is Line 1-2
56 | int is_solo = !!get_elem_by_name(
57 | elems, "Line In 1 Phantom Power Capture Switch"
58 | );
59 |
60 | for (int i = 0; i < 2; i++) {
61 | char s[20];
62 | snprintf(s, 20, "%d", i + 1);
63 | GtkWidget *label = gtk_label_new(s);
64 | gtk_grid_attach(GTK_GRID(input_grid), label, i, 0, 1, 1);
65 | }
66 |
67 | for (int i = 0; i < elems->len; i++) {
68 | struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
69 | GtkWidget *w;
70 |
71 | // if no card entry, it's not a bool/enum/int elem
72 | if (!elem->card)
73 | continue;
74 |
75 | if (strstr(elem->name, "Validity"))
76 | continue;
77 |
78 | int line_num = get_num_from_string(elem->name);
79 |
80 | if (strstr(elem->name, "Level Capture Enum")) {
81 | w = make_boolean_alsa_elem(elem, "Inst", NULL);
82 | gtk_widget_add_css_class(w, "inst");
83 | gtk_widget_set_tooltip_text(w, level_descr);
84 | gtk_grid_attach(GTK_GRID(input_grid), w, line_num - 1, 1, 1, 1);
85 | } else if (strstr(elem->name, "Air Capture Switch")) {
86 | w = make_boolean_alsa_elem(elem, "Air", NULL);
87 | gtk_widget_add_css_class(w, "air");
88 | gtk_widget_set_tooltip_text(w, air_descr);
89 | gtk_grid_attach(
90 | GTK_GRID(input_grid), w, line_num - 1, 1 + !is_solo, 1, 1
91 | );
92 | } else if (strstr(elem->name, "Phantom Power Capture Switch")) {
93 | w = make_boolean_alsa_elem(elem, "48V", NULL);
94 | gtk_widget_add_css_class(w, "phantom");
95 | gtk_widget_set_tooltip_text(w, phantom_descr);
96 | gtk_grid_attach(GTK_GRID(input_grid), w, 0, 3, 1 + !is_solo, 1);
97 | } else if (strcmp(elem->name, "Direct Monitor Playback Switch") == 0) {
98 | w = make_boolean_alsa_elem(elem, "Direct Monitor", NULL);
99 | gtk_widget_add_css_class(w, "direct-monitor");
100 | gtk_widget_set_tooltip_text(
101 | w,
102 | "Direct Monitor sends the analogue input signals to the "
103 | "analogue outputs for zero-latency monitoring."
104 | );
105 | gtk_grid_attach(GTK_GRID(output_grid), w, 0, 0, 1, 1);
106 | } else if (strcmp(elem->name, "Direct Monitor Playback Enum") == 0) {
107 | w = make_drop_down_alsa_elem(elem, "Direct Monitor");
108 | gtk_widget_add_css_class(w, "direct-monitor");
109 | gtk_widget_set_tooltip_text(
110 | w,
111 | "Direct Monitor sends the analogue input signals to the "
112 | "analogue outputs for zero-latency monitoring. Mono sends "
113 | "both inputs to the left and right outputs. Stereo sends "
114 | "input 1 to the left, and input 2 to the right output."
115 | );
116 | gtk_grid_attach(GTK_GRID(output_grid), w, 0, 0, 1, 1);
117 | }
118 | }
119 |
120 | card->window_startup = create_subwindow(
121 | card, "Startup Configuration", G_CALLBACK(window_startup_close_request)
122 | );
123 |
124 | GtkWidget *startup = create_startup_controls(card);
125 | gtk_window_set_child(GTK_WINDOW(card->window_startup), startup);
126 |
127 | return top;
128 | }
129 |
--------------------------------------------------------------------------------
/src/iface-no-mixer.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | GtkWidget *create_iface_no_mixer_main(struct alsa_card *card);
9 |
--------------------------------------------------------------------------------
/src/iface-none.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "alsa.h"
5 | #include "iface-none.h"
6 | #include "gtkhelper.h"
7 | #include "menu.h"
8 |
9 | GtkWidget *create_window_iface_none(GtkApplication *app) {
10 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 50);
11 | gtk_widget_set_margin(box, 50);
12 | GtkWidget *picture = gtk_picture_new_for_resource(
13 | "/vu/b4/alsa-scarlett-gui/icons/vu.b4.alsa-scarlett-gui.png"
14 | );
15 | GtkWidget *label = gtk_label_new("No Scarlett/Clarett/Vocaster interface found.");
16 |
17 | gtk_box_append(GTK_BOX(box), picture);
18 | gtk_box_append(GTK_BOX(box), label);
19 |
20 | GtkWidget *w = gtk_application_window_new(app);
21 | gtk_window_set_resizable(GTK_WINDOW(w), FALSE);
22 | gtk_window_set_title(GTK_WINDOW(w), "ALSA Scarlett Control Panel");
23 | gtk_window_set_child(GTK_WINDOW(w), box);
24 | gtk_application_window_set_show_menubar(
25 | GTK_APPLICATION_WINDOW(w), TRUE
26 | );
27 | add_window_action_map(GTK_WINDOW(w));
28 | if (!alsa_has_reopen_callbacks()) {
29 | gtk_widget_set_visible(w, TRUE);
30 | }
31 |
32 | return w;
33 | }
34 |
--------------------------------------------------------------------------------
/src/iface-none.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | GtkWidget *create_window_iface_none(GtkApplication *app);
9 |
--------------------------------------------------------------------------------
/src/iface-unknown.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "gtkhelper.h"
5 | #include "iface-unknown.h"
6 |
7 | GtkWidget *create_iface_unknown_main(void) {
8 | GtkWidget *label = gtk_label_new(
9 | "Sorry, I don’t recognise the controls on this card.\n\n"
10 |
11 | "These Focusrite models should be supported:\n"
12 | "– Gen 1: 6i6/8i6/18i6/18i8/18i20\n"
13 | "– Gen 2: 6i6/18i8/18i20\n"
14 | "– Gen 3: Solo/2i2/4i4/8i6/18i8/18i20\n"
15 | "– Gen 4: Solo/2i2/4i4/16i16/18i16/18i20\n"
16 | "– Vocaster One and Two\n"
17 | "– Clarett USB and Clarett+ 2Pre/4Pre/8Pre\n\n"
18 |
19 | "Please check the prerequisites at:\n"
20 | "https://github.com/geoffreybennett/alsa-scarlett-gui/"
21 | );
22 | gtk_widget_set_margin(label, 30);
23 |
24 | return label;
25 | }
26 |
--------------------------------------------------------------------------------
/src/iface-unknown.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | GtkWidget *create_iface_unknown_main(void);
9 |
--------------------------------------------------------------------------------
/src/iface-update.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 |
6 | #include "alsa.h"
7 | #include "device-update-firmware.h"
8 | #include "gtkhelper.h"
9 | #include "scarlett2-firmware.h"
10 |
11 | GtkWidget *create_iface_update_main(struct alsa_card *card) {
12 | GtkWidget *top = gtk_frame_new(NULL);
13 | gtk_widget_add_css_class(top, "window-frame");
14 |
15 | GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 30);
16 | gtk_widget_add_css_class(content, "window-content");
17 | gtk_widget_add_css_class(content, "top-level-content");
18 | gtk_widget_add_css_class(content, "big-padding");
19 | gtk_frame_set_child(GTK_FRAME(top), content);
20 |
21 | // explanation
22 | GtkWidget *w;
23 |
24 | w = gtk_label_new("Firmware Update Required");
25 | gtk_widget_add_css_class(w, "window-title");
26 | gtk_box_append(GTK_BOX(content), w);
27 |
28 | if (!card->best_firmware_version) {
29 | w = gtk_label_new(NULL);
30 | gtk_label_set_markup(
31 | GTK_LABEL(w),
32 | "A firmware update is required for this device in order to\n"
33 | "access all of its features. Please obtain the firmware from\n"
34 | ""
36 | "https://github.com/geoffreybennett/scarlett2-firmware,\n"
37 | "and restart this application."
38 | );
39 |
40 | gtk_box_append(GTK_BOX(content), w);
41 | return top;
42 | }
43 |
44 | w = gtk_label_new(
45 | "A firmware update is required for this device in order to\n"
46 | "access all of its features. This process will take about 15\n"
47 | "seconds. Please do not disconnect the device during the\n"
48 | "update."
49 | );
50 | gtk_box_append(GTK_BOX(content), w);
51 |
52 | w = gtk_button_new_with_label("Update");
53 | g_signal_connect(
54 | GTK_BUTTON(w), "clicked", G_CALLBACK(create_update_firmware_window), card
55 | );
56 | gtk_box_append(GTK_BOX(content), w);
57 |
58 | return top;
59 | }
60 |
--------------------------------------------------------------------------------
/src/iface-update.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | GtkWidget *create_iface_update_main(struct alsa_card *card);
9 |
--------------------------------------------------------------------------------
/src/iface-waiting.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 |
6 | #include "alsa.h"
7 | #include "iface-waiting.h"
8 | #include "scarlett2-ioctls.h"
9 | #include "window-iface.h"
10 |
11 | // Structure to hold timeout-related widgets
12 | struct timeout_data {
13 | GtkWidget *box;
14 | GtkWidget *spinner;
15 | GtkWidget *message_label;
16 | guint timeout_id;
17 | };
18 |
19 | // Timeout callback function
20 | static gboolean on_timeout(gpointer user_data) {
21 | struct timeout_data *data = (struct timeout_data *)user_data;
22 |
23 | // Remove spinner
24 | gtk_box_remove(GTK_BOX(data->box), data->spinner);
25 |
26 | // Update message with clickable link
27 | if (data->message_label && GTK_IS_WIDGET(data->message_label))
28 | gtk_label_set_markup(
29 | GTK_LABEL(data->message_label),
30 | "Driver not detected. Please ensure "
31 | "fcp-server from "
32 | ""
33 | "https://github.com/geoffreybennett/fcp-support "
34 | "has been installed."
35 | );
36 |
37 | // Reset the timeout ID since it won't be called again
38 | data->timeout_id = 0;
39 |
40 | // Return FALSE to prevent the timeout from repeating
41 | return FALSE;
42 | }
43 |
44 | // Weak reference callback for cleanup
45 | static void on_widget_dispose(gpointer data, GObject *where_the_object_was) {
46 | struct timeout_data *timeout_data = (struct timeout_data *)data;
47 |
48 | // Cancel the timeout if it's still active
49 | if (timeout_data->timeout_id > 0)
50 | g_source_remove(timeout_data->timeout_id);
51 |
52 | // Free the data structure
53 | g_free(timeout_data);
54 | }
55 |
56 | GtkWidget *create_iface_waiting_main(struct alsa_card *card) {
57 | struct timeout_data *data;
58 |
59 | // Main vertical box
60 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20);
61 | gtk_widget_set_margin_start(box, 40);
62 | gtk_widget_set_margin_end(box, 40);
63 | gtk_widget_set_margin_top(box, 40);
64 | gtk_widget_set_margin_bottom(box, 40);
65 |
66 | // Heading
67 | GtkWidget *label = gtk_label_new(NULL);
68 | gtk_label_set_markup(GTK_LABEL(label),
69 | "Waiting for FCP Server");
70 | gtk_box_append(GTK_BOX(box), label);
71 |
72 | // Add picture (scaled down properly)
73 | GtkWidget *picture_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
74 | gtk_widget_set_hexpand(picture_box, TRUE);
75 | gtk_widget_set_halign(picture_box, GTK_ALIGN_CENTER);
76 |
77 | GtkWidget *picture = gtk_picture_new_for_resource(
78 | "/vu/b4/alsa-scarlett-gui/icons/vu.b4.alsa-scarlett-gui.png"
79 | );
80 | gtk_picture_set_can_shrink(GTK_PICTURE(picture), TRUE);
81 | gtk_widget_set_size_request(picture, 128, 128);
82 |
83 | gtk_box_append(GTK_BOX(picture_box), picture);
84 | gtk_box_append(GTK_BOX(box), picture_box);
85 |
86 | // Add spinner
87 | GtkWidget *spinner = gtk_spinner_new();
88 | gtk_spinner_start(GTK_SPINNER(spinner));
89 | gtk_widget_set_size_request(spinner, 48, 48);
90 | gtk_box_append(GTK_BOX(box), spinner);
91 |
92 | // Description
93 | label = gtk_label_new(
94 | "Waiting for the user-space FCP driver to initialise..."
95 | );
96 | gtk_label_set_wrap(GTK_LABEL(label), TRUE);
97 | gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
98 | gtk_label_set_max_width_chars(GTK_LABEL(label), 1);
99 | gtk_widget_set_hexpand(label, TRUE);
100 | gtk_widget_set_halign(label, GTK_ALIGN_FILL);
101 |
102 | gtk_box_append(GTK_BOX(box), label);
103 |
104 | // Setup timeout
105 | data = g_new(struct timeout_data, 1);
106 | data->box = box;
107 | data->spinner = spinner;
108 | data->message_label = label;
109 |
110 | // Set timeout
111 | data->timeout_id = g_timeout_add_seconds(5, on_timeout, data);
112 |
113 | // Ensure data is freed when the box is destroyed
114 | g_object_weak_ref(G_OBJECT(box), on_widget_dispose, data);
115 |
116 | return box;
117 | }
118 |
--------------------------------------------------------------------------------
/src/iface-waiting.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | GtkWidget *create_iface_waiting_main(struct alsa_card *card);
9 |
--------------------------------------------------------------------------------
/src/img/audio-volume-high.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/img/audio-volume-low.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/img/audio-volume-medium.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/src/img/audio-volume-muted.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/src/img/socket.svg:
--------------------------------------------------------------------------------
1 |
2 |
19 |
--------------------------------------------------------------------------------
/src/img/vu.b4.alsa-scarlett-gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geoffreybennett/alsa-scarlett-gui/d731819de5f8a6b966e000f90782a1a4179085f9/src/img/vu.b4.alsa-scarlett-gui.png
--------------------------------------------------------------------------------
/src/main.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "alsa.h"
5 | #include "alsa-sim.h"
6 | #include "main.h"
7 | #include "menu.h"
8 | #include "scarlett2-firmware.h"
9 | #include "window-hardware.h"
10 | #include "window-iface.h"
11 |
12 | GtkApplication *app;
13 |
14 | // CSS
15 |
16 | static void load_css(void) {
17 | GtkCssProvider *css = gtk_css_provider_new();
18 | GdkDisplay *display = gdk_display_get_default();
19 |
20 | gtk_style_context_add_provider_for_display(
21 | display, GTK_STYLE_PROVIDER(css), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
22 | );
23 | gtk_css_provider_load_from_resource(
24 | css,
25 | "/vu/b4/alsa-scarlett-gui/alsa-scarlett-gui.css"
26 | );
27 |
28 | g_object_unref(css);
29 | }
30 |
31 | // gtk init
32 |
33 | static void startup(GtkApplication *app, gpointer user_data) {
34 | gtk_application_set_menubar(app, G_MENU_MODEL(create_app_menu(app)));
35 |
36 | load_css();
37 |
38 | scarlett2_enum_firmware();
39 | alsa_init();
40 |
41 | create_no_card_window();
42 | create_hardware_window(app);
43 | }
44 |
45 | // not called when any files are opened from the command-line so we do
46 | // everything in startup(), but GTK wants this signal handled
47 | // regardless
48 | static void activate(GtkApplication *app, gpointer user_data) {
49 | }
50 |
51 | static void open_cb(
52 | GtkApplication *app,
53 | GFile **files,
54 | gint n_files,
55 | const gchar *hint
56 | ) {
57 | for (int i = 0; i < n_files; i++) {
58 | char *fn = g_file_get_path(files[i]);
59 | create_sim_from_file(NULL, fn);
60 | g_free(fn);
61 | }
62 | }
63 |
64 | int main(int argc, char **argv) {
65 | app = gtk_application_new(
66 | "vu.b4.alsa-scarlett-gui", G_APPLICATION_HANDLES_OPEN
67 | );
68 | g_signal_connect(app, "startup", G_CALLBACK(startup), NULL);
69 | g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
70 | g_signal_connect(app, "open", G_CALLBACK(open_cb), NULL);
71 | int status = g_application_run(G_APPLICATION(app), argc, argv);
72 | g_object_unref(app);
73 |
74 | return status;
75 | }
76 |
--------------------------------------------------------------------------------
/src/main.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | extern GtkApplication *app;
9 |
--------------------------------------------------------------------------------
/src/menu.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "about.h"
5 | #include "file.h"
6 | #include "menu.h"
7 | #include "window-hardware.h"
8 |
9 | // helper for common code of activate_*() functions
10 | static void update_visibility(
11 | GSimpleAction *action,
12 | GtkWidget *widget
13 | ) {
14 | GVariant *state = g_action_get_state(G_ACTION(action));
15 | gboolean new_state = !g_variant_get_boolean(state);
16 |
17 | g_action_change_state(G_ACTION(action), g_variant_new_boolean(new_state));
18 | gtk_widget_set_visible(widget, new_state);
19 | }
20 |
21 | static void activate_hardware(
22 | GSimpleAction *action,
23 | GVariant *parameter,
24 | gpointer data
25 | ) {
26 | (void) data;
27 | update_visibility(action, window_hardware);
28 | }
29 |
30 | static void activate_quit(
31 | GSimpleAction *action,
32 | GVariant *parameter,
33 | gpointer data
34 | ) {
35 | g_application_quit(G_APPLICATION(data));
36 | }
37 |
38 | static void activate_routing(
39 | GSimpleAction *action,
40 | GVariant *parameter,
41 | gpointer data
42 | ) {
43 | struct alsa_card *card = data;
44 |
45 | update_visibility(action, card->window_routing);
46 | }
47 |
48 | static void activate_mixer(
49 | GSimpleAction *action,
50 | GVariant *parameter,
51 | gpointer data
52 | ) {
53 | struct alsa_card *card = data;
54 |
55 | update_visibility(action, card->window_mixer);
56 | }
57 |
58 | static void activate_levels(
59 | GSimpleAction *action,
60 | GVariant *parameter,
61 | gpointer data
62 | ) {
63 | struct alsa_card *card = data;
64 |
65 | update_visibility(action, card->window_levels);
66 | }
67 |
68 | static void activate_startup(
69 | GSimpleAction *action,
70 | GVariant *parameter,
71 | gpointer data
72 | ) {
73 | struct alsa_card *card = data;
74 |
75 | update_visibility(action, card->window_startup);
76 | }
77 |
78 | static const GActionEntry app_entries[] = {
79 | {"hardware", activate_hardware, NULL, "false"},
80 | {"quit", activate_quit},
81 | };
82 |
83 | struct menu_item {
84 | const char *label;
85 | const char *action_name;
86 | const char *accelerators[2];
87 | };
88 |
89 | struct menu_data {
90 | const char *label;
91 | struct menu_item *items;
92 | };
93 |
94 | static const struct menu_data menus[] = {
95 | {
96 | "_File",
97 | (struct menu_item[]){
98 | { "_Load Configuration", "win.load", { "O", NULL } },
99 | { "_Save Configuration", "win.save", { "S", NULL } },
100 | { "_Interface Simulation", "win.sim", { "I", NULL } },
101 | { "E_xit", "app.quit", { "Q", NULL } },
102 | {}
103 | }
104 | },
105 | {
106 | "_View",
107 | (struct menu_item[]){
108 | { "_Routing", "win.routing", { "R", NULL } },
109 | { "_Mixer", "win.mixer", { "M", NULL } },
110 | { "_Levels", "win.levels", { "L", NULL } },
111 | { "_Startup", "win.startup", { "T", NULL } },
112 | {}
113 | }
114 | },
115 | {
116 | "_Help",
117 | (struct menu_item[]){
118 | { "_Supported Hardware", "app.hardware", { "H", NULL } },
119 | { "_About", "win.about", { "slash", NULL } },
120 | {}
121 | }
122 | },
123 | {}
124 | };
125 |
126 | static void populate_submenu(
127 | GtkApplication *app,
128 | GMenu *menu,
129 | const struct menu_data *data
130 | ) {
131 | GMenu *submenu = g_menu_new();
132 | g_menu_append_submenu(menu, data->label, G_MENU_MODEL(submenu));
133 |
134 | // An empty-initialised menu_item marks the end
135 | for (struct menu_item *item = data->items; item->label; item++) {
136 | g_menu_append(submenu, item->label, item->action_name);
137 | gtk_application_set_accels_for_action(
138 | app, item->action_name, item->accelerators
139 | );
140 | }
141 | }
142 |
143 | GMenu *create_app_menu(GtkApplication *app) {
144 | g_action_map_add_action_entries(
145 | G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app
146 | );
147 |
148 | GMenu *menu = g_menu_new();
149 |
150 | for (const struct menu_data *menu_data = menus;
151 | menu_data->label;
152 | menu_data++)
153 | populate_submenu(app, menu, menu_data);
154 |
155 | return menu;
156 | }
157 |
158 | static const GActionEntry win_entries[] = {
159 | {"about", activate_about},
160 | {"sim", activate_sim}
161 | };
162 |
163 | void add_window_action_map(GtkWindow *w) {
164 | g_action_map_add_action_entries(
165 | G_ACTION_MAP(w), win_entries, G_N_ELEMENTS(win_entries), w
166 | );
167 | }
168 |
169 | static const GActionEntry load_save_entries[] = {
170 | {"load", activate_load},
171 | {"save", activate_save}
172 | };
173 |
174 | void add_load_save_action_map(struct alsa_card *card) {
175 | g_action_map_add_action_entries(
176 | G_ACTION_MAP(card->window_main),
177 | load_save_entries,
178 | G_N_ELEMENTS(load_save_entries),
179 | card
180 | );
181 | }
182 |
183 | static const GActionEntry startup_entry[] = {
184 | {"startup", activate_startup, NULL, "false"}
185 | };
186 |
187 | void add_startup_action_map(struct alsa_card *card) {
188 | g_action_map_add_action_entries(
189 | G_ACTION_MAP(card->window_main),
190 | startup_entry,
191 | G_N_ELEMENTS(startup_entry),
192 | card
193 | );
194 | }
195 |
196 | static const GActionEntry mixer_entries[] = {
197 | {"routing", activate_routing, NULL, "false"},
198 | {"mixer", activate_mixer, NULL, "false"}
199 | };
200 |
201 | static const GActionEntry levels_entries[] = {
202 | {"levels", activate_levels, NULL, "false"}
203 | };
204 |
205 | void add_mixer_action_map(struct alsa_card *card) {
206 | g_action_map_add_action_entries(
207 | G_ACTION_MAP(card->window_main),
208 | mixer_entries,
209 | G_N_ELEMENTS(mixer_entries),
210 | card
211 | );
212 |
213 | // Hide the levels menu item if there is no "Firmware Version"
214 | // control (working kernel support for level meters was added in the
215 | // same version as the "Firmware Version" control)
216 | if (get_elem_by_name(card->elems, "Firmware Version")) {
217 | g_action_map_add_action_entries(
218 | G_ACTION_MAP(card->window_main),
219 | levels_entries,
220 | G_N_ELEMENTS(levels_entries),
221 | card
222 | );
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/menu.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | GMenu *create_app_menu(GtkApplication *app);
11 | void add_window_action_map(GtkWindow *w);
12 | void add_load_save_action_map(struct alsa_card *card);
13 | void add_startup_action_map(struct alsa_card *card);
14 | void add_mixer_action_map(struct alsa_card *card);
15 |
--------------------------------------------------------------------------------
/src/routing-drag-line.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "routing-drag-line.h"
5 | #include "routing-lines.h"
6 |
7 | static void drag_enter(
8 | GtkDropControllerMotion *motion,
9 | gdouble x,
10 | gdouble y,
11 | gpointer data
12 | ) {
13 | struct alsa_card *card = data;
14 |
15 | card->drag_x = x;
16 | card->drag_y = y;
17 | gtk_widget_queue_draw(card->drag_line);
18 | gtk_widget_queue_draw(card->routing_lines);
19 | }
20 |
21 | static void drag_leave(
22 | GtkDropControllerMotion *motion,
23 | gpointer data
24 | ) {
25 | struct alsa_card *card = data;
26 |
27 | card->drag_x = -1;
28 | card->drag_y = -1;
29 | gtk_widget_queue_draw(card->drag_line);
30 | gtk_widget_queue_draw(card->routing_lines);
31 | }
32 |
33 | static void drag_motion(
34 | GtkDropControllerMotion *motion,
35 | gdouble x,
36 | gdouble y,
37 | gpointer data
38 | ) {
39 | struct alsa_card *card = data;
40 |
41 | card->drag_x = x;
42 | card->drag_y = y;
43 |
44 | // Retrieve the scrolled window and its child
45 | GtkWindow *win = GTK_WINDOW(card->window_routing);
46 | GtkScrolledWindow *sw = GTK_SCROLLED_WINDOW(gtk_window_get_child(win));
47 | GtkWidget *child = gtk_scrolled_window_get_child(sw);
48 |
49 | // Get horizontal and vertical adjustments for the scrolled window
50 | GtkAdjustment *hadj = gtk_scrolled_window_get_hadjustment(sw);
51 | GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(sw);
52 |
53 | // Calculate the total scrollable width and height
54 | double w = gtk_adjustment_get_upper(hadj) -
55 | gtk_adjustment_get_page_size(hadj);
56 | double h = gtk_adjustment_get_upper(vadj) -
57 | gtk_adjustment_get_page_size(vadj);
58 |
59 | // Determine the relative size of the scrollable area
60 | double rel_w = gtk_adjustment_get_upper(hadj) -
61 | gtk_widget_get_allocated_width(GTK_WIDGET(sw)) +
62 | gtk_widget_get_allocated_width(child);
63 | double rel_h = gtk_adjustment_get_upper(vadj) -
64 | gtk_widget_get_allocated_height(GTK_WIDGET(sw)) +
65 | gtk_widget_get_allocated_height(child);
66 |
67 | // Add margin
68 | rel_w -= 100;
69 | rel_h -= 100;
70 | x -= 50;
71 | y -= 50;
72 | if (x < 0) x = 0;
73 | if (y < 0) y = 0;
74 | if (x > rel_w) x = rel_w;
75 | if (y > rel_h) y = rel_h;
76 |
77 | // Calculate new scroll positions based on mouse coordinates
78 | double new_hpos = (x / rel_w) * w;
79 | double new_vpos = (y / rel_h) * h;
80 |
81 | // Update the scrolled window's position
82 | gtk_adjustment_set_value(vadj, new_vpos);
83 | gtk_adjustment_set_value(hadj, new_hpos);
84 |
85 | gtk_widget_queue_draw(card->drag_line);
86 | gtk_widget_queue_draw(card->routing_lines);
87 | }
88 |
89 | void add_drop_controller_motion(
90 | struct alsa_card *card,
91 | GtkWidget *routing_overlay
92 | ) {
93 |
94 | // create an area to draw the drag line on
95 | card->drag_line = gtk_drawing_area_new();
96 | gtk_widget_set_can_target(card->drag_line, FALSE);
97 | gtk_drawing_area_set_draw_func(
98 | GTK_DRAWING_AREA(card->drag_line), draw_drag_line, card, NULL
99 | );
100 | gtk_overlay_add_overlay(
101 | GTK_OVERLAY(routing_overlay), card->drag_line
102 | );
103 |
104 | // create a controller to handle the dragging
105 | GtkEventController *controller = gtk_drop_controller_motion_new();
106 | g_signal_connect(controller, "enter", G_CALLBACK(drag_enter), card);
107 | g_signal_connect(controller, "leave", G_CALLBACK(drag_leave), card);
108 | g_signal_connect(controller, "motion", G_CALLBACK(drag_motion), card);
109 | gtk_widget_add_controller(card->routing_grid, controller);
110 | }
111 |
--------------------------------------------------------------------------------
/src/routing-drag-line.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | void add_drop_controller_motion(
9 | struct alsa_card *card,
10 | GtkWidget *routing_overlay
11 | );
12 |
--------------------------------------------------------------------------------
/src/routing-lines.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | void draw_routing_lines(
9 | GtkDrawingArea *drawing_area,
10 | cairo_t *cr,
11 | int width,
12 | int height,
13 | void *user_data
14 | );
15 |
16 | void draw_drag_line(
17 | GtkDrawingArea *drawing_area,
18 | cairo_t *cr,
19 | int width,
20 | int height,
21 | void *user_data
22 | );
23 |
--------------------------------------------------------------------------------
/src/scarlett2-firmware.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | // System-wide firmware directory
9 | #define SCARLETT2_FIRMWARE_DIR "/usr/lib/firmware/scarlett2"
10 |
11 | #define MAGIC_STRING "SCARLETT"
12 |
13 | struct scarlett2_firmware_header {
14 | char magic[8]; // "SCARLETT"
15 | uint16_t usb_vid; // Big-endian
16 | uint16_t usb_pid; // Big-endian
17 | uint32_t firmware_version; // Big-endian
18 | uint32_t firmware_length; // Big-endian
19 | uint8_t sha256[32];
20 | } __attribute__((packed));
21 |
22 | struct scarlett2_firmware_file {
23 | struct scarlett2_firmware_header header;
24 | uint8_t *firmware_data;
25 | };
26 |
27 | struct scarlett2_firmware_header *scarlett2_read_firmware_header(
28 | const char *fn
29 | );
30 |
31 | void scarlett2_free_firmware_header(
32 | struct scarlett2_firmware_header *firmware
33 | );
34 |
35 | struct scarlett2_firmware_file *scarlett2_read_firmware_file(
36 | const char *fn
37 | );
38 |
39 | void scarlett2_free_firmware_file(
40 | struct scarlett2_firmware_file *firmware
41 | );
42 |
43 | void scarlett2_enum_firmware(void);
44 |
45 | uint32_t scarlett2_get_best_firmware_version(uint32_t pid);
46 | struct scarlett2_firmware_file *scarlett2_get_best_firmware(uint32_t pid);
47 |
--------------------------------------------------------------------------------
/src/scarlett2-ioctls.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 | #include
6 |
7 | #include "scarlett2.h"
8 |
9 | #include "scarlett2-ioctls.h"
10 |
11 | int scarlett2_open_card(char *alsa_name, snd_hwdep_t **hwdep) {
12 | return snd_hwdep_open(hwdep, alsa_name, SND_HWDEP_OPEN_DUPLEX);
13 | }
14 |
15 | int scarlett2_get_protocol_version(snd_hwdep_t *hwdep) {
16 | int version = 0;
17 | int err = snd_hwdep_ioctl(hwdep, SCARLETT2_IOCTL_PVERSION, &version);
18 |
19 | if (err < 0)
20 | return err;
21 | return version;
22 | }
23 |
24 | int scarlett2_close(snd_hwdep_t *hwdep) {
25 | return snd_hwdep_close(hwdep);
26 | }
27 |
28 | int scarlett2_reboot(snd_hwdep_t *hwdep) {
29 | return snd_hwdep_ioctl(hwdep, SCARLETT2_IOCTL_REBOOT, 0);
30 | }
31 |
32 | static int scarlett2_select_flash_segment(snd_hwdep_t *hwdep, int segment) {
33 | return snd_hwdep_ioctl(hwdep, SCARLETT2_IOCTL_SELECT_FLASH_SEGMENT, &segment);
34 | }
35 |
36 | static int scarlett2_erase_flash_segment(snd_hwdep_t *hwdep) {
37 | return snd_hwdep_ioctl(hwdep, SCARLETT2_IOCTL_ERASE_FLASH_SEGMENT, 0);
38 | }
39 |
40 | int scarlett2_erase_config(snd_hwdep_t *hwdep) {
41 | int err;
42 |
43 | err = scarlett2_select_flash_segment(hwdep, SCARLETT2_SEGMENT_ID_SETTINGS);
44 | if (err < 0)
45 | return err;
46 | return scarlett2_erase_flash_segment(hwdep);
47 | }
48 |
49 | int scarlett2_erase_firmware(snd_hwdep_t *hwdep) {
50 | int err;
51 |
52 | err = scarlett2_select_flash_segment(hwdep, SCARLETT2_SEGMENT_ID_FIRMWARE);
53 | if (err < 0)
54 | return err;
55 | return scarlett2_erase_flash_segment(hwdep);
56 | }
57 |
58 | int scarlett2_get_erase_progress(snd_hwdep_t *hwdep) {
59 | struct scarlett2_flash_segment_erase_progress progress;
60 |
61 | int err = snd_hwdep_ioctl(
62 | hwdep, SCARLETT2_IOCTL_GET_ERASE_PROGRESS, &progress
63 | );
64 | if (err < 0)
65 | return err;
66 |
67 | // translate progress from [1..num_blocks, 255] to [[0..100), 255]]
68 | if (progress.num_blocks == 0 ||
69 | progress.progress == 0 ||
70 | progress.progress == 255)
71 | return progress.progress;
72 |
73 | return (progress.progress - 1) * 100 / progress.num_blocks;
74 | }
75 |
--------------------------------------------------------------------------------
/src/scarlett2-ioctls.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #ifndef SCARLETT2_IOCTLS_H
5 | #define SCARLETT2_IOCTLS_H
6 |
7 | #include
8 |
9 | int scarlett2_open_card(char *alsa_name, snd_hwdep_t **hwdep);
10 | int scarlett2_get_protocol_version(snd_hwdep_t *hwdep);
11 | int scarlett2_lock(snd_hwdep_t *hwdep);
12 | int scarlett2_unlock(snd_hwdep_t *hwdep);
13 | int scarlett2_close(snd_hwdep_t *hwdep);
14 |
15 | int scarlett2_reboot(snd_hwdep_t *hwdep);
16 | int scarlett2_erase_config(snd_hwdep_t *hwdep);
17 | int scarlett2_erase_firmware(snd_hwdep_t *hwdep);
18 | int scarlett2_get_erase_progress(snd_hwdep_t *hwdep);
19 | int scarlett2_write_firmware(
20 | snd_hwdep_t *hwdep,
21 | off_t offset,
22 | unsigned char *buf,
23 | size_t buf_len
24 | );
25 |
26 | #endif // SCARLETT2_IOCTLS_H
27 |
--------------------------------------------------------------------------------
/src/scarlett2.h:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
2 | /*
3 | * Focusrite Scarlett 2 Protocol Driver for ALSA
4 | * (including Scarlett 2nd Gen, 3rd Gen, Clarett USB, and Clarett+
5 | * series products)
6 | *
7 | * Copyright (c) 2023 by Geoffrey D. Bennett
8 | */
9 | #ifndef __UAPI_SOUND_SCARLETT2_H
10 | #define __UAPI_SOUND_SCARLETT2_H
11 |
12 | #include
13 | #include
14 |
15 | #define SCARLETT2_HWDEP_MAJOR 1
16 | #define SCARLETT2_HWDEP_MINOR 0
17 | #define SCARLETT2_HWDEP_SUBMINOR 0
18 |
19 | #define SCARLETT2_HWDEP_VERSION \
20 | ((SCARLETT2_HWDEP_MAJOR << 16) | \
21 | (SCARLETT2_HWDEP_MINOR << 8) | \
22 | SCARLETT2_HWDEP_SUBMINOR)
23 |
24 | #define SCARLETT2_HWDEP_VERSION_MAJOR(v) (((v) >> 16) & 0xFF)
25 | #define SCARLETT2_HWDEP_VERSION_MINOR(v) (((v) >> 8) & 0xFF)
26 | #define SCARLETT2_HWDEP_VERSION_SUBMINOR(v) ((v) & 0xFF)
27 |
28 | /* Get protocol version */
29 | #define SCARLETT2_IOCTL_PVERSION _IOR('S', 0x60, int)
30 |
31 | /* Reboot */
32 | #define SCARLETT2_IOCTL_REBOOT _IO('S', 0x61)
33 |
34 | /* Select flash segment */
35 | #define SCARLETT2_SEGMENT_ID_SETTINGS 0
36 | #define SCARLETT2_SEGMENT_ID_FIRMWARE 1
37 | #define SCARLETT2_SEGMENT_ID_COUNT 2
38 |
39 | #define SCARLETT2_IOCTL_SELECT_FLASH_SEGMENT _IOW('S', 0x62, int)
40 |
41 | /* Erase selected flash segment */
42 | #define SCARLETT2_IOCTL_ERASE_FLASH_SEGMENT _IO('S', 0x63)
43 |
44 | /* Get selected flash segment erase progress
45 | * 1 through to num_blocks, or 255 for complete
46 | */
47 | struct scarlett2_flash_segment_erase_progress {
48 | unsigned char progress;
49 | unsigned char num_blocks;
50 | };
51 | #define SCARLETT2_IOCTL_GET_ERASE_PROGRESS \
52 | _IOR('S', 0x64, struct scarlett2_flash_segment_erase_progress)
53 |
54 | #endif /* __UAPI_SOUND_SCARLETT2_H */
55 |
--------------------------------------------------------------------------------
/src/stringhelper.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | #include "stringhelper.h"
9 |
10 | // return the first number found in the string
11 | int get_num_from_string(const char *s) {
12 | int num;
13 |
14 | while (*s) {
15 | if (isdigit(*s))
16 | break;
17 | s++;
18 | }
19 |
20 | if (!*s)
21 | return -1;
22 |
23 | if (!sscanf(s, "%d", &num))
24 | return 0;
25 |
26 | return num;
27 | }
28 |
29 | // return the first two numbers found in the string
30 | void get_two_num_from_string(const char *s, int *a, int *b) {
31 | *a = -1;
32 | *b = -1;
33 |
34 | while (*s) {
35 | if (isdigit(*s))
36 | break;
37 | s++;
38 | }
39 |
40 | if (!*s)
41 | return;
42 |
43 | if (!sscanf(s, "%d", a))
44 | return;
45 |
46 | while (*s) {
47 | if (!isdigit(*s))
48 | break;
49 | s++;
50 | }
51 |
52 | while (*s) {
53 | if (isdigit(*s))
54 | break;
55 | s++;
56 | }
57 |
58 | if (!sscanf(s, "%d", b))
59 | return;
60 | }
61 |
62 | // check if the given string ends with the given suffix
63 | int string_ends_with(const char *s, const char *suffix) {
64 | if (!s || !suffix)
65 | return 0;
66 | int s_len = strlen(s);
67 | int suffix_len = strlen(suffix);
68 | if (s_len < suffix_len)
69 | return 0;
70 | return strcmp(s + s_len - suffix_len, suffix) == 0;
71 | }
72 |
--------------------------------------------------------------------------------
/src/stringhelper.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | int get_num_from_string(const char *s);
7 | void get_two_num_from_string(const char *s, int *a, int *b);
8 | int string_ends_with(const char *s, const char *suffix);
9 |
--------------------------------------------------------------------------------
/src/tooltips.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "tooltips.h"
5 |
6 | // tooltips that are used from multiple files
7 |
8 | const char *level_descr =
9 | "Mic/Line or Instrument Level (Impedance)";
10 |
11 | const char *air_descr =
12 | "Enabling Air will transform your recordings and inspire you while "
13 | "making music.";
14 |
15 | const char *phantom_descr =
16 | "Enabling 48V sends “Phantom Power” to the XLR microphone input. "
17 | "This is required for some microphones (such as condensor "
18 | "microphones), and damaging to some microphones (particularly "
19 | "vintage ribbon microphones).";
20 |
--------------------------------------------------------------------------------
/src/tooltips.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | extern const char *level_descr;
7 | extern const char *air_descr;
8 | extern const char *phantom_descr;
9 |
--------------------------------------------------------------------------------
/src/vu.b4.alsa-scarlett-gui.desktop.template:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Application
3 | Name=ALSA Scarlett Control Panel
4 | Icon=vu.b4.alsa-scarlett-gui
5 | Exec=PREFIX/bin/alsa-scarlett-gui
6 | Categories=GTK;AudioVideo;Audio;Mixer;
7 | Keywords=focusrite;
8 |
--------------------------------------------------------------------------------
/src/widget-boolean.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "gtkhelper.h"
5 | #include "widget-boolean.h"
6 |
7 | struct boolean {
8 | struct alsa_elem *elem;
9 | int backwards;
10 | GtkWidget *button;
11 | guint source;
12 | const char *text[2];
13 | GtkWidget *icons[2];
14 | };
15 |
16 | static void button_clicked(GtkWidget *widget, struct boolean *data) {
17 | int value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
18 |
19 | alsa_set_elem_value(data->elem, value ^ data->backwards);
20 | }
21 |
22 | static void toggle_button_set_text(struct boolean *data, int value) {
23 | const char *text = data->text[value];
24 |
25 | if (!text)
26 | return;
27 |
28 | if (*text == '*')
29 | gtk_button_set_child(GTK_BUTTON(data->button), data->icons[value]);
30 | else
31 | gtk_button_set_label(GTK_BUTTON(data->button), text);
32 | }
33 |
34 | static void toggle_button_updated(
35 | struct alsa_elem *elem,
36 | void *private
37 | ) {
38 | struct boolean *data = private;
39 |
40 | int is_writable = alsa_get_elem_writable(elem);
41 | gtk_widget_set_sensitive(data->button, is_writable);
42 |
43 | int value = !!alsa_get_elem_value(elem) ^ data->backwards;
44 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->button), value);
45 |
46 | toggle_button_set_text(data, value);
47 | }
48 |
49 | static gboolean update_toggle_button(struct boolean *data) {
50 | toggle_button_updated(data->elem, data);
51 |
52 | return G_SOURCE_CONTINUE;
53 | }
54 |
55 | static void on_destroy(struct boolean *data) {
56 | if (data->source)
57 | g_source_remove(data->source);
58 |
59 | for (int i = 0; i < 2; i++)
60 | if (data->icons[i])
61 | g_object_unref(data->icons[i]);
62 |
63 | g_free(data);
64 | }
65 |
66 | static void load_icons(struct boolean *data) {
67 | for (int i = 0; i < 2; i++)
68 | if (data->text[i] && *data->text[i] == '*') {
69 | char *path = g_strdup_printf(
70 | "/vu/b4/alsa-scarlett-gui/icons/%s.svg", data->text[i] + 1
71 | );
72 | data->icons[i] = gtk_image_new_from_resource(path);
73 | gtk_widget_set_align(data->icons[i], GTK_ALIGN_CENTER, GTK_ALIGN_CENTER);
74 | g_object_ref(data->icons[i]);
75 | g_free(path);
76 | }
77 | }
78 |
79 | GtkWidget *make_boolean_alsa_elem(
80 | struct alsa_elem *elem,
81 | const char *disabled_text,
82 | const char *enabled_text
83 | ) {
84 | struct boolean *data = g_malloc0(sizeof(struct boolean));
85 | data->elem = elem;
86 | data->button = gtk_toggle_button_new();
87 |
88 | if (strncmp(elem->name, "Master", 6) == 0 &&
89 | strstr(elem->name, "Playback Switch"))
90 | data->backwards = 1;
91 |
92 | g_signal_connect(
93 | data->button, "clicked", G_CALLBACK(button_clicked), data
94 | );
95 | alsa_elem_add_callback(elem, toggle_button_updated, data);
96 | data->text[0] = disabled_text;
97 | data->text[1] = enabled_text;
98 | load_icons(data);
99 |
100 | // find the maximum width and height of both possible labels
101 | int max_width = 0, max_height = 0;
102 | for (int i = 0; i < 2; i++) {
103 | toggle_button_set_text(data, i);
104 |
105 | GtkRequisition *size = gtk_requisition_new();
106 | gtk_widget_get_preferred_size(data->button, size, NULL);
107 |
108 | if (size->width > max_width)
109 | max_width = size->width;
110 | if (size->height > max_height)
111 | max_height = size->height;
112 | }
113 |
114 | // set the widget minimum size to the maximum label size so that the
115 | // widget doesn't change size when the label changes
116 | gtk_widget_set_size_request(data->button, max_width, max_height);
117 |
118 | toggle_button_updated(elem, data);
119 |
120 | // periodically update volatile controls
121 | if (alsa_get_elem_volatile(elem))
122 | data->source =
123 | g_timeout_add_seconds(1, (GSourceFunc)update_toggle_button, data);
124 |
125 | g_object_weak_ref(G_OBJECT(data->button), (GWeakNotify)on_destroy, data);
126 |
127 | return data->button;
128 | }
129 |
--------------------------------------------------------------------------------
/src/widget-boolean.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | GtkWidget *make_boolean_alsa_elem(
11 | struct alsa_elem *alsa_elem,
12 | const char *disabled_text,
13 | const char *enabled_text
14 | );
15 |
--------------------------------------------------------------------------------
/src/widget-drop-down.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 |
6 | #include "gtkhelper.h"
7 | #include "widget-drop-down.h"
8 |
9 | struct drop_down {
10 | struct alsa_elem *elem;
11 | GtkWidget *button;
12 | GtkWidget *popover;
13 | GtkWidget *listview;
14 | GtkSingleSelection *selection;
15 | int fixed_text;
16 | };
17 |
18 | static void sanitise_class_name(char *s) {
19 | char *dst = s;
20 |
21 | while (*s) {
22 | if (isalnum(*s) || *s == '-')
23 | *dst++ = tolower(*s);
24 | s++;
25 | }
26 |
27 | *dst = '\0';
28 | }
29 |
30 | static void add_class(GtkWidget *widget, const char *class) {
31 | char *class_name = g_strdup_printf("selected-%s", class);
32 |
33 | sanitise_class_name(class_name);
34 | gtk_widget_add_css_class(widget, class_name);
35 | g_free(class_name);
36 | }
37 |
38 | static void list_item_activated(
39 | GtkListItem *list_item,
40 | guint index,
41 | struct drop_down *data
42 | ) {
43 | alsa_set_elem_value(data->elem, index);
44 |
45 | gtk_popover_popdown(GTK_POPOVER(data->popover));
46 | }
47 |
48 | static void toggle_button_clicked(GtkWidget *widget, struct drop_down *data) {
49 | gtk_popover_popup(GTK_POPOVER(data->popover));
50 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->button), FALSE);
51 | }
52 |
53 | static void setup_factory(
54 | GtkListItemFactory *factory,
55 | GtkListItem *list_item,
56 | gpointer user_data
57 | ) {
58 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
59 |
60 | GtkWidget *label = gtk_label_new(NULL);
61 | gtk_label_set_xalign(GTK_LABEL(label), 0.0);
62 | gtk_box_append(GTK_BOX(box), label);
63 |
64 | GtkWidget *icon = gtk_image_new_from_icon_name("object-select-symbolic");
65 | gtk_box_append(GTK_BOX(box), icon);
66 |
67 | gtk_list_item_set_child(list_item, box);
68 | }
69 |
70 | static void update_list_item(
71 | GtkListItem *list_item,
72 | struct drop_down *data
73 | ) {
74 | GtkWidget *box = gtk_list_item_get_child(list_item);
75 | GtkWidget *icon = gtk_widget_get_last_child(box);
76 |
77 | int index = gtk_single_selection_get_selected(data->selection);
78 |
79 | if (index == gtk_list_item_get_position(list_item))
80 | gtk_widget_set_opacity(icon, 1.0);
81 | else
82 | gtk_widget_set_opacity(icon, 0.0);
83 | }
84 |
85 | static void bind_factory(
86 | GtkListItemFactory *factory,
87 | GtkListItem *list_item,
88 | gpointer user_data
89 | ) {
90 | struct drop_down *data = user_data;
91 |
92 | GtkWidget *box = gtk_list_item_get_child(list_item);
93 | GtkWidget *label = gtk_widget_get_first_child(box);
94 |
95 | int index = gtk_list_item_get_position(list_item);
96 | const char *text = alsa_get_item_name(data->elem, index);
97 | gtk_label_set_text(GTK_LABEL(label), text);
98 |
99 | update_list_item(list_item, data);
100 | }
101 |
102 | static void drop_down_updated(
103 | struct alsa_elem *elem,
104 | void *private
105 | ) {
106 | struct drop_down *data = private;
107 |
108 | int is_writable = alsa_get_elem_writable(elem);
109 | gtk_widget_set_sensitive(data->button, is_writable);
110 |
111 | int value = alsa_get_elem_value(elem);
112 | gtk_single_selection_set_selected(data->selection, value);
113 |
114 | gtk_widget_remove_css_classes_by_prefix(data->button, "selected-");
115 | add_class(data->button, alsa_get_item_name(elem, value));
116 |
117 | if (data->fixed_text)
118 | return;
119 |
120 | gtk_button_set_label(
121 | GTK_BUTTON(data->button),
122 | alsa_get_item_name(elem, value)
123 | );
124 | }
125 |
126 | static void drop_down_destroy(GtkWidget *widget, GtkWidget *popover) {
127 | gtk_widget_unparent(popover);
128 | }
129 |
130 | GtkWidget *make_drop_down_alsa_elem(
131 | struct alsa_elem *elem,
132 | const char *label_text
133 | ) {
134 | struct drop_down *data = g_malloc(sizeof(struct drop_down));
135 | data->elem = elem;
136 |
137 | data->button = gtk_toggle_button_new_with_label(label_text);
138 | gtk_widget_add_css_class(data->button, "drop-down");
139 | data->fixed_text = !!label_text;
140 |
141 | data->popover = gtk_popover_new();
142 | gtk_popover_set_has_arrow(GTK_POPOVER(data->popover), FALSE);
143 | gtk_widget_set_parent(
144 | data->popover,
145 | gtk_widget_get_first_child(data->button)
146 | );
147 | g_signal_connect(
148 | gtk_widget_get_first_child(data->button),
149 | "destroy", G_CALLBACK(drop_down_destroy), data->popover
150 | );
151 |
152 | GListModel *model = G_LIST_MODEL(gtk_string_list_new(NULL));
153 |
154 | int count = alsa_get_item_count(elem);
155 | for (int i = 0; i < count; i++) {
156 | const char *text = alsa_get_item_name(elem, i);
157 |
158 | gtk_string_list_append(GTK_STRING_LIST(model), text);
159 | }
160 |
161 | GtkListItemFactory *factory = gtk_signal_list_item_factory_new();
162 | g_signal_connect(
163 | factory, "setup", G_CALLBACK(setup_factory), data
164 | );
165 | g_signal_connect(
166 | factory, "bind", G_CALLBACK(bind_factory), data
167 | );
168 |
169 | data->selection = gtk_single_selection_new(model);
170 | data->listview = gtk_list_view_new(
171 | GTK_SELECTION_MODEL(data->selection),
172 | factory
173 | );
174 | gtk_list_view_set_single_click_activate(GTK_LIST_VIEW(data->listview), TRUE);
175 |
176 | gtk_popover_set_child(GTK_POPOVER(data->popover), data->listview);
177 |
178 | g_signal_connect(
179 | data->button, "clicked", G_CALLBACK(toggle_button_clicked), data
180 | );
181 | g_signal_connect(
182 | data->listview, "activate", G_CALLBACK(list_item_activated), data
183 | );
184 | drop_down_updated(elem, data);
185 |
186 | alsa_elem_add_callback(elem, drop_down_updated, data);
187 |
188 | return data->button;
189 | }
190 |
--------------------------------------------------------------------------------
/src/widget-drop-down.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | GtkWidget *make_drop_down_alsa_elem(
11 | struct alsa_elem *elem,
12 | const char *label_text
13 | );
14 |
--------------------------------------------------------------------------------
/src/widget-dual.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "widget-dual.h"
5 |
6 | struct dual_button {
7 | struct alsa_elem *elem;
8 | GtkWidget *button1;
9 | GtkWidget *button2;
10 | const char *text[4];
11 | };
12 |
13 | static void dual_button_clicked(GtkWidget *widget, struct dual_button *data) {
14 | int value1 = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->button1));
15 | int value2 = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data->button2));
16 |
17 | int value = value1 ? value2 + 1 : 0;
18 |
19 | alsa_set_elem_value(data->elem, value);
20 |
21 | gtk_widget_set_sensitive(data->button2, value1);
22 | }
23 |
24 | static void dual_button_updated(
25 | struct alsa_elem *elem,
26 | void *private
27 | ) {
28 | struct dual_button *data = private;
29 |
30 | // value (from ALSA control) is 0/1/2
31 | // value1 (first button) is 0/1/1
32 | // value2 (second button) is X/0/1
33 | int value = alsa_get_elem_value(elem);
34 | int value1 = !!value;
35 |
36 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->button1), value1);
37 | gtk_button_set_label(GTK_BUTTON(data->button1), data->text[value1]);
38 | gtk_widget_set_sensitive(data->button2, value1);
39 | if (value1) {
40 | int value2 = value - 1;
41 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->button2), value2);
42 | gtk_button_set_label(
43 | GTK_BUTTON(data->button2), data->text[value2 + 2]
44 | );
45 | }
46 | }
47 |
48 | // speaker switch and talkback have three states, controlled by two
49 | // buttons:
50 | // first button disables/enables the feature
51 | // second button switches between the two enabled states
52 | GtkWidget *make_dual_boolean_alsa_elems(
53 | struct alsa_elem *elem,
54 | const char *label_text,
55 | const char *disabled_text_1,
56 | const char *enabled_text_1,
57 | const char *disabled_text_2,
58 | const char *enabled_text_2
59 | ) {
60 | struct dual_button *data = g_malloc(sizeof(struct dual_button));
61 | data->elem = elem;
62 | data->button1 = gtk_toggle_button_new();
63 | data->button2 = gtk_toggle_button_new();
64 |
65 | g_signal_connect(
66 | data->button1, "clicked", G_CALLBACK(dual_button_clicked), data
67 | );
68 | g_signal_connect(
69 | data->button2, "clicked", G_CALLBACK(dual_button_clicked), data
70 | );
71 | alsa_elem_add_callback(elem, dual_button_updated, data);
72 | data->text[0] = disabled_text_1;
73 | data->text[1] = enabled_text_1;
74 | data->text[2] = disabled_text_2;
75 | data->text[3] = enabled_text_2;
76 |
77 | gtk_button_set_label(GTK_BUTTON(data->button2), disabled_text_2);
78 |
79 | dual_button_updated(elem, data);
80 |
81 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
82 | GtkWidget *label = gtk_label_new(label_text);
83 | gtk_box_append(GTK_BOX(box), label);
84 | gtk_box_append(GTK_BOX(box), GTK_WIDGET(data->button1));
85 | gtk_box_append(GTK_BOX(box), GTK_WIDGET(data->button2));
86 |
87 | return box;
88 | }
89 |
--------------------------------------------------------------------------------
/src/widget-dual.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | // speaker switch and talkback have three states, controlled by two
9 | // buttons:
10 | // first button disables/enables the feature
11 | // second button switches between the two features states
12 | GtkWidget *make_dual_boolean_alsa_elems(
13 | struct alsa_elem *alsa_elem,
14 | const char *label_text,
15 | const char *disabled_text_1,
16 | const char *enabled_text_1,
17 | const char *disabled_text_2,
18 | const char *enabled_text_2
19 | );
20 |
--------------------------------------------------------------------------------
/src/widget-gain.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "gtkdial.h"
5 | #include "stringhelper.h"
6 | #include "widget-gain.h"
7 | #include "db.h"
8 |
9 | struct gain {
10 | struct alsa_elem *elem;
11 | struct alsa_elem *direct_monitor_elem;
12 | struct alsa_elem *monitor_mix_elem[2];
13 | GtkWidget *vbox;
14 | GtkWidget *dial;
15 | GtkWidget *label;
16 | int zero_is_off;
17 | float scale;
18 | };
19 |
20 | static void gain_changed(GtkWidget *widget, struct gain *data) {
21 | int value = gtk_dial_get_value(GTK_DIAL(data->dial));
22 | alsa_set_elem_value(data->elem, value);
23 |
24 | // check if there is a corresponding Direct Monitor Mix control to
25 | // update as well
26 |
27 | // Direct Monitor control?
28 | if (!data->direct_monitor_elem)
29 | return;
30 |
31 | // Direct Monitor enabled?
32 | int direct_monitor = alsa_get_elem_value(data->direct_monitor_elem);
33 |
34 | if (!direct_monitor)
35 | return;
36 |
37 | // Get the corresponding Mix control
38 | struct alsa_elem *monitor_mix = data->monitor_mix_elem[direct_monitor - 1];
39 | if (!monitor_mix)
40 | return;
41 |
42 | // Update it
43 | alsa_set_elem_value(monitor_mix, value);
44 | }
45 |
46 | static void gain_updated(
47 | struct alsa_elem *elem,
48 | void *private
49 | ) {
50 | struct gain *data = private;
51 |
52 | int is_writable = alsa_get_elem_writable(elem);
53 | gtk_widget_set_sensitive(data->dial, is_writable);
54 |
55 | int alsa_value = alsa_get_elem_value(elem);
56 | gtk_dial_set_value(GTK_DIAL(data->dial), alsa_value);
57 |
58 | char s[20];
59 | char *p = s;
60 | float value;
61 | int min_db = round(elem->min_cdB / 100.0);
62 | int max_db = round(elem->max_cdB / 100.0);
63 |
64 | if (elem->dB_type == SND_CTL_TLVT_DB_LINEAR) {
65 | value = linear_value_to_db(
66 | alsa_value,
67 | elem->min_val,
68 | elem->max_val,
69 | min_db,
70 | max_db
71 | );
72 | } else {
73 | value = ((float)(alsa_value - elem->min_val)) * data->scale + (elem->min_cdB / 100.0);
74 | if (value > max_db)
75 | value = max_db;
76 | else if (value < min_db)
77 | value = min_db;
78 | }
79 |
80 | if (data->zero_is_off && value == min_db) {
81 | p += sprintf(p, "−∞");
82 | } else {
83 | if (data->scale <= 0.5)
84 | value = round(value * 10) / 10;
85 | if (value < 0)
86 | p += sprintf(p, "−");
87 | else if (value > 0)
88 | p += sprintf(p, "+");
89 | if (data->scale <= 0.5)
90 | p += snprintf(p, 10, "%.1f", fabs(value));
91 | else
92 | p += snprintf(p, 10, "%.0f", fabs(value));
93 | }
94 | if (data->scale > 0.5)
95 | p += sprintf(p, "dB");
96 |
97 | gtk_label_set_text(GTK_LABEL(data->label), s);
98 | }
99 |
100 | // 4th Gen Solo and 2i2 have Mix & Direct Monitor controls which
101 | // interact. If direct monitor is enabled and the Mix A/B controls are
102 | // changed, then the Monitor Mix Playback Volume controls are changed
103 | // too so that the mix settings are restored when direct monitor is
104 | // later enabled again.
105 | static void find_direct_monitor_controls(struct gain *data) {
106 | struct alsa_elem *elem = data->elem;
107 | GArray *elems = elem->card->elems;
108 |
109 | // Card has no direct monitor control?
110 | struct alsa_elem *direct_monitor_elem = get_elem_by_prefix(
111 | elems,
112 | "Direct Monitor Playback"
113 | );
114 | if (!direct_monitor_elem)
115 | return;
116 |
117 | // Card has no mixer?
118 | if (strncmp(elem->name, "Mix ", 4) != 0 ||
119 | !strstr(elem->name, "Playback Volume"))
120 | return;
121 |
122 | char mix_letter = elem->name[4];
123 | int input_num = get_num_from_string(elem->name);
124 |
125 | // Find the Monitor Mix control for the 4th Gen Solo
126 | if (strstr(direct_monitor_elem->name, "Switch")) {
127 | char s[80];
128 | sprintf(
129 | s,
130 | "Monitor Mix %c Input %02d Playback Volume",
131 | mix_letter, input_num
132 | );
133 |
134 | struct alsa_elem *monitor_mix_elem = get_elem_by_name(elems, s);
135 | if (!monitor_mix_elem)
136 | return;
137 |
138 | data->direct_monitor_elem = direct_monitor_elem;
139 | data->monitor_mix_elem[0] = monitor_mix_elem;
140 |
141 | // Find the Monitor Mix controls for the 4th Gen 2i2
142 | } else if (strstr(direct_monitor_elem->name, "Enum")) {
143 | for (int i = 0; i <= 1; i++) {
144 | char s[80];
145 | sprintf(
146 | s,
147 | "Monitor %d Mix %c Input %02d Playback Volume",
148 | i + 1, mix_letter, input_num
149 | );
150 |
151 | struct alsa_elem *monitor_mix_elem = get_elem_by_name(elems, s);
152 | if (!monitor_mix_elem)
153 | return;
154 |
155 | data->direct_monitor_elem = direct_monitor_elem;
156 | data->monitor_mix_elem[i] = monitor_mix_elem;
157 | }
158 |
159 | } else {
160 | fprintf(stderr, "Couldn't find direct monitor mix control\n");
161 | }
162 | }
163 |
164 | //GList *make_gain_alsa_elem(struct alsa_elem *elem) {
165 | GtkWidget *make_gain_alsa_elem(
166 | struct alsa_elem *elem,
167 | int zero_is_off,
168 | int widget_taper,
169 | int can_control
170 | ) {
171 | struct gain *data = calloc(1, sizeof(struct gain));
172 | data->elem = elem;
173 | data->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
174 | gtk_widget_set_hexpand(data->vbox, TRUE);
175 | gtk_widget_set_valign(data->vbox, GTK_ALIGN_START);
176 | gtk_widget_set_vexpand(data->vbox, TRUE);
177 |
178 | gboolean is_linear = elem->dB_type == SND_CTL_TLVT_DB_LINEAR;
179 | double step;
180 |
181 | if (is_linear) {
182 | data->scale = 0.5;
183 | step = 0.5;
184 | } else {
185 | data->scale = (float)(elem->max_cdB - elem->min_cdB) / 100.0 /
186 | (elem->max_val - elem->min_val);
187 | step = 1;
188 | }
189 | data->dial = gtk_dial_new_with_range(
190 | elem->min_val,
191 | elem->max_val,
192 | step,
193 | 3 / data->scale
194 | );
195 |
196 | // calculate 0dB value
197 | int zero_db_value;
198 |
199 | if (is_linear) {
200 | zero_db_value = cdb_to_linear_value(
201 | 0,
202 | elem->min_val,
203 | elem->max_val,
204 | elem->min_cdB,
205 | elem->max_cdB
206 | );
207 | } else {
208 | zero_db_value =
209 | (int)((0 - elem->min_cdB) / 100.0 / data->scale + elem->min_val);
210 | }
211 |
212 | gtk_dial_set_zero_db(GTK_DIAL(data->dial), zero_db_value);
213 | gtk_dial_set_is_linear(GTK_DIAL(data->dial), is_linear);
214 |
215 | // convert from widget_taper to gtk_dial_taper
216 | int gtk_dial_taper;
217 | if (widget_taper == WIDGET_GAIN_TAPER_LINEAR)
218 | gtk_dial_taper = GTK_DIAL_TAPER_LINEAR;
219 | else if (widget_taper == WIDGET_GAIN_TAPER_LOG)
220 | gtk_dial_taper = GTK_DIAL_TAPER_LOG;
221 | else
222 | gtk_dial_taper = GTK_DIAL_TAPER_LINEAR;
223 | gtk_dial_set_taper(GTK_DIAL(data->dial), gtk_dial_taper);
224 |
225 | if (widget_taper == WIDGET_GAIN_TAPER_GEN4_VOLUME)
226 | gtk_dial_set_taper_linear_breakpoints(
227 | GTK_DIAL(data->dial),
228 | (const double[]){ 0.488, 0.76 },
229 | (const double[]){ 0.07, 0.4 },
230 | 2
231 | );
232 |
233 | gtk_dial_set_can_control(GTK_DIAL(data->dial), can_control);
234 |
235 | data->label = gtk_label_new(NULL);
236 | gtk_widget_add_css_class(data->label, "gain");
237 | gtk_widget_set_vexpand(data->dial, TRUE);
238 |
239 | data->zero_is_off = zero_is_off;
240 |
241 | find_direct_monitor_controls(data);
242 |
243 | g_signal_connect(
244 | data->dial, "value-changed", G_CALLBACK(gain_changed), data
245 | );
246 |
247 | alsa_elem_add_callback(elem, gain_updated, data);
248 |
249 | gain_updated(elem, data);
250 |
251 | gtk_box_append(GTK_BOX(data->vbox), data->dial);
252 | gtk_box_append(GTK_BOX(data->vbox), data->label);
253 |
254 | return data->vbox;
255 | }
256 |
--------------------------------------------------------------------------------
/src/widget-gain.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | enum {
11 | WIDGET_GAIN_TAPER_LINEAR,
12 | WIDGET_GAIN_TAPER_LOG,
13 | WIDGET_GAIN_TAPER_GEN4_VOLUME
14 | };
15 |
16 | GtkWidget *make_gain_alsa_elem(
17 | struct alsa_elem *elem,
18 | int zero_is_off,
19 | int taper_type,
20 | int can_control
21 | );
22 |
--------------------------------------------------------------------------------
/src/widget-input-select.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "stringhelper.h"
5 | #include "widget-input-select.h"
6 |
7 | struct input_select {
8 | struct alsa_elem *elem;
9 | GtkWidget *button;
10 | int line_num;
11 | };
12 |
13 | static void input_select_clicked(
14 | GtkWidget *widget,
15 | struct input_select *data
16 | ) {
17 | int count = alsa_get_item_count(data->elem);
18 |
19 | // select the item that matches the line number that was clicked on
20 | for (int i = 0; i < count; i++) {
21 | const char *text = alsa_get_item_name(data->elem, i);
22 | int a, b;
23 | get_two_num_from_string(text, &a, &b);
24 |
25 | if ((b == -1 && a == data->line_num) ||
26 | (a <= data->line_num && b >= data->line_num)) {
27 | alsa_set_elem_value(data->elem, i);
28 | break;
29 | }
30 | }
31 | }
32 |
33 | static void input_select_updated(
34 | struct alsa_elem *elem,
35 | void *private
36 | ) {
37 | struct input_select *data = private;
38 | int line_num = data->line_num;
39 | int is_writable = alsa_get_elem_writable(elem);
40 |
41 | int value = alsa_get_elem_value(elem);
42 | const char *text = alsa_get_item_name(elem, value);
43 |
44 | int a, b;
45 | get_two_num_from_string(text, &a, &b);
46 |
47 | // set the button active if it's the selected line number
48 | // (or in the range)
49 | int active = b == -1
50 | ? a == line_num
51 | : a <= line_num && b >= line_num;
52 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->button), active);
53 | gtk_widget_set_sensitive(data->button, !active && is_writable);
54 | }
55 |
56 | GtkWidget *make_input_select_alsa_elem(
57 | struct alsa_elem *elem,
58 | int line_num
59 | ) {
60 | struct input_select *data = malloc(sizeof(struct input_select));
61 | data->elem = elem;
62 | data->button = gtk_toggle_button_new();
63 | data->line_num = line_num;
64 |
65 | gtk_widget_add_css_class(data->button, "input-select");
66 |
67 | char s[20];
68 | snprintf(s, 20, "%d", line_num);
69 | gtk_button_set_label(GTK_BUTTON(data->button), s);
70 |
71 | g_signal_connect(
72 | data->button, "clicked", G_CALLBACK(input_select_clicked), data
73 | );
74 | alsa_elem_add_callback(elem, input_select_updated, data);
75 |
76 | input_select_updated(elem, data);
77 |
78 | return data->button;
79 | }
80 |
--------------------------------------------------------------------------------
/src/widget-input-select.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | GtkWidget *make_input_select_alsa_elem(
11 | struct alsa_elem *alsa_elem,
12 | int line_num
13 | );
14 |
--------------------------------------------------------------------------------
/src/widget-label.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "widget-label.h"
5 |
6 | struct label {
7 | struct alsa_elem *elem;
8 | GtkWidget *label;
9 | };
10 |
11 | static void label_updated(struct alsa_elem *elem, void *private) {
12 | struct label *data = private;
13 |
14 | const char *text = alsa_get_item_name(elem, alsa_get_elem_value(elem));
15 |
16 | gtk_label_set_text(GTK_LABEL(data->label), text);
17 | }
18 |
19 | GtkWidget *make_label_alsa_elem(struct alsa_elem *elem) {
20 | struct label *data = g_malloc(sizeof(struct label));
21 | data->label = gtk_label_new(NULL);
22 |
23 | gtk_widget_set_halign(data->label, GTK_ALIGN_CENTER);
24 | gtk_widget_set_valign(data->label, GTK_ALIGN_CENTER);
25 |
26 | alsa_elem_add_callback(elem, label_updated, data);
27 |
28 | label_updated(elem, data);
29 |
30 | return data->label;
31 | }
32 |
--------------------------------------------------------------------------------
/src/widget-label.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | GtkWidget *make_label_alsa_elem(struct alsa_elem *elem);
11 |
--------------------------------------------------------------------------------
/src/widget-sample-rate.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "gtkhelper.h"
5 | #include "widget-boolean.h"
6 |
7 | struct sample_rate {
8 | struct alsa_card *card;
9 | GtkWidget *button;
10 | guint source;
11 | char *path;
12 | int sample_rate;
13 | };
14 |
15 | static void button_set_text(GtkWidget *button, int value) {
16 | gtk_widget_remove_css_classes_by_prefix(button, "sample-rate-");
17 |
18 | if (!value) {
19 | gtk_button_set_label(GTK_BUTTON(button), "N/A");
20 | return;
21 | }
22 |
23 | char *text;
24 | if (value % 1000 == 0)
25 | text = g_strdup_printf("%dkHz", value / 1000);
26 | else
27 | text = g_strdup_printf("%.1fkHz", value / 1000.0);
28 | gtk_button_set_label(GTK_BUTTON(button), text);
29 | g_free(text);
30 |
31 | char *css_class = g_strdup_printf(
32 | "sample-rate-%d", value
33 | );
34 | gtk_widget_add_css_class(button, css_class);
35 | g_free(css_class);
36 | }
37 |
38 | // Read the sample rate from /proc/asound/cardN/stream0
39 | // and return it as an integer
40 | //
41 | // Looking for a line containing:
42 | // Momentary freq = 48000 Hz (0x6.0000)
43 | static int get_sample_rate(struct sample_rate *data) {
44 | if (!data->path)
45 | return 0;
46 |
47 | FILE *file = fopen(data->path, "r");
48 | if (!file) {
49 | perror("fopen /proc/asound/cardN/stream0");
50 | return 0;
51 | }
52 |
53 | char *line = NULL;
54 | size_t len = 0;
55 | ssize_t read;
56 |
57 | int sample_rate = 0;
58 |
59 | while ((read = getline(&line, &len, file)) != -1) {
60 | if (strstr(line, "Momentary freq = ")) {
61 | char *start = strstr(line, "Momentary freq = ") + 17;
62 | char *end = strstr(start, " Hz");
63 |
64 | if (!start || !end)
65 | continue;
66 |
67 | *end = '\0';
68 | sample_rate = atoi(start);
69 |
70 | break;
71 | }
72 | }
73 |
74 | free(line);
75 | fclose(file);
76 |
77 | return sample_rate;
78 | }
79 |
80 | static gboolean update_sample_rate(struct sample_rate *data) {
81 | int sample_rate = get_sample_rate(data);
82 |
83 | if (sample_rate != data->sample_rate) {
84 | data->sample_rate = sample_rate;
85 | button_set_text(data->button, sample_rate);
86 | }
87 |
88 | return G_SOURCE_CONTINUE;
89 | }
90 |
91 | static void on_destroy(struct sample_rate *data, GObject *widget) {
92 | if (data->source)
93 | g_source_remove(data->source);
94 | g_free(data->path);
95 | g_free(data);
96 | }
97 |
98 | GtkWidget *make_sample_rate_widget(
99 | struct alsa_card *card
100 | ) {
101 | struct sample_rate *data = g_malloc0(sizeof(struct sample_rate));
102 | data->card = card;
103 | data->button = gtk_toggle_button_new();
104 | data->sample_rate = -1;
105 |
106 | gtk_widget_set_sensitive(data->button, FALSE);
107 | gtk_widget_add_css_class(data->button, "fixed");
108 | gtk_widget_add_css_class(data->button, "sample-rate");
109 |
110 | // can only update if it's a real card
111 | if (card->num != SIMULATED_CARD_NUM) {
112 | data->path = g_strdup_printf("/proc/asound/card%d/stream0", card->num);
113 | data->source =
114 | g_timeout_add_seconds(1, (GSourceFunc)update_sample_rate, data);
115 | }
116 |
117 | // initial update (will show "N/A" for simulated card)
118 | update_sample_rate(data);
119 |
120 | // cleanup when the button is destroyed
121 | g_object_weak_ref(G_OBJECT(data->button), (GWeakNotify)on_destroy, data);
122 |
123 | return data->button;
124 | }
125 |
--------------------------------------------------------------------------------
/src/widget-sample-rate.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | GtkWidget *make_sample_rate_widget(
11 | struct alsa_card *alsa_card
12 | );
13 |
--------------------------------------------------------------------------------
/src/window-hardware.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "window-hardware.h"
5 |
6 | GtkWidget *window_hardware;
7 |
8 | struct hw_info {
9 | char *name;
10 | };
11 |
12 | struct hw_cat {
13 | char *name;
14 | struct hw_info *info;
15 | };
16 |
17 | struct hw_info gen_1_info[] = {
18 | { "Scarlett 6i6 1st Gen" },
19 | { "Scarlett 8i6 1st Gen" },
20 | { "Scarlett 18i6 1st Gen" },
21 | { "Scarlett 18i8 1st Gen" },
22 | { "Scarlett 18i20 1st Gen" },
23 | { }
24 | };
25 |
26 | struct hw_info gen_2_info[] = {
27 | { "Scarlett 6i6 2nd Gen" },
28 | { "Scarlett 18i8 2nd Gen" },
29 | { "Scarlett 18i20 2nd Gen" },
30 | { }
31 | };
32 |
33 | struct hw_info gen_3_info[] = {
34 | { "Scarlett Solo 3rd Gen" },
35 | { "Scarlett 2i2 3rd Gen" },
36 | { "Scarlett 4i4 3rd Gen" },
37 | { "Scarlett 8i6 3rd Gen" },
38 | { "Scarlett 18i8 3rd Gen" },
39 | { "Scarlett 18i20 3rd Gen" },
40 | { }
41 | };
42 |
43 | struct hw_info gen_4_info[] = {
44 | { "Scarlett Solo 4th Gen" },
45 | { "Scarlett 2i2 4th Gen" },
46 | { "Scarlett 4i4 4th Gen" },
47 | { "Scarlett 16i16 4th Gen" },
48 | { "Scarlett 18i16 4th Gen" },
49 | { "Scarlett 18i20 4th Gen" },
50 | { }
51 | };
52 |
53 | struct hw_info clarett_usb_info[] = {
54 | { "Clarett 2Pre USB" },
55 | { "Clarett 4Pre USB" },
56 | { "Clarett 8Pre USB" },
57 | { }
58 | };
59 |
60 | struct hw_info clarett_plus_info[] = {
61 | { "Clarett+ 2Pre" },
62 | { "Clarett+ 4Pre" },
63 | { "Clarett+ 8Pre" },
64 | { }
65 | };
66 |
67 | struct hw_info vocaster_info[] = {
68 | { "Vocaster One" },
69 | { "Vocaster Two" },
70 | { }
71 | };
72 |
73 | struct hw_cat hw_cat[] = {
74 | { "1st Gen",
75 | gen_1_info
76 | },
77 | { "2nd Gen",
78 | gen_2_info
79 | },
80 | { "3rd Gen",
81 | gen_3_info
82 | },
83 | { "4th Gen",
84 | gen_4_info
85 | },
86 | { "Clarett USB",
87 | clarett_usb_info
88 | },
89 | { "Clarett+",
90 | clarett_plus_info
91 | },
92 | { "Vocaster",
93 | vocaster_info
94 | },
95 | { }
96 | };
97 |
98 | gboolean window_hardware_close_request(
99 | GtkWindow *w,
100 | gpointer data
101 | ) {
102 | GtkApplication *app = data;
103 |
104 | g_action_group_activate_action(
105 | G_ACTION_GROUP(app), "hardware", NULL
106 | );
107 | return true;
108 | }
109 |
110 | GtkWidget *make_notebook_page(struct hw_cat *cat) {
111 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
112 | for (struct hw_info *info = cat->info; info->name; info++) {
113 | GtkWidget *label = gtk_label_new(info->name);
114 | gtk_box_append(GTK_BOX(box), label);
115 | }
116 | return box;
117 | }
118 |
119 | void add_notebook_pages(GtkWidget *notebook) {
120 | for (struct hw_cat *cat = hw_cat; cat->name; cat++) {
121 | GtkWidget *page = make_notebook_page(cat);
122 | GtkWidget *label = gtk_label_new(cat->name);
123 | gtk_notebook_append_page(GTK_NOTEBOOK(notebook), page, label);
124 | }
125 | }
126 |
127 | void create_hardware_window(GtkApplication *app) {
128 | window_hardware = gtk_window_new();
129 | g_signal_connect(
130 | window_hardware,
131 | "close_request",
132 | G_CALLBACK(window_hardware_close_request),
133 | app
134 | );
135 |
136 | gtk_window_set_title(
137 | GTK_WINDOW(window_hardware),
138 | "ALSA Scarlett Supported Hardware"
139 | );
140 |
141 | GtkWidget *notebook = gtk_notebook_new();
142 | gtk_window_set_child(GTK_WINDOW(window_hardware), notebook);
143 |
144 | add_notebook_pages(notebook);
145 | }
146 |
--------------------------------------------------------------------------------
/src/window-hardware.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | extern GtkWidget *window_hardware;
9 |
10 | void create_hardware_window(GtkApplication *app);
11 |
--------------------------------------------------------------------------------
/src/window-helper.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include "window-helper.h"
5 |
6 | gboolean window_startup_close_request(GtkWindow *w, gpointer data) {
7 | struct alsa_card *card = data;
8 |
9 | gtk_widget_activate_action(
10 | GTK_WIDGET(card->window_main), "win.startup", NULL
11 | );
12 | return true;
13 | }
14 |
15 | static gboolean on_key_press(
16 | GtkEventControllerKey *controller,
17 | guint keyval,
18 | guint keycode,
19 | GdkModifierType state,
20 | gpointer user_data
21 | ) {
22 | GtkWidget *widget = gtk_event_controller_get_widget(
23 | GTK_EVENT_CONTROLLER(controller)
24 | );
25 |
26 | if (keyval == GDK_KEY_Escape) {
27 | gtk_window_close(GTK_WINDOW(widget));
28 | return 1;
29 | }
30 |
31 | return 0;
32 | }
33 |
34 | GtkWidget *create_subwindow(
35 | struct alsa_card *card,
36 | const char *name,
37 | GCallback close_callback
38 | ) {
39 | char *title = g_strdup_printf("%s %s", card->name, name);
40 |
41 | GtkWidget *w = gtk_window_new();
42 | gtk_window_set_resizable(GTK_WINDOW(w), FALSE);
43 | gtk_window_set_title(GTK_WINDOW(w), title);
44 | g_signal_connect(w, "close_request", G_CALLBACK(close_callback), card);
45 |
46 | GtkEventController *key_controller = gtk_event_controller_key_new();
47 | gtk_widget_add_controller(w, key_controller);
48 | g_signal_connect(
49 | key_controller, "key-pressed", G_CALLBACK(on_key_press), NULL
50 | );
51 |
52 | g_free(title);
53 | return w;
54 | }
55 |
--------------------------------------------------------------------------------
/src/window-helper.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | gboolean window_startup_close_request(GtkWindow *w, gpointer data);
11 |
12 | GtkWidget *create_subwindow(
13 | struct alsa_card *card,
14 | const char *name,
15 | GCallback close_callback
16 | );
17 |
--------------------------------------------------------------------------------
/src/window-iface.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 |
6 | #include "iface-mixer.h"
7 | #include "iface-no-mixer.h"
8 | #include "iface-none.h"
9 | #include "iface-unknown.h"
10 | #include "iface-update.h"
11 | #include "iface-waiting.h"
12 | #include "main.h"
13 | #include "menu.h"
14 | #include "window-iface.h"
15 | #include "window-startup.h"
16 |
17 | static GtkWidget *no_cards_window;
18 | static int window_count;
19 |
20 | void create_card_window(struct alsa_card *card) {
21 | if (no_cards_window) {
22 | gtk_window_destroy(GTK_WINDOW(no_cards_window));
23 | no_cards_window = NULL;
24 | }
25 |
26 | // Replacing an existing window
27 | if (card->window_main)
28 | gtk_window_destroy(GTK_WINDOW(card->window_main));
29 |
30 | // New window
31 | else
32 | window_count++;
33 |
34 | int has_startup = true;
35 | int has_mixer = true;
36 |
37 | // Check if the FCP driver is not initialised yet
38 | if (card->driver_type == DRIVER_TYPE_SOCKET_UNINIT) {
39 | card->window_main_contents = create_iface_waiting_main(card);
40 | has_startup = false;
41 | has_mixer = false;
42 |
43 | // Create minimal window with only the waiting interface
44 | card->window_main = gtk_application_window_new(app);
45 | gtk_window_set_resizable(GTK_WINDOW(card->window_main), FALSE);
46 | gtk_window_set_title(GTK_WINDOW(card->window_main), card->name);
47 | gtk_window_set_child(GTK_WINDOW(card->window_main), card->window_main_contents);
48 | gtk_widget_set_visible(card->window_main, TRUE);
49 |
50 | return;
51 | }
52 |
53 | struct alsa_elem *msd_elem =
54 | get_elem_by_name(card->elems, "MSD Mode Switch");
55 | int in_msd_mode = msd_elem && alsa_get_elem_value(msd_elem);
56 |
57 | struct alsa_elem *firmware_elem =
58 | get_elem_by_name(card->elems, "Firmware Version");
59 | struct alsa_elem *min_firmware_elem =
60 | get_elem_by_name(card->elems, "Minimum Firmware Version");
61 | int firmware_version = 0;
62 | int min_firmware_version = 0;
63 | if (firmware_elem && min_firmware_elem) {
64 | firmware_version = alsa_get_elem_value(firmware_elem);
65 | min_firmware_version = alsa_get_elem_value(min_firmware_elem);
66 | }
67 |
68 | // Firmware update required
69 | // or firmware version available and in MSD mode
70 | // (updating will disable MSD mode)
71 | if (firmware_version < min_firmware_version ||
72 | (card->best_firmware_version > firmware_version &&
73 | in_msd_mode)) {
74 | card->window_main_contents = create_iface_update_main(card);
75 | has_startup = false;
76 | has_mixer = false;
77 |
78 | // Scarlett Gen 1
79 | } else if (get_elem_by_prefix(card->elems, "Matrix")) {
80 | card->window_main_contents = create_iface_mixer_main(card);
81 | has_startup = false;
82 |
83 | // Scarlett Gen 2, Gen 3 4i4+, Gen 4, Clarett, or Vocaster
84 | } else if (get_elem_by_prefix(card->elems, "Mixer")) {
85 | card->window_main_contents = create_iface_mixer_main(card);
86 |
87 | // Scarlett Gen 3 Solo or 2i2
88 | } else if (get_elem_by_prefix(card->elems, "Phantom")) {
89 | card->window_main_contents = create_iface_no_mixer_main(card);
90 | has_mixer = false;
91 |
92 | // Scarlett Gen 3+ or Vocaster in MSD Mode
93 | } else if (msd_elem) {
94 | card->window_main_contents = create_startup_controls(card);
95 | has_startup = false;
96 | has_mixer = false;
97 |
98 | // Unknown
99 | } else {
100 | card->window_main_contents = create_iface_unknown_main();
101 | has_startup = false;
102 | has_mixer = false;
103 | }
104 |
105 | card->window_main = gtk_application_window_new(app);
106 | gtk_window_set_resizable(GTK_WINDOW(card->window_main), FALSE);
107 | gtk_window_set_title(GTK_WINDOW(card->window_main), card->name);
108 | gtk_application_window_set_show_menubar(
109 | GTK_APPLICATION_WINDOW(card->window_main), TRUE
110 | );
111 | add_window_action_map(GTK_WINDOW(card->window_main));
112 | if (has_startup)
113 | add_startup_action_map(card);
114 | if (has_mixer)
115 | add_mixer_action_map(card);
116 | if (card->device)
117 | add_load_save_action_map(card);
118 |
119 | gtk_window_set_child(
120 | GTK_WINDOW(card->window_main),
121 | card->window_main_contents
122 | );
123 | gtk_widget_set_visible(card->window_main, TRUE);
124 | }
125 |
126 | void create_no_card_window(void) {
127 | if (!window_count)
128 | no_cards_window = create_window_iface_none(app);
129 | }
130 |
131 | void destroy_card_window(struct alsa_card *card) {
132 | // remove the windows
133 | gtk_window_destroy(GTK_WINDOW(card->window_main));
134 | if (card->window_routing)
135 | gtk_window_destroy(GTK_WINDOW(card->window_routing));
136 | if (card->window_mixer)
137 | gtk_window_destroy(GTK_WINDOW(card->window_mixer));
138 | if (card->window_levels)
139 | gtk_window_destroy(GTK_WINDOW(card->window_levels));
140 | if (card->window_startup)
141 | gtk_window_destroy(GTK_WINDOW(card->window_startup));
142 | if (card->window_modal) {
143 | gtk_window_destroy(GTK_WINDOW(card->window_modal));
144 | }
145 |
146 | // if last window, display the "no card found" blank window
147 | window_count--;
148 | create_no_card_window();
149 | }
150 |
151 | void check_modal_window_closed(void) {
152 | if (!window_count)
153 | gtk_widget_set_visible(no_cards_window, TRUE);
154 | }
155 |
--------------------------------------------------------------------------------
/src/window-iface.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | void create_card_window(struct alsa_card *card);
9 | void create_no_card_window(void);
10 | void destroy_card_window(struct alsa_card *card);
11 | void check_modal_window_closed(void);
12 |
--------------------------------------------------------------------------------
/src/window-levels.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | GtkWidget *create_levels_controls(struct alsa_card *card);
9 |
--------------------------------------------------------------------------------
/src/window-mixer.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 |
6 | #include "gtkhelper.h"
7 | #include "stringhelper.h"
8 | #include "widget-gain.h"
9 | #include "window-mixer.h"
10 |
11 | static void mixer_gain_enter(
12 | GtkEventControllerMotion *controller,
13 | double x, double y,
14 | gpointer user_data
15 | ) {
16 | GtkWidget *widget = GTK_WIDGET(user_data);
17 | GtkWidget *mix_left = g_object_get_data(G_OBJECT(widget), "mix_label_left");
18 | GtkWidget *mix_right = g_object_get_data(G_OBJECT(widget), "mix_label_right");
19 | GtkWidget *source_top = g_object_get_data(G_OBJECT(widget), "source_label_top");
20 | GtkWidget *source_bottom = g_object_get_data(G_OBJECT(widget), "source_label_bottom");
21 |
22 | if (mix_left)
23 | gtk_widget_add_css_class(mix_left, "mixer-label-hover");
24 | if (mix_right)
25 | gtk_widget_add_css_class(mix_right, "mixer-label-hover");
26 | if (source_top)
27 | gtk_widget_add_css_class(source_top, "mixer-label-hover");
28 | if (source_bottom)
29 | gtk_widget_add_css_class(source_bottom, "mixer-label-hover");
30 | }
31 |
32 | static void mixer_gain_leave(
33 | GtkEventControllerMotion *controller,
34 | gpointer user_data
35 | ) {
36 | GtkWidget *widget = GTK_WIDGET(user_data);
37 | GtkWidget *mix_left = g_object_get_data(G_OBJECT(widget), "mix_label_left");
38 | GtkWidget *mix_right = g_object_get_data(G_OBJECT(widget), "mix_label_right");
39 | GtkWidget *source_top = g_object_get_data(G_OBJECT(widget), "source_label_top");
40 | GtkWidget *source_bottom = g_object_get_data(G_OBJECT(widget), "source_label_bottom");
41 |
42 | if (mix_left)
43 | gtk_widget_remove_css_class(mix_left, "mixer-label-hover");
44 | if (mix_right)
45 | gtk_widget_remove_css_class(mix_right, "mixer-label-hover");
46 | if (source_top)
47 | gtk_widget_remove_css_class(source_top, "mixer-label-hover");
48 | if (source_bottom)
49 | gtk_widget_remove_css_class(source_bottom, "mixer-label-hover");
50 | }
51 |
52 | static void add_mixer_hover_controller(GtkWidget *widget) {
53 | GtkEventController *motion = gtk_event_controller_motion_new();
54 | g_signal_connect(motion, "enter", G_CALLBACK(mixer_gain_enter), widget);
55 | g_signal_connect(motion, "leave", G_CALLBACK(mixer_gain_leave), widget);
56 | gtk_widget_add_controller(widget, motion);
57 | }
58 |
59 | static struct routing_snk *get_mixer_r_snk(
60 | struct alsa_card *card,
61 | int input_num
62 | ) {
63 | for (int i = 0; i < card->routing_snks->len; i++) {
64 | struct routing_snk *r_snk = &g_array_index(
65 | card->routing_snks, struct routing_snk, i
66 | );
67 | struct alsa_elem *elem = r_snk->elem;
68 |
69 | if (elem->port_category != PC_MIX)
70 | continue;
71 |
72 | if (elem->lr_num == input_num)
73 | return r_snk;
74 | }
75 | return NULL;
76 | }
77 |
78 | GtkWidget *create_mixer_controls(struct alsa_card *card) {
79 | GtkWidget *top = gtk_frame_new(NULL);
80 | gtk_widget_add_css_class(top, "window-frame");
81 |
82 | GtkWidget *mixer_top = gtk_grid_new();
83 | gtk_widget_add_css_class(mixer_top, "window-content");
84 | gtk_widget_add_css_class(mixer_top, "top-level-content");
85 | gtk_widget_add_css_class(mixer_top, "window-mixer");
86 | gtk_frame_set_child(GTK_FRAME(top), mixer_top);
87 |
88 | gtk_widget_set_halign(mixer_top, GTK_ALIGN_CENTER);
89 | gtk_widget_set_valign(mixer_top, GTK_ALIGN_CENTER);
90 | gtk_grid_set_column_homogeneous(GTK_GRID(mixer_top), TRUE);
91 |
92 | GArray *elems = card->elems;
93 |
94 | GtkWidget *mix_labels_left[MAX_MIX_OUT];
95 | GtkWidget *mix_labels_right[MAX_MIX_OUT];
96 |
97 | // create the Mix X labels on the left and right of the grid
98 | for (int i = 0; i < card->routing_in_count[PC_MIX]; i++) {
99 | char name[10];
100 | snprintf(name, 10, "Mix %c", i + 'A');
101 |
102 | GtkWidget *l_left = mix_labels_left[i] = gtk_label_new(name);
103 | gtk_grid_attach(
104 | GTK_GRID(mixer_top), l_left,
105 | 0, i + 2, 1, 1
106 | );
107 |
108 | GtkWidget *l_right = mix_labels_right[i] = gtk_label_new(name);
109 | gtk_grid_attach(
110 | GTK_GRID(mixer_top), l_right,
111 | card->routing_out_count[PC_MIX] + 1, i + 2, 1, 1
112 | );
113 | }
114 |
115 | // go through each element and create the mixer
116 | for (int i = 0; i < elems->len; i++) {
117 | struct alsa_elem *elem = &g_array_index(elems, struct alsa_elem, i);
118 |
119 | // if no card entry, it's an empty slot
120 | if (!elem->card)
121 | continue;
122 |
123 | // looking for "Mix X Input Y Playback Volume"
124 | // or "Matrix Y Mix X Playback Volume" elements (Gen 1)
125 | if (!strstr(elem->name, "Playback Volume"))
126 | continue;
127 | if (strncmp(elem->name, "Mix ", 4) &&
128 | strncmp(elem->name, "Matrix ", 7))
129 | continue;
130 |
131 | char *mix_str = strstr(elem->name, "Mix ");
132 | if (!mix_str)
133 | continue;
134 |
135 | // extract the mix number and input number from the element name
136 | int mix_num = mix_str[4] - 'A';
137 | int input_num = get_num_from_string(elem->name) - 1;
138 |
139 | if (mix_num >= MAX_MIX_OUT) {
140 | printf("mix_num %d >= MAX_MIX_OUT %d\n", mix_num, MAX_MIX_OUT);
141 | continue;
142 | }
143 |
144 | // create the gain control and attach to the grid
145 | GtkWidget *w = make_gain_alsa_elem(elem, 1, WIDGET_GAIN_TAPER_LOG, 0);
146 | gtk_grid_attach(GTK_GRID(mixer_top), w, input_num + 1, mix_num + 2, 1, 1);
147 |
148 | // look up the r_snk entry for the mixer input number
149 | struct routing_snk *r_snk = get_mixer_r_snk(card, input_num + 1);
150 | if (!r_snk) {
151 | printf("missing mixer input %d\n", input_num);
152 | continue;
153 | }
154 |
155 | // lookup the top label for the mixer input
156 | GtkWidget *l_top = r_snk->mixer_label_top;
157 |
158 | // if the top label doesn't already exist the bottom doesn't
159 | // either; create them both and attach to the grid
160 | if (!l_top) {
161 | l_top = r_snk->mixer_label_top = gtk_label_new("");
162 | GtkWidget *l_bottom = r_snk->mixer_label_bottom = gtk_label_new("");
163 | gtk_widget_add_css_class(l_top, "mixer-label");
164 | gtk_widget_add_css_class(l_bottom, "mixer-label");
165 |
166 | gtk_grid_attach(
167 | GTK_GRID(mixer_top), l_top,
168 | input_num, (input_num + 1) % 2, 3, 1
169 | );
170 | gtk_grid_attach(
171 | GTK_GRID(mixer_top), l_bottom,
172 | input_num, card->routing_in_count[PC_MIX] + input_num % 2 + 2, 3, 1
173 | );
174 | }
175 |
176 | g_object_set_data(G_OBJECT(w), "mix_label_left", mix_labels_left[mix_num]);
177 | g_object_set_data(G_OBJECT(w), "mix_label_right", mix_labels_right[mix_num]);
178 | g_object_set_data(G_OBJECT(w), "source_label_top", r_snk->mixer_label_top);
179 | g_object_set_data(G_OBJECT(w), "source_label_bottom", r_snk->mixer_label_bottom);
180 |
181 | // add hover controller to the gain widget
182 | add_mixer_hover_controller(w);
183 |
184 | }
185 |
186 | update_mixer_labels(card);
187 |
188 | return top;
189 | }
190 |
191 | void update_mixer_labels(struct alsa_card *card) {
192 | for (int i = 0; i < card->routing_snks->len; i++) {
193 | struct routing_snk *r_snk = &g_array_index(
194 | card->routing_snks, struct routing_snk, i
195 | );
196 | struct alsa_elem *elem = r_snk->elem;
197 |
198 | if (elem->port_category != PC_MIX)
199 | continue;
200 |
201 | int routing_src_idx = alsa_get_elem_value(elem);
202 |
203 | struct routing_src *r_src = &g_array_index(
204 | card->routing_srcs, struct routing_src, routing_src_idx
205 | );
206 |
207 | if (r_snk->mixer_label_top) {
208 | gtk_label_set_text(GTK_LABEL(r_snk->mixer_label_top), r_src->name);
209 | gtk_label_set_text(GTK_LABEL(r_snk->mixer_label_bottom), r_src->name);
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/window-mixer.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | GtkWidget *create_mixer_controls(struct alsa_card *card);
9 | void update_mixer_labels(struct alsa_card *card);
10 |
--------------------------------------------------------------------------------
/src/window-modal.c:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #include
5 | #include "gtkhelper.h"
6 | #include "window-iface.h"
7 | #include "window-modal.h"
8 |
9 | static void modal_no_callback(GtkWidget *w, struct modal_data *modal_data) {
10 | GtkWidget *dialog = modal_data->dialog;
11 |
12 | alsa_unregister_reopen_callback(modal_data->serial);
13 |
14 | gtk_window_destroy(GTK_WINDOW(dialog));
15 | modal_data->card->window_modal = NULL;
16 | check_modal_window_closed();
17 | }
18 |
19 | static void modal_yes_callback(GtkWidget *w, struct modal_data *modal_data) {
20 | // remove the buttons
21 | GtkWidget *child;
22 | while ((child = gtk_widget_get_first_child(modal_data->button_box)))
23 | gtk_box_remove(GTK_BOX(modal_data->button_box), child);
24 |
25 | // add a progress bar
26 | modal_data->progress_bar = gtk_progress_bar_new();
27 | gtk_box_append(GTK_BOX(modal_data->button_box), modal_data->progress_bar);
28 |
29 | // change the title
30 | gtk_window_set_title(
31 | GTK_WINDOW(modal_data->dialog), modal_data->title_active
32 | );
33 |
34 | // if the card goes away, don't close this window
35 | modal_data->card->window_modal = NULL;
36 |
37 | modal_data->callback(modal_data);
38 | }
39 |
40 | static void free_modal_data(gpointer user_data) {
41 | struct modal_data *modal_data = user_data;
42 |
43 | g_free(modal_data->serial);
44 | g_free(modal_data);
45 | }
46 |
47 | void create_modal_window(
48 | GtkWidget *w,
49 | struct alsa_card *card,
50 | const char *title,
51 | const char *title_active,
52 | const char *message,
53 | modal_callback callback
54 | ) {
55 | if (card->window_modal) {
56 | fprintf(stderr, "Error: Modal window already open\n");
57 | return;
58 | }
59 |
60 | GtkWidget *dialog = gtk_window_new();
61 |
62 | struct modal_data *modal_data = g_new0(struct modal_data, 1);
63 | modal_data->card = card;
64 | modal_data->serial = g_strdup(card->serial);
65 | modal_data->title_active = title_active;
66 | modal_data->dialog = dialog;
67 | modal_data->callback = callback;
68 |
69 | gtk_window_set_title(GTK_WINDOW(dialog), title);
70 | gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
71 | gtk_widget_add_css_class(dialog, "window-frame");
72 |
73 | GtkWidget *content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 50);
74 | gtk_window_set_child(GTK_WINDOW(dialog), content_box);
75 | gtk_widget_add_css_class(content_box, "window-content");
76 | gtk_widget_add_css_class(content_box, "top-level-content");
77 | gtk_widget_add_css_class(content_box, "big-padding");
78 |
79 | modal_data->label = gtk_label_new(message);
80 | gtk_box_append(GTK_BOX(content_box), modal_data->label);
81 |
82 | GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
83 | gtk_widget_set_margin(sep, 0);
84 | gtk_box_append(GTK_BOX(content_box), sep);
85 |
86 | modal_data->button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 50);
87 | gtk_widget_set_halign(modal_data->button_box, GTK_ALIGN_CENTER);
88 | gtk_box_append(GTK_BOX(content_box), modal_data->button_box);
89 |
90 | g_object_set_data_full(
91 | G_OBJECT(dialog), "modal_data", modal_data, free_modal_data
92 | );
93 |
94 | GtkWidget *no_button = gtk_button_new_with_label("No");
95 | g_signal_connect(
96 | no_button, "clicked", G_CALLBACK(modal_no_callback), modal_data
97 | );
98 | gtk_box_append(GTK_BOX(modal_data->button_box), no_button);
99 |
100 | GtkWidget *yes_button = gtk_button_new_with_label("Yes");
101 | g_signal_connect(
102 | yes_button, "clicked", G_CALLBACK(modal_yes_callback), modal_data
103 | );
104 | gtk_box_append(GTK_BOX(modal_data->button_box), yes_button);
105 |
106 | gtk_widget_set_visible(dialog, TRUE);
107 |
108 | card->window_modal = dialog;
109 | }
110 |
111 | gboolean modal_update_progress(gpointer user_data) {
112 | struct progress_data *progress_data = user_data;
113 | struct modal_data *modal_data = progress_data->modal_data;
114 |
115 | // Done? Replace the progress bar with an Ok button.
116 | if (progress_data->progress < 0) {
117 | GtkWidget *child;
118 | while ((child = gtk_widget_get_first_child(modal_data->button_box)))
119 | gtk_box_remove(GTK_BOX(modal_data->button_box), child);
120 |
121 | GtkWidget *ok_button = gtk_button_new_with_label("Ok");
122 | g_signal_connect(
123 | ok_button, "clicked", G_CALLBACK(modal_no_callback), modal_data
124 | );
125 | gtk_box_append(GTK_BOX(modal_data->button_box), ok_button);
126 | } else {
127 | gtk_progress_bar_set_fraction(
128 | GTK_PROGRESS_BAR(modal_data->progress_bar),
129 | progress_data->progress / 100.0
130 | );
131 | }
132 |
133 | // Update the label text if we have a new message.
134 | if (progress_data->text)
135 | gtk_label_set_text(GTK_LABEL(modal_data->label), progress_data->text);
136 |
137 | g_free(progress_data->text);
138 | g_free(progress_data);
139 | return G_SOURCE_REMOVE;
140 | }
141 |
142 | // make the progress bar move along
143 | // if it gets to the end twice, something probably went wrong
144 | static gboolean update_progress_bar_reboot(gpointer user_data) {
145 | struct progress_data *progress_data = user_data;
146 | struct modal_data *modal_data = progress_data->modal_data;
147 |
148 | if (progress_data->progress >= 200) {
149 | // Done?
150 | gtk_label_set_text(
151 | GTK_LABEL(modal_data->label),
152 | "Reboot failed? Try unplugging/replugging/power-cycling the device."
153 | );
154 |
155 | GtkWidget *child;
156 | while ((child = gtk_widget_get_first_child(modal_data->button_box)))
157 | gtk_box_remove(GTK_BOX(modal_data->button_box), child);
158 |
159 | GtkWidget *ok_button = gtk_button_new_with_label("Ok");
160 | g_signal_connect(
161 | ok_button, "clicked", G_CALLBACK(modal_no_callback), modal_data
162 | );
163 | gtk_box_append(GTK_BOX(modal_data->button_box), ok_button);
164 |
165 | modal_data->timeout_id = 0;
166 |
167 | return G_SOURCE_REMOVE;
168 | }
169 |
170 | progress_data->progress++;
171 | gtk_progress_bar_set_fraction(
172 | GTK_PROGRESS_BAR(modal_data->progress_bar),
173 | (progress_data->progress % 100) / 100.0
174 | );
175 |
176 | return G_SOURCE_CONTINUE;
177 | }
178 |
179 | // this is called when the card is seen again so we can close the
180 | // modal window
181 | void modal_reopen_callback(void *user_data) {
182 | struct modal_data *modal_data = user_data;
183 |
184 | // stop the progress bar
185 | if (modal_data->timeout_id)
186 | g_source_remove(modal_data->timeout_id);
187 |
188 | // close the window
189 | gtk_window_destroy(GTK_WINDOW(modal_data->dialog));
190 | }
191 |
192 | // make a progress bar that moves while the device is rebooting
193 | gboolean modal_start_reboot_progress(gpointer user_data) {
194 | struct modal_data *modal_data = user_data;
195 |
196 | gtk_label_set_text(GTK_LABEL(modal_data->label), "Rebooting...");
197 |
198 | struct progress_data *progress_data = g_new0(struct progress_data, 1);
199 | progress_data->modal_data = modal_data;
200 | progress_data->progress = 0;
201 |
202 | g_object_set_data_full(
203 | G_OBJECT(modal_data->progress_bar), "progress_data", progress_data, g_free
204 | );
205 |
206 | modal_data->timeout_id = g_timeout_add(
207 | 55, update_progress_bar_reboot, progress_data
208 | );
209 |
210 | alsa_register_reopen_callback(
211 | modal_data->card->serial, modal_reopen_callback, modal_data
212 | );
213 |
214 | return G_SOURCE_REMOVE;
215 | }
216 |
--------------------------------------------------------------------------------
/src/window-modal.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2024-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 | #include "alsa.h"
8 |
9 | // create a modal window with a message and yes/no buttons
10 | // the callback is called with the modal_data when yes is clicked
11 |
12 | struct modal_data;
13 |
14 | typedef void (*modal_callback)(struct modal_data *data);
15 |
16 | struct modal_data {
17 | struct alsa_card *card;
18 | char *serial;
19 | const char *title_active;
20 | GtkWidget *dialog;
21 | GtkWidget *label;
22 | GtkWidget *button_box;
23 | GtkWidget *progress_bar;
24 | guint timeout_id;
25 | modal_callback callback;
26 | };
27 |
28 | void create_modal_window(
29 | GtkWidget *w,
30 | struct alsa_card *card,
31 | const char *title,
32 | const char *title_active,
33 | const char *message,
34 | modal_callback callback
35 | );
36 |
37 | // update the progress bar in a modal window
38 |
39 | struct progress_data {
40 | struct modal_data *modal_data;
41 | char *text;
42 | int progress;
43 | };
44 |
45 | gboolean modal_update_progress(gpointer user_data);
46 |
47 | // start a progress bar for a reboot
48 |
49 | gboolean modal_start_reboot_progress(gpointer user_data);
50 |
--------------------------------------------------------------------------------
/src/window-routing.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include
7 |
8 | #include "alsa.h"
9 |
10 | GtkWidget *create_routing_controls(struct alsa_card *card);
11 |
--------------------------------------------------------------------------------
/src/window-startup.h:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022-2025 Geoffrey D. Bennett
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | #pragma once
5 |
6 | #include "alsa.h"
7 |
8 | GtkWidget *create_startup_controls(struct alsa_card *card);
9 |
--------------------------------------------------------------------------------
/vu.b4.alsa-scarlett-gui.yml:
--------------------------------------------------------------------------------
1 | app-id: vu.b4.alsa-scarlett-gui
2 | runtime: org.gnome.Platform
3 | runtime-version: "47"
4 | sdk: org.gnome.Sdk
5 | command: alsa-scarlett-gui
6 | build-options:
7 | secret-env:
8 | - APP_VERSION
9 | finish-args:
10 | # X11 + XShm access
11 | - --share=ipc
12 | - --socket=fallback-x11
13 | # Wayland access
14 | - --socket=wayland
15 | # Needs access to ALSA device nodes:
16 | - --device=all
17 | # Point to the firmware directory
18 | - --env=SCARLETT2_FIRMWARE_DIR=/app/lib/firmware/scarlett2
19 | modules:
20 | - name: alsa-utils
21 | sources:
22 | - type: archive
23 | url: https://www.alsa-project.org/files/pub/lib/alsa-lib-1.2.12.tar.bz2
24 | sha256: 4868cd908627279da5a634f468701625be8cc251d84262c7e5b6a218391ad0d2
25 | dest: .deps/alsa-lib
26 | - type: archive
27 | url: https://www.alsa-project.org/files/pub/utils/alsa-utils-1.2.12.tar.bz2
28 | sha256: 98bc6677d0c0074006679051822324a0ab0879aea558a8f68b511780d30cd924
29 | buildsystem: autotools
30 | config-opts:
31 | # We are only interested in alsactl
32 | - --bindir=/app/null
33 | - --with-udev-rules-dir=/app/null
34 | - --with-systemdsystemunitdir=/app/null
35 | # https://github.com/alsa-project/alsa-utils/issues/33
36 | - --enable-alsa-topology
37 | - --disable-alsaconf
38 | - --disable-alsatest
39 | - --disable-alsabat-backend-tiny
40 | - --disable-alsamixer
41 | - --disable-alsaloop
42 | - --disable-nhlt
43 | - --disable-xmlto
44 | - --disable-rst2man
45 | - --with-alsa-inc-prefix=.deps/alsa-lib/include
46 | post-install:
47 | - install -Dm755 /app/sbin/alsactl /app/bin/alsactl
48 | cleanup:
49 | - /lib/debug
50 | - /lib/alsa-topology
51 | - /null
52 | - /sbin
53 | - /share/alsa
54 | - /share/locale
55 | - /share/man
56 | - /share/runtime
57 | - /share/sounds
58 | - name: alsa-scarlett-gui
59 | sources:
60 | - type: dir
61 | path: ./src
62 | # Use the following and remove the above for Flathub publishing
63 | # - type: git
64 | # url: https://github.com/geoffreybennett/alsa-scarlett-gui.git
65 | # tag: "0.2"
66 | buildsystem: simple
67 | build-commands:
68 | - make -j8 install PREFIX=$FLATPAK_DEST
69 | cleanup:
70 | - /lib/debug
71 | - /lib/source
72 | - name: scarlett2-firmware
73 | sources:
74 | - type: archive
75 | url: https://github.com/geoffreybennett/scarlett2-firmware/archive/refs/tags/2128b.tar.gz
76 | sha256: 4a17fdbe2110855c2f7f6cfc5ea1894943a6e58770f3dff5ef283961f8ae2a03
77 | buildsystem: simple
78 | build-commands:
79 | - mkdir -p $FLATPAK_DEST/lib/firmware/scarlett2
80 | - cp -a LICENSE.Focusrite firmware/* $FLATPAK_DEST/lib/firmware/scarlett2
81 |
--------------------------------------------------------------------------------