├── .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 | ![Demonstration](img/demo.gif) 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 | ![No Interface Connected](../img/iface-none.png) 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 | ![Firmware Update Required (Firmware 30 | Missing)](../img/firmware-missing.png) 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 | ![Firmware Update Required](../img/firmware-update-required.png) 39 | 40 | Click “Update”, then “Yes” to update the firmware. 41 | 42 | ![Firmware Update Progress](../img/firmware-updating.png) 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 | ![MSD Mode](../img/iface-msd.png) 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 | ![Routing Window](../img/scarlett-1st-gen-6i6-routing.png) 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 | ![Main Window](../img/iface-4th-gen-big.png) 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 | ![4th Gen 16i16 Routing](../img/scarlett-4th-gen-16i16-routing.png) 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 | ![Levels](../img/window-levels-4th-gen-big.png) 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 | ![Gen 3 Small Interfaces](../img/iface-small-gen3.png) 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 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/audio-volume-low.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/audio-volume-medium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/audio-volume-muted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/socket.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 18 | 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 | --------------------------------------------------------------------------------