Skip to content

Add Nodus Drift, an offline ambient-sound mixer in Nodus Tools - #998

Open
Drakonis96 wants to merge 6 commits into
mainfrom
feature/nodus-drift-native-tool-541a5d
Open

Drakonis96 wants to merge 6 commits into
mainfrom
feature/nodus-drift-native-tool-541a5d

Conversation

@Drakonis96

@Drakonis96 Drakonis96 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

Nodus Drift is a new native tool in Nodus Tools: an offline mixer of ambient sounds for reading, studying and resting. It is the "built-in ambient sounds for studying" alternative listed under Media player in #779.

  • A Nodus Drift card in Nodus Tools opens a page with a searchable catalogue (accent- and case-insensitive), category and favourite filters, and a mix of up to six sounds with a volume per sound and a master volume.
  • A new profile starts with Nodus Drift pinned (toolkit:drift), so its shortcut is in the sidebar and its pin is marked in the hub from the start, the way Nodus Browser is in the sidebar. It can be unpinned and pinned again.
  • The header media button now also appears when a Drift mix exists, and its popover has a Browser | Drift selector. The Drift tab has play and pause, the master volume, clear, and a button that opens the tool. Choosing a tab never changes what is playing, and Drift never writes the system volume.
  • Playback survives navigation, vault switches and closing the popover. A mix restored on launch is always paused: no audio context exists and nothing plays until the user presses play.
  • The sounds are 8 generators computed on the listener's machine (white, pink and brown noise; binaural Delta 2, Theta 5, Alpha 10, Beta 20 and Gamma 40 Hz on a 100 Hz carrier) and 81 recordings from the active catalogue of Moodist.
  • Everything works offline. There is no remote catalogue, download, account, telemetry or export, and no new dependency.

Related issue

Closes #997.

Refs #779 (the roadmap thread; its "Media player" item could be updated once this merges).

Licensing of the recordings

The recordings are bundled unmodified and keep the licenses the Moodist repository declares for its audio. The third-party notice quotes that declaration as it is written (README, section "License", "Third-Party Assets"): some sounds are under the Pixabay Content License and others under CC0. It links both licenses, Moodist's README and Moodist's LICENSE file (MIT, its code) at the pinned commit 11c0be22…, and says that the recordings are not covered by Nodus's AGPL.

  • Moodist does not say which of the two licenses covers which file, and this PR does not claim to know. The catalogue therefore records the recordings as licenseStatus: 'declared' with LicenseRef-Moodist-declared-Pixabay-or-CC0, not as verified.
  • The decision to bundle them is the maintainer's. It is written in legal/drift/REVIEW.md with what it rests on and what it does not establish, and the gate in shared/drift.ts only checks that the record exists and is cited. An entry without one stays unresolved and is held back.
  • The Pixabay Content License is a platform license with restrictions of its own, among them on distributing content on its own. For that reason the recordings are never published as independent files: they are not in Git, not a CI artifact and not a release asset of their own, only part of the app, which has no way to export them.
  • No Moodist source code is used. The catalogue takes ids, English labels and relative paths from Moodist's data files.
  • legal/drift/PROVENANCE.md lists the pinned commit, how every technical field (bytes, SHA-256, duration, loop crossfade) was measured, and each of the 81 files.

What changes

  • Registration. 'drift' in TOOLKIT_TOOL_PAGES, a TOOLKIT_TOOLS entry (state wip), 36 new vector icons and a route in ToolkitView. It is not a View, a sidebar item of its own or a vault-type entry. The exact-set tests, including the Toolkit list in e2e-smoke.mjs, are updated without loosening them.
  • Audio engine (src/components/drift/audio/). One AudioContext per window, owned by DriftProvider in main.tsx and created and resumed inside the explicit play. Sources are single-use with AudioParam fades, stale async work is cancelled by per-voice and global tokens, loads run two at a time and are deduplicated, and decoded audio is capped at 192 MiB (inactive buffers are evicted first, an active voice never, and a voice that does not fit is refused with a notice). Recordings loop natively, with a precomputed circular crossfade for the 14 files that need one. Noise is generated once (pink by Voss-McCartney, brown by a leaky integrator with DC removal), and binaural presets are two sine oscillators on a channel merger, one preset at a time.
  • IPC. drift:catalog and drift:read-audio, for the main frame of the main window only. The main process takes an id and returns bytes: the path comes from the catalogue and is confined under the bundled audio directory (symlink escapes included), size and SHA-256 are checked, and a plain exactly-sized Uint8Array crosses the bridge. Nothing is exposed to Browser pages, iframes, auxiliary windows or the reduced bridges.
  • State. localStorage key nodus:drift:v1, a pure reducer and a debounced write. It holds configuration only, is normalized when corrupt, and is not part of backups, sync or the database.
  • Packaging. build/beforePack.cjs runs scripts/prepare-drift-assets.mjs, which fetches the recordings from the pinned commit, checks each against its size and SHA-256, retries a lost connection and keeps a file that is already in place. build/afterPack.cjs runs verify-drift-assets.mjs --asar … --require-all, so an app that lacks any of them does not become an installer. electron/assets/**/* is already packaged, so there is no configuration change.
  • i18n. 224 new rows in the eleven language tables. The copy describes the audio physically and makes no therapeutic or cognitive claim.
  • Scripts. prepare-drift-assets and verify-drift-assets (which report the real number and size of packaged recordings), and npm run test:drift, test:e2e:drift, verify:drift-assets and drift:prepare-assets. The scripts flush their output before exiting: process.exit() right after printing to a pipe cut a listing short under load, which one test caught in the full run.

Type of change

  • Bug fix
  • New feature or improvement
  • Documentation or translation
  • Refactor or maintenance
  • Database, privacy, security, or infrastructure change (a new IPC surface, and the packaging hooks)

Validation

CI on this PR is the verification of record. This is what I ran locally, and when.

Before the last two commits, on the branch merged with main at 5.7.2:

  • npm run lint, npm run typecheck, npm run build and npm run build:server-web.
  • A full npm test: 4206 tests, of which 4204 pass, 1 is skipped as before (Compass needs the Electron ABI) and 1 failed. The failure was the list command names every pending recording precisely (78 of 81 lines: the script exited before its piped output drained), fixed in the last commit.
  • node scripts/e2e-drift.mjs, 21 checks against the real app and against the packaged macOS arm64 binary. It measures the signal that reaches the audio output with an AnalyserNode and no hook in the product. It also covers keyboard activation, pause and resume, navigation, a vault switch through the real switcher, independence from Browser media (it only reads the system volume), no request that leaves the machine, and a restore that stays silent.

After them (the recordings and the licenses):

  • npm run typecheck, the Drift suites (187 tests) and npm run licenses:verify, plus the license, packaging-config and backfill-parity tests.
  • For the default pin (the last commit): the Toolkit and Drift UI tests and the electron type check.
  • Not run locally: the full npm test, npm run build, npm run test:e2e (its Toolkit card list changed) and the Drift end-to-end script with the real recordings.
  • Not run at all yet: a packaging run with the new hooks, that is, the beforePack download of about 99 MiB from raw.githubusercontent.com and the afterPack check. The first packaging run will exercise them. A non-release run of electron-builder --dir would be a cheap way to see them before the next release.

Measured separately, with Electron's own decoder and not through the catalogue: Electron 43 decodes all 81 files, and their decoded durations match the measured ones to 0.5 ms. Each fits the 192 MiB decode budget on its own, but six of the longest together (about 488 MiB) do not, and the engine then refuses the extra voice with a notice.

Screenshots

Dark and light theme, the empty page, a mix with an error row, the seventh-voice notice and the header popover were reviewed in the real window. The images are not embedded in this description, because the command line cannot upload them.

Privacy and data review

  • No secrets, credentials, private vault content, personal data, student data, or confidential documents are included. The test recordings are synthesised at test time.
  • New network access is explicit, documented, and initiated by the user: none is added at runtime. Drift makes no network request. Only the packaging step downloads the recordings, from their pinned commit and checked against a SHA-256.
  • The change does not send rosters, grades, student answers, or other protected data to AI providers.
  • The change does not use AI to grade, rank, profile, or evaluate students.
  • Database, backup, synchronization, and migration effects are documented and tested: there are none. The state is a localStorage key that is not part of backups or sync.

Contributor checklist

  • This pull request is written in English and references an existing issue.
  • I attached screenshots whenever possible (before-and-after for UI changes, reproduction evidence for bug fixes).
  • I have read CLA.md and accepted it through the CLA / signature check.
  • I added or updated focused tests.
  • I updated documentation where behavior changed: the README, THIRD_PARTY_NOTICES.md and legal/drift. The Nodi documentation is not updated.
  • I updated every language table for new static UI text.
  • I preserved local-first behavior and existing privacy boundaries.
  • I have read and followed CONTRIBUTING.md and CODE_OF_CONDUCT.md.

Things to know before merging

  • Installer size. Each build grows by about 99 MiB (81 MP3 files that do not compress further).
  • Pinned by default for new profiles only. toolkitPinnedPages now starts as ['drift']. A profile that already saved its pins, an empty list included, keeps them, so an existing user pins it once from the hub. Pinning it for existing profiles too would take a one-time migration that changes their sidebar, which this PR does not do.
  • Version and release notes are not touched: feature PRs here do not bump the version, and the release PR does.
  • Not verified on Windows or Linux. The packaging hooks are plain Node steps, like the ones next to them, but only macOS has been tried.
  • A checkout has no audio until it is prepared. npm run drift:prepare-assets fetches it, and the packaging hook does the same. Until then the recordings are listed as unavailable and the generators still work.

…us Tools

Nodus Drift is a native tool in Nodus Tools: a page to build a mix of up to six
sounds, and a Browser | Drift selector in the existing header media popover for
quick play, pause and volume.

- One AudioContext per window, owned by DriftProvider, so navigating, switching
  vault or opening the popover never stops or duplicates it. Nothing is created
  or played until an explicit action, and a restored mix always starts paused.
- Sounds are recordings played with native looping (with a precomputed circular
  crossfade where a file needs one) and generators: white, pink and brown noise
  and five binaural presets, all computed locally.
- Recordings reach the renderer through two IPC channels, drift:catalog and
  drift:read-audio, restricted to the main window's main frame. The main process
  takes an id only, confines the path under the bundled audio directory
  (symlinks included), checks size and SHA-256, and returns the bytes.
- The catalogue is derived from the active Moodist catalogue at a pinned commit.
  Moodist declares Pixabay and CC0 for its audio only in general, never per file,
  so all 81 recordings are recorded as unresolved and pending review and none is
  bundled: the package ships the eight generators only. legal/drift holds the
  provenance record, the review protocol and the upstream MIT text.
- prepare-drift-assets and verify-drift-assets generate and audit the bundled
  set; the audit reports the real number and size of packaged recordings.
- UI text goes through the existing i18n in the eleven languages, with a neutral
  description of the audio and no claim about effects on the listener.

Tests: unit and integration suites for the catalogue, state, DSP, engine, IPC,
assets, UI, i18n and provider, and an end-to-end script that drives the real app
and the packaged binary, measuring the signal that reaches the audio output.
…he README

The smoke test pins the exact, alphabetical list of Toolkit cards, so the new
tool has to be in it. The README gets one sentence about the tool.
… declares

All 81 recordings are now approved and bundled, unmodified. Their licenses are
the ones the Moodist repository declares for its audio, and the notice says so
as Moodist states it: some sounds are under the Pixabay Content License and
others under CC0, with links to both and to Moodist's README and LICENSE at the
pinned commit. Moodist does not say which one covers which file and Nodus did
not verify it, so the entries are recorded as `declared`, not `verified`.

- shared/drift: a `declared` license status. The gate still needs a license,
  evidence, an approved review and the record that says so. The generators stay
  `verified`; an entry nobody approved stays `unresolved` and is held back.
- The decision is written in legal/drift/REVIEW.md as the maintainer's, with
  what it rests on and what it does not establish. THIRD_PARTY_NOTICES.md,
  legal/drift/README.md and PROVENANCE.md carry Moodist's declaration verbatim.
- The recordings are not committed. build/beforePack.cjs fetches them from the
  pinned commit (each checked against its size and SHA-256, retried on a lost
  connection, kept when already in place) and build/afterPack.cjs refuses an app
  that does not carry all of them, through verify-drift-assets --require-all.
- The preparation and verification scripts flush their output before exiting:
  process.exit() right after printing to a pipe cut a listing short under load.
- Tests cover the declared status, the recorded decision, the wording of the
  notices, retries, keep-if-intact, --require-all and the unavailable page, and
  none of them can reach the network.
A new profile now starts with Nodus Drift pinned, so its shortcut is in the
sidebar and its pin is marked in the Toolkit hub from the start, the way Nodus
Browser is in the sidebar. A profile that already saved its pins keeps them:
this is only the value a profile without any starts from.
Discard obsolete queued reads and decode results while preserving paused
mix caches and useful re-selections. Settle cancelled queue promises.

Write circular crossfades directly into their destination AudioBuffer and
check the actual PCM peak before allocating it. Close the parent popover
anchor when its header trigger is detached.

Add 13 engine/DSP regressions and 3 React header lifecycle regressions.
Preserve the new-profile default pin introduced in d58ab00.

Copy link
Copy Markdown
Owner Author

Patched the three review findings in 94ded55, on top of d58ab00. The new-profile default pin is unchanged.

  • Cancel obsolete queued loads and discard unused read/decode results after Clear/Remove, without losing the cache of a paused mix or duplicating useful re-selections. Cancelled queue promises are settled, including during disposal.
  • Reset the header's parent anchor when its trigger is detached, so clearing the final source cannot reopen a later popover against a removed button. Connected triggers, normal updates and surviving Browser sessions retain their existing behavior.
  • Write crossfades directly into the destination AudioBuffer, removing the extra full PCM intermediate, and check the actual source + destination budget before allocating that destination.

Added 16 regression cases: 13 engine/DSP cases plus 3 rendered React header cases, including StrictMode and the Browser/Drift coexistence path. Existing tests were not weakened or replaced.

Validation in this editing environment: all 13 engine/DSP regression cases pass in an isolated TypeScript-transpiled harness using the repository's Web Audio fakes; against the pre-fix sources, 10 fail and 3 pass. Syntax and whitespace checks also pass. This environment could not install the full dependency tree, so this is NOT a claim that the full npm suite, the new React/jsdom cases or packaged Electron testing ran locally. The normal CI has been triggered for this exact commit: https://github.com/Drakonis96/nodus/actions/runs/36646065049 . Its result remains the verification of record.

Only three source files and the two new test files changed. No merge, release, audio/licensing changes or settings/default-pin changes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Nodus Drift: offline ambient sounds inside Nodus Tools

1 participant