relanote

Architecture Overview

This document explains the internal architecture of Relanote, from source code to audio output.

System Architecture

Relanote System Architecture

Package Structure

Relanote is organized as MoonBit packages:

PackagePurpose
coreSource files, spans, diagnostics, reports
lexerTokenizes source code into tokens
astDefines AST (Abstract Syntax Tree) types
parserParses tokens into AST
hirHolds the lowered representation
resolverResolves modules and names
typesInfers and checks types
evalEvaluates AST and produces music values
stdlibStandard library prelude, scales, chords, synth presets
renderRenders music values to MIDI bytes
formatCode formatter
lspEditor protocol framing and dispatch
cmdCommand-line interface
studioVapor Moon playground view and bridge

Compilation Pipeline

1. Lexical Analysis (Lexer)

The lexer transforms source code into tokens:

text
Source: "let melody = | C4 E4 G4 |"

Tokens:
  ├─ Keyword(Let)
  ├─ Identifier("melody")
  ├─ Operator(Eq)
  ├─ Pipe
  ├─ Pitch(C4)
  ├─ Pitch(E4)
  ├─ Pitch(G4)
  └─ Pipe

2. Parsing (Parser)

The parser constructs an Abstract Syntax Tree:

text
AST:
  └─ LetBinding
       ├─ name: "melody"
       └─ value: Block
                   └─ notes: [C4, E4, G4]

3. Evaluation (Evaluator)

The evaluator transforms AST into concrete music values:

text
Value:
  └─ Block {
       notes: [
         Note { pitch: 60, duration: 1.0, velocity: 80 },
         Note { pitch: 64, duration: 1.0, velocity: 80 },
         Note { pitch: 67, duration: 1.0, velocity: 80 },
       ],
       synth: None,
       tempo: 120,
     }

4. Rendering (Render)

The renderer converts music values to playable formats:

Data Flow Example

Here's how a simple melody flows through the system:

rela
; Input .rela file
scale Major = { R, M2, M3, P4, P5, M6, M7 }
let melody = | <1> <3> <5> | |> voice Lead
melody
text
┌────────────────────────────────────────────────────────────────┐
│ 1. Parser reads scale definition and melody                    │
└────────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────────┐
│ 2. Evaluator resolves <1> <3> <5> to actual pitches            │
│    using the Major scale intervals                              │
│    <1>R   → root pitch (e.g., C4 = MIDI 60)                 │
│    <3>M3  → root + 4 semitones (e.g., E4 = MIDI 64)         │
│    <5>P5  → root + 7 semitones (e.g., G4 = MIDI 67)         │
└────────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────────┐
│ 3. Evaluator applies "Lead" synth to the block                 │
│    Block.synth = SynthValue { osc: Saw, env: ..., ... }        │
└────────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────────┐
│ 4. Renderer produces JSON for WebAudio                         │
│    { notes: [...], synth: { osc: "saw", ... }, tempo: 120 }    │
└────────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────────┐
│ 5. WebAudio creates OscillatorNode and plays the sound         │
└────────────────────────────────────────────────────────────────┘

Key Design Decisions

Pure Functional Design

Relanote is designed as a pure functional language:

Relative Intervals

Music is expressed using relative intervals rather than absolute pitches:

Pipe-Based Composition

The pipe operator |> enables fluent composition:

rela
melody |> transpose M3 |> voice Lead |> volume 0.8

This is equivalent to nested function calls:

text
volume(voice(transpose(melody, M3), Lead), 0.8)

Studio

The Vapor Moon studio calls the same MoonBit bridge functions as the CLI pipeline:

File Types

ExtensionPurpose
.relaRelanote source files
.midExported MIDI files
.jsonInternal render format