Handel · Dixit Dominus

Rehearsal-aid technical reference

Modular player architecture

Reusable Web Audio player, individual tracks, looping, transport and gain settings

Read first: current production settings

Some values within these source documents are historical. Movement 3 is now final at solo 0.7665 / piano 1.4741 (master 1.0000); Movement 10 piano is 1.7786. The modular-player and piano reference documents cover examples through Movement 8. See all current-status notes.

Download original MarkdownAll documents
Contents · 20 sections

Handel Dixit Dominus Rehearsal Player

Modular Player v1 FINAL — architecture and extension guide

Status: frozen production architecture; operational guide/current lock updated through Movement 8 FINAL
Scope: Movements 1–8 of Handel, Dixit Dominus, HWV 232
Date: 8 Oct 2026
Purpose: one reusable rehearsal-player engine that can accept additional movements without cloning or editing movement-specific JavaScript.


1. Design decision

The accepted architecture has one player engine and one visual shell.

Movement-specific differences are data, not code.

The player is divided into four layers:

  1. Shared engine — playback state, Web Audio graph, looping, seeking, tempo control, replay, autoplay-next, mixer rendering.
  2. Shared presentation — common CSS and a generic HTML shell.
  3. Movement configuration — title, stems, calibration, credits, and optional gain rules.
  4. Score/audio assets — movement-meta.json plus the rendered MP3 stems.

The central rule is:

Adding a movement must not require forking player.js.

If a new movement needs different stems, balance, credits, or score-dependent gain behaviour, those differences belong in movement.json or score metadata.


2. Canonical directory layout

The frozen modular player lives under:

/home/ajp/music/dixit-dominus/_modular/

Structure:

_modular/
├── shared/
│   ├── player.js
│   └── player.css
├── movements.json
├── mvt1/
│   ├── index.html
│   └── movement.json
├── mvt2/
│   ├── index.html
│   └── movement.json
└── mvt3/
    ├── index.html
    └── movement.json

The existing score metadata and audio remain in their movement asset locations and are referenced by URL.

Examples:

/music/dixit-dominus/movement-meta.json
/music/dixit-dominus/audio/*.mp3

/music/dixit-dominus/virgam-virtutis/movement-meta.json
/music/dixit-dominus/virgam-virtutis/audio/*.mp3

/music/dixit-dominus/mvt3/movement-meta.json
/music/dixit-dominus/mvt3/audio/*.mp3

The modular tree therefore contains the player application and movement configuration, while the rendered musical assets remain canonical in their existing locations.


3. One shared state machine

The player has one explicit playback state model:

loading
  ↓
ready / paused
  ↓
playing
  ↓
ended

Looping temporarily uses a loopwait state.

The important invariant is that every user route into playback uses the same state transition.

Both:

Play button
Space key

call the same togglePlayback() function.

This prevents divergent behaviour between mouse and keyboard controls.

End-of-movement behaviour

When a movement finishes:

When Replay or Space is pressed from ended:

position = 0
startAt(0)

If autoplay-next is enabled and another movement exists, the player navigates to that movement instead.

At the end of the final installed movement, the player stays at the end and offers Replay.


4. Shared engine responsibilities

shared/player.js owns all generic behaviour:

None of these behaviours should be reimplemented inside an individual movement.


5. Shared visual shell

shared/player.css defines the common visual language used by all movements:

Each movement's index.html is intentionally generic.

It should not contain movement-specific JavaScript.


6. movements.json

The global manifest defines order and navigation.

Schema/example (abbreviated; the production manifest now contains Movements 1–8):

[
  {
    "movement": 1,
    "title": "Dixit Dominus",
    "url": "/music/dixit-dominus/_modular/mvt1/"
  },
  {
    "movement": 2,
    "title": "Virgam virtutis tuae",
    "url": "/music/dixit-dominus/_modular/mvt2/"
  },
  {
    "movement": 3,
    "title": "Tecum principium",
    "url": "/music/dixit-dominus/_modular/mvt3/"
  }
]

Movement order in this file determines autoplay-next order.

To add a movement, add one manifest entry. No navigation code changes are required.


7. movement.json contract

Each movement has one configuration file.

Typical structure:

{
  "movement": 3,
  "title": "Tecum principium",
  "meta_url": "/music/dixit-dominus/mvt3/movement-meta.json",
  "stems": [
    {
      "key": "soprano",
      "label": "Soprano I",
      "file": "/music/dixit-dominus/mvt3/audio/soprano1.mp3",
      "calibration": 1.0,
      "default_percent": 70
    },
    {
      "key": "piano",
      "label": "Piano",
      "file": "/music/dixit-dominus/mvt3/audio/piano.mp3",
      "calibration": 9.0,
      "default_percent": 70
    }
  ],
  "master_gain": 1.0,
  "compressor": null,
  "loop_pause_ms": 500,
  "calnote": "...",
  "credits": [
    "..."
  ]
}

Required movement fields

movement : integer movement number.

title : displayed movement title.

meta_url : URL of the score-derived timing metadata.

stems : array of audio channels.

Stem fields

key : stable internal identifier.

label : human-readable mixer label.

file : URL of the MP3 stem.

calibration : hidden multiplicative gain that compensates for raw render-level differences.

default_percent : visible mixer starting point, normally 70.

The visible slider and hidden calibration remain deliberately separate.

A user's 70% does not mean the same raw numerical gain for every voicebank or accompaniment stem.


8. Movement-specific accepted calibration

Movement 1 — Dixit Dominus

Seven stems:

The hidden calibration values were migrated from the accepted v14 player into movement.json.

Movement 1 also retains score-dependent gain behaviour.

Movement 2 — Virgam virtutis tuae

Accepted calibration:

Alto   1.50
Piano  3.59

These are the production values from the final Movement-2 player.

Movement 3 — Tecum principium

Accepted calibration:

Soprano I  1.00
Piano      9.00

The high piano calibration compensates for the deliberately conservative raw répétiteur render; the frozen piano master itself is not modified.

Movement 6 — Dominus a dextris tuis

Accepted final calibration, all with visible mixer default 70%:

Soprano I   0.5000
Soprano II  0.5000
Alto        0.5357
Tenor       1.1800
Bass        1.6000
Piano       1.7382

Movement 6 deliberately has no solo-vs-chorus gain switch. The first 80 bars remain restrained; the natural tutti expansion begins at bar 81 through the written increase in vocal and orchestral forces.

Movement 7 — Judicabit

Accepted final calibration, all with visible mixer default 70%:

Soprano I   0.5357
Soprano II  0.5357
Alto        0.5357
Tenor       1.1800
Bass        1.6000
Piano       2.3179

Score/MIDI timing ends at 146.400 s; the accepted piano/player endpoint is 148.402 s.

Movement 8 — Conquassabit

Accepted final calibration, all with visible mixer default 70%:

Soprano I   0.5357
Soprano II  0.5357
Alto        0.5357
Tenor       1.1800
Bass        1.6000
Piano       1.5852

Movement 8 uses the accepted randomized-note-timing Alex Bass render. This is a master-audio/source decision, not a player gain rule. The exact accepted timing offsets are preserved in the canonical Movement-8 USTX.

Score/MIDI timing ends at 90.000 s; the accepted FluidR3/player endpoint is 92.704 s.


9. Score metadata

movement-meta.json contains timing information derived from the score. Audio length is never the authority for musical bar timing.

For movements whose audio ends exactly with the score, duration_seconds can match the score duration directly. Movement 5 established the general case in which the transport playback endpoint extends slightly beyond the last printed bar to carry a final consonant.

Core fields therefore support:

{
  "duration_seconds": 72.190,
  "score_duration_seconds": 72.000,
  "release_tail_seconds": 0.190,
  "bar_count": 30,
  "bars": [
    {
      "bar": 30,
      "start_seconds": 69.6,
      "end_seconds": 72.0,
      "beats": 4,
      "beat_unit": 4
    }
  ],
  "tempo_events": [],
  "time_signatures": []
}

Interpretation:

If no post-score tail exists, duration_seconds and score_duration_seconds may be identical.

The player uses score bar boundaries for:

It uses duration_seconds for the overall transport/timeline endpoint and natural-completion state.

Never create a fictitious extra bar merely to accommodate a renderer release. Movement 5 remains 30 bars ending at 72.000 s even though playback continues to 72.190 s.


10. Declarative gain rules

The shared player can alter a stem's hidden gain according to score-defined time ranges.

This exists for musical reasons, not as a substitute for fixing bad audio.

Dedicated solo passages

Movement 1 contains dedicated Soloists audio while some complete-part renders also contain the same written solo notes.

During those passages, the duplicate chorus stem is suppressed so the solo is heard only from the dedicated Soloists stem.

The timing comes from:

solo_ranges_by_voice

in Movement-1 metadata.

Soprano I / Soprano II written unisons

The accepted behaviour is not hard suppression.

During the metadata ranges:

soprano_unison_ranges

both Soprano channels remain audible.

Each is attenuated to:

0.75 × normal channel gain

approximately −2.5 dB per channel.

Musical rationale:

When two choral parts share the written line, both singers still need to be represented, but neither needs to project at the same individual level as an isolated part.

Current Movement-1 rule:

"gain_rules": {
  "suppress_voice_during_solos": true,
  "suppress_s2_in_unison": false,
  "explicit": [
    {
      "stem": "s1",
      "ranges_from_meta": "soprano_unison_ranges",
      "factor": 0.75
    },
    {
      "stem": "s2",
      "ranges_from_meta": "soprano_unison_ranges",
      "factor": 0.75
    }
  ]
}

This pattern is intentionally generic: future movements can add gain rules without altering player.js.


11. Audio architecture

At normal speed the player uses Web Audio directly:

decoded AudioBuffer
      ↓
BufferSource per stem
      ↓
GainNode per stem
      ↓
mix bus
      ↓
master gain
      ↓
optional compressor
      ↓
destination

All stems start against the same AudioContext clock.

This gives sample-locked multi-stem synchronization.

Tempo changes

At 100%:

mix bus → master

At altered tempo:

mix bus → SoundTouch AudioWorklet → master

SoundTouch is loaded lazily.

Therefore ordinary 100% playback does not depend on the external SoundTouch module.

Pitch remains unchanged when rehearsal tempo is altered.

Asset cache identity when replacing accepted stems

The shared loader fetches movement metadata/configuration freshly, but stem audio is intentionally cache-friendly. Consequently, replacing an MP3 on disk under the same URL can leave a browser playing an older cached stem.

This was confirmed during Movement-5 v14 deployment: the new v14 files and longer playback metadata were correct, yet Chrome initially replayed cached pre-v14 audio and the new terminal release was not heard.

Canonical deployment rule:

new accepted audio revision + same pathname
    -> give the stem a new URL identity

The preferred lightweight implementation is a version query in movement.json:

"file": "/music/dixit-dominus/mvt5/audio/soprano1.mp3?v=v14"

Changing the filename itself is equally valid. This is movement-specific configuration and does not justify modifying player.js.

A hard browser refresh can be useful diagnostically, but canonical deployment should not depend on the listener manually clearing caches.


12. Loop behaviour

Loop boundaries are whole score bars.

The selected range is inclusive.

Example:

Bars 12 to 15

means playback from the start of bar 12 through the end of bar 15.

At the loop boundary:

  1. audio stops;
  2. the player waits approximately 500 ms;
  3. playback restarts at the first bar of the loop.

The pause is an intentional rehearsal feature.

Default:

loop_pause_ms = 500

13. Keyboard contract

Shared keyboard behaviour:

Space        Play / Pause / Replay
Left Arrow   previous score bar
Right Arrow  next score bar

Keyboard controls and buttons call the same underlying player functions.

Do not create keyboard-specific playback logic.


14. Adding a new movement

The normal extension workflow is:

A. Create the musical assets

Produce and accept the final stems.

Example:

/home/ajp/music/dixit-dominus/mvt4/audio/soprano1.mp3
/home/ajp/music/dixit-dominus/mvt4/audio/piano.mp3

B. Create movement-meta.json

It must contain accurate score-derived:

C. Create modular movement directory

_modular/mvt4/
├── index.html
└── movement.json

index.html should be copied unchanged from another modular movement.

D. Define movement.json

Specify:

E. Add the manifest entry

Append to _modular/movements.json:

{
  "movement": 4,
  "title": "...",
  "url": "/music/dixit-dominus/_modular/mvt4/"
}

That is all the player code should require.


15. When a new movement appears to require code changes

Before editing player.js, ask:

Is this really a new playback capability, or is it movement-specific data?

Examples that belong in configuration or metadata:

Examples that may justify shared-engine work:

If shared-engine code changes are required, all installed movements must be regression-tested.


16. Regression test before promoting a new shared engine

For at least one complex and one simple movement verify:

Transport

Timeline

Looping

Mixer

Movement-1 musical regression

Specifically verify:

Tempo


17. Freeze policy

The accepted player is:

Modular Player v1 FINAL

The frozen unit consists of:

Do not modify a frozen file casually.

For future development:

  1. copy or branch the modular player;
  2. make the change;
  3. regression-test all installed movements;
  4. promote only after acceptance;
  5. record a new version.

The freeze script supplied alongside this document creates a reproducibility snapshot and SHA-256 manifest without altering the working player.


18. Current production lock

At the 8 Oct 2026 Movement-8 FINAL lock:

Soprano I   0.5000
Soprano II  0.5000
Alto        0.5357
Tenor       1.1800
Bass        1.6000
Piano       1.7382
Soprano I   0.5357
Soprano II  0.5357
Alto        0.5357
Tenor       1.1800
Bass        1.6000
Piano       2.3179
Soprano I   0.5357
Soprano II  0.5357
Alto        0.5357
Tenor       1.1800
Bass        1.6000
Piano       1.5852

The architectural version remains Modular Player v1 FINAL.

Movement 8 reinforces the intended separation of responsibilities:

score / edition decisions
        -> canonical musical source

synthetic-singer timing decorrelation
        -> canonical USTX / vocal WAV

orchestral duplicate-key reduction
        -> canonical piano MIDI

stable per-stem balance
        -> hidden player calibration

substantial score-defined doubling balance
        -> declarative movement gain rules

transport / looping / tempo / navigation
        -> shared player engine

Keeping those layers separate is now a production invariant.