Nine Miles of Visibility in a Major Seventh
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 Field | Musical Parameter | Rationale |
|---|---|---|
| Ceiling height | Chord root | Lower ceiling → lower root |
| Visibility | Chord quality | Poor vis → minor, good vis → major |
| Wind gusts | Added tensions | Gusts → 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.