# The LucidDream script language This page is the complete definition of a LucidDream script, written to be read by an AI assistant (or a person) who is going to write one. A script is a YAML document that says what the phone plays through a night and when. It cannot do anything else: it has no access to files, the network or the device, and the app checks every script before it will run it. ## Shape ```yaml name: Three soft cues version: 2 volume: 0.5 body: - wait: 1h30m - repeat: 3 body: - play: { signal: soft-pulse, gain: 0.6, wait: true } - wait: 20m ``` Top-level keys, and no others: - `name` (required): a short name, plain text. - `version`: write `2`. (Version 1 exists for older scripts; the differences are noted below.) - `volume`: the script's overall loudness, from 0 to 1. Default 1. For sleep, 0.3 to 0.6. - `sounds`: optional sound slots, explained below. - `body` (required): a list of statements, run in order from the top. Every statement is a YAML mapping with exactly one of these keys naming what it does: `play`, `wait`, `repeat`, `if`, `with`, `set`, `log`, `stop`. Anything else is an error, including a misspelt key. ## Statements ### play Plays a sound. Short form and full form: ```yaml name: Play forms version: 2 body: - play: chime - play: { signal: chime, gain: 0.5, wait: true } - play: { signal: "Rain on roof", gain: 0.4, soften: gentle, wait: true } ``` - `signal`: the sound's name, exactly as it appears in the list of available sounds. Put a name the user gave their own sound in double quotes, always: names may contain spaces, digits and capitals. - `gain`: loudness of this play, multiplied by everything around it. Not negative. Usually 0.2 to 1. - `rate`: playback speed, greater than 0. Default 1. Rarely needed. - `wait`: `true` makes the script wait until the sound has finished before the next statement. Without it the next statement starts at once. Use `wait: true` unless you want sounds to overlap. - `soften` (version 2): `gentle` or `strong`. Asks the phone to play a softened rendering: lower, darker, with a slow start. Use it for any sound that is bright or starts abruptly. ### wait Pauses. A duration is a number and a unit, `ms`, `s`, `m` or `h`, and may be joined: `90m`, `1h30m`, `45s`. Three named periods may be used instead, which the user sets in the app's Settings: `$short` (5 minutes unless changed), `$medium` (20 minutes), `$long` (one hour). ```yaml name: Waits version: 2 body: - wait: 1h30m - play: { signal: soft-chime, wait: true } - wait: $medium - play: { signal: soft-chime, wait: true } ``` Prefer the named periods for the pace of a night, so the user can adjust it without editing. ### repeat Runs its `body` a number of times, or `infinite` times. An optional `until` condition is checked before each round, including the first. ```yaml name: Repeat forms version: 2 body: - repeat: 4 body: - play: { signal: soft-pulse, wait: true } - wait: $long - repeat: infinite until: { elapsed: { gte: 7h } } body: - play: { signal: warm-hum, gain: 0.4, wait: true } - wait: $medium ``` Every `repeat` body must contain a `wait`, or the sounds follow each other without pause. An `infinite` repeat without `until` goes on until the user stops the night; prefer a number or an `until`. ### if Runs `then` when a condition holds, and `else` (optional) when it does not. ```yaml name: If form version: 2 body: - wait: $long - if: { charging: true } unavailable: then then: - play: { signal: singing-bowl, gain: 0.4, soften: strong, wait: true } else: - play: { signal: soft-pulse, gain: 0.4, wait: true } ``` ### with Applies `gain`, `rate` or (version 2) `soften` to everything in its `body`. Nested ones multiply. ```yaml name: With form version: 2 body: - wait: $long - with: { gain: 0.5, soften: gentle } body: - play: { signal: breath-swell, wait: true } - wait: 20s - play: { signal: warm-hum, wait: true } ``` ### set, log, stop ```yaml name: Small statements version: 2 body: - set: { volume: 0.3 } - log: "first part done" - wait: 10m - stop: ``` - `set: { volume: 0.3 }` changes the overall volume, 0 to 1, for the rest of the night. - `log: "text"` writes a note into the night's record. It makes no sound. - `stop:` ends the script. ## Conditions A condition is a mapping with exactly one key. Either a field: | Field | Value | Meaning | | ------------ | ---------------------------- | ---------------------------------------------------- | | `elapsed` | a duration | Time since the night began | | `clock` | `"HH:MM"`, 24-hour, quoted | The time of day | | `iteration` | a number | Which round of the enclosing `repeat`, from 0 | | `charging` | `true` or `false` | Whether the phone is plugged in | | `movement` | a number | How much the phone moved in the last minute | | `soundLevel` | a number (dBFS, negative) | The loudest room sound in the last minute | | `rem` | `true` or `false` | REM sleep, when a source for it exists (often none) | | `sleepStage` | `awake`, `light`, `deep`, `rem` | Likewise | | `hr`, `hrv` | a number | Heart rate and its variability, likewise | or a combination: `all: [ ... ]`, `any: [ ... ]`, `not: { ... }`. A field is compared with one of `eq`, `ne`, `lt`, `lte`, `gt`, `gte`: `{ elapsed: { gte: 6h } }`, `{ clock: { gte: "05:30" } }`, `{ iteration: { gte: 2 } }`. A bare value means equals: `{ charging: true }`. ```yaml name: Conditions version: 2 body: - repeat: infinite until: { any: [{ clock: { gte: "05:30" } }, { elapsed: { gte: 8h } }] } body: - if: { all: [{ iteration: { gte: 1 } }, { not: { charging: false } }] } unavailable: then then: - play: { signal: soft-chime, gain: 0.5, wait: true } - wait: $long ``` Rules that matter: - In version 2, `clock` times are ordered within the night, which begins at noon: 23:00 comes before 05:30. So `until: { clock: { gte: "05:30" } }` ends a loop at half past five in the morning. - A reading may be missing: most phones have no source for `rem`, `sleepStage`, `hr` or `hrv`, and `movement` and `soundLevel` need a setting switched on. In version 2 a condition on a missing reading is unknown, and then: an `if` runs neither branch unless it says `unavailable: then` or `unavailable: else`; a `repeat`'s `until` does not end the loop unless it says `unavailable: stop`. - Unless the user asks for one of those readings, use only `elapsed`, `clock` and `iteration`. They are always known. - `unavailable` and `soften` need `version: 2`. ## Sound slots (optional) A script may name roles instead of sounds, so the user can change the sound in the app without editing the script. Declare each role under `sounds:` with the sound it plays by default, and play it as `$Role`. ```yaml name: Slots version: 2 volume: 0.5 sounds: Cue: soft-pulse Finish: soft-chime body: - wait: $long - repeat: 5 body: - play: { signal: $Cue, gain: 0.6, wait: true } - wait: $long - play: { signal: $Finish, gain: 0.5, wait: true } ``` The only role names are: `Cue`, `SecondCue`, `VoicePrompt`, `WakeSound`, `Background`, `Start`, `Finish`, `REMCue`, `SoftSound`, `DeepCue`. A role that is used must be declared. A default must be a sound's name, not another role. ## What a script may not contain - Any top-level key other than `name`, `version`, `volume`, `sounds`, `body`. - Any option not listed above. There is no `fade`, `delay`, `at`, `time`, `loop`, `volume` on a `play`, `duration` on a `play`, `random`, variables, arithmetic or comments that carry meaning. (YAML `#` comments are allowed and ignored.) - A sound name that is not in the list of available sounds. - YAML anchors and aliases (`&name`, `*name`). - More than 2,000 statements in one body, or nesting deeper than 32 levels. ## Sounds built into the app `soft-pulse`, `soft-chime`, `singing-bowl`, `breath-swell`, `warm-hum`, `chime`, `bell`. (`alert` also exists; it is a sharp double beep and does not belong in a night.) The user may have sounds of their own; they will tell you their names.