Nine Miles of Visibility in a Major Seventh

METAR Chord Briefings
🎮 Play: METAR Chord Decoder

My hobby is collecting hobbies, and hobby number nine is METAR Chord Briefings — building a desk synth that fetches local aviation weather reports and maps wind, ceiling, and visibility to chord progressions. The idea arrived while planning a flight last week: I was staring at CYEG 091700Z 27015G25KT 9SM SCT040 BKN080 M12/M18 A3002 and realized I’d been parsing these strings for years without once hearing them. What if the briefing spoke in chords instead of alphanumerics?

This is a technical walkthrough of the mapping logic — specifically, how to translate METAR fields into harmonic decisions a microcontroller can execute.

The METAR Structure

A METAR is a compressed weather observation, standardized by ICAO, issued every hour (with SPECI updates when conditions change significantly). The format dates to the 1960s and prioritizes terseness over readability:

CYEG 091700Z 27015G25KT 9SM SCT040 BKN080 M12/M18 A3002
│    │       │          │   │      │      │       └─ altimeter (inches Hg)
│    │       │          │   │      │      └─ temp/dewpoint
│    │       │          │   │      └─ broken ceiling at 8000 ft
│    │       │          │   └─ scattered clouds at 4000 ft
│    │       │          └─ visibility (statute miles)
│    │       └─ wind from 270° at 15 kt, gusts 25 kt
│    └─ day 09, time 1700 Zulu
└─ station identifier

The Z suffix means Zulu time — NATO phonetic for UTC, adopted when worldwide aviation coordination chose Greenwich as the reference meridian. Canadian METARs use statute miles for visibility; most of the world uses metres. Any parsing code needs to handle both.

Mapping Strategy

Three fields carry the most operational meaning for VFR flight: wind, ceiling, and visibility. Each maps to a different harmonic dimension:

METAR FieldMusical ParameterRationale
Ceiling heightChord rootLower ceiling → lower root
VisibilityChord qualityPoor vis → minor, good vis → major
Wind gustsAdded tensionsGusts → 7ths, 9ths, suspensions

The logic: a low, murky, gusty day should sound tense and dark. Clear and calm should sound open and bright. The mapping isn’t arbitrary — it follows the emotional logic pilots already use when reading weather.

Ceiling → Root Note

Cloud layers report in hundreds of feet AGL. The significant values for VFR pilots are:

  • OVC below 1000 ft — IFR, marginal at best
  • BKN 1000–3000 ft — marginal VFR
  • SCT/FEW above 3000 ft — comfortable VFR
  • CLR/SKC — clear skies

Map these bands to chord roots descending chromatically:

def ceiling_to_root(metar):
    lowest = get_lowest_ceiling(metar)  # BKN or OVC layer
    if lowest is None:
        return 'C'   # clear skies → bright root
    elif lowest >= 3000:
        return 'A'   # high ceiling
    elif lowest >= 2000:
        return 'G'
    elif lowest >= 1000:
        return 'E'
    else:
        return 'D'   # low ceiling → darker root

The descending chromatic motion (C → A → G → E → D) creates a “sinking” sensation as ceilings drop — the same spatial intuition pilots use when reading the sky.

Visibility → Chord Quality

Visibility ranges from 0SM (fog) to 10SM (effectively unlimited). The mapping:

def visibility_to_quality(vis_sm):
    if vis_sm >= 6:
        return 'maj7'    # clear, open
    elif vis_sm >= 3:
        return 'dom7'    # some haze, tension
    elif vis_sm >= 1:
        return 'min7'    # restricted
    else:
        return 'dim7'    # foggy, ominous

A 10SM day produces a bright major seventh. Dropping to 2SM shifts to minor. Below 1SM you get diminished — the auditory equivalent of “ceiling and visibility okay” versus “VFR not recommended.”

Wind → Chord Tensions

Wind encodes three values: direction, sustained speed, and gust factor. Direction doesn’t map cleanly to harmony (it matters for runway selection, not emotional state), but the gust differential does:

def wind_to_tensions(wind_kts, gust_kts):
    tensions = []
    delta = (gust_kts or wind_kts) - wind_kts
    
    if wind_kts >= 20:
        tensions.append('sus4')   # sustained wind → suspended quality
    if delta >= 10:
        tensions.append('add9')   # gusty → upper extensions
    if delta >= 15:
        tensions.append('#11')    # very gusty → dissonance
        
    return tensions

A calm day (00000KT) produces no tensions — just the clean triad. A gusty day (27015G30KT) stacks upper extensions until the chord becomes harmonically unstable.

Assembly

Combining the three functions yields a chord symbol:

def metar_to_chord(metar):
    root = ceiling_to_root(metar)
    quality = visibility_to_quality(metar.visibility)
    tensions = wind_to_tensions(metar.wind, metar.gust)
    
    return f"{root}{quality}{''.join(tensions)}"

Today’s CYEG report (27015G25KT 9SM SCT040 BKN080) produces:

  • Ceiling: BKN at 8000 → root A
  • Visibility: 9SM → quality maj7
  • Wind: 15 gusting 25 (delta 10) → add9

Result: Amaj7(add9) — a lush, open chord with a slight shimmer of instability. That matches what I see out the window: high broken clouds, good visibility, a bit of wind.

Hardware Notes

The synth runs on a Raspberry Pi Pico W pulling METARs from Aviation Weather Center’s text server. A Python script parses the report and sends MIDI to a cheap FM chip (YM2612 — the Sega Genesis sound chip, which I had lying around from the generative soundscape experiments). The chord sustains until the next METAR or SPECI arrives, then crossfades to the new voicing.

When conditions change mid-hour, a SPECI interrupts the stream — the weather station’s way of saying this matters now. The chord shifts while you’re watching. That’s the moment I keep chasing: data from the sky, translated into something the ear understands before the conscious mind catches up.