1.2.1 2026-08-16
Added
- A digital loopback backend. What the playback lane quantises is what the capture lane delivers, with no OS device in between - a bench with a known noise floor: the samples are dithered at the last bit of the selected depth, so the measured floor is the arithmetic one and any deviation is the code under test rather than a sound card. It needs no hardware and is available on every platform.
- The web app gains that loopback backend too. The browser port picks up the same hardware-free bench: playback returns to capture with nothing but arithmetic in between, TPDF dither on the last bit of the selected depth, and a noise floor that is therefore the computed one - 0.500 LSB RMS, one bit deeper moving it down by 6 dB. It is selectable beside Web Audio and the QA40x, offers the full rate ladder at 16, 20, 24 or 32 bits on both directions, and needs no device, no permission and no hardware at all. Where the desktop gives each lane a thread, a page has none, so both lanes are paced by timer callbacks against an absolute schedule.
- The QA40x is free the moment nothing measures with it. The analyzer is claimed while a measurement runs and handed back when the last of the generator, the scope and the FFT stops - so the vendor software, a second Phonalyser or another browser tab can have it without anything being restarted. Reading its telemetry or its calibration page is atomic in the same sense: the device is opened, read and released again, instead of the panel keeping it for the life of the session. An analyzer another program is holding is not offered as a local device at all, and reappears by itself once it is free. Desktop and web alike.
- The analyzer's factory data is read once. The calibration page - a hundred and twenty-eight register round trips - and the telemetry reading are kept per analyzer SERIAL NUMBER, so a panel that only displays values touches no hardware, and a reopen after a release costs nothing. The device scan maintains that store: an analyzer that has just appeared is read once, one that has left the bus is forgotten, and a unit swapped for another of the same model cannot inherit the first one's factors. The open no longer resets the USB device either - the analyzer is taken as the operating system initialised it.
- A standalone device scanner. A small command-line tool that prints, for every audio backend of the running platform, the devices with the exact rates and depths they take - the same listing the app itself sees - for bug reports and bench setup.
- An FAQ chapter in the help. A question catalog with its own live search box, covering among others the time-discontinuity warnings, busy devices, volume pinning and server security; the audio-backend theory chapter gains a loopback measurement section. All help languages.
- The distortion table's level cells have a unit selector. The THD/IMD table's per-line levels can be read in the unit of choice instead of dBV alone. Desktop and web alike.
- Web version catch-up. The browser port picks up this release's bench-side work. An upload to a bench now says so: the same notice the desktop raises while the bytes move, covering the whole prepare-and-upload phase rather than the transfer alone, and coming down on every outcome - the file playing, a refusal, a link that died. It is a modal carrying its own Cancel, because nothing is playing yet: the page underneath is blocked for as long as the bytes move, and the one way out is to cancel the transfer. A calibration the bench refuses is no longer silent either: the crosshair calibrate of the scope, of the FFT and of the generator all report it, through the same message the card-copy path already used, so a full scale that never reached the server cannot be mistaken for one in force. The message bus is the desktop's own: the event announcing that a server's backends appeared or vanished carries the desktop's name and value, and the shared capture is acquired and released by direct call as it is there, leaving on the bus the one batch notification a page still needs, having no blocking threads to wake. The analyser's restarts behave as the desktop's do: Reset statistics restarts the collection as well as the average, so a fresh average opens with a full window instead of the previous one slid forward a single hop; a capture overrun keeps the collected depth - it is a gap in coverage, not damaged data - restarts only the window, and names itself in the warning banner instead of re-anchoring in silence; the averages readout is read live, so a start or a reset shows zero at once rather than the previous run's depth until the next frame lands; and the first window of a collection is analysed the moment it is full, no longer a hop later. The oscilloscope's per-sample dots are placed on fractional coordinates, so at a zoom wide enough to show them they sit on their samples instead of on the nearest whole pixel. A range chosen for a bench analyzer now reaches the bench - the browser sent only the front-panel port, so an input or output range picked for a remote QA40x moved nothing. The help gains the FAQ chapter in English, German and Ukrainian - adapted where the browser measures through a bench rather than a local driver - alongside this release's help refresh and rebuilt search indexes.
Changed
- The net protocol is v2: every capture streams on its own connection. Audio no longer shares the control WebSocket - a capture attaches its own data connection, so control commands stay responsive while PCM streams at full rate. Server and clients speak v2 only: a 1.2.1 client pairs with a 1.2.1 server.
- Linux audio devices carry their real names. A device is named by its card's product name and socket - "CUBILUX CB5 - Line In" - taken from the kernel's own card list, instead of the chip-and-address spelling ("CB5 [plughw:1,1]"). Devices that report no formats at all - phantom PCMs with nothing behind them, such as an HDMI output without a sink - are no longer listed: the device combos, a server's device list and the scanner all show the same set. The system default entry stays, and Windows/macOS names are unchanged.
- A QA40x range nobody has chosen is the protected one. A card created for an analyzer that has never had a range selected now comes up at +42 dBV in and -12 dBV out - maximum input attenuation and the low output range, the same state every teardown parks to - instead of the vendor's 0 dBV / +18 dBV pair. An unconfigured analyzer therefore starts protected rather than at full input sensitivity. Desktop and web alike.
- The browser's tips are the browser's own. The tip that advised picking a driver backend for a faster oscilloscope described something a page cannot do, so the web catalogue drops it; the desktop keeps it. A tip that promises what its platform lacks is worse than no tip.
- A closed net session names its close code. The session-end log line of both ends now carries the WebSocket close code the socket reported and how many keepalive pings had gone unanswered, so an orderly server shutdown reads differently from a bench that had been failing for a while.
- The server's jar is one file for every platform. The headless server ships as
phonalyser-server-, with no architecture in its name: it carries the device libraries for all of them, so the same jar runs on Windows, Linux and macOS, on x86-64 and on arm64, and a bare.jar java -jaropens devices with nothing staged beside it. What the six downloads still do is bring their platform's natives, launcher and service scripts around that one jar. - Server downloads are the per-platform ZIPs alone. Each ZIP already carries the server's fat jar beside the natives, the launcher and the service scripts; the separate bare jars were duplicate downloads and are no longer attached to a release.
Fixed
- The oscilloscope's Utility tab stayed English in every language. Its caption was the one of the eight tab captions built from a literal rather than the message catalogue, so it never translated. Desktop and web alike.
- A typed 0 dBFS redisplayed as "-0 dBFS". The amplitude fields store volts, and the conversion back to the sticky dBFS display could land a few 1e-12 dB below exact zero - the formatter then kept the minus sign of the tiny negative. Display values are now rounded numerically to the shown decimals before formatting, which goes through a signless integer zero, so every unit's zero point renders clean. Fixed in the desktop app and the web version alike.
- The amplitude tooltips did not mention dBFS. The generator and frequency-response amplitude tooltips now name the
dbfs/dbfsuffix and its full-scale reference, in every UI language; the tune-notch amplitude field, which had no explanatory tooltip at all, shares the generator's. - Exclusive-mode devices taking 24 or 32 bits only as plain WAVEFORMATEX were refused. The JavaSound WASAPI-exclusive path applied the documented legacy-format retry only to 16-bit formats, so a driver that accepts higher depths for mono or stereo only in the legacy spelling was reported unsupported and refused at open. The retry now runs wherever the legacy structure can express the format, and the exclusive format enumeration answers in a stable order, so a device's default format no longer varies from run to run.
- The scope's calibrate button stayed off on wide input ranges. The gate demanded that the signal fill a quarter of the ADC's range whatever the range's width, so on a 10 V RMS front end nothing under about 7 V peak-to-peak could be calibrated from - refusing levels whose absolute accuracy is beyond question. A calibrated selection now also enables at 0.5 V RMS regardless of how much of the range it occupies; the anti-silence floor of an uncalibrated selection is unchanged. Desktop and web alike.
- Closing the frequency-response progress window did not stop the sweep. ESC or a window-manager close only disposed the window: the sweep kept playing and the pane's controls stayed locked until it ran out on its own. A user close now issues the same cooperative cancel as the Cancel button, and the window closes when the teardown finishes; the calibration wizard's progress window, whose trial sweep has no cancel path, refuses to close instead of orphaning a running sweep.
- Backends of other platforms were probed at startup. Every build carries all backend classes, and each backend tried to load its native library wherever it found itself - a Linux machine logged failed loads of the CoreAudio and Windows audio libraries it can never have. Every native-library loader now checks the operating system and architecture first, so a platform's log shows only its own backends.
- Linux capability scans covered only cards /proc/asound can describe. The format and rate probe read the USB stream descriptors, so a PCI or HDA card answered with nothing. When /proc/asound has no answer, the exact formats and rates are now read from the direct hardware device (
aplay/arecord --dump-hw-paramsonhw:card,device), never through the resampling plug layer, so every card reports what its converter actually takes. - ALSA unity-gain pinning skipped cards whose volume controls match no port name. The pin looked a control up by the selected jack's port word and gave up when the card names its controls differently - the measurement then ran at whatever volume the mixer happened to hold. When no name matches, every volume control of the selected card's direction is now pinned to unity and restored afterwards.
- WASAPI-exclusive devices taking 24 valid bits in a 32-bit container were not offered at 24. The exclusive probe tested only the packed spelling of each depth, so such devices scanned as 16- or 32-bit only. The probe now reports the exact valid bits whichever container the driver takes, and the open path resolves the container separately, repacking samples at the device boundary - the depth selected is the converter's true depth.
- "Snap frequency to nearest FFT bin" used the output clock. With unequal input and output rates the snapped tone landed off the analysis bin grid - and the pane's rate label followed the wrong clock - so the averaged spectrum smeared. The snap, the labels and the dual-tone bracket now follow the input rate the analysis actually runs at; the waveform period alignment keeps the DAC clock. Desktop and web alike.
- Switching between sine and dual-sine while playing rebuilt the previous waveform. The live restart replayed the record of the tone before the change - entering dual-tone kept a single tone sounding, leaving it kept both. The restart now rebuilds from the just-committed form.
- The largest FFT lengths produced no spectrum at all. The analysis span was clamped to the length of the capture buffer and then demanded in one contiguous read, so a frame longer than that buffer - four million points at 48 kS/s is 87 seconds of audio against a 22-second buffer - could never be assembled: the analyser waited for a span the buffer cannot hold, the capture lapped it, and the measurement stayed in a permanent buffer-overrun re-sync with nothing on screen. The analyser now gathers its frame into its own buffer across successive reads, taking each tick's share as it arrives, so the capture buffer only has to cover one tick and every FFT length collects normally.
- The web generator could not start from a deployed build. The DDS worklet was addressed relative to the source tree, which resolves outside the app once bundled - every production deployment answered the start with "could not open the output device" while a dev-served tree worked. The worklet address now resolves in both layouts.
- Web: generator start and stop did not restart the statistics. The FFT accumulator and the scope's running statistics averaged across the silence/tone boundary. Both transitions now reset them, exactly like a form change - and form edits made while the generator is off no longer reset anything, because the emitted signal did not change.
- Web: the interface font preferences reached only plain text. The FFT statistics block and the frequency-response tables are drawn on the canvas and stayed on built-in fonts whatever the preference said; they now follow the normal and bold interface fonts, and the scope table's columns scale with the font size instead of clipping larger fonts.
- Web: infinite averaging fell back to the default on reload. The forever entry of the averages selector has no JSON spelling, so it saved as nothing and every reload silently restored the default; it now round-trips.
- Web: Firefox offered no output devices. Firefox reveals audio outputs only through its speaker picker; a scan that comes back bare now raises the picker - on the scan click itself - so an output can be selected at all.
1.2.0 2026-08-07
Added
- Measurement over the network: the Phonalyser server. A new headless application,
phonalyser-server, turns the machine that is physically wired to the measurement hardware into a bench other Phonalyser installations use from elsewhere on the network. It serves every audio backend present on that machine - WASAPI, WDM-KS, CoreAudio, JavaSound and the QA40x - to several clients at once, with exclusive per-device locks that name the holder, so two operators cannot talk over each other's measurement. One TCP port carries everything - the control channel, the audio stream and the informational endpoints; default 8377,--port- and a multicast beacon announces the server, so clients find it with no configuration at all. Hardware is rescanned every two seconds, so plugging a device in or out is seen without a restart, and shutting the server down parks the hardware - the QA40x's input attenuator included - exactly as the desktop application does on exit. It ships as its own fat JAR plus a ZIP per platform - Windows x64 and x86, Linux x64 and aarch64, macOS Intel and Apple Silicon, all attached to the release - starts from the console (--name,--bind,-dfor daemon logging), and installs as a Windows service (bundled service wrapper,install-service.cmd), a Linux systemd unit or a macOS per-user launch agent - per user deliberately, because CoreAudio's microphone consent is granted per user. It keeps its ownphonalyser-server.log. There is deliberately no authentication: the server is a bench instrument for a trusted network, and its READMEs say so plainly. The wire protocol is specified indoc/NET-PROTOCOL.md;BUILD.mdandPACKAGING.mdcarry the build and packaging story. - The desktop application measures through such a server. Preferences ▸ Audio gains a Servers... button opening a server list: every server the multicast discovery hears, plus any added by address - with an optional HTTP proxy for a bench beyond the local network - repainting by itself as servers come and go (live, offline, connected), with Connect, Disconnect and Remove. A connected server's backends join the backend selector as
entries beside the local ones, and from there everything works as though the hardware were local. The device combos list the server's devices, and one held by another client says so - "in use by" plus that client's name. Device, sample rate and bit depth are remembered per server and backend; an existing preferences.yaml reads unchanged, and entries written by a newer release or a server this installation has not met survive a save untouched. A refusal or failure on the bench crosses the wire as the same six translated reasons a local failure has. Cards and calibration belong to the bench: the card combo offers the bench's cards, a crosshair calibration writes into the bench's store, and a locally calibrated card matching an uncalibrated bench device is offered for copying over - once per session, never silently. The QA40x settings panel and its range radios drive an analyzer on the bench exactly as a local one, telemetry included. The signal generator runs on the server - commanded, not streamed - so a frequency-response sweep is deconvolved against exactly what the bench rendered, its first sample marked in-band in the capture stream; a live capture granted a different sample rate than asked runs and displays at the granted rate, while a sweep in that position is refused rather than deconvolved wrong. File playback crosses over too - its own entry below tells that story. A connection that drops is retried every five seconds until the operator says otherwise, and browsing the backend list only previews a server - Cancel puts the previous routing back untouched.-> - 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 shortdbf), 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 formsdanddbnow work as dBV on these fields too, alongsidedbv. 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, named after the backend exactly 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 overhttps://orlocalhost, 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.
- Preferences says so when the selected device has no calibration behind it. Closing Preferences ▸ Audio with OK after changing the backend or either device now checks whether the committed selection actually resolves to a measured full scale, and raises one warning naming the input, the output or both - instead of letting the session run on the nominal placeholder and report volts, dBV and everything derived from them as though they had been measured. A card bound to the device but carrying zeros in its active range counts as uncalibrated: an empty row is not a measurement. An instrument that reports its own full scale - a QA40x-class analyzer - is exempt, since there is nothing for the operator to do about it. The warning is tied to the selection changing, so re-opening the dialog to adjust something unrelated does not repeat it.
- A frequency-response sweep that clipped the input now says so. The analyser records the largest absolute sample of the raw capture, taken before any deconvolution, and a run whose peak reached 0.9995 of full scale raises a warning as soon as the sweep stops and its trace is on screen. The threshold sits a hair below the rail rather than on it because the anti-alias filter typically shaves a hard-limited sweep just under full scale - and −0.0043 dBFS is clipping territory in any case: no deliberate measurement level sits there. It is worth stating out loud, because the deconvolution is perfectly happy to turn a railed recording into a smooth-looking curve: the distortion the clipping creates comes back as a fine regular ripple rather than as an obviously broken trace, which is easy to read as noise. The calibration wizard warns on either leg, before a clipped curve can be walked into a calibration file.
- A frequency-response sweep can be cancelled while it runs. The modal window that shows the live RMS-versus-time meter gained a Cancel button. It is cooperative: the button raises the worker's cancel flag and the sweep ends through the ordinary stopped path, so the window closes and the pane unlocks exactly as they do after a completed run, with the capture and the generator handed back. A long sweep - a low start frequency at a large FFT size runs for minutes - no longer has to be waited out.
- "Play from..." works on a network bench. Picking a Phonalyser server as the backend used to grey the file player out with an explanation: the generator was a network away and the protocol carries no audio from the client to the bench. It carries the file instead. The chosen WAV / AIFF / FLAC is uploaded once - up to 50 MB, the limit the server itself enforces, checked here before a byte leaves so an over-size file is refused immediately rather than after the transfer - and the bench's own generator lane decodes and plays it, out of the bench's DAC at the file's own rate and depth. In loop is honoured at the far end, and the indicator goes out when the bench says the file ended, not when you press stop - a file that runs out stops the player here exactly as a local one does. Stop reaches across too. The upload lives in the server's memory, belongs to the session that referenced it, and is dropped when the connection closes, so nothing accumulates on the bench. A refusal - the link down, the store full, the file too big - is reported in your own language, with the server's own words left in the log where they belong.
- 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
.corruptkey 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
audioDeviceslist 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
, 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 tolocalhost- 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 plainhttp://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 plainhttp://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 headless as a measurement agent. Nothing changes for someone installing the app; for someone building it, the artefacts now appear undermodules/phonalyser-app/target/instead oftarget/, andmvn clean installfrom 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-javamodule 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.cmdtakes an architecture: no argument builds the 64-bit fat JAR and the application image as before,x86builds 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.mdandREADME.mdnow 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 packagefor 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 underlib/for both Windows architectures and both macOS ones, so there is nothing to obtain. - A device that will not open now says why, in the operator's language. A failed capture or playback used to surface whatever the driver had said - a PortAudio return code, an HRESULT, a libusb string - or, worse, a sentence guessed by sniffing English keywords out of that text, which said nothing at all to someone running the application in another language. Each backend now classifies its own failure into one of six reasons: the device was disconnected, it does not answer, it is in use by another application, it was not found, it does not support this format, or the reason is unknown. The dialog is written from that reason and translated like every other string. The backend that owns the native error owns the mapping - PortAudio's table serves WDM-KS and CoreAudio, JavaSound reads csjsound's texts, the QA40x reads libusb's, WASAPI reads HRESULTs - and anything a backend does not recognise is "reason unknown", which is always a legal answer and never an error in itself. The raw driver text keeps going to the log every time: it is what a developer needs and the only place it belongs. A bench refusing a device across the network reports through the same six reasons, so a remote failure reads like a local one.
- The card you pick for a device now beats the name-matching guess. Choosing a card for the selected input or output in Preferences ▸ Audio records that choice against the device name and stores it beside the cards themselves; resolving a device consults the choice first and falls back to the recognition patterns and the longest-name match only when nothing was ever chosen. Until now a card that merely recognised part of the device name could overrule the one the operator had picked - the failure mode on a machine with two interfaces whose names share a word. A choice pointing at a card that has since been deleted or renamed falls through to matching rather than leaving the device unknown, so a stale entry can never uncalibrate a device. Like every other edit in that dialog it is staged and committed by OK.
- The tune-notch target marker is graded against the null this sweep actually reached. The dashed marker at the target frequency used to be coloured on absolute depth - red at −50 dB, green at −90 dB. Those figures came from one particular twin-T and mean nothing on a network built around a different impedance or measured through a different front end: one whose best null is −70 dB could never turn green however perfectly it was trimmed. The marker now measures the attenuation at the target against the deepest point of the same sweep - within 1 dB of that floor is green (the target is in the notch), 20 dB above it is red, linear in between. It still grades the attenuation *at the target frequency* rather than the depth of a null sitting somewhere nearby, which is the whole point of the marker.
- Saving a frequency response as a calibration is offered only when the result can be one. The Save-to button is enabled for a curve this session measured itself and swept above 0.5 Vrms, and greyed otherwise. A file loaded back from disk and saved again is a copy, not a calibration - it has already had one subtracted from it - and a sweep driven at a few tens of millivolts carries a noise floor that would be baked into every later measurement de-embedded against it.
- A multi-channel file plays as the sum of its channels. File playback now goes out through the same single-lane path the generated tone uses, so a file with more than one channel is mixed down: every channel contributes and the sum is divided by their count, and none is dropped. A stereo file whose two sides carry different material is therefore heard as their average rather than as a stereo pair - the deliberate consequence of playing files through the calibrated generator lane instead of opening a raw line of the file's own shape.
- Preferences opens on the Audio tab. It is the tab a measurement session actually starts from - backend, devices, sample rate, card and calibration - and it is now both first in the strip and the one selected when the dialog opens. Look & Feel follows it.
- Readouts follow the signal actually emitted. The scope's dual-tone readouts and beat-envelope overlay, the FFT's expected-fundamental hint, the IMD product grid and the ΔF / Δf1 / Δf2 drift rows now ask the generator what it is emitting instead of re-deriving frequencies from the preferences. Three things follow. Readouts track the emitted signal even where it deliberately differs from the entered value - a bin-snapped or sample-period-aligned frequency. An external tone is auto-detected instead of being pinned to the generator's nominal, and THD no longer depends on which box the higher tone of a dual tone was typed into: the hint is the lower tone either way. And with no generator running, rows that could only have been computed from a nominal are omitted rather than presented as measurements.
- The FFT magnitude axis zooms a hundred times finer. The minimum span of the magnitude window dropped from 1 dB to 0.01 dB, so passband ripple and converter-grade flatness can be examined at their own scale.
- Signal-file export writes a fixed full scale. An exported waveform is rendered against a fixed 2·√2 V full scale rather than the live DAC calibration, so a file's absolute level no longer depends on which converter happened to be calibrated on the machine that wrote it.
- The Preferences dialog and the tune-notch wizard resize. Both gained resizable windows with a sensible minimum: widening Preferences widens its entry fields in place, and the wizard's plot grows with its window. The per-backend settings button is now labelled with the backend's own name - "QA40x preferences".
- Errors reach the terminal. The desktop application no longer detaches the console log appender at start-up, so ERROR-level lines appear on the console it was started from; the log file is unchanged.
- 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 preferences 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
DOMExceptiontext - 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 plainN 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 plainN 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.
- A file that does not loop could stop before its first audible sample. Every playback backend runs a warm-up before the device starts pulling real audio, driving the sample-encoding path in a tight loop for about half a second so the compiler has finished with it - and any speculative deoptimisation has settled - by the time the audio thread first calls it. The warm-up consumes real samples and then rewinds the generator, which works for a computed waveform and does not for a file: it read a non-looping file through to its end, and the end-of-file watcher saw the stream finish and stopped the session before the rewind could take effect. Half a second of flat-out decoding is minutes of audio, so a file could be swallowed whole before its first audible sample - this was never a hazard reserved for short clips. The warm-up is now skipped for a file source, where it bought nothing anyway - decoding is I/O-bound rather than a compiled arithmetic loop - and the file's own rewind is kept correct, so any other pre-play consumption still starts from sample zero.
- 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_*.dllappearing 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/andi18n/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; ahelp/ori18n/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. Outside the application entirely, the Windows launcher script now caps the heap it starts with: a 32-bit Java defaults to a 256 MB maximum and cannot reserve much beyond ~1.4 GB of contiguous address space, so the
.batpasses-Xmx1200mwhenever the JAR it picks is the x86 one, and the platform JAR's read-me states the flag for anyone startingjava -jarby hand. - 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
.debrefused 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.debinstalls 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.
- Preferences showed the previous device's card until OK. Picking a backend filled the device combos at once, but the card combo and the ranges table under them kept showing the device committed last time - they caught up only after the dialog was confirmed and reopened. The card section now follows the device the combo actually shows, the moment it shows 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. The same parking now also runs at application start, for every backend present, whichever one is selected - an analyzer left hot by a previous session, or by another machine, is brought to its safe state before the first measurement.
--fft-analyzemisread 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.
- Losing the capture or playback device mid-measurement went unreported. A device unplugged, taken exclusively by another application or simply stopped by its driver left the oscilloscope drawing a flat line and the FFT averaging silence, with the capture rate still ticking - a measurement of nothing presented as a measurement. Nothing above the driver could tell the difference, because a listener that is never called again looks exactly like a quiet input. The capture layer now ends a dead capture explicitly, and the end travels the path the samples did - up through the ring buffer and its readers to the panes. A backend whose driver never tells it anything watches its own delivery instead: a stream that has been started is contracted to deliver a block every audio period, so nothing at all for two seconds is not a quiet input but a stopped stream (silence is blocks of zeroes, not an absence of blocks). Either way, the scope and the FFT stop and one dialog names the backend and says which of the two it was - the device was lost, or it stopped delivering - and a streaming save to disk is aborted rather than left recording nothing. A frequency-response sweep whose capture lost samples or stalled mid-run is refused with the reason instead of being deconvolved across the hole. Playback is covered the same way: the play loop now distinguishes a stream we stopped from one that ended by itself, and the generator reports an output device that went away instead of leaving its play indicator blinking over silence.
- The oscilloscope's measurements were being taken through the display filters. The per-channel high-frequency cleanup - the 80 kHz low-pass and the median de-spike - ran before the measurement pass as well as before the trace, on the reasoning that the numbers ought to match the picture. That was backwards: it is a display setting chosen to make a trace readable, and the moment it moves Vpp, Vmean, rise and fall time or duty cycle, those stop being measurements of the signal and become measurements of our rendering of it. It also erred in one direction only, because the filter is reset at the start of every pass and was therefore measured ringing its way out of a step of up to the full peak-to-peak - which a peak detector reads as signal. On the bench a clean sine measured about 12 % high in Vpp and a quarter long in rise time. The measurement pass now takes the window as captured, save for the mains rejection - that one is a measurement decision rather than a display one, so it stays. The view still filters what it draws, and the frequency readout goes on being measured off the raw signal as it always has.
- Readouts over the frequency-response trace disagreed with the curve on screen. At a deep null the painter, the crosshair and the target marker were each arriving at the depth their own way - the raw bin minimum, the per-pixel Lanczos reconstruction the painter actually draws, and a plain interpolation between bins - and on a razor-sharp notch the three differed by 2-3 dB. Every readout that quotes the trace now reads the trace: the crosshair's level and the target marker take the same reconstruction the painter uses when it is active and the plain bin interpolation when it is not, and the null itself is found by scanning the painter's own pixel columns rather than a grid of its own. The three-point parabola that used to refine the null's depth goes with it - a well-trimmed twin-T null is a V, not a parabola, and fitting a curve across its two walls under-reported how deep it went. The automatic vertical window is now fitted after the null is known, so the axis bottom leaves a decibel of clearance below it, and the rolling range statistics are reset when the amplitude or the swept band is edited, so sweeps measured under the old settings no longer steer the axis the new ones are judged on.
- A live notch sweep that failed to start died in the log. If the audio device could not be opened - busy, gone, or refusing the sample rate - the streaming sweep behind the tune-notch wizard threw on its own thread, the trace simply never appeared, and the only record of the failure was a log line. The wizard now puts the failure on its status line and raises a dialog saying the sweep stopped and what to check; the driver's own words stay in the log.
- Applying an FFT preset could blank the plot. A preset writes the frequency and magnitude window straight into the preferences, and it was the one writer of those four values that did not announce the change - so the pane's clamp, which every other writer goes through, never saw it. A stored preset whose magnitude top and bottom were equal, inverted, or outside the current dynamic range reached the axis mapping unchecked and the spectrum vanished until the range was nudged by hand. A preset now takes the same path as every other range change: it is clamped to the bin size and Nyquist horizontally and to the floor and the dynamic ceiling vertically, and the scrollbars are re-aligned with it.
- One card's calibration could become the machine-wide default. The two shared full-scale scalars that predate per-card calibration - the ADC's and the DAC's - were written back to
preferences.yamlon every save. But selecting a calibrated card pushes that card's values into those very scalars, so saving them stored the last card used as the fallback for every device that has none, and the next uncalibrated interface measured as though it had been calibrated on the previous one's front end. They are runtime state only now: an existing file still seeds them once, so nothing changes for an installation that never bound a card, and nothing writes them back, so the entries disappear with the first save. In the same pass, a card whose active range holds a zero full scale is treated as the uncalibrated row it is - no calibration is applied from it and the fallback stands - instead of being taken at face value as a full scale of zero volts. - A settings file that could not be read was silently replaced by defaults. A
preferences.yamlordevices.yamlthat exists but will not parse - the observed case is a kernel crash mid-save leaving a renamed file whose data never reached the disk - was skipped in silence, and the next save cemented a defaults-only file over it. Every saved setting and every stored calibration went, with nothing on screen or in the log saying so. Such a file is now renamed tobeside itself and the failure logged at ERROR, so whatever survives in it can still be recovered by hand and the next save cannot overwrite the evidence. The write side is hardened to match: a settings file is forced to disk before the atomic rename, so the crash that produced such a file can no longer truncate a save..corrupt - Axis tick labels took the wrong number of decimals, and zero could print as "-0". The shared chart axes - one engine draws the FFT, the frequency response, the oscilloscope, its zoom window and this release's amplitude histogram - chose their label precision independently of the tick step, so a fine dB grid rounded neighbouring ticks to the same text while a coarse one carried decimals it did not need. Each label now takes its decimals from the step that generated it, to four places at most. Zero prints as zero: the linear tick generator indexes its ticks as whole multiples of the step instead of accumulating from the axis minimum, so the tick that lands on zero no longer arrives as a tiny negative number and formats with a minus sign in front of it - most visible on the linear axes that carry zero in the middle of their range, the oscilloscope's and the histogram's. A wide logarithmic frequency axis labels its decades at the decade's own precision, and count axes are written in k and M.
- Untranslated strings and swallowed placeholders in the desktop catalogues. About twenty-six keys had been carried over in English in every locale but German - the QA40x settings readouts, the histogram window, the heap-cap warnings, the capture and frequency-response loss texts and a set of tooltips - alongside a handful of per-locale leftovers (the fade in / fade out labels, Ukrainian's HP / LP / ADC / Nyquist terms, Japanese's raw-samples text). All are translated. Separately, six messages that carry an inserted value - three Ukrainian, two French, one Italian - contained a bare apostrophe, which the message formatter reads as the start of a quoted section: the placeholder after it was printed as literal braces instead of the device or file name it stood for. Those apostrophes are doubled, so the values survive.
- A card plugged in after start-up stayed invisible - or dead - until the application restarted. WDM-KS and CoreAudio enumerate devices once, at driver initialisation, so a card unplugged and replugged kept failing with the driver's "Invalid device (-9996)" for the life of the process. The device list is now re-enumerated on demand - a new Scan devices button in Preferences ▸ Audio - and by itself: that exact failure triggers one automatic rescan and retry, and a saved device missing from the list is looked for once more after a fresh enumeration before being declared absent.
- A hot backend switch could crash Windows. Switching from WASAPI to WDM-KS tore the departing backend down and opened the next in the same breath - which twice drove the USB audio class driver into a 0x10D WDF_VIOLATION bluescreen on the bench. A backend switch now lets the departing driver settle for half a second before the next one opens.
- A replugged QA40x needed an application restart. An analyzer that comes back on another USB address is recognised again: the stale session is discarded and the next use re-finds the device, at the cost of a single re-open, while an analyzer that stays away fails cleanly instead of wedging the session. A QA40x passed through into a virtual machine - enumerable but not openable - is no longer offered as a local device. And the equal-rates rule keeps its memory per analyzer, so two analyzers no longer inherit each other's clock.
- A saved rectangle or triangle did not match the played one. The generator emits both waveforms at the nearest integer-sample-period frequency; the signal-file export still rendered the frequency exactly as typed. An exported file now renders at the same aligned frequency the DAC plays, so the file and the output are the same signal.
- The Look & Feel tab dropped its ampersand. SWT reads "&" in a label as a mnemonic marker, so the tab title never showed the character; the label now escapes it and reads as written.
- 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
Offis typed straight in aso/of/offrather 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.yamlnext 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
.debinstaller 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.yamlthe 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 - ditherOff, and FFT averages down to a single spectrum shown asOffplus∞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:
Offis 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 asOff. 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/offandi/in/inf.
Changed
- Loaded
.frcmeasurements 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/dacFsVoltageRmsin preferences.yaml) are superseded by the per-card calibration profiles. They remain only as the fallback for devices that have no card indevices.yamland 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.
.frcde-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
.frccalibration 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+Zrollback, 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.mdengineering 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,
.frccalibration), 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).