◀ Back to contents
What's new
The release history below is drawn from the project's canonical
CHANGELOG.md.
1.2.0 - 2026-08-07
Added
- Amplitude histogram for the oscilloscope. A new toolbar button opens a resizable window showing how often the signal sat at each voltage - horizontal bars, voltage up the left axis with 0 V always on the centre line, occupancy along the bottom. A sine draws the bathtub of its two turning points, noise a Gaussian bell, a clipped signal a spike where the rail is; a waveform clipped on one side only shows as unequal bars about the centre. The window has its own L/R pick (greyed for a channel switched off in the scope) and its own reset, so clearing the distribution never disturbs the measurement table's statistics. Collection is unbounded while the window is open and survives a V/div change, a range switch and a recalibration - the axis is relabelled, not recounted. Resolution follows the signal rather than the converter: the range is sized by the signal's own peak over the measurement-average window, so a millivolt residual is resolved as finely as a full-scale tone instead of collapsing into a couple of bars. Bar count is configurable under Preferences ▸ Oscilloscope (default 50); it is display resolution only, so changing it re-draws what has already been collected instead of discarding it.
- dBr - a new magnitude unit for the FFT view. The unit selector gains dBr next to V, V/√Hz, dBV and dBFS: every level is shown relative to the fundamental, which therefore sits at exactly 0 dBr. The vertical axis, the cursor's level readout and the magnitude readout all follow, so harmonics and noise read off directly as "so many dB below the tone" with no mental arithmetic. The reference is the fundamental as displayed - a manually entered one included - and is re-read on every repaint, so it keeps tracking a drifting level.
- Output amplitude can be entered in dBFS. The generator, the frequency-response sweep and the tune-notch wizard now accept an amplitude typed in dBFS (
dbfs, or the short dbf), relative to the converter's full scale, and keep displaying it in that unit until another one is typed - wheel and arrows step it in dB, as they do for dBV. 0 dBFS is a full-scale sine (AES17), so the figure does not change meaning with the waveform. The short forms d and db now work as dBV on these fields too, alongside dbv. The FFT's manual fundamental is deliberately left out: it states an ADC-side reference level that routinely exceeds the converter's full scale, where a dBFS figure would be meaningless.
- QA40x settings dialog. Preferences ▸ Audio gains a settings button beside the backend selector, shown for a backend that has settings of its own - for now the QA40x. It reads the analyzer's identity and live telemetry straight off the device: firmware version, serial number, USB voltage and current, ISO-supply current (QA402 only), board temperature and the two capability words. It also switches the front-panel I2S expansion port on and off: the port is started before a measurement session and stopped after it, at the frame width taken from the output bit depth - while it is on, that setting offers the port's 16 and 32 bits in place of the analyzer's 24, and the input stays at 24 throughout. The settings live under the backend's own section of preferences.yaml and, like every other page of the dialog, take effect only when it is closed with OK. Switching the port is experimental: QuantAsylum documents the connector itself, but not the USB registers that enable it and set its frame width, so that control path is not vendor-confirmed - which the dialog says plainly.
- QA40x analyzer in the browser, over WebUSB. The web version now drives a QA402 / QA403 directly from the tab. Preferences ▸ Audio gains a backend selector, and Scan devices then asks for the analyzer over WebUSB - the browser's own chooser, which needs a click - opens it, reads its factory calibration page and builds the device card from the analyzer's own levels - so device-provided full-scale, until now the one desktop calibration feature a browser could not support, works here too. Capture and generation run full duplex over the vendor protocol at 48 / 96 / 192 kHz - plus 384 kHz on a QA403 - with the input and output rates locked equal to the single hardware clock; the front-panel I2S port and the identity and telemetry panel sit behind the backend's own settings button - QA40x preferences - as on the desktop; the attenuator ranges are device-owned, so they cannot be renamed, added to or removed; the frequency-response sweep and the file player go out through the analyzer's own DAC rather than the computer's sound card, and sample-for-sample at that - its converters share one clock, so a sweep is deconvolved against exactly what was played, with no resampler in between; Input and Output bit depth appear for a backend that has one - the analyzer's 24 bits, or the front-panel port's 16 / 32 while it is on - and stay hidden for Web Audio, whose samples reach the page as float32 taken after the system mixer, with no depth to choose and none to report; and the analyzer is parked at its safe ranges and handed back to other applications as soon as the last module stops using it, rather than being held for the life of the tab. It needs Google Chrome - a narrower requirement than the application's own, which is any Chromium browser: the analyzer has only been verified to work in Chrome, and on the same machine, over
https:// with a valid certificate, Edge, Opera and Brave did not reach it. Being built on Chromium is not enough; the browser must also expose WebUSB and let a page use it. It further needs a page served over https:// or localhost, and on Windows the analyzer bound to a WinUSB-class driver, which is a per-machine step a web page cannot perform for you.
- 384 kHz for the QA403. The QA40x backend now offers 384 kHz alongside 48 / 96 / 192 kHz when the attached analyzer is a QA403 - the sample-rate register has a fourth code the QA402 does not have, so the rate list follows the model. Capture and generator both run at the selected rate, as at every other rate: the two directions share the one hardware clock. The rate had been held back on the strength of a third-party driver comment calling it input-only; a QA403 user running it in practice reports otherwise, and the protocol notes now record that evidence.
- Documentation. A new frequency-response section on reading a wobbly trace as input clipping - why a swept sine puts each harmonic's impulse response a fixed interval *ahead* of the wanted one, how that combs the magnitude curve at a spacing that identifies the harmonic responsible, and why the cure is drive level rather than any setting. The Preferences chapter and Theory ▸ Audio backend gain the QA40x: the per-backend settings button, the telemetry panel and the front-panel I2S port, with fresh screenshots. All in English, German and Ukrainian. The QA40x protocol notes gain the wire-confirmed extended register map - firmware, capability, serial number, telemetry and the I2S control and frame-width registers - plus the third front-panel endpoint pair, and now cite three independent implementations rather than one.
- The web version states its browser requirement up front. It needs a Chromium-based browser - Chrome, Edge, Opera or Brave. The page checks for one at start-up and, on anything else, shows a full-page notice naming them instead of loading the application and failing later inside some particular module. The requirement is not the QA40x's alone: the measurement path is built on the AudioWorklet and on frame-accurate capture-rate probing, neither of which Firefox or Safari provides. Translated into every UI language.
- Web version catch-up. Besides the QA40x backend above, the browser port picks up the rest of this release's measurement work. It gains the oscilloscope's amplitude histogram, ported class for class from the desktop - the same accumulator and range fitting, the same voltage axis read relative to the distribution's own mean with 0 V on the centre line, the same nice-number linear axes, the window's own L / R pick and reset, the histogram button appearing beside the measurement gauge once a signal is present, and the bar count under Preferences ▸ Oscilloscope; it differs from the desktop in one place, the window being a resizable, draggable panel inside the page, because a web page cannot own a separate top-level window. Alongside it come the dBr magnitude unit with its axis, cursor readout and amplitude display, and output amplitude typed in dBFS against the live full scale. Its Preferences also reach the desktop's behaviour now that there is a backend to choose: devices are enumerated at startup and by Scan devices rather than on every backend switch, the list names them instead of showing the browser's raw device id, an input range is staged until OK confirms it and Cancel puts the previous one back, changing the output range retunes the DAC full scale so the generated amplitude stays where it was set, and a calibration change resets the running statistics and distributions. It also picks up the desktop behaviours of this release that the browser did not have at all: the warning raised when the committed device has no calibration behind it, the clipping warning after a sweep, Cancel on the sweep's busy window, the tune-notch marker graded against the null the sweep actually reached, the dialog a live notch sweep raises when it cannot start instead of dying in the log, and the quarantine of a settings or device store that will not parse - kept aside under a
.corrupt key in the browser's local storage, where the browser keeps settings and cards instead of files, and logged, never overwritten by defaults. Help chapter and screenshots in English, German and Ukrainian.
- The browser edits its own settings as JSON. Tools gains two submenus, Preferences and Devices, each offering Current - which opens that store's live document, pretty-printed, in a proper JSON editor. It is not a text box: the editor knows the shape of the document it is showing, so typing offers only the keys that store actually allows, and pointing at any of them explains in a sentence what it is for and what its values mean. Nothing changes while you look. OK applies the document to the running application and writes it, so a rate, a colour or a whole card takes effect without a reload; Cancel discards it, and text that is not valid JSON keeps the window open and says what is wrong instead of closing over your work. The window is movable by its title bar and resizable from its corner. When a store has been quarantined, a Corrupt item appears beside Current and opens the kept copy read-only - formatted if it can be parsed, and otherwise exactly as stored, because that text is the only evidence of what went wrong and repairing it would destroy the thing worth reading. One refusal is deliberate: a device document with no
audioDevices list is rejected rather than applied, since applying it would delete every card profile and its calibration - to empty the store on purpose you write "audioDevices": [], which is accepted and does exactly that.
- The browser measures through a Phonalyser server too. The web version is a full client of the server above, with nothing to install: Servers... in Preferences ▸ Audio lists the benches the page can reach - typing one address is enough, because the server answers with its whole peer table, and a double-click on a row connects it - with Connect, Disconnect and Remove beside it. A connected bench's backends join the backend selector as
<server> ▸ <backend>, its devices fill the device combos with the holder's name beside one another client is already measuring on, and from there the page measures as though the hardware were plugged into it. The capture arrives as binary frames whose gaps are confessed rather than papered over - a block the bench could not send is reported, never silently concatenated into the record - and the generator runs on the bench, commanded rather than streamed, because the protocol deliberately carries no audio from the browser: a frequency-response sweep is therefore deconvolved against the chirp the bench itself rendered, cut to the start marker the server writes in-band into the capture stream. A bench QA40x brings its settings, telemetry and range radios with it. Cards and calibration belong to the bench: the card combo offers the bench's cards, a pick is staged like every other Preferences edit and sent on OK, and a locally calibrated card matching an uncalibrated bench device is offered for copying across, once per session. Anything the bench refuses is reported in your own language, the server's own sentence kept for the log. The session is watched from both ends by the protocol's half-second keepalive - four unanswered pings and it is declared dead, every module stopped and the reason shown - a dropped connection is retried, and a page served over https:// explains that a browser will not open a plaintext session from a secure page - except to localhost - instead of merely failing. Playing a file to the bench works too: the story is under “Play from...” above, and the browser's half of it is the upload - the file's type read from its own bytes rather than trusted from its name, with a notice while it is on the wire, and In loop switched at the far end while it plays.
- A Phonalyser server serves the web version. Asking a server for
/ returns the browser app itself, built as a second packaging of the same source tree with the local backends compiled out: the page a bench serves offers that bench's backends and nothing else, and is already connected to the server that sent it. That is deliberate rather than cosmetic - such a page arrives over plain http:// from a machine on the LAN, which is not a secure context, so Web Audio and WebUSB do not exist on it and offering them would offer a choice whose every open must fail. It is the shortest path from the machine the interface is plugged into to a measurement taken on a laptop that has no interface at all, with nothing installed on either. Two consequences of that plain http:// worth knowing before you rely on it: the File System Access API is not there, so Save to... downloads the file to the browser's download folder under the suggested name rather than opening a save dialog, and the oscilloscope's record-forward-to-disk save - anything longer than the capture buffer - refuses outright and says so. The full web app at its own address is unchanged, except that it asks its own origin whether it is a Phonalyser server, and if so hides the local backends for the same reason; the embedded packaging simply has none to hide.
- Hardware volume discipline on Linux and macOS. Opening a device now sets its own volume controls to the device's 0 dB point - read from the control's dB scale on Linux and through CoreAudio's dB translation on macOS, never simply "maximum", which on a capture side is the top of the gain range - and closing restores every control exactly as the open found it. Only the opened device and direction are touched; a macOS aggregate device is pinned through its active members. A new help chapter, Audio backends, documents the discipline.
Changed
- The desktop application is now built as Maven modules. What was one source tree is fifteen modules under
modules/: a core that carries the measurement engine and depends on neither a GUI toolkit nor any backend, one module per audio backend, the command-line front end, the shared GUI infrastructure, the GUI itself, a backend's own settings panel, and a packaging module that produces the fat JAR and the installer. Backends are found at runtime through the service loader, so no module names a concrete backend and the dependency arrows only ever point one way - which is what makes a build without any GUI toolkit possible at all, the prerequisite for running Phonalyser.web headless as a measurement agent. Nothing changes for someone installing the app; for someone building it, the artefacts now appear under modules/phonalyser-app/target/ instead of target/, and mvn clean install from the repository root still builds everything.
- A fresh clone builds with no preparatory step. Project Nayuki's FLAC library is not on Maven Central and used to have to be installed into the local Maven repository by hand before the first build would resolve. It is now the
modules/flac-library-java module and the reactor builds it in order, so the instruction - and the failure that followed forgetting it - are both gone. It keeps its own standalone build file, so this project's style checks are not applied to third-party source.
- Windows builds can produce the 32-bit JAR on its own.
make-windows.cmd takes an architecture: no argument builds the 64-bit fat JAR and the application image as before, x86 builds only the 32-bit fat JAR. The 32-bit target has no application image by nature - the packaging tool bundles the Java runtime it is itself running on, so a 32-bit image would need a 32-bit JDK, while the JAR runs on the user's own 32-bit runtime.
- Build documentation now matches what a fresh clone actually needs.
BUILD.md, PACKAGING.md and README.md now list the platform profiles with the installer each one produces, together with the rule that building for anything other than the machine you are on means deactivating the host's own auto-activated profile - mvn "-P!windows-x64,windows-x86" -DskipTests package for the 32-bit Windows JAR, the only target that never activates by itself and the only one shipped as a fat JAR with no installer. The native libraries were described as downloads to fetch or compile; they are committed under lib/ for both Windows architectures and both macOS ones, so there is nothing to obtain.
- The web's Preferences dialog is laid out like the desktop's. Audio is the first tab and the one it opens on. Every field row puts its control immediately after the label column instead of stranding it against the right edge, and the controls fill the width they are given and grow with the window rather than staying at a fixed 180 px - the labels are content-sized, exactly as the desktop's grid does it. The Audio tab's top row reads in the desktop's order and on one line whatever a connected server is named: Scan devices, Servers..., “Backend:”, the per-backend properties button when the chosen backend has one, then the selector taking the rest of the row. The dialog itself is now resizable, with its former fixed 640 × 480 as the minimum, so a long device or server name can be read in full by widening it; it opens at the minimum every time, as the desktop's freshly packed window does.
- Web. Several behaviours the browser version already had now match the desktop's. A device that will not open says which of the six reasons it was, in the operator's language, instead of showing the browser's own
DOMException text - the raw text still goes to the console, which is where it belongs. The scope's measurements are computed on the unfiltered signal while the trace keeps its HF filter, so a reading no longer depends on a display setting. Applying an FFT preset clamps its stored ranges to the bin size, Nyquist, the floor and the dynamic ceiling the way every other range change does, instead of blanking the plot. A file with more than one channel plays as the average of all of them, none dropped. And the two shared full-scale scalars are runtime state only: selecting a calibrated card no longer writes that card's levels into the stored settings as the machine-wide default for every device that has none. The QA40x ranges table carries the desktop's true-reference input labels below - 0 "dBV" real 0 dBFS or -9 dBV - for the local analyzer and a bench one alike, display only, the stored keys staying plain N dBV.
- QA40x input ranges are labelled with what they really are. A QA "N dBV" input range is not an N-dBV RMS full scale: it is an N-dBFS (Vpp-differential) reference whose true RMS full scale sits about 9 dB lower. The ranges table in Preferences ▸ Audio now says so on every input row -
0 "dBV" real 0 dBFS or -9 dBV - for the analyzer on this machine and for one reached on a server. Display only: the card keys, the stored cards and the range radios keep the plain N dBV, and the output ranges - genuine dBV - are untouched.
Fixed
- "Play from..." ignored the selected backend and output device. File playback opened an audio line of its own and always went through JavaSound, so whatever was chosen in the preferences made no difference - a QA40x user's file came out of the computer's sound card. It now takes the device from the preferences and opens it through the same path the generator's tone uses, at the file's own sample rate and bit depth, with no resampling and no dither added to material that is already quantised.
- Noise metrics were pessimistic by the analysis window's noise bandwidth. The integrated-noise sums behind SNR, N, N+D, SINAD, ENOB and THD+N were taken over windowed spectrum bins without dividing by the window's normalized equivalent noise bandwidth (NENBW) - a windowed bin holds NENBW bin-widths of broadband power, so every noise figure read low: 1.76 dB with Hann, 3.0 dB with Blackman-Harris 4, 5.8 dB with flat-top, up to 7.5 dB with HFT248D. Tones were never affected. The factor is computed from the actual window table - exact for every window, including future ones - and stamped into each result so a later band change recomputes with the window that produced the spectrum. The same pass makes two smaller noise-accuracy corrections: the noise integral is rescaled for the excluded signal zones (the slots under the fundamental's skirt and the harmonics are estimated at the surrounding floor instead of counted as zero-noise), and the fundamental's skirt-exclusion walk is overlap-robust - it ends only after a look-ahead window sized to the frame-overlap correlation length has fallen back to the floor, instead of at the first sub-floor bin of a wobbly overlap-averaged floor. Validated against a hardware simulator whose dither power is constructed and independently computed; the Theory of operation's FFT chapter now documents the correction and tabulates every window's factor. Fixed in the desktop app and the web version alike.
- The A-weighted figures were not actually A-weighted. N+D, THD+N and - through SINAD - ENOB carry an "A" to mark the residual as weighted for audibility, but the noise-and-distortion integral behind them was summed flat; the IEC 61672 A-curve was never applied. It now is: both terms of the SINAD denominator - the integrated noise and the harmonic distortion - are scaled bin-by-bin by the A-curve before summing, so N+D, THD+N, SINAD and ENOB reflect audibility rather than raw wideband energy. SNR and N stay deliberately unweighted, which is why they carry no "A". On a converter with a rising treble noise floor the difference is several dB - about 5 dB on an E1DA Cosmos-class floor - bringing the figures into line with dedicated analysers. Fixed in the desktop app and the web version alike.
- QA40x streaming crashed the 32-bit Windows build. libusb's public API is stdcall on 32-bit Windows while the binding called it as cdecl - heap corruption the moment streaming started (64-bit was never affected: the conventions coincide there). Calls and the transfer callback now use the correct convention per platform.
- The Windows platform JARs lacked the exclusive-mode JavaSound mixers. The WASAPI-exclusive JavaSound provider shipped only with the installer, so bare-JAR installs had JavaSound capped at DirectSound's 16 bits. The provider and its native library now ride inside the fat JARs; at startup the DLL is staged next to the JAR - the one location a running JVM can still load natives from - so 24-bit / high-rate exclusive mixers work identically on the installer and JAR routes. (A
csjsound_*.dll appearing beside the JAR is expected; it needs the Microsoft Visual C++ runtime matching the JVM's bitness.)
- Bare-JAR installs had no help and no UI translations. The in-app help and the locale bundles shipped only with the installer (as its external
help/ and i18n/ folders), so plain platform-JAR installs showed the "help not found" dialog and an English-only language menu. Both now ride inside the fat JARs and are staged once per app version into the per-user data folder at startup; a help/ or i18n/ folder next to the JAR still overrides the staged copies, so the translate-without-rebuilding workflow keeps working. The installer keeps using the folders from the installation package - nothing is duplicated there.
- Out-of-memory hardening. After a full audit of every size-scaled allocation, in three layers. Settings a small heap (a 32-bit Java) cannot possibly run are no longer offered: the FFT length list there ends at 2M and the frequency-response FFT size at 4M, with a larger persisted value clamping itself and the combo tooltip explaining the cap. Stopping the FFT now releases the analyser's working buffers (several hundred MB idled after Stop at 4M), and the displayed spectrum refreshes in place instead of copying ~64 MB per displayed frame at large FFT sizes - far less garbage-collector pressure on every platform. And every whole-capture path (the scope's Open signal, FLAC decoding, the CLI capture / export / calibration modes) pre-checks the required memory and refuses with the needed-vs-free numbers instead of dying mid-operation with an OutOfMemoryError; malformed WAV bit depths are rejected.
- Web help ignored the UI language. In the browser build, switching the app language left the in-app help stuck in English - it took the help language from the page locale, which the language switch never updated. Help now follows the UI language, and an already-open help window re-points to the same page in the new language.
- The output amplitude could be set past full scale. The generator, frequency-response and tune-notch amplitude fields hold an RMS voltage but were capped at the converter's *peak* full-scale figure - a unit mismatch that allowed a level up to √2 (3 dB) above digital full scale, where the signal can only clip. The ceiling is now the level at which the signal actually reaches full scale, so it follows the waveform: a rectangle may go 3 dB higher in RMS than a sine, a triangle sits between them, and a dual tone tracks its own amplitude split. It also follows the live full-scale calibration - the sweep and tune-notch fields previously read it once when their field was built and never again. Volts, dBV and dBFS all trim to that same maximum, so 0 dBFS is exactly the top of the range. Fixed in the desktop app and the web version alike.
- The FFT averages dial skipped Off and 1. The wheel and the arrow keys walked the preset series 2, 4, ... 128, ∞, so a single spectrum - "Off" - could only be typed, never dialled. The series now starts below 2: Off ↔ 1 ↔ 2 ↔ 4 ... ↔ ∞, and the settings-tab tile spells a single spectrum "Off" the way the field already did. Off and 1 mean the same thing to the analyser, which averages only from two spectra up; they are simply two distinct stops on the dial.
- "Stop after N averages" stopped one average early. Entering 50 paused the run with the on-screen counter reading 49: the check counted analysis ticks, while the counter deliberately shows one less - the first tick seeds the average rather than being one of them. The run now stops when the displayed count reaches N.
- The Linux
.deb refused to install on pre-24.04 systems. "Depends: libasound2t64 but it is not installable" - the installer's dependency list is generated from the package names of the machine that builds it, and that is now Ubuntu 24.04, whose time_t transition renamed a number of libraries. Every such dependency is now recorded as an alternative that accepts the old and the new name alike, so one .deb installs on 24.04 and on 22.04 / Debian 12 both.
- Cards with independent left/right ranges could not have both channels set. In Preferences ▸ Audio a card whose channels switch range separately - the E1DA Cosmos ADC's per-channel DIP switches, for instance - shows a Left and a Right active-range button on every range row. The two behaved as one group, so choosing a range for one channel cleared the other channel's choice and the buttons no longer agreed with the stored setting. The two columns are independent again, and clicking the already-active range no longer clears it.
- The QA40x kept its input attenuator engaged after use. Stopping the engine, switching to another backend or quitting the app all left the analyzer at whatever range the last measurement had selected - a 0 dBV session left the input at maximum sensitivity, unprotected, after the process had gone. The device is now parked at its safe idle state - input +42 dBV (maximum attenuation), output −12 dBV, stream stopped - when the backend is deactivated and on the way out.
--fft-analyze misread 32-bit WAVs. Samples are stored offset-binary, and a 32-bit code exceeds the signed-integer range, so the half-range subtraction wrapped and every positive half-wave came out 2³² low. The result was not subtly wrong but nonsense - THD around 102 %, SNR near 10 dB - on files the app had written itself. 8/16/24-bit were never affected.
- The generator's dither dBV no longer carries the FFT window's noise bandwidth. The field folded the analysis window's equivalent-noise-bandwidth term into the figure it displayed, so the same dither read as a different dBV when the FFT window changed - a generator setting moving because an unrelated analyser setting moved. It now states the physical level of the dither it applies, and stays put.
- Stopping the generator did not restart the analyzers' averages. Starting a tone restarts the FFT average and the scope's running statistics, but stopping it did not, so an average kept accumulating across the moment the signal went away - a spectrum blending tone and silence presented as one measurement. A real stop transition now publishes the same signal change a start does, and the accumulators start over.
- The frequency-response Filters tab clipped its bottom field row. The toolbar row was 200 px tall and the Filters tab is the tallest of the three, so its last field row sat cut out of view. The row is now 232 px - the measured fit.
- Web. Losing the input device mid-capture went unreported: the device could be unplugged or taken exclusively by another application and the scope carried on drawing a flat line with the capture rate still ticking - a measurement of nothing presented as a measurement. The capture source now watches the media track, and losing it shows the error and stops the scope and the FFT. The playback lane is watched the same way, so a converter pulled out mid-tone stops the generator with a reason instead of leaving a lit Play button over silence, and a capture that goes quiet without ever erroring - the device still open, no samples arriving - is held to a delivery deadline rather than waited on forever. Axis tick labels take their decimals from the tick step they belong to, and zero prints as zero: the linear axes that carry it in the middle of their range, the oscilloscope's and the amplitude histogram's, no longer label it “-0”. Also, 83 interface strings across 31 languages had drifted behind the desktop catalogue - the preset tooltips were still English, and two Ukrainian terms did not match the translation glossary - and were refreshed.
- macOS sample-rate lists now state the device's truth. The offered rates used to come from probing through the system's converter, which resamples silently and answered yes to nearly everything - a device could be offered every rate from 8 kHz to 768 kHz. The list now comes from the physical stream formats the device itself declares.
- A device pulled on macOS no longer leaves the measurement running. The tone played on into the void and the capture drew a flat line, because CoreAudio leaves such streams nominally active; both now ask the live system once a second whether the device is still there - an aggregate through its active members - and stop with the reason within moments. Plugging the device back in is picked up without restarting anything.
- A compensated sine whose predistortion file is missing says so. Starting the generator pointed at a .dpd file that no longer exists reported "reason unknown"; it now names the missing file.
- A stopped oscilloscope no longer redraws from the live input (web). With the capture still running for other panes, a mouse-over or a window resize on a stopped scope could repaint the frozen trace from the live ring; the held frame is now the only source until recording starts again.
1.1.0 - 2026-07-21
Added
- Frequency-response ideal-filter overlay & compare. Overlay an ideal low-pass / high-pass / band-pass / notch response (Butterworth, Chebyshev, inverse Chebyshev, elliptic, Bessel; designed by spec or by order) on the measured curve, anchored to it per type, and compare measured vs ideal the same way as against RIAA. Each filter type remembers its own parameters; presets carry them.
- Frequency-response flatness readout. New Unevenness modes: band edges where the response first leaves a ±X dB corridor from its peak (or, with the explicit Notch switch, from its minimum), or the ± deviation over a chosen range - with on-plot annotation lines and a detachable measurements window.
- Oscilloscope residual view. Per-channel captured-minus-best-fit-tone display that exposes distortion, noise and glitches hidden under the fundamental; works for single and dual tone, with both dual-tone frequencies measured from the capture itself, so independent DAC/ADC clocks (no FLL) cannot smear the subtraction.
- Per-card calibration profiles. Full-scale calibration now belongs to the physical card, not the app: each card carries a device-name match list - so it is recognised across every backend - and a range table with one row per attenuator / DIP position (e.g. both settings of a Cosmos ADC); the crosshair calibrations write into the card's active range - creating the profile automatically on first calibrate - and switching devices or backends never mixes calibrations up. Cards without a profile keep using the previous shared values.
- Dither depth in bits or dBV, checkable on the FFT. The generator's dither control is a numeric field you drive in bits or directly in dBV. The dBV is full-scale-aware and also carries the FFT analysis window's equivalent-noise-bandwidth term, so it reads straight off the FFT noise floor - enter −100 dBV and, with incoherent (power) averaging, the floor sits at −100 dBV. Wheel/arrows step ±1 bit or ±10 dBV, and
Off is typed straight in as o / of / off rather than picked from the old drop-down; the entered value is held when you change the FFT window or recalibrate.
- Per-channel (left / right) calibration. A stereo card calibrates each channel into its own full-scale: the calibration dialog - now one unified form for the ADC and the DAC, with values entered directly in nV / µV / mV / V - shows a Left row and a Right row (during an FFT calibration the row for the channel it is not analyzing is disabled). Every scope trace then uses its own channel's full-scale and the FFT dBV axis follows the analyzed channel. This now applies to range-linked stereo cards as well - shared range switching, but separate left / right values per row - not only cards whose channels switch range independently; the latter (the E1DA Cosmos ADC's independent left / right DIP) additionally get Left / Right active columns in the ranges table.
- Card editor & automatic recognition. A dialog in Preferences creates and edits cards - name, the device-name match list, mono / stereo, per-direction range coupling (linked or independent), and a flag for cards whose full-scale is supplied by the device itself. Selecting a device with no matching card offers to create one, pre-filled from that device; a device that does match a card selects it automatically.
- Known-card catalog. Cards the app already knows (E1DA Cosmos ADC, JLsounds I2SoverUSB) are recognised by device name and offered pre-configured with their nominal ranges; your own crosshair calibration then refines each unit's values. All card profiles live in
devices.yaml next to the preferences file - seeded on first run, editable through the Preferences dialog or by hand in a compact, documented format (one line per range, per-channel value pairs; the help's Preferences chapter describes it). An upgrade adds newly known cards and ranges and refreshes the nominals of ranges you have not calibrated, while never altering a row you calibrated, a card you created, or your active-range selections - so no calibration you made is ever lost.
- QA40x analyzer backend. A QuantAsylum QA402 / QA403 can be driven directly over USB (libusb), with the vendor software closed - a new QA40x backend alongside WASAPI / WDM-KS / JavaSound. It runs the analyzer as one always-duplex session on the device's single sample-rate clock, so the input and output rates are held equal; delivers true 24-bit samples; and takes full-scale from the device's own range calibration rather than a crosshair calibration, so the dBV axis and scope readouts are right as soon as you pick the range. Where the native libusb library is absent the backend is simply reported unavailable; it ships bundled on Windows and macOS, while Linux uses the distribution's own libusb - there the
.deb installer also sets up the analyzer's USB access permissions (udev) automatically, and the README carries the one-file manual setup for JAR installs.
- Output-channel selection. The signal generator, the frequency-response sweep and the notch tuner each gain a Left / Right / Both output selector that gates the driven lane live.
- DSO-grade dense trace rendering. Above one sample per pixel the scope now rasterises the whole capture window the way a digital-phosphor oscilloscope does - a per-pixel dwell histogram plus a round coverage pen of exactly the configured trace width, swept along the band-limited (sin x/x) crest and trough of every column - instead of decimating to one point per column. Narrow pulses, noise bands and dual-tone beat envelopes keep their true peak-to-peak at any zoom; steep flanks anti-alias with proper per-row edge ramps (Xiaolin-Wu style) on both sides; and because the result is a single blitted intensity image rather than a stroke of every period, a one-second window renders at full capture rate where brute-force drawing dropped to ~1.5 captures/s. The rasterisation runs on a half-pixel grid and the trace keeps the same brightness and AA fringe as the sparse sin x/x stroke, so nothing changes visually when zooming across the one-sample-per-pixel boundary.
- Documentation. Help chapters for the new Filters / Unevenness tabs and the residual view, the scope's per-channel mains-rejection and low-pass controls, and the per-card calibration setup with a worked two-card example - in English, German and Ukrainian.
- Tip of the day learns the new features. Seven new tips - the residual view, the ideal-filter overlay and Compare, the Unevenness readout, per-card calibration, the output-channel selectors, the DSO-grade dense rendering and the discontinuity guard's off switch - in English and all 31 translations, each naming the controls by that language's own UI labels. A few catalog defects went with it: two English typos, a stray Cyrillic letter, and a Ukrainian tip that had inverted the meaning of WASAPI exclusive mode. The web app gains the Tip-of-the-day popup itself - in the Help menu, and at startup under the same preference as the desktop.
- Web version catch-up. The browser port now carries the full per-card calibration system - cards, ranges, the card editor, the known-card catalog (read 1:1 from the very
devices.yaml the desktop ships, parsed in the browser; the profile store lives in the browser's local storage and follows the same upgrade-merge rules), per-channel left / right calibration through the unified ADC / DAC dialog, and per-channel full-scale in every view. It also gains the Left / Right / Both output-channel selectors, the DSO-grade digital-phosphor dense trace renderer, scope display persistence (WebGL2) with the same sample-dots-defer-to-persistence rule, the dual-tone frequency-lock loop and IMD de-embedding with F1/F2 marker dots and pre-calibration dots, the manual-fundamental lobe stretch - the fundamental's whole main lobe lifted to the entered level with a blue dot marking the original height, "not measurable" (---) IMD readouts instead of fictitious floor values, sample-grid-aligned rectangle AND triangle generation with bracketed corrected frequency / duty labels, the dither field entered in bits or a full-scale-aware dBV level and applied live to the generated signal so it reads straight off the FFT floor, the typed named values on both numeric fields - dither Off, and FFT averages down to a single spectrum shown as Off plus ∞ averaging - each taken in full or in any short form (o / of / off, i / in / inf), a startup splash, Java-parity preferences in a fixed 640 × 480 dialog with free numeric entry, an output-sample-rate probe with an honest resampling warning, a Preferences audio-device or sample-rate change applied live to the running measurement - the capture restarts on an input change, a playing generator (tone or file) on an output change, at the new settings - scope V/div down to 1 nV/div, and a web-only help page on input-device sample rates (en / de / uk). The one desktop calibration feature the web does not support is device-provided full-scale (the QA40x-style flag) - a browser cannot reach a device's USB calibration interface; such cards keep their catalog values.
- FFT averages:
Off is reachable at all, and ∞ can be typed. The averages field started at 2, so switching averaging off was not possible from the UI - even though the analyser already treats fewer than two averages as no averaging. The count now goes down to a single spectrum, shown as Off. And ∞, until now only reachable by rolling the wheel or stepping with the arrows, can be typed directly. Both names are taken in full or in any short form - o / of / off and i / in / inf.
Changed
- Loaded
.frc measurements now take their analysis bandwidth from the file's own recorded sample rate (the live device rate only for legacy, header-less files).
- Compare auto-zoom fits the difference curve with a symmetric 2 dB margin, and switching compare off re-fits the view to the measured curve.
- Notch tuning now measures both channels on every pass and lets you choose which channel the embedded view shows (Left / Right), with the null readout repositioned clear of the display controls.
- Sample dots defer to persistence. With display persistence active the scope no longer draws the per-sample dots - repeated frame after frame they piled into opaque blobs on the afterglow and buried the trace history it is there to show. They return, as configured, the moment persistence is off.
Deprecated
- Shared full-scale calibration. The single shared ADC / DAC full-scale values (
adcFsVoltageRms / dacFsVoltageRms in preferences.yaml) are superseded by the per-card calibration profiles. They remain only as the fallback for devices that have no card in devices.yaml and will be removed in the release after this one.
Fixed
- Linux: GTK input-method startup crash. A configured ibus / fcitx input module whose daemon is dead or missing crashed the app at launch; the input method is now pre-flighted and falls back to XIM only when actually broken - working IME setups keep their input method.
- Linux: GPU scope under Wayland. The GL trace canvas could not obtain a context on a Wayland session; with GPU rendering enabled the GTK backend now switches to X11 (XWayland) automatically.
- Glitch / discontinuity rejection. The time-domain discontinuity guard behind the scope's glitch trigger and the FFT's frame rejection now references its threshold to the signal amplitude, so it no longer false-triggers on clean tones as their frequency rises - detection is flat across frequency and independent of level, while still catching the real sample-loss splices it is meant to reject. A new switch on the FFT's settings tab turns the guard off entirely.
- Audio changes restart what was running. Changing the backend, device, sample rate or bit depth in Preferences now stops every running module before the change and brings it back on the new settings afterwards - the FFT analyzer (previously left stopped), the scope capture, and a playing generator, tone or file alike (previously left playing into the old device).
- Generator changes restart the FFT and clear the scope afterglow. Changing the dither, the output level or the output-channel selection now restarts the FFT statistics and averaging accumulator and clears the scope display persistence - the same reset a frequency or amplitude change already performed - so an averaged measurement never mixes the previous signal with the new one.
- Scope channel guards. The measurements table and the trigger source can no longer be pointed at a disabled channel - selecting one auto-switches to a live channel, and the choice survives starting a capture and applying a preset.
- Scope screenshots. The built-in screenshot now includes the digital-phosphor persistence trails and draws the traces - main view and condensed overview alike - at the configured trace width (they always came out 1 px before, whatever the preference).
- Held-trace horizontal pan. After switching the trigger mode from Auto to Normal or Single with no trigger event yet, the held trace ignored horizontal moves (vertical worked); the held frame now pans and zooms exactly like a triggered one.
- Beat envelope on frozen captures. A held or frozen Single / Normal dual-tone capture now draws the reconstructed beat envelope over its captured samples - it previously vanished the moment the trace froze.
- Full-scale markers at fine V/div. The ±FS dashed lines anchor to the raw (virtual-capable) channel offset, so at fine resolution they keep tracking until ±FS/2 reaches the vertical middle instead of sticking early.
- Triangle duty on the sample grid. A triangle's duty corner has to land on a sample just like the rectangle's step edge - off an integer-sample period it drifts against the grid cycle to cycle and the tone smears. The triangle now runs at the nearest whole-samples-per-period frequency, the Frequency and Duty cycle labels show the corrected values in brackets exactly as for the rectangle, and WAV export uses the same aligned frequency so looped files have no corner seam.
- Persistence keeps the anti-aliasing. With display persistence on, each new frame was composited into the afterglow with source-over, so a stationary trace re-painted its own anti-aliased fringe pixels over themselves until they saturated solid - the persisted trace turned hard-edged and a fringe wider than the pen. The deposit is now brightest-wins (per-component maximum): a fringe pixel can never exceed its single-frame coverage, so the persisted trace keeps exactly the anti-aliasing and width of a persistence-off frame while decayed history fades underneath - and infinite persistence no longer saturates. Desktop (OpenGL) and web (WebGL2) alike.
- Web.
.frc de-embedding now corrects dual-tone IMD product lobes too (readout table, marker dots and IMD power / DFD all read the corrected bins), the frequency-lock loop steers both dual tones instead of one, and dual tones entered high-frequency-first no longer cross the F1/F2 pairs - readout rows, marker dots, intermod product formulas and both lock loops all read the correctly sorted pair.
- IMD readout. Intermod products whose frequency falls outside the measurable range - SMPTE-style pairs put 2f1 − f2 below DC, high orders can land beyond the spectrum - no longer show a physically impossible −600 dBV: they read "---", a one-sided DFD3 still reports its measurable sideband, and the combined IMD power skips them. Fixed in the desktop app and the web version alike.
- Low sample rates were never offered. The selectable rate list was effectively floored at 44.1 kHz. The standard lower rates - 8000, 11025, 16000 and 22050 Hz - are now probed on every backend and offered wherever the device actually supports them (except the QA40x, whose 48 / 96 / 192 kHz are fixed in hardware).
- Blue dot missing on a manual-fundamental lobe. With a manual fundamental set (and no calibration loaded), the fundamental lobe was stretched up to the entered level but the blue dot marking the original measured height was not drawn. It now appears, as it already does with a calibration loaded.
- Scope rubber-band zoom at deep magnification. At a few samples per screen - where the whole view is a sin x/x reconstruction between a handful of samples - the zoom box and the edge time marks were mapped over the continuous time-per-division product while the trace is drawn over a whole-sample window. That round-off, multiplied by a trigger parked screens outside the view, slid each successive zoom sideways and let the time marks disagree with the trace. Zoom capture, zoom commit and the edge marks now all use the exact sample window the renderer draws, so the boxed detail lands under the box at any depth and the marks match the trace.
- Scope handles and value labels at the view edges. The channel-offset and trigger-level triangles sat on the border pixel - half swallowed by the view frame - and their voltage labels clipped when a handle reached the top or bottom edge; both now stay fully visible just inside the view. The offset voltage label was also computed from the clamped on-screen handle position, so once a deep zoom parked the offset outside the grid the label froze and wheel moves appeared to do nothing; it now reads the true offset however far outside the grid it sits.
- Glitch trigger selectable in Auto after a capture start. Starting the scope re-enables the whole trigger toolbar, and that blanket enable resurrected the Glitch type button in Auto mode - where a caught glitch frame would be overwritten by the next free-run repaint, the very reason the combination is blocked. The Glitch-outside-Auto gate is re-applied now.
- macOS: GPU scope window placement. The GL trace window opened shifted upward until the first mouse-over or resize forced a reposition, and with the scope idle it did not follow the main window when dragged. It now re-tracks its pane while the opening window chrome settles and on every main-window move.
- macOS: crash on exit. Quitting could die with "Graphic is disposed": tearing down the GL scope window pumps the event loop mid-shutdown, which could deliver one last paint to a toolbar arrow whose icon was already disposed. That late paint is now skipped.
1.0.3 - 2026-07-04
Added
- Notch-filter tuning module. Live tuning of a passive twin-T notch: a continuously looping log-sweep tracks the null in real time - its power-of-two period makes the transform shift-invariant, so no trigger or alignment is needed - and the notch's own response is then saved as a
.frc calibration for de-embedding.
- DAC pre-distortion calibration. A closed-loop wizard that iteratively cancels the converter's own harmonics (and dual-tone IMD), pushing the playback chain's distortion far below what the DAC produces alone.
- Web version. A browser port of the analyzer - FFT, oscilloscope and signal generator - running with no install in the web version.
- Oscilloscope. Rectangular rubber-band zoom with
Ctrl+Z rollback, and a glitch trigger backed by a signal-discontinuity gate.
- Documentation. Expanded Theory of operation (tune-notch measurement, ADC characterisation, adaptive mains cancellers, digital-phosphor persistence) and the
ALGORITHMS.md engineering catalogue; the project landing site.
Changed
- Oscilloscope. Whole-period integration for Vmean/Vrms (removes the partial-cycle scatter that grew with amplitude); exact horizontal pan/zoom positioning.
1.0.2 - 2026-06-29
Added
- Oscilloscope digital-phosphor persistence (GPU path) with off / fixed / infinite / manual decay modes.
- Scope pan/zoom engine with connected-envelope rendering.
- Microsoft Store (MSIX) packaging and Store listing assets.
Fixed
- Frequency response - restored the FS/2 sweep-points entry.
- Dropped unused JOGL/GlueGen dependencies (CI build fix).
1.0-RC1 - 2026-06-19
- First public release candidate: FFT analyzer (THD, THD+N, IMD, SNR, SINAD, ENOB, per-harmonic readout, coherent averaging, selectable windows,
.frc calibration), oscilloscope, frequency response (Farina log-sweep deconvolution), signal generator (DDS), and multi-backend audio (WASAPI & WDM-KS on Windows, CoreAudio on macOS, JavaSound on Linux).
◀ Back to contents