openapi: 3.1.0
info:
  title: AudioLab API
  version: 0.1.0-preview
  summary: "Audio analysis as building blocks: loudness, voice QA, and signal indexing."
  description: |
    The AudioLab API exposes the same verified DSP engine that powers the
    in-browser labs at https://audiolab.tools. Submit an audio file, get back
    structured analysis (loudness to EBU R128 / BS.1770-4, voice quality, or a
    signal index).

    STATUS: the hosted endpoints are **live for design partners** at
    https://audiolab.tools/v1/* behind a Bearer key (hand-issued; email
    partners@audiolab.tools for one). The interactive playground on /api runs
    the identical engine client-side in your browser, no key, so you can see
    the exact response shape today. Self-serve keys + billing open after the
    first design partners.

    Honesty note: the API is live but access is gated (hand-issued keys, not yet
    self-serve). The browser labs are fully live and free.
  contact:
    name: AudioLab
    url: https://audiolab.tools/contact
  license:
    name: Proprietary
    url: https://audiolab.tools

servers:
  - url: https://audiolab.tools
    description: "Production, live for design partners (hand-issued Bearer keys)"

security:
  - ApiKeyAuth: []

tags:
  - name: MixLab
    description: Loudness, dynamics, stereo, and spectral analysis (EBU R128 / BS.1770-4).
  - name: VoiceLab
    description: "Speech quality analysis: clarity, pacing, noise, room, clipping."
  - name: SignalLab
    description: "Signal indexer: tags, content type, clipping/silence, metadata schema."
  - name: Jobs
    description: Poll long-running analyses.

paths:
  /v1/mixlab/analyze:
    post:
      tags: [MixLab]
      operationId: mixlabAnalyze
      summary: Analyze loudness, dynamics, stereo, and spectrum
      description: |
        Submit an audio file and get back the same JSON the in-browser MixLab
        analyzer renders. Most analyses complete in under a second and return
        200 directly; very large files may return 202 with a job to poll.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Audio file (wav, mp3, flac, m4a, ogg).
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, description: 'Public http(s) URL to the audio file (alternative to multipart upload).' }
      responses:
        '200':
          description: Analysis complete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AnalysisResult' }
        '202':
          description: Accepted; analysis running. Poll the job.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/voicelab/qa:
    post:
      tags: [VoiceLab]
      operationId: voicelabQa
      summary: Analyze speech quality
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, description: 'Public http(s) URL to the audio file (alternative to multipart upload).' }
      responses:
        '200':
          description: Voice analysis complete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VoiceAnalysis' }
        '202':
          description: Accepted; poll the job.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/signallab/index:
    post:
      tags: [SignalLab]
      operationId: signallabIndex
      summary: Index an audio file (tags, content type, clipping/silence, metadata)
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, description: 'Public http(s) URL to the audio file (alternative to multipart upload).' }
      responses:
        '200':
          description: Index complete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SignalIndex' }
        '202':
          description: Accepted; poll the job.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/mixlab/check-target:
    post:
      tags: [MixLab]
      operationId: mixlabCheckTarget
      summary: Check audio against a loudness target (Spotify, EBU, podcast, etc.)
      description: |
        Run MixLab and verify against a delivery target. Returns pass/fail, per-metric
        deltas, issue messages, and an ffmpeg loudnorm command to fix when failing.
        Presets: spotify (-14), apple-music (-16), youtube (-14), tidal (-14),
        amazon-music (-14), podcast (-16), ebu-broadcast (-23), atsc-broadcast (-24).
        Use "custom" with lufs+tp for a custom target.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, target]
              properties:
                url: { type: string, format: uri, description: 'Public http(s) URL to the audio file.' }
                target: { type: string, description: 'Preset name or "custom".' }
                lufs: { type: number, description: 'Custom target LUFS (when target="custom").' }
                tp: { type: number, description: 'Custom target dBTP (when target="custom").' }
      responses:
        '200':
          description: Target check complete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CheckTargetResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/mixlab/timeseries:
    post:
      tags: [MixLab]
      operationId: mixlabTimeseries
      summary: Loudness and waveform over time (for graphing)
      description: |
        Returns short-term LUFS samples (~10 Hz, EBU R128 3s window) with their time-base,
        plus downsampled waveform peaks. For loudness charts, level meters, waveform displays.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                waveformPoints: { type: integer, minimum: 50, maximum: 2000, description: 'Number of waveform peaks to return (downsampled). Default 200.' }
      responses:
        '200':
          description: Time-series data complete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimeseriesResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/mixlab/spectrum:
    post:
      tags: [MixLab]
      operationId: mixlabSpectrum
      summary: FFT spectrum bins + band energies
      description: |
        Returns paired frequency/magnitude arrays plus 7-band energies and spectral
        descriptors (centroid, rolloff, flatness). For spectrum-analyzer UIs and
        tonal-balance diagnosis beyond the harshness/muddiness labels.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
      responses:
        '200':
          description: Spectrum data complete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SpectrumResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/voicelab/segments:
    post:
      tags: [VoiceLab]
      operationId: voicelabSegments
      summary: Voiced speech segments with timestamps
      description: |
        List voiced speech regions with start/end timestamps and per-segment RMS level.
        For auto-trim, chapter generation, speaker-turn detection. RMS-based voice-
        activity detection (when speech happens, not who is speaking).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
      responses:
        '200':
          description: Segments complete.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SpeechSegmentsResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/jobs/{id}:
    get:
      tags: [Jobs]
      operationId: getJob
      summary: Poll a long-running analysis job
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Job status (and result when complete).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |
        Bearer API key in the Authorization header. Keys are issued from the
        dashboard once the hosted API opens (design preview, not live yet).

  responses:
    BadRequest:
      description: Malformed request.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Missing or invalid API key.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: Resource not found.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    PayloadTooLarge:
      description: File exceeds the size limit for your plan.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unprocessable:
      description: File could not be decoded as audio.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Too many requests. See Retry-After.
      headers:
        Retry-After: { schema: { type: integer }, description: Seconds to wait. }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, message]
          properties:
            type: { type: string, example: invalid_request_error }
            code: { type: string, example: file_undecodable }
            message: { type: string, example: "The uploaded file could not be decoded as audio." }
            param: { type: string }

    Job:
      type: object
      required: [id, status]
      properties:
        id: { type: string, example: job_a1b2c3 }
        status: { type: string, enum: [queued, running, done, error] }
        kind: { type: string, enum: [mixlab, voicelab, signallab] }
        result:
          description: Present when status is done; shape depends on kind.
          oneOf:
            - { $ref: '#/components/schemas/AnalysisResult' }
            - { $ref: '#/components/schemas/VoiceAnalysis' }
            - { $ref: '#/components/schemas/SignalIndex' }
        error: { $ref: '#/components/schemas/Error' }

    Band:
      type: object
      description: Spectral band energies (relative).
      properties:
        sub: { type: number }
        bass: { type: number }
        lowMid: { type: number }
        mid: { type: number }
        highMid: { type: number }
        presence: { type: number }
        air: { type: number }

    AnalysisResult:
      type: object
      description: MixLab analysis (mirrors the engine AnalysisResult type).
      required: [durationSec, sampleRate, channels, integratedLufs, truePeakDb]
      properties:
        durationSec: { type: number }
        sampleRate: { type: integer }
        channels: { type: integer }
        integratedLufs: { type: number, description: "Integrated loudness (LUFS), BS.1770-4." }
        reportUrl: { type: string, description: "Shareable numbers-only report page for this analysis (https://audiolab.tools/r/<id>). Present on every metered analysis response." }
        shortTermLufsMax: { type: number }
        loudnessRange: { type: number, description: LRA (LU). }
        truePeakDb: { type: number, description: True peak (dBTP). }
        peakDb: { type: number }
        rmsDb: { type: number }
        crestFactorDb: { type: number }
        stereoWidth: { type: number }
        midSideRatio: { type: number }
        stereoCorrelation: { type: number }
        monoCompatible: { type: boolean }
        spectralCentroidHz: { type: number }
        spectralRolloffHz: { type: number }
        spectralFlatness: { type: number }
        lowEndDensity: { type: number }
        bandEnergy: { $ref: '#/components/schemas/Band' }
        harshness:
          type: object
          properties:
            score: { type: number }
            label: { type: string, enum: [Low, Medium, High] }
            band: { type: string }
        muddiness:
          type: object
          properties:
            score: { type: number }
            label: { type: string, enum: [Low, Medium, High] }
        spectrum:
          type: object
          properties:
            freqs: { type: array, items: { type: number } }
            mags: { type: array, items: { type: number } }
        waveform: { type: array, items: { type: number }, description: Downsampled waveform peaks. }
        shortTermLufs: { type: array, items: { type: number } }
        fileName: { type: string }

    VoiceSegment:
      type: object
      properties:
        startSec: { type: number }
        endSec: { type: number }
        meanRms: { type: number }

    VoiceAnalysis:
      type: object
      description: VoiceLab speech-quality analysis (mirrors the engine VoiceAnalysis type).
      required: [durationSec, sampleRate, speechRatio]
      properties:
        durationSec: { type: number }
        sampleRate: { type: integer }
        speechSec: { type: number }
        silenceSec: { type: number }
        speechRatio: { type: number }
        segments: { type: array, items: { $ref: '#/components/schemas/VoiceSegment' } }
        estimatedSyllableRate: { type: number }
        speakingRateLabel: { type: string, enum: [Slow, Normal, Fast, Variable] }
        pacingVariance: { type: number }
        longSilences: { type: number }
        burstiness: { type: number }
        peakDb: { type: number }
        averageSpeechRms: { type: number }
        averageSpeechRmsDb: { type: number }
        noiseFloorDb: { type: number }
        signalToNoiseDb: { type: number }
        clipping:
          type: object
          properties:
            count: { type: integer }
            severity: { type: string, enum: [None, Low, Medium, High] }
        brightness: { type: number }
        roomEchoScore: { type: number }
        roomEchoLabel: { type: string, enum: [Dry, Tight, Live, Reverberant] }
        sibilanceRisk: { type: string, enum: [Low, Medium, High] }
        rmsEnvelope: { type: array, items: { type: number } }
        voicedMask: { type: array, items: { type: boolean } }
        fileName: { type: string }

    TimelineRegion:
      type: object
      properties:
        startSec: { type: number }
        endSec: { type: number }
        kind: { type: string, enum: [voice, silence, music, noise, clipping] }
        confidence: { type: number }

    SignalIndex:
      type: object
      description: SignalLab indexer output (mirrors the engine SignalIndex type).
      required: [fileName, durationSec, sampleRate, channels, contentTypeGuess]
      properties:
        fileName: { type: string }
        fileSizeKb: { type: number }
        format: { type: string }
        durationSec: { type: number }
        sampleRate: { type: integer }
        channels: { type: integer }
        bitDepthGuess: { type: string }
        peakDb: { type: number }
        rmsDb: { type: number }
        noiseFloorDb: { type: number }
        dcOffset: { type: number }
        hasClipping: { type: boolean }
        clippingRegions: { type: integer }
        silenceRegions: { type: integer }
        silenceTotalSec: { type: number }
        contentTypeGuess: { type: string, enum: [voice, music, mixed, noise, silence] }
        contentConfidence: { type: number }
        brightnessBucket: { type: string, enum: [Dark, Balanced, Bright, "Very bright"] }
        dynamicsBucket: { type: string, enum: [Flat, Compressed, Modern, Dynamic] }
        dominantBand: { type: string }
        monophonicProbability: { type: number }
        regions: { type: array, items: { $ref: '#/components/schemas/TimelineRegion' } }
        energyEnvelope: { type: array, items: { type: number } }
        centroidEnvelope: { type: array, items: { type: number } }
        tags: { type: array, items: { type: string } }

    CheckTargetResult:
      type: object
      description: Pass/fail verdict for a loudness target check, with concrete deltas and an ffmpeg loudnorm fix command.
      required: [pass, target, measurements, issues]
      properties:
        pass: { type: boolean, description: "True only if both LUFS within ±0.5 LU of target and true-peak ≤ target+0.5 dB." }
        target:
          type: object
          properties:
            lufs: { type: number, description: "Target integrated loudness (LUFS)." }
            tp: { type: number, description: "Target maximum true-peak (dBTP)." }
            label: { type: string, description: "Human-readable target name (e.g. \"Spotify\")." }
        measurements:
          type: object
          properties:
            integratedLufs: { type: number }
            truePeakDb: { type: number }
            lufsDelta: { type: number, description: "measured - target." }
            tpDelta: { type: number, description: "measured - target." }
        issues:
          type: array
          items:
            type: object
            properties:
              metric: { type: string, enum: [integratedLufs, truePeakDb] }
              issue: { type: string, enum: [too_loud, too_quiet, true_peak_exceeded] }
              delta: { type: number }
              message: { type: string }
        ffmpegFix: { type: string, nullable: true, description: "ffmpeg loudnorm one-liner to fix when failing; null when passing." }

    SeriesArray:
      type: object
      description: A time-indexed array with its sampling interval.
      properties:
        intervalSec: { type: number, nullable: true, description: "Seconds between samples." }
        count: { type: integer, description: "Number of samples." }
        samples: { type: array, items: { type: number } }
        unit: { type: string, description: "Unit of measurement (e.g. \"LUFS\", \"peak (-1..1)\")." }

    TimeseriesResult:
      type: object
      description: Loudness and waveform data over time, for graphing or visualization.
      required: [durationSec, loudnessOverTime, waveformOverTime, summary]
      properties:
        durationSec: { type: number }
        sampleRate: { type: integer }
        loudnessOverTime: { $ref: '#/components/schemas/SeriesArray' }
        waveformOverTime: { $ref: '#/components/schemas/SeriesArray' }
        summary:
          type: object
          properties:
            integratedLufs: { type: number }
            shortTermLufsMax: { type: number }
            loudnessRange: { type: number }
            truePeakDb: { type: number }

    SpectrumResult:
      type: object
      description: FFT spectrum bins plus band energies and spectral descriptors.
      required: [sampleRate, bandEnergy, spectrum]
      properties:
        sampleRate: { type: integer }
        spectralCentroidHz: { type: number }
        spectralRolloffHz: { type: number }
        spectralFlatness: { type: number }
        bandEnergy: { $ref: '#/components/schemas/Band' }
        spectrum:
          type: object
          properties:
            freqs: { type: array, items: { type: number } }
            mags: { type: array, items: { type: number } }
            count: { type: integer }

    SpeechSegment:
      type: object
      properties:
        startSec: { type: number }
        endSec: { type: number }
        durationSec: { type: number }
        meanRmsDb: { type: number, description: "Mean RMS of the segment in dBFS." }

    SpeechSegmentsResult:
      type: object
      description: Voiced regions with timestamps. For auto-trim, chapter generation, speaker-turn detection.
      required: [durationSec, speechRatio, segments]
      properties:
        durationSec: { type: number }
        speechSec: { type: number }
        silenceSec: { type: number }
        speechRatio: { type: number, description: "0..1 fraction of audio that is voiced." }
        longSilences: { type: number, description: "Count of unusually long silences (e.g. potential dead air)." }
        segments: { type: array, items: { $ref: '#/components/schemas/SpeechSegment' } }
