Contributing
docs/site/building.md ↗Building
Building from source, the test gates, and making the installers.
Requires Flutter 3.44.5-stable — pinned in .tool-versions, and CI pins the
same — and a C toolchain: Xcode command line tools, MSVC, or gcc/clang.
git clone https://github.com/JonasGrunau/open_audio_analyzer
cd open_audio_analyzer
flutter pub get
flutter run -d macos # or windows, linux
flutter run -d <ipad> # the display build; `flutter devices` names it
On iOS the engine is compiled as Objective-C, because miniaudio’s Core
Audio backend is: it configures an AVAudioSession there, and iOS offers no C
way to do that. The build hook handles it — the reason to know is the failure
if it is ever undone, which is several hundred errors inside Apple’s own
Foundation headers naming no file in Open Audio Analyzer.
Four Flutter plugins are pulled in: desktop_drop and file_selector to get a
path from a user, flutter_riverpod for configuration, and mobile_scanner
(MIT) for the host picker’s QR scanner. The last is the one with a native half
that is not vendored here, and the one that does not ship everywhere — Android,
iOS and macOS only, which canScanQrCodes asks before drawing the row. It
integrates through Swift Package Manager, so there is still no Podfile in
this repository and nothing to pod install. The QR encoder on the other
side of the same feature is written here rather than depended on, in
packages/oaa_ui/lib/src/qr.dart.
There is no podspec, no build.gradle and no per-platform CMakeLists.txt
for the application. packages/oaa_engine/hook/build.dart compiles the C
through native_toolchain_c and bundles it as a code asset. One build
description that works on five platforms beats five that each work on one.
engine/CMakeLists.txt describes the same compile for consumers that are not
Dart — the plugin, and a CI runner with no Flutter SDK. Two descriptions of one
compile is a real cost, paid deliberately: plugin/test/sources_match.sh fails
the build if the two source lists drift apart, so a new file in engine/src
goes in both.
Linux dependencies
sudo apt-get install clang cmake ninja-build pkg-config \
libgtk-3-dev liblzma-dev libstdc++-12-dev libasound2-dev
Tests
All six are the CI gate.
flutter analyze # lints, whole workspace
flutter test # widget and golden tests
dart test packages/oaa_core # domain layer, no toolchain needed
dart test packages/oaa_wire # the wire protocol, incl. the C++ golden
cd packages/oaa_engine && dart test # engine, through FFI
cd cli && dart test # the `oaa` binary, as a subprocess
The engine tests are worth a look even if you never touch the C. A sine of amplitude A has a peak of A and an RMS of A/√2 — exactly 3.0103 dB lower. That is arithmetic, not convention, so the built-in test tone doubles as a reference the meters can be held against on a headless runner with no sound hardware anywhere near it. The same job runs the EBU Tech 3341 and 3342 conformance cases, and a red conformance run is a red build.
The official vector files are a separate, manual run — they may not be redistributed, so they are not a gate:
cd packages/oaa_engine
OAA_VECTORS=~/ebu-loudness-test-set OAA_VECTORS_ITU=~/bs2217 \
dart test test/vectors_test.dart
The EBU set is at https://tech.ebu.ch/publications/ebu_loudness_test_set and the ITU’s at https://www.itu.int/oth/R1102000001/en. Either group skips when its variable is unset. Run this after touching the engine’s loudness, K-weighting or true-peak code.
Running with a different configuration
Two flags, both useful while working on the interface:
flutter run -d macos --dart-entrypoint-args --config-dir=/tmp/oaa-scratch
flutter run -d macos --dart-entrypoint-args --open-panel=settings
--config-dir points settings, presets, targets and skins somewhere
disposable, so an experiment cannot eat the configuration you actually use.
--open-panel opens one panel once the first frame is up — settings,
presets, calibration, theme, report or shortcuts — which is how a
panel gets looked at without clicking through to it. It is a debug-build affordance and a
release build says so rather than ignoring it.
On a built macOS bundle, pass them with open --args:
open "build/macos/Build/Products/Debug/Open Audio Analyzer.app" --args --open-panel=shortcuts
The plugin
cmake -B plugin/build -S plugin -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build plugin/build
Products land in plugin/build/OaaPlugin_artefacts/Release/. Nothing is copied
into a system plugin folder unless you copy it — a build that installed itself
would mean the DAW you have open is now running a binary you did not knowingly
install. Install says which folder that is on each
platform. JUCE is fetched and pinned, not vendored, so a fresh clone builds
without checking out a framework by hand.
On macOS each bundle is signed once it is fully built, then verified with
codesign --verify --strict, which fails the build rather than producing a
bundle a DAW would refuse. Signing is ad-hoc; -DOAA_CODESIGN_IDENTITY=<id>
uses a Developer ID instead, and adds the hardened runtime and a secure
timestamp, both of which notarisation requires and neither of which is a
default. A bundle you built yourself carries no quarantine flag, so nothing has
to be stripped from it.
The bundles are built for arm64 and x86_64, targeting macOS 14.2. Both are
CMake variables whose defaults are the machine doing the build, which is how
every release up to 0.5.0 shipped an arm64-only plugin that also refused to load
on any macOS older than the runner’s. Pass
-DCMAKE_OSX_ARCHITECTURES=arm64 for a build you will only ever load on the
machine that made it — it halves the compile.
plugin/ is the one AGPL-3.0-or-later directory, because JUCE 7 and 8 are
AGPL-or-commercial. Nothing there may move into engine/ or oaa_core/, which
are MIT and must stay linkable by people who are not writing free software.
Installers
One script per artefact, under packaging/. Each builds the application first
unless told to skip it, and each says plainly whether what it produced can
actually be installed.
sh packaging/macos/make_pkg.sh # carries the VST3 and the AU
pwsh packaging/windows/make_installer.ps1 # carries the VST3
sh packaging/linux/make_installer.sh # carries the VST3
sh packaging/linux/make_appimage.sh # application only
sh packaging/linux/make_flatpak.sh # application only
sh packaging/ios/make_ipa.sh # the iPad build, for TestFlight
sh packaging/ios/testflight.sh # and the upload, separately
Everything lands in build/packaging/.
The first three need the plugin bundles and refuse to run without them, on the grounds that an installer quietly missing the thing it exists to install looks exactly like one that has it. Build them first, or point the script at an unpacked release archive:
cmake -B plugin/build -S plugin -DCMAKE_BUILD_TYPE=Release
cmake --build plugin/build
sh packaging/macos/make_pkg.sh # finds plugin/build itself
sh packaging/macos/make_pkg.sh --plugins ./unpacked # or an oaa-plugin-*.tar.gz
In CI they take the second route: the macos-pkg, windows-installer and
linux-tarball jobs needs: plugin and unpack the bundles that job already
signed and notarised, rather than building a second copy of the same code.
Signing is by environment variable, and every script produces an unsigned
artefact and warns rather than failing when the variables are absent — a fork
has no secrets, and a build that stopped there would be useless to it. The
IPA is the one exception, because there is no unsigned form of it: an App
Store export either has a distribution signature or does not exist, so
make_ipa.sh produces nothing at all and says so.
| Variable | For |
|---|---|
OAA_SIGNING_IDENTITY |
macOS Developer ID, e.g. Developer ID Application: Name (TEAMID). Signs code — the app, the VST3 and the Audio Unit. It does not sign the package that carries them; that is the identity below |
OAA_INSTALLER_IDENTITY |
The other macOS Developer ID, e.g. Developer ID Installer: Name (TEAMID). A distinct certificate from the one above and not interchangeable with it — that one signs code, this one signs a .pkg and nothing else. keychain.sh fails when this names an identity the .p12 does not contain, rather than letting the run reach productbuild and stop there |
OAA_NOTARY_PROFILE |
A xcrun notarytool store-credentials profile. Your own machine only — it lives in that machine’s keychain, so a CI runner given this name finds nothing |
OAA_NOTARY_APPLE_ID, OAA_NOTARY_TEAM_ID, OAA_NOTARY_PASSWORD |
The same credentials in a form a runner can be handed. The password is an app-specific password, not the Apple ID’s own |
OAA_SIGNING_CERTIFICATE, OAA_SIGNING_CERTIFICATE_PASSWORD |
base64 of a .p12 and its export password, for a machine whose keychain is empty. One file holds every Developer ID identity the release signs with — select them all in Keychain Access → My Certificates and Export Items in a single pass, because a second secret would be a second thing to rotate. packaging/macos/keychain.sh imports it; on your own Mac use Keychain Access and skip both |
OAA_IOS_CERTIFICATE, OAA_IOS_CERTIFICATE_PASSWORD |
base64 of a .p12 holding an Apple Distribution certificate, and its export password. A different certificate type from the Developer ID above and not interchangeable with it — one signs a Mac app for direct download, the other signs an iOS app for the store. keychain.sh imports either |
OAA_IOS_PROFILE |
base64 of an App Store .mobileprovision for com.openaudioanalyzer.oaa. A development or ad-hoc profile exports an IPA that builds, signs and verifies cleanly and is refused at the end of the upload, so make_ipa.sh checks the profile before it builds |
OAA_IOS_TEAM_ID, OAA_IOS_SIGNING_IDENTITY |
Optional. They default to the team in the Xcode project and to Apple Distribution, which matches whichever such certificate the keychain holds. Set the team when the profile was created under a different one from the project’s — make_ipa.sh compares the two before it builds, because a mismatch presents as Xcode finding no profile at all. Neither takes quotes: the value is written into an xcconfig verbatim, where a stray " is part of the setting |
OAA_ASC_KEY_ID, OAA_ASC_ISSUER_ID, OAA_ASC_KEY |
An App Store Connect API key — its id, the issuer uuid, and base64 of the .p8. Needed by testflight.sh and nothing else. The key needs the App Manager role or the upload is refused with a permissions error that names no role |
OAA_BUILD_NUMBER |
Optional. CFBundleVersion for the iPad build; unset, pubspec.yaml’s +N is used. CI passes the workflow run counter, because App Store Connect refuses a build number it has already accepted for the same version string |
OAA_WINDOWS_CERT |
Path to a .pfx. Your own machine — a runner has no file to point at |
OAA_WINDOWS_CERT_BASE64 |
base64 of that same .pfx, which is the form CI can be handed. Used in preference to the path when both are set |
OAA_WINDOWS_CERT_PASS |
The export password, for either form |
Four things that will otherwise cost you an afternoon:
- A signed but un-notarised download is still refused by Gatekeeper. The quarantine flag needs notarisation, not merely a signature. This is true of the plugin bundles as well as the pkg, and the plugin’s version of the refusal is worse: a modal with nothing in System Settings to override it, because “Open Anyway” is only offered for a blocked launch and loading a plugin is a library load. An identity and notarisation credentials, or neither.
- An App Store rejection arrives after the release is published. The iOS
path has no equivalent of
codesign --verify:flutter build ipaexits 0 on an export that fell back to automatic signing, and App Store Connect is the first thing that says no — during an upload thatci.ymldeliberately runs after the release exists.make_ipa.shtherefore checks what it can before and after the build: the profile’s bundle id, that it provisions no devices and allows no debugging, and then the signing authority read back off the finished archive. Run it once by hand, or withworkflow_dispatch, before trusting a tag to it — a dispatch builds and signs the IPA and does not upload, which is the useful half of the check. - A pkg needs a
Developer ID Installercertificate, which is not the one that signs code.productbuild --signgiven aDeveloper ID Applicationidentity fails with “no identity found”, naming a certificate that is in the keychain and is merely the wrong type. Both live in one.p12, andkeychain.shnow fails if an identity a job names is not in it — an installer certificate never appears insecurity find-identity -p codesigning, so the import used to look correct while being half missing. - Signing the Windows installer does not remove SmartScreen. An ordinary OV certificate still produces “Windows protected your PC” until the file accumulates download reputation, which takes weeks and resets whenever the certificate does; only EV carries reputation from the first download. Current releases are unsigned and warn, which is a state a user can click through — unlike the macOS plugin refusal, which they cannot.
The icon
dart run packaging/icon/make_icons.dart
Regenerates every size and shape the six platforms ask for — the five desktop
downloads, Android’s adaptive icon, and the layered AppIcon.icon that macOS
and iOS render for themselves — into the platform directories, packaging/,
assets/brand/ and website/public/.
The mark is read, not described: assets/brand/oaa-logo.svg is the drawing,
and everything else — the icon with its tile, the mark on its own, the ramp on
its own, the favicon the site serves — is written from it. Redraw that one file
and run this; nothing is brought across by hand. The outputs are committed, so
a release runner never runs this.
The documentation site
dart run tool/docs.dart --out build/docs
These pages, from the Markdown in the repository. No second toolchain: the documents it publishes are normative and held by tests, and a site that can break on a machine where the code is fine is a site that will.
keyboard.md is generated rather than written — it comes from the same table
the application binds, and test/shortcuts_test.dart fails if the checked-in
page has drifted:
UPDATE_DOCS=1 flutter test test/shortcuts_test.dart
Contributing
The two boundaries that carry weight:
engine/knows nothing about Flutter, andoaa_coreknows nothing aboutdart:ffi. Four things need the domain vocabulary — the app, the tablet display, the CLI and the plugin — and three of them have no engine of their own.- One
liboaaserves all three tiers. That is what makes standalone, remote display and plugin tractable as one project rather than three.
And the rule everything else follows from: never invent a measurement. A
quantity the engine has not computed is NaN with an unavailability flag, and
the interface draws an em dash. If you are tempted to return 0.0 so something
looks right, you are about to ship a number nobody measured.
CLAUDE.md and the AGENTS.md tree in the repository carry the rest, in more
detail than a documentation page should.