Name: the app is record.label (approved by 1982, 11 Nov 2026; formerly record.label). Always lowercase. In Inter 200 the period is drawn as the small red REC dot:
record●label. Internal file and folder names (og-label-*,the-og-ios, hub paths) are unchanged.
Status: design + tappable prototype for 1982's approval. Rev 2 (11 Nov 2026): bottom tab bar, generic for any artist (no personal inboxes or accounts; every platform starts with a "connect your …" flow), two distribution routes. Nothing in the shipping iOS app (/Users/test/the-og-ios) or the live hub was changed, and nothing was published, posted, uploaded or connected. The widgets are on hold and untouched.
Prototype: proto/index.html (self-contained; serve the folder, e.g. python3 -m http.server -d proto). Generic sample data for any artist (sample takes and tracks, real device names, no real accounts).
record●label: Inter 200, white on black, the period drawn as the small red REC dot. Icon masters in brand/.https://1982.studio/mcp (planned). Share links: https://1982.studio/t/<id> (planned).| token | value | source |
|---|---|---|
| background | #000 (cards #0b0b0b, lines #1f1f1f / #2c2c2c) |
record.label black |
| ink | #fff · dim #8e8e8e · dim2 #5c5c5c |
|
| red | #ff452b (= Color(red: 1, green: 0.27, blue: 0.17) in OGWidgets.swift) |
REC dot, Grokbot, warnings, selection |
| type | Inter variable, weight 200, all lowercase. Numbers are tabular. Small mono only for counters (position, timecode). | Widgets/Fonts/Inter-Variable.ttf |
| corner radius | widgets 22 pt, sheets 26 pt, pills full | Apple widget feel |
| device art | white line art on black: chassis #fff 0.715, controls/caps #fff 0.68, panels/holes #4d4d4d 0.39–0.52, non-scaling strokes, red REC dot fill. "Off" = same file with #9a9a9a. |
og-v41 art/*.svg grammar |
Rules: white and red on black only. No neural web. No gradients except the faint red glow on Grokbot surfaces. One accent action per screen in red (REC, post, bump, connect).
Components (TE component kit for the design teammate):
live white, busy blinking white dot, warn red, off dim).home · studio · video studio · tracks · account · ✦ grokbot
TabView): 49 pt bar + home-indicator inset, line icon over a 10 pt label, hairline top border, black with blur. TE look: 1.3 pt line icons, Inter 200 lowercase labels, selected = white (icon stroke 1.6), unselected = dim2.TabView with .sidebarAdaptable on iPad) to keep all six visible.1982://record.label label is gone and "hub" is renamed studio.Widgets (after the brief's changes: Stems removed, Design + Marketing merged; Uploads holds private files and SP-1 exports, with no personal inboxes):
| id | size | shows | tap |
|---|---|---|---|
| connected device | 2×2 | live device line art, name, connection type, dots for several connected devices | device sheet (all devices, connected lit white) |
| rec | 1×1 | big red dot (becomes rounded square while recording), timer, which device | start/stop a take on that device |
| takes | 1×1 | count, last take | tracks |
| studio | 1×1 (2×1 on big grids) | BPM, last session | studio |
| finished | 1×1 (2×1) | count, live count | tracks › finished |
| grokbot | 1×1 (2×1) | on/off, red glow | grokbot |
| video studio | 1×1 | clip drafts | video studio |
| uploads | 1×1 | private uploads + sp-1 exports | uploads sheet |
| design + marketing | 1×1 | covers, clips, socials | sheet |
| distribution | 1×1 (2×1) | mini bars + "connect soundcloud" / "soundcloud n live" · "dsp via distributor" | tracks |
| sp-1 loader | 1×1 | state | loader |
| socials | 1×1 | connected count 0/7 → "tap to connect" |
account › connect your platforms |
Grid algorithm (layoutHome() in proto/app.js, port as a SwiftUI Layout):
target tile = 150 pt on phones, otherwise clamp(172, min(w,h)/4.4, 236).cols = clamp(2, floor((w + gap) / (target + gap)), 6), square side from that.rows = max(2, floor((h + gap) / (side + gap))). Add one more row if rows can stay ≥ 72% of side tall.cols × rows cells (dense flow). If a page has ≥ 4 spare cells, promote studio/finished/grokbot/distribution/video/takes to 2×1 instead of leaving holes.Overflow check with the bottom tab bar: 26/26 pass (shots/home-overflow-check.json). Each size runs with simulated safe areas (iPhone notch 59 pt top / 34 pt home indicator in portrait, 21 pt in landscape; SE 20 / 0; Duo and iPad 24 / 20). It checks that the document doesn't scroll, every widget sits inside the page, no widget content overflows, there are no scrollable elements, the dots and pages sit above the tab bar, the tab bar is fully on screen with all 6 tabs, and no tab label is clipped.
| device (pt) | portrait grid · pages | landscape grid · pages |
|---|---|---|
| iPhone SE 375×667 | 2×3 · 3 | 4×2 · 2 |
| iPhone mini 375×812 | 2×4 · 2 | 5×2 · 2 |
| iPhone 16 / 17 393×852 | 2×4 · 2 | 5×2 · 2 |
| iPhone Plus 430×932 | 2×4 · 2 | 5×2 · 2 |
| iPhone Pro Max 440×956 | 2×4 · 2 | 5×2 · 2 |
| iPhone Duo outer 466×678* | 2×3 · 3 | 4×2 · 2 |
| iPhone Duo inner 669×951* | 3×4 · 2 | 5×3 · 1 |
| iPad mini 744×1133 | 4×6 · 1 | 6×4 · 1 |
| iPad 10th 820×1180 | 4×6 · 1 | 6×4 · 1 |
| iPad Air 11 820×1180 | 4×6 · 1 | 6×4 · 1 |
| iPad Pro 11 834×1210 | 4×6 · 1 | 6×4 · 1 |
| iPad Air 13 1024×1366 | 4×5 · 1 | 6×4 · 1 |
| iPad Pro 13 1032×1376 | 4×5 · 1 | 6×4 · 1 |
*The Duo sizes are derived from the Duo simulator screenshots on the box (2007×2853 inner and 1398×2034 outer, at @3x). Apple should confirm the real point sizes.
Aligned with Apple's plan: native SwiftUI. Other tabs can stay in the WebView hub at first.
Screen
bar.beat.sixteenth · BPM (−/+, tap to type) · save · bump (red).Gestures (all working in the prototype; work/studio-test.txt shows every check passing)
| gesture | action |
|---|---|
| tap clip | select (its track becomes selected) |
| drag clip body | move, snapped to quarter notes. Drag vertically to move it to another lane. |
| drag clip left/right edge | trim (non-destructive: changes sourceStart / sourceLength) |
| tap ruler or empty lane | move the playhead (snapped). Applied when the finger lifts, so starting a pinch never moves the playhead. |
| pinch with both fingers on one lane | track zoom: that track only (see Zoom below) |
| pinch on the ruler, the empty space under the lanes, or across two lanes | project zoom: the shared timescale, 8–220 pt per beat, anchored at the pinch centre |
| double-tap a lane (empty part) | toggle that track between 1× and 2.2× |
double-tap the ruler or empty space · fit key |
fit to screen: whole song fits the width, every track back to 1× |
| ctrl/⌘-wheel or trackpad pinch | over a lane = track zoom, elsewhere (or with ⇧) = project zoom |
| cut | split the selected clip at the playhead (or at its middle if the playhead is outside it) |
| splice | join with the next clip. If they're the same source and contiguous, they become one clip; otherwise the gap is closed. |
| copy / paste | paste at the playhead on the selected track |
| duplicate | copy placed right after the clip |
| keyboard (iPad) | space play, B cut, J splice, ⌘C/⌘V/⌘D, ⌫ delete, ⌘Z / ⇧⌘Z |
Zoom: two levels, one locked time grid (prototype + work/zoomtest.mjs, all checks pass on iPhone and Duo inner)
ppb, points per beat. It is the only time→x mapping, shared by every lane, the ruler and the playhead. Range 8–220. Quarter-note ticks appear at ≥ 18 pt/beat; peaks switch from 512- to 128-sample blocks at ≥ 80.trackZoom[trackId], 0.6–3×, default 1. It changes only what that lane shows inside the same timescale:×2.2 badge and a white edge, so it's obvious which tracks are zoomed. Track zoom is anchored under the fingers (the point you pinched stays put vertically).MagnifyGesture doesn't report where each finger is, so the timeline uses a UIPinchGestureRecognizer (via UIViewRepresentable) and classifies with location(ofTouch: 0/1, in:) on .began. Both values are view state saved in session.json (view.ppb, view.trackZoom) and are not undo steps.Engine and model (Apple's architecture, phases A–E)
StudioCore: pure Swift model with commands and undo. AudioEngineKit: wraps AVAudioEngine. SwiftUI views sit on an @Observable StudioSession.sourceStart and sourceLength in samples, timelineStart in beats, plus gain, fadeIn and fadeOut. Positions resolve through a tempo map, so the grid stays locked when BPM changes (checked in the prototype: clip starts are unchanged after 92→120 BPM).Canvas from two peak caches (512- and 128-sample blocks, picked by zoom). Pinch is classified per the Zoom rules above. The playhead runs from TimelineView.Track in Finished with distribution set to draft, and links the session.session.json (Codable) plus an audio/ folder of immutable sources.soundcloud · not connected, spotify · set up distribution, apple music · watching catalogue · isrc, …). Ready to bump, share and distribute.set up / hand off): SoundCloud row uploads directly once connected (otherwise connect). Spotify and Apple Music rows follow the distribution route in §11:set up distribution, action set up opens the route picker: card 1 your distributor (default; pick DistroKid, Too Lost, CD Baby, TuneCore, Amuse or other → use this route), card 2 LabelGrid Engine · API shown dashed and disabled with a coming later chip.hand off · <distributor>, action hand off opens the handoff sheet: 1) download release kit (WAV, artwork, credits), 2) upload it at the distributor, 3) paste the ISRC → i submitted it · start watching. Both rows go to watching catalogue · isrc, then live per store when the ISRC shows up.@yourname until one is connected).pps, points per second, shared by all three lanes and the ruler. It starts at fit (whole clip fills the width) and goes up to 14× fit. Ruler labels go 5 s → 1 s, with ¼ s ticks at ≥ 90 pt/s. The timeline then scrolls sideways inside its box; the page itself still never scrolls.laneZoom[video|audio|files], 0.8–3×. Taller lane plus bigger thumbnails: the video lane shows larger frames (with timestamps from 1.4×), the audio lane shows a taller, finer waveform, the files lane shows larger covers. The x position of every second stays the same on all lanes.zoom/fit corner button, fits everything. Ctrl/⌘-wheel follows the same rule.connect chip that opens the generic connect sheet; Instagram shows needs creator account until confirmed; Snapchat shows coming later. Then save, bump to library (attaches the clip to the track) and post selected (red).Generic for any artist; nothing is pre-filled.
edit banner).@handle · city and label line (with Grokbot state) start as placeholders ("your artist name"); edit profile sheet sets them.— until a platform reports them), takes.connect or its status chip (connected, needs creator account, link added, coming later). Phones show this as a compact two-column grid.sign in with <platform>. The real app runs that platform's own OAuth; record.label never sees a password. Connected rows show the handle and disconnect.The tab
https://1982.studio/mcp (planned, not live yet), 2) sign in with record.label and pick what the bot may do, 3) say hi ("what should i finish this week?"). There's a copy connector url button. The connect sheet repeats the steps and offers a personal access token for bots that can't do OAuth.record.label runs its own remote MCP server. The artist adds it to their Grok Bot as a custom connector (URL + sign-in). The bot then calls record.label tools the same way it calls any other connector.
Transport and hosting
https://1982.studio/mcp. Planned, not live: 1982.studio is not registered yet (domains get bought last), so the endpoint doesn't exist until then. Until it does, a *.workers.dev URL can serve as the dev endpoint.McpAgent, one Durable Object per session) bound to the same data stores the sync backend uses (catalogue DB, R2 audio/art bucket), so there's no second copy of anyone's catalogue. The iOS app keeps syncing exactly as it does today; the MCP server is just another client of the same backend API.Auth
workers-oauth-provider in front of the Worker.Authorization: Bearer ogx_pat_…, scoped the same way, revocable any time.catalog:read, catalog:write (names, notes, artwork, finished), drafts:write (posts, releases), status:read. There is deliberately no scope for publishing, releasing, spending, connecting platforms or changing the account.Outbound rule (hard rule, enforced on the server)
awaiting_approval.list_drafts, get_draft), but cannot approve, send or retry an outbound action itself.Tools
| tool | scope | does | notes |
|---|---|---|---|
list_takes |
catalog:read | takes, filterable by device, date, bpm; paged | grouped by device like the Tracks tab |
get_take |
catalog:read | one take: name, device, bpm, length, notes, waveform peaks | |
list_tracks |
catalog:read | finished tracks with per-destination status | |
get_track |
catalog:read | one track: metadata, notes, credits, artwork, videos, ISRC, distribution | |
get_audio_preview |
catalog:read | short-lived signed URL (15 min) to a low-bitrate preview | lets the bot "listen" for a&r; never the master |
rename_track |
catalog:write | rename a take or finished track | undoable in the app |
set_notes |
catalog:write | replace or append notes | |
set_artwork |
catalog:write | set artwork from an uploaded image or a generated one (image URL or base64) | previous artwork kept in history |
attach_video |
catalog:write | attach an exported Video Studio clip to a track | |
set_credits |
catalog:write | title credits, roles, splits | used by release drafts |
bump_to_finished |
catalog:write | move a take or studio bounce into Finished (distribution starts as draft) | internal to record.label, not outbound, so no approval needed |
get_distribution_status |
status:read | per-destination state for a track (SoundCloud, Spotify, Apple Music), via, URL, last checked | from §11 states |
list_connections |
status:read | which platforms and which distribution route are connected | names only, never tokens |
draft_post |
drafts:write | create a post draft: clip or track, platforms, caption, schedule time | status awaiting_approval; the app posts after approval |
draft_release |
drafts:write | create a release draft: track(s), title, credits, release date, target (SoundCloud upload and/or distributor handoff) | builds the release kit; artist approves, then uploads/hands off |
list_drafts / get_draft |
drafts:write | the bot's drafts and their status (awaiting, approved, rejected, posted, failed) | |
update_draft / cancel_draft |
drafts:write | edit or withdraw a draft while it is still awaiting approval | |
get_stats |
status:read | plays and followers per connected platform, for the weekly report | — where a platform doesn't report |
Share links use https://1982.studio/t/<id> (planned, not live).
Also exposed as MCP resources (read-only): recordlabel://track/{id}, recordlabel://take/{id}, recordlabel://draft/{id}. Role toggles in the Grokbot tab map to tool groups (e.g. turning off social manager hides draft_post).
Not in v1: any publish/approve tool (never), editing Studio sessions (clips, mix) from the bot, and artist-to-artist actions.
New SVGs in art/svg/ (<id>.svg and <id>-off.svg) and in proto/art/. Contact sheet with the reference fit checks: art/preview.png. They use the same grammar as the og-v41 art:
Pipeline: art/prim_dev.py + art/devices.json (re-runnable). Overlays: art/build/*-overlay.png.
| device | reference (official, straight-on) | confidence |
|---|---|---|
| OP-1 (original) | teenage.engineering asset 627cab62… (already in og-device-outlines/refs) |
high |
| OP-Z | teenage.engineering 5bc066ec… (top view, 1024 px source upscaled; top edge rebuilt as a rounded rect) |
medium. The source is small and the top edge was cropped in the photo. |
| TX-6 | 66e88d84… |
high (knob rows and fader slots hand-placed on measured centres) |
| TP-7 | 66f277fd… |
high |
| CM-15 | 66e88dfe… (front, grille side) |
high for the outline. The mesh is drawn as a 34×32 square-hole grid (stylised weave), so it's medium for the grille. |
| EP-133 K.O. II (standard) | 66a8e547… |
high |
| EP-1320 medieval | 66b09bcb… |
high (same chassis as EP-133) |
| OB-4 | 64e4af8e… |
high for the body and cones. The handle and small grilles are approximate (medium). |
| PO-33 K.O! (pocket operator) | 66211af6… |
high. Other POs share the chassis and differ in print. |
| Akai MPC Sample | akaipro.com product image 7bd3ef0d… (slight perspective). Squashed to Akai's published 19.4 × 23.6 cm footprint, and the left edge was estimated because of a soft shadow. |
medium |
| EP-40 riddim, EP-2350 ting, OP-1 field / OP-XY / EP-133 Supreme | — | Not drawn. The EP-40 and EP-2350 have no clean top view on teenage.engineering. The OP-1 field, OP-XY and EP-133 Supreme already exist in og-v41 art/ and were reused, not redrawn. |
Not drawn: the SP-1 (no reference found this pass). The hub's te.js still has a generic sampler glyph for it.
// Swift: Codable structs with the same fields
Device { id: "op-xy"|"ep-133"|"op-1f"|"cm-15"|"mpc-sample"|…, name, maker, art: "op-xy.svg", usbMatch: RegExp, kind }
Source { id, file: "audio/<uuid>.caf", sampleRate, frames, channels, peaks: "peaks/<uuid>.bin", recordedOn: Device.id, createdAt } // immutable
Take { id, name, sourceId, deviceId, bpm, durationSec, notes, createdAt } // a raw recording, grouped by deviceId
Session { id, name, tempoMap: [{ beat: 0, bpm: 92, meter: "4/4" }], tracks: [Track_], masterGain, createdAt, updatedAt }
Track_ { id, name, deviceId?, volume: 0…1.2, pan: -1…1, mute, solo,
fx: { comp: {on, thresholdDb, ratio}, hp: {on, hz}, lp: {on, hz}, reverbSend: 0…1 }, clips: [Clip] }
Clip { id, sourceId, sourceStart: samples, sourceLength: samples, timelineStart: beats, gain, fadeIn: beats, fadeOut: beats }
Finished { id, title, sessionId?, takeId?, bpm, durationSec, mixFile, artwork?, videos: [ClipExport.id], notes,
isrc?, upc?, credits: [{name, role, split}], releaseDate?,
distribution: { soundcloud: DestStatus, spotify: DestStatus, appleMusic: DestStatus } }
ClipExport { id, title, aspect: "9:16"|"1:1"|"16:9", lanes: { video: [VClip], audio: [VClip], files: [VClip] }, file?,
posts: { instagram: PostStatus, youtube: PostStatus, tiktok: PostStatus, snapchat: PostStatus, x: PostStatus } }
DestStatus { state, remoteId?, url?, updatedAt, error?, via?: "native"|"handoff:<distributor>"|"catalog-poll"|"labelgrid" }
Distribution { route: null|"handoff"|"labelgrid" /* labelgrid = designed, inactive */, distributor?: "distrokid"|"too lost"|"cd baby"|"tunecore"|"amuse"|"other",
labelgrid?: { accountRef, webhookSecretRef } /* reserved, unused in v1 */ }
PostStatus { state, remoteId?, url?, updatedAt, error?, via?: "native"|"ayrshare"|"buffer" }
Social { platform, handle, connected: bool, accountType?, tokenRef (Keychain only), needs?: "creator-account"|"audit"|"allowlist"|"account" }
GrokbotLink { connected, botId?, roles: {anr, release, social, dist, report}, approvalRequired: true /* locked */ }
Profile { name, handle, location, avatar?, banner?, label?, stats }
SessionView { ppb: 8…220, trackZoom: { [trackId]: 0.6…3 }, scrollX, scrollY } // saved in session.json, not undoable
ClipView { pps: number|null /* null = fit */, laneZoom: { video?, audio?, files?: 0.8…3 } }
BotDraft { id, kind: "post"|"release", payload, createdBy: "grokbot:<clientId>", status: "awaiting_approval"|"approved"|"rejected"|"posted"|"failed", approvedAt?, approvedBy? /* artist session only */ }
McpToken { id, kind: "oauth"|"pat", scopes: ["catalog:read","catalog:write","drafts:write","status:read"], createdAt, lastUsedAt, revokedAt? } // empty for a new artist; set in edit profile
States shown as chips. This is based on XXX's research (/workspace/ogxy/marketing_connections.md, internal path unchanged); no platforms were re-researched.
Music destinations
| destination | path | chip states | how status is read |
|---|---|---|---|
| SoundCloud (P0) | native upload API to the artist's own SoundCloud after they connect it. record.label registers one SoundCloud API app (Neo's existing app can serve as the dev app). OAuth 2.1 + PKCE, POST /tracks. |
not connected → draft → uploading → encoding → live (public or private) / failed · retry | poll GET /tracks/:id until transcodings exist |
| Spotify | via a distributor (no artist upload API), route below | set up distribution → hand off · |
route 1: Spotify Web API search isrc: (catalogue poll; dev app). route 2 (later): LabelGrid webhooks. |
| Apple Music | via a distributor, route below | same as Spotify | route 1: Apple Music API filter[isrc] (Apple Developer $99/yr + MusicKit key). route 2 (later): LabelGrid webhooks. |
Distribution routes (Spotify + Apple Music). The artist picks one; record.label never signs up or pays for them.
live with its URL when found. via: "handoff:<distributor>".coming later chip; the data model reserves route: "labelgrid". Not built in v1.Social destinations (Video Studio)
| platform | path | chip states (before connect: not connected → generic connect sheet) |
|---|---|---|
| YouTube (P0) | Data API videos.insert, OAuth youtube.upload. Unverified projects are no longer forced private (as of 2 Nov 2026). Default 100 uploads/day. ≤3 min vertical becomes a Short. |
ready → uploading → processing → live / failed |
| X (P0) | v2 chunked media upload + POST /2/tweets, OAuth 2.0 user context. Pay-per-use: $0.015/post, $0.20 if it has a URL. |
ready → uploading → posted / failed |
| Instagram (P0) | Graph API Reels: container → status_code → media_publish. Instagram Login; the artist's account must be Creator or Business. |
needs creator account (if personal) → ready → processing → posted / error / expired |
| TikTok (P0, inbox) | Content Posting API inbox upload (video.upload). Direct Post needs a TikTok app audit. |
ready · inbox → uploading → in tiktok inbox · tap publish in tiktok → posted / failed |
| Snapchat (later) | Public Profile API is allowlist-only and needs a Business org + Public Profile. Or Ayrshare, or the Creative Kit share sheet. | coming later |
| Aggregator option | Ayrshare $149/mo covers all five (the only one with Snapchat). Buffer is cheaper but has no Snapchat. | via: ayrshare |
P0 order: SoundCloud → YouTube → X → Instagram → TikTok inbox → distribution route 1 (release kit + ISRC catalogue polling). LabelGrid (route 2) and Snapchat come later.
Only the artist can: pick and pay for their distributor, switch Instagram to Creator/Business, authorise each OAuth, and claim Spotify for Artists / Apple Music for Artists. Only 1982 (as the record.label owner) can: approve an aggregator or LabelGrid spend and own the platform dev apps and audits. Tokens go in the Keychain only.
Grok Bot connector: record.label's own MCP server, see §8a. Drafts only; the artist approves in the app.
Shapes from the same primitives).Layout port of layoutHome(), paging TabView(.page)). Wire it to the existing DeviceBridge / NativeRecorder for live device art and REC. Apple.Canvas timeline, TimelineView playhead.ppb + per-track zoom, pinch routed by touch hit-test, fit).Ownership: Apple owns core, engine, timeline and native shell. XXX owns marketing and social connections (and status semantics). Neo owns the hub, the WebView tabs in the interim, and the existing SoundCloud app. Me (Brooklyn) coordinates, owns design and spec, and guides the team.
SPEC.md: this documentbrand/: record.label app icon masters icon-a-1024.png (one line) and icon-b-1024.png (stacked), source icon.html; preview shots/brand-icon-preview.pngproto/: prototype (index.html, style.css, app.js, studio.js, data.js, art/, fonts/Inter-Var.ttf; no personal assets)shots/: screenshots <tab>-<iphone|duo-inner|ipad>.png, home-p2-*.png, per-track zoom studio-trackzoom-iphone.png / .gif (animated pinch), studio-trackzoom-projectzoom-iphone.png, video-lanezoom-iphone.png, iPhone sheets tracks-distribute-iphone.png, distribute-routes-iphone.png, distribute-handoff-iphone.png, account-connect-iphone.png, and home-overflow-check.jsonart/svg/: new device SVGs (on + off) · art/preview.png contact sheet · art/build/*-overlay.png fit checks · art/prim_dev.py, art/devices.json pipelinerefs/: downloaded official reference images (refs/hi/) and contact sheetswork/: screenshot, overflow, studio and zoom test scripts; results in work/studio-test.txt and work/zoom-test.txt