transcode

package
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 27, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package transcode owns offline PCM-upscaling: building sox(1) invocations, choosing variant identifiers + sidecar paths, and the worker pool the CLI subcommand and (in Phase 2.5) the HTTP `POST /v1/upscale` handler share.

**Why shell out to sox** instead of cgo + libSoXr: keeping the bridge build pure-Go preserves `make build-all` cross-compilation to darwin/linux/windows × amd64/arm64 from a single host. SoXr is the same engine via a different surface; perceived audio quality is identical at the `-v` ("very high") preset we use here. The cost is a runtime dependency the operator installs once (`apt install sox` / `brew install sox` / `choco install sox`).

Bridge `serve` and the `bridge upscale` CLI both probe for `sox` on PATH at startup; if missing, the CLI exits with an install hint and `serve` logs an error then disables the feature in- memory (graceful degradation — the rest of the server keeps running).

**Bit-exact mission preserved**: transcoding is offline; the bridge never modulates bytes in flight. The sidecar `.flac` is a real on-disk file the bridge then serves bit-exact via the same `serveFile` path that the original source uses.

Index

Constants

View Source
const (
	FLACCompressionFactor16Bit = 0.55
	FLACCompressionFactor24Bit = 0.65
)

Compression factors for ProjectedSize. Validated empirically against existing track_variants samples in the dev fixture; revisit if real- world ratios drift outside these bounds.

  • 16-bit FLAC tends toward ~0.55× the raw PCM size on typical dynamic-range material (classical / jazz). Lower with very sparse signals, higher with electronic / heavily-compressed masters.
  • 24-bit FLAC carries more LSB noise that FLAC's predictor can't fully model, so the ratio creeps up to ~0.65. The four additional bits per sample are roughly orthogonal to the predictor's residual coding, so the size delta vs 16-bit is close to the bit-depth ratio.
  • 32-bit cases are rare in this pipeline (target rarely 32); same factor as 24-bit is a defensible upper-bound estimate.

The factor is multiplicative on top of the raw PCM size projection — it is NOT the compression ratio relative to source FLAC bytes. ProjectedSize composes them correctly.

View Source
const (
	VariantPrefixUpscaled  = "upscaled"
	VariantPrefixOptimized = "optimized"
)

Variant ID prefixes. Two transcode classes coexist today:

  • "upscaled-" — render a hi-res target (e.g. 192/24) FROM a lower-res source, for home-DAC playback. Operator-driven (POST /v1/upscale + `bridge upscale --all`).

  • "optimized-" — render a CarPlay-compatible 16/44.1 or 16/48 target FROM a hi-res source. iOS auto-routes to these when CarPlay is the active audio output; the sample-rate-family selector (`TargetRateForOptimize`) keeps SoX on the integer- ratio fast path.

**Don't share the variant ID cache between kinds** — the (44100, 16) entry exists in BOTH spaces and the upscale memo returns `upscaled-v2-44100-16` for either kind's lookup, silently corrupting variant routing at the SQL persistence boundary. Two parallel memos (this file: `variantIDCache` + `optimizeIDCache`) keep the prefix semantics tight.

View Source
const DefaultDiskSafetyMargin = 0.10

DefaultDiskSafetyMargin is the headroom fraction applied on top of the projected variant size before comparing against AvailableDiskSpace. 0.10 = refuse a batch when projected + 10% would consume the free space — leaves the bridge dataDir's WAL, sox temp file, and any concurrent enrichment writes some breathing room. Operators can override per-batch via the Submit API; this is the admin Library Inspector's pre-flight default.

View Source
const OutputDirSubdir = "transcoded"

OutputDirSubdir is the fixed subdirectory under `cfg.DataDir` where transcoded sidecar files land. Single source of truth so cmd/bridge (which wires the runtime pool's `OutputDir`) and the admin handlers (which surface "Stored at <path>" to operators) can't drift. Changing this value rehouses the cache; existing rows in `track_variants.sidecar_path` continue to point at the old location until they're rewritten by `bridge upscale --gc`.

View Source
const VariantSchemaVersion = "v2"

VariantSchemaVersion is bumped when the on-disk sidecar layout or the SoX command shape changes in a way that makes prior sidecars semantically different from what a fresh run would produce. Today's only producer is `bridge upscale --quality very-high`. A future change of resampler preset (e.g. switching to a min-phase filter) bumps this so the iOS picker can distinguish "v1 upscale" from "v2 upscale" if they ever coexist during a transition.

**v2 (this version)**: `-G` (gain-guard) added to SoxArgs. Sox pre-scans the input for the headroom needed to prevent clipping during the rate-conversion + dither pipeline and applies a matching attenuation. Fires only when the source has 0 dBFS peaks (most modern pop masters); the typical attenuation is well under 1 dB. The audio CONTENT shifts under guard, so the schema bump produces a fresh VariantID — operators run `bridge upscale` once after upgrade and the iOS client picks up the new guard-clean variants automatically. Pre-v2 sidecars stay served by their existing track_variants rows until the next `bridge upscale --gc` pass cleans them up.

Variables

View Source
var ErrBatchNotFound = errors.New("upscale batch not found")

ErrBatchNotFound is returned by Cancel / inspect operations when the batchID isn't in `upscale_batches`. Distinct from a no-op transition (terminal-status batch — Cancel returns nil for that).

View Source
var ErrInsufficientDiskSpace = errors.New("insufficient disk space")

ErrInsufficientDiskSpace is returned by DiskHasHeadroom when the projected size (after applying the safety margin) exceeds the free space. The error embeds the numbers so the coordinator can surface them in the operator-facing response without re-running the probe.

View Source
var ErrPoolClosed = errors.New("transcode pool is closed")

ErrPoolClosed is returned when Enqueue is called after Stop. Production consumers shouldn't see this — `bridge serve` only stops the pool during graceful shutdown, when no new requests are accepted. Tests use it.

View Source
var ErrQueueFull = errors.New("transcode pool queue is full")

ErrQueueFull is returned by Enqueue when the pending-job channel is at capacity. HTTP callers map this to a 503 `queue_full` response so the iOS toast can say "Queue full — wait for current conversions to finish, then try again."

View Source
var ErrSoxMissing = errors.New("sox binary not found on PATH")

ErrSoxMissing is returned by PrecheckSox when `sox` isn't on PATH. Test for it via errors.Is so callers can surface a targeted install-hint message vs the generic "something went wrong" path.

Functions

func AvailableDiskSpace added in v0.1.3

func AvailableDiskSpace(dir string) (int64, error)

AvailableDiskSpace returns the number of bytes available to a non-privileged caller on the volume that holds `dir`. The probe is non-recursive — Statfs reads the volume's superblock-equivalent stats, not a per-directory quota.

Bavail (rather than Bfree) is the right choice: Bfree includes reserved space the kernel won't actually let a non-root caller allocate. On a typical Linux mount the gap is ~5% of the volume — using Bfree would consistently over-report and let DiskHasHeadroom approve batches that hit -ENOSPC mid-write.

func CreatedAtNow

func CreatedAtNow() int64

CreatedAtNow returns wall-clock UTC nanoseconds — the value callers stamp into `track_variants.created_at` on insert. Centralised here so test stubs can override (today: a const, good enough).

func DefaultCompressionFactor added in v0.1.3

func DefaultCompressionFactor(targetBits int) float64

DefaultCompressionFactor returns the FLAC compression factor matching the given target bit depth. Defaults to the 24-bit factor for any non-16 value — the larger factor is the conservative direction for pre-flight space checks (over-estimates by a few percent at most; under-estimating risks the disk-full-mid-batch failure the helper exists to prevent).

func DiskHasHeadroom added in v0.1.3

func DiskHasHeadroom(dir string, projectedBytes int64, safetyMargin float64) (ok bool, freeBytes int64, err error)

DiskHasHeadroom probes the free space on the volume containing `dir` and returns (ok, freeBytes, err). It refuses (`ok=false` with an *InsufficientDiskSpaceError) if projectedBytes × (1 + safetyMargin) > freeBytes. A negative or NaN safetyMargin is clamped to 0; a zero or negative projectedBytes is treated as "no work to do" and always returns ok=true.

Probe errors (Statfs failure, missing directory) surface as the returned `err`; callers should treat those as "can't check, refuse the batch" — silently proceeding risks a disk-full mid-batch.

func OptimizeEligible added in v0.1.4

func OptimizeEligible(sourcePath, codec string, sourceRate, sourceBits int) bool

OptimizeEligible returns true when the source is structurally higher fidelity than any CarPlay route accepts. Pre-filters at the eligibility layer so the downstream rate-resolver isn't called on no-op candidates.

Carries `sourcePath` AND `codec` so legacy DB rows scanned before the codec column was populated (codec == "") can fall back to the on-disk extension. Without that fallback, `bridge optimize --all` on a pre-upgrade library would silently skip every track until the operator runs a destructive full re-scan to backfill the codec column.

Eligibility:

  • PCM-only (DSD / lossy MP3/AAC / unknown rejected). .m4a is treated as PCM-candidate because ALAC-in-M4A is the common lossless case; AAC-in-M4A fails the rate/bits gate at the bottom (lossy is always 44.1/16 in practice).
  • Higher than CarPlay floor: sourceRate > 48000 OR sourceBits > 16. Already-at-floor 44.1/16 and 48/16 sources return false.

func OutputDirFor added in v0.1.3

func OutputDirFor(dataDir string) string

OutputDirFor returns the absolute on-disk directory where the pool writes converted sidecars given the bridge's `dataDir`. Mirrors the path the runtime pool is configured with at startup; safe to call from any goroutine (pure path arithmetic).

func PickTargetRate

func PickTargetRate(sourceRate int) int

PickTargetRate decides the output sample rate from the source rate when the operator passed `--target-rate auto`. Picks the highest integer-ratio target within the 44.1/48 family that stays at or below 192 kHz — the sweet spot for modern DACs' integer-ratio oversampling filters:

44100  → 176400 (4× — two octaves above source)
88200  → 176400 (2× — one octave above source)
176400 → 0 (skip; source is already at the auto target)
48000  → 192000 (4×)
96000  → 192000 (2×)
192000 → 0 (skip)
other  → 0 (don't auto-pick a target for unfamiliar rates)

Returns 0 when no auto target makes sense; callers treat 0 as "skip this track". Operators with DACs that prefer a higher rate (Chord M-Scaler at 705.6) override via explicit `--target- rate 705600`.

func PrecheckSox

func PrecheckSox() error

PrecheckSox returns nil if `sox` is on PATH and runnable. It is a thin wrapper over ProbeSox (one probe implementation, no duplication); the FLAC-aware callers use ProbeSox directly while the boolean-only sites (e.g. the public /v1/upscale/stats adapter) keep this signature.

func ProjectedSize added in v0.1.3

func ProjectedSize(
	sourceSize int64,
	sourceRate, sourceBits int,
	targetRate, targetBits int,
	compressionFactor float64,
) int64

ProjectedSize estimates the on-disk size of a FLAC variant produced at (targetRate, targetBits) from a source of (sourceSize, sourceRate, sourceBits).

The model: rate ratio × bits ratio × compressionFactor × source size. Each factor is independent — sample rate scales linearly with byte count; bit depth scales linearly within a single PCM frame; compression factor captures FLAC's predictor + Rice coding efficiency.

All arithmetic is performed in float64 to avoid wrap-around on gigabyte-scale sources; the result is converted back to int64 at the end. Negative or zero source parameters return 0 — the caller (typically the Coordinator's batch walk) should filter those rows before calling.

Pure, allocation-free, suitable for hot loops over thousands of tracks.

func ResolveTargetRate

func ResolveTargetRate(flagValue string, sourceRate int) (int, error)

ResolveTargetRate converts the user-facing `--target-rate` flag (string, possibly "auto") into the integer Hz value to feed into SoxArgs. Returns (0, nil) when the resolution is "skip this source" (auto + already at/above target, or a deliberate integer at/below the source). Returns (>0, nil) when there's a real target. Returns (0, err) on a malformed flag.

func ResolveTargetRateForOptimize added in v0.1.4

func ResolveTargetRateForOptimize(sourceRate int) (int, error)

ResolveTargetRateForOptimize is the optimize-kind analog of `ResolveTargetRate`. Pure pick — returns the family-preserving target rate unconditionally. Eligibility is the authoritative gate (see `OptimizeEligible`); this function does NOT re-evaluate "is the source already at the target".

**Why no rate-only skip**: a 44.1/24 source is at the target rate (44.1k) but `OptimizeEligible` correctly returns true (bits > 16 → needs 24→16 downsample). A previous version of this function returned 0 when `sourceRate <= target`, which silently skipped legitimate bit-only optimize candidates at every call site that checked `if target == 0 { skip }`. The gate AND the resolver must agree on eligibility for the bit- only case to flow through. Per Gemini bot review on PR #270.

**Don't reuse `ResolveTargetRate`** — its `target <= sourceRate` branch is correct for upscale (refuse downsampling), but wrong for optimize.

func RunSox

func RunSox(ctx context.Context, j JobSpec) (int64, error)

RunSox executes one JobSpec, writes the sidecar to disk, and returns the on-disk size on success. Atomic-rename pattern: the sox output goes to `<sidecar>.tmp`, then we `os.Rename` to the final path on success — a crash mid-conversion leaves at most a `.tmp` file behind (cleaned up by `bridge upscale --gc`).

The `sox` binary is located via PATH. Caller is expected to have already probed for it via PrecheckSox; we don't repeat the probe here so a worker-pool body doesn't pay the LookPath cost per iteration.

**Cancellation**: ctx is plumbed via `exec.CommandContext`. A SIGINT to the CLI / SIGTERM to `bridge serve` cancels the outer context, exec.CommandContext SIGKILLs the in-flight sox process, and we clean up the partial `.tmp`. Without this a half-converted album could hang the operator's terminal until the largest file finishes (Gemini bot review on PR #108).

Output directory created if missing — `bridge upscale` only guarantees DataDir exists, not the `transcoded` subdir.

func TargetRateForOptimize added in v0.1.4

func TargetRateForOptimize(sourceRate int) int

TargetRateForOptimize picks the CarPlay-compatible downsample target while preserving sample-rate-family alignment so SoX takes the integer-ratio fast path:

44.1k family (44100 / 88200 / 176400) → 44100  (exact /1, /2, /4)
48k family   (48000 / 96000 / 192000) → 48000  (exact /1, /2, /4)
Anything else (rare/exotic) → 48000  (broader compatibility floor)

Wired CarPlay accepts up to 16-bit / 48 kHz uncompressed LPCM, wireless CarPlay encodes through a 16-bit / 48 kHz AAC pipeline, so 48 kHz is just as valid a CarPlay target as 44.1 kHz — picking the same family as the source avoids the fractional-resample CPU cost (`96000/44100 = 2.176…`) and the resampling artifacts a variable-rate filter has to defend against.

Types

type ActiveJob added in v0.1.7

type ActiveJob struct {
	SourceRel        string  // SourceLibraryRel — the display path
	SourceSampleRate int     // Hz, 0 if unknown
	SourceBits       int     // bit depth, 0 if unknown
	TargetSampleRate int     // Hz
	TargetBits       int     // 16/24/32
	Quality          Quality // resampler quality (very-high / high / medium)
	Kind             JobKind // upscale / optimize
	StartedAtUnixMs  int64
}

ActiveJob is the immutable snapshot of the job a worker is currently running, stored in Pool.activeJobs[workerID]. Built once at job start and never mutated (a future phase field would allocate + Store a fresh one), so concurrent ActiveWorkers() reads are torn-state-free without a lock. Carries StartedAtUnixMs (not a ticking elapsed) so the worker grid's wire shape stays stable while a job runs — the client ticks the elapsed display locally.

type ActiveJobView added in v0.1.7

type ActiveJobView struct {
	WorkerID         int    `json:"workerId"`
	Busy             bool   `json:"busy"`
	SourceRel        string `json:"sourceRel,omitempty"`
	SourceSampleRate int    `json:"sourceSampleRate,omitempty"`
	SourceBits       int    `json:"sourceBits,omitempty"`
	TargetSampleRate int    `json:"targetSampleRate,omitempty"`
	TargetBits       int    `json:"targetBits,omitempty"`
	Quality          string `json:"quality,omitempty"`
	Kind             string `json:"kind,omitempty"`
	StartedAtUnixMs  int64  `json:"startedAtUnixMs,omitempty"`
}

ActiveJobView is the per-worker value snapshot ActiveWorkers returns — one entry per worker slot, Busy=false for idle workers. Plain value type safe to hand to the admin / SSE layer.

type BatchProgressEvent added in v0.1.3

type BatchProgressEvent struct {
	BatchID        string    `json:"batchID"`
	Path           string    `json:"path"`
	Status         string    `json:"status"`
	TargetRate     int       `json:"targetRate"`
	TargetBits     int       `json:"targetBits"`
	TotalFiles     int       `json:"totalFiles"`
	ProcessedFiles int       `json:"processedFiles"`
	FailedFiles    int       `json:"failedFiles"`
	Error          string    `json:"error,omitempty"`
	UpdatedAt      time.Time `json:"updatedAt"`
}

BatchProgressEvent is the wire shape published on the SSE `upscale.batch` topic. Admin Library Inspector + Jobs page render from these; the iOS-facing API does NOT consume them (iOS uses the per-track `upscale.complete` event from PR #B2 / v1.3).

`BatchID` is stringified UUID for JSON-decode friendliness; the raw 16-byte form lives in the SQLite BLOB column.

type Coordinator added in v0.1.3

type Coordinator struct {
	// contains filtered or unexported fields
}

Coordinator wraps a Pool with per-batch enrollment, live counters, throughput math, and SSE event emission.

func NewCoordinator added in v0.1.3

func NewCoordinator(
	pool *Pool,
	store *manifest.Store,
	dataDir string,
	publish CoordinatorPublishFunc,
	resolver ResolverFunc,
) (*Coordinator, error)

NewCoordinator constructs a Coordinator and runs the boot-time `RecoverInterruptedBatches` pass against `store`. The recovery pass MUST complete before the Coordinator accepts new Submit calls — repeated boots between which a batch was running would otherwise leave phantom in-flight rows.

`publish` is the SSE broker emitter; nil disables event emission (test harness) but counter writes still land in the DB.

`resolver` converts library-relative paths to absolute paths at JobSpec construction time. nil is permitted (test harness) but Submit then refuses with an explicit error rather than enqueueing broken JobSpecs.

func (*Coordinator) Cancel added in v0.1.3

func (c *Coordinator) Cancel(batchID uuid.UUID) error

Cancel transitions a batch to `cancelled` AND tries to drain the pool of its remaining jobs. The pool doesn't currently support targeted dequeue (jobs are FIFO; the next worker pulls whichever is at the head), so the practical effect is:

  • any worker that started a job from this batch finishes it (the variant lands on disk + in `track_variants`)
  • any job still on the pool's pending channel runs when picked up (variant also lands)
  • the batch row is marked `cancelled` so the admin Jobs page stops surfacing it as live

"Cancel" therefore means "stop tracking; stop counting" rather than "kill in-flight." A future enhancement can add per-batch dequeue if operators need a harder stop.

func (*Coordinator) OnJobComplete added in v0.1.3

func (c *Coordinator) OnJobComplete(path, variantID string, sampleRate, bitsPerSample int, durationSeconds float64, batchID uuid.UUID, completedAt time.Time)

OnJobComplete is the Pool-side callback invoked when a sox invocation succeeded AND `UpsertVariant` committed. Signature matches the pool's `SetOnJobComplete` parameter exactly.

`durationSeconds` is the wall-clock cost of the sox run (captured at worker startedAt → completedAt). Feeds the rolling throughput ring so ETAs reflect real bridge throughput; previously this path passed 0, leaving the ring empty even on a busy bridge (CodeRabbit high on PR #201).

Bumps `processed_files` on the owning batch (no-op when batchID is zero — legacy single-track jobs that didn't go through Submit don't have a batch to attribute to). Transitions the batch to `completed` when all expected jobs have reported back.

func (*Coordinator) OnJobFailed added in v0.1.3

func (c *Coordinator) OnJobFailed(path, variantID, errMsg string, durationSeconds float64, batchID uuid.UUID, failedAt time.Time)

OnJobFailed mirrors OnJobComplete for the failure side. Bumps `failed_files`; if every job in the batch has reported back and failed_files > 0, transitions the batch to `failed`.

func (*Coordinator) SetPublish added in v0.1.3

func (c *Coordinator) SetPublish(publish CoordinatorPublishFunc)

SetPublish wires (or rewires) the SSE broker emit closure. Construction-time callers may pass nil and call SetPublish later once the broker handle is in scope (the canonical wiring pattern in cmd/bridge/main.go — the Coordinator is built BEFORE the SSE broker so `RecoverInterruptedBatches` can run synchronously during startup). Concurrency-safe via the same coordinator mutex that guards the live-batch state.

func (*Coordinator) Submit added in v0.1.3

func (c *Coordinator) Submit(ctx context.Context, path string, targetRate, targetBits int, outputDir string) (*SubmitResult, error)

Submit walks every track under `path`, filters ineligible / already-covered, computes the projected variant size, refuses on insufficient disk headroom, inserts an `upscale_batches` row, and enqueues filtered jobs into the pool with their `BatchID` stamped. Returns ErrInsufficientDiskSpace (wrapped) if pre-flight refuses.

`targetRate` / `targetBits` come from the admin Settings (DB- backed via `Store.GetUpscaleTarget`); the caller is responsible for falling back to YAML bootstrap when scan_state is unseeded.

`outputDir` is `<dataDir>/transcoded` — same path the legacy per-track CLI / HTTP path uses. The Pool dedups on `(source, variant_id)` so a track that already has a variant at the same target is silently skipped (counted as `AlreadyCovered`).

func (*Coordinator) SubmitOptimize added in v0.1.4

func (c *Coordinator) SubmitOptimize(ctx context.Context, path string, outputDir string) (*SubmitResult, error)

SubmitOptimize is the CarPlay-targeted batch entry point. Pipeline: build candidates → disk-headroom preflight → insert batch row → enqueue jobs (with truncation handling). The pure-helpers split keeps cognitive complexity below the repo gate while preserving the structural pipeline upscale's `Submit` follows.

func (*Coordinator) Throughput added in v0.1.3

func (c *Coordinator) Throughput() ThroughputSnapshot

Throughput returns the rolling average jobs-per-hour + a forward projection for the currently-live batches' remaining files. Both numbers are zero when fewer than `throughputMinSamples` samples have landed.

type CoordinatorPublishFunc added in v0.1.3

type CoordinatorPublishFunc func(evt BatchProgressEvent)

CoordinatorPublishFunc is the broker-emit closure injected at NewCoordinator time. cmd/bridge/main.go wires this to `broker.Publish("upscale.batch", evt)` so internal/transcode doesn't import internal/api / internal/api/event_broker.

Nil-safe: a nil closure (test harness with no broker) is silently dropped at the call site. Production deployments always wire it.

type InsufficientDiskSpaceError added in v0.1.3

type InsufficientDiskSpaceError struct {
	ProjectedBytes int64
	RequiredBytes  int64 // projected × (1 + safetyMargin)
	AvailableBytes int64
	Dir            string
}

InsufficientDiskSpaceError carries the projection details for rendering operator-facing messages ("needs 8.2 GB, 4.1 GB free"). Returned alongside ErrInsufficientDiskSpace via errors.As so callers that don't care about the numbers can still match on the sentinel.

func (*InsufficientDiskSpaceError) Error added in v0.1.3

func (*InsufficientDiskSpaceError) Unwrap added in v0.1.3

func (e *InsufficientDiskSpaceError) Unwrap() error

type JobKind added in v0.1.4

type JobKind string

JobKind discriminates upscale (default) from optimize jobs across the full pool / coordinator / handler / CLI surface. Zero-value `JobKindUpscale` preserves legacy behavior for every existing JobSpec construction site that didn't yet learn about the field.

const (
	JobKindUpscale  JobKind = "upscale"
	JobKindOptimize JobKind = "optimize"
)

type JobSpec

type JobSpec struct {
	SourceAbsPath    string
	SourceLibraryRel string
	SourceMTimeNS    int64
	SourceSize       int64
	SourceSampleRate int // Hz; 0 if unknown (won't be used in target-rate selection)
	// SourceBits is the source bit depth (16/24/32), 0 if unknown.
	// Display-only — feeds the live worker grid's signal-chain string
	// ("44.1/16 ➔ 176.4/24"); never used in target-rate/bits selection.
	SourceBits       int
	TargetSampleRate int // Hz; e.g. 176400 / 192000
	TargetBits       int // 16/24/32
	Quality          Quality
	OutputDir        string // <dataDir>/transcoded

	// Kind discriminates the transcode class — upscale (default,
	// zero-value preserves legacy behavior) or optimize (CarPlay-
	// targeted downsample to 16/44.1 or 16/48). Drives the variant
	// ID prefix in `VariantID()` and the eligibility-gate branch in
	// `Coordinator.Submit`.
	Kind JobKind

	// BatchID groups this JobSpec into one operator-initiated batch
	// (v1.3 Coordinator). The zero-value uuid.UUID means the job was
	// submitted outside the batch path — legacy `POST /v1/upscale`
	// single-track requests and the `bridge upscale` CLI both leave
	// it zero. The Coordinator stamps it at Submit time; pool
	// callbacks (onJobComplete / onJobFailed) propagate it back so
	// the coordinator can attribute completion / failure to the
	// right `upscale_batches` row without a path-to-batch lookup.
	BatchID uuid.UUID
}

JobSpec describes one source-to-sidecar conversion. Constructed by the CLI / handler from a (Track, target params) pair; passed to RunSox.

`SourceAbsPath` is the absolute filesystem path to read from; `SourceLibraryRel` is the library-relative wire form that lands in the `track_variants.source_path` column (matches the manifest `Track.Path` field that iOS uses to construct download URLs). Keeping both fields here avoids a re-resolve at write time.

func (*JobSpec) FreshnessFromFile

func (j *JobSpec) FreshnessFromFile() error

FreshnessFromFile populates the SourceMTimeNS / SourceSize fields on a JobSpec by stat-ing SourceAbsPath. Used by the CLI to capture "what version of the source did we convert from" at the moment of conversion, which the variant-resolve path later uses to detect drift.

func (JobSpec) SidecarPath

func (j JobSpec) SidecarPath() string

SidecarPath returns the absolute filesystem path the converted FLAC will land at. Filename pattern (v1.4):

<OutputDir>/<libRel-dirname>/<libRel-basename>.<VariantID>.flac

Example: SourceLibraryRel `Diana Krall/The Look of Love/01 Love Letters.flac` + VariantID `upscaled-v2-176400-24` →

<OutputDir>/Diana Krall/The Look of Love/01 Love Letters.flac.upscaled-v2-176400-24.flac

**Why source-path-mirrored layout**: operators with write access to their library can `mv` the OutputDir contents directly into the library and slot variants alongside source files. The variantID suffix on the filename is load-bearing (multiple targets coexist: `01 Love Letters.flac.upscaled-v2-176400-24.flac` next to `01 Love Letters.flac.upscaled-v2-352800-24.flac`).

**Why keep the original extension** (`.flac`) in the basename: a folder with `Track1.flac` AND `Track1.wav` (legit A/B testing case) would otherwise both write to `Track1.upscaled-…flac` — zero collision tolerance for that exact case is structural.

**Migration**: existing hash-flat sidecars on disk stay served by their existing `track_variants.sidecar_path` rows (absolute paths; DB lookup is the only resolver — never filesystem-walked). Only newly-transcoded variants use the new layout. Mixed-layout state is fine; `bridge variants relayout` (separate CLI) opt-in renames legacy variants to match.

**Filesystem safety**: long basenames + Windows-illegal characters + reserved names route through `safeVariantFilename` which:

  • replaces `: * ? " < > |` with `_` (deterministic),
  • appends a `~<sha8>.` disambiguation segment derived from the RAW (pre-sanitization) basename bytes whenever sanitization touched the name OR the total would exceed 255 bytes (ext4 / NTFS / exFAT cap). The raw-bytes hash is load-bearing: two source files differing only in FAT-illegal characters (`Track:A.flac` + `Track*A.flac`) sanitize to identical bytes, and pre-fix the hash was computed over the sanitized form so it collapsed too — silently overwriting one variant with the other via `os.Rename`. The current shape keeps distinct raw inputs distinct on disk; DO NOT move the hash computation past `sanitiseForFAT` at any new call site.

Dirname segments are NOT sanitised — they came from the source filesystem which already accepts them.

func (JobSpec) SoxArgs

func (j JobSpec) SoxArgs() ([]string, string)

SoxArgs builds the argv for the sox invocation. Returns the exact slice exec.Command will receive (including the leading input/output sentinels) and a JSON-friendly settings string for `track_variants.sox_settings` (forensic record of what produced this sidecar). Pure function — no I/O — so unit tests can pin the exact command shape without invoking sox.

Quality preset → `rate` flag mapping:

QualityVeryHigh → "-v"  (very high; ~95 dB stopband, 32-bit internal)
QualityHigh     → "-h"
QualityMedium   → "-m"

`dither -s` is shaped TPDF dither when truncating the 32-bit internal float to the 24/32-bit integer FLAC output. Required for audible transparency at 24-bit and benign at 32-bit (the dither noise is below the LSB).

`-G` (gain-guard, leading global option) instructs sox to pre- scan the input for the headroom needed to prevent clipping through the rate-conversion + dither pipeline and apply a matching attenuation. Required for upscaling 0 dBFS-mastered material (most modern pop / loudness-war masters): without it, intersample peaks the rate filter reconstructs above 0 dBFS would clip on the integer FLAC output. The attenuation is computed from the actual peak — typically well under 1 dB and inaudible — and only fires when the source has the headroom problem. Cost is one extra peak-scan pass; invisible against the rate-conversion CPU dominating the run.

`rate … -L` pins LINEAR phase response on the resampler. This is already sox's default for the `rate` effect, so the pinned form is byte-identical to the prior unpinned `rate -v <Hz>` output — verified with deterministic `sox -R` runs (the signal-path md5 matches; only the always-present dither PRNG differs run-to-run). Pinning it is pure defense: a future sox release that changed the default phase could otherwise silently flip our cached variants to minimum/intermediate phase, and it lets the admin worker grid label the chain "linear phase" truthfully. Because the deterministic output is unchanged, NO VariantSchemaVersion bump / regeneration is required — existing sidecars stay valid.

func (JobSpec) VariantID

func (j JobSpec) VariantID() string

VariantID returns the opaque identifier that uniquely names this JobSpec's output variant. Convention:

upscaled-<schemaVersion>-<targetRate>-<targetBits>   // JobKindUpscale (default)
optimized-<schemaVersion>-<targetRate>-<targetBits>  // JobKindOptimize

e.g. `upscaled-v2-176400-24` or `optimized-v2-44100-16`. iOS keys on the prefix to slot the variant into the share-level "prefer upscaled" toggle vs. the runtime CarPlay-routing path. Future variant kinds (e.g. PCM→DSD synthesis) get their own prefix.

Hot path during manifest scan + every pool callback. The finite (rate × bits) cross-product across all real DACs makes memoization a clean O(1) win — two parallel caches keep the prefix discriminator strict at the SQL boundary (see the docblock on the prefix constants).

**Don't share `variantIDCache` between kinds** — pre-seeded with `upscaled-v2-*` for `(44100, 16)`, returning that for an optimize job would silently emit the wrong variant ID into `track_variants`.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool is the long-lived worker pool that hosts SoX conversions for the v1.2 PCM upscaling feature. Two consumers in production:

  1. `bridge serve`: instantiates one Pool at startup (only when `cfg.Upscale.Enabled == true` AND the sox-on-PATH probe passes), attaches it to the api.Server, and feeds it with `POST /v1/upscale` requests.
  2. `bridge upscale` CLI: instantiates its own per-invocation Pool with worker count from `--workers`. Same primitive, different lifetime.

The Pool's pending-job channel is bounded (`QueueCap`); enqueues are non-blocking via `select` + default → return ErrQueueFull instead of waiting. The HTTP handler maps that to a `503 queue_full` response so a user spamming "Generate" on a 50k- track library bounces against a clean rejection rather than exhausting memory.

**Dedup keyed on (source_path, variant_id)**: a duplicate enqueue while a job is already queued or running is a silent no-op. The hash set is mutex-guarded; reads happen on the HTTP handler goroutine, writes on the worker goroutines and the enqueue site. Same lock covers both — contention is bounded by the worker count.

func NewPool

func NewPool(store *manifest.Store, workers, queueCap int) *Pool

NewPool constructs a Pool sized at `workers` parallel sox processes with a `queueCap`-bounded pending-job channel. Starts `workers` goroutines immediately; they live until Stop is called. `store` is the SQLite-backed manifest store the completion path writes the row into.

Caller is expected to have already verified sox is on PATH via PrecheckSox — the worker doesn't repeat the probe per job.

func (*Pool) ActiveWorkers added in v0.1.7

func (p *Pool) ActiveWorkers() []ActiveJobView

ActiveWorkers returns one snapshot per worker slot (WorkerID 0..N-1), Busy=false for idle workers. Lock-free: each slot is an atomic.Pointer read; the *ActiveJob behind it is immutable. Order is stable (slot index), so the SSE worker grid renders deterministically.

func (*Pool) DropInflight added in v0.1.3

func (p *Pool) DropInflight(matches func(sourcePath string) bool) int

DropInflight removes entries from the dedup `inflight` map whose source_path satisfies the supplied predicate. Returns the count dropped.

Used by the variant-delete handler (DELETE /v1/upscale/variants) and the integrity watcher to make sure a re-submission of an upscale request for a deleted path doesn't no-op against the stale dedup slot — without dropping the entry, a follow-up Enqueue would silently coalesce against an in-flight worker that's about to write a sidecar the caller intends to delete.

Does NOT cancel in-flight workers — there is no per-job cancellation primitive (workers run under the pool's stopCtx only). The unlink race is the caller's problem; the caller removes the sidecar file BEFORE the worker can write it, and if a worker beats the unlink the next `--gc` reverse pass / the integrity watcher reaps the orphan within ≤1 h. Document the race shape at the call site, not here.

Lock discipline: `p.mu` held only for the iteration window. Predicate is called synchronously under the lock; predicate authors keep predicates allocation-light (no DB calls, no map lookups on shared state). Caller proceeds to any I/O / SSE publishes AFTER this method returns, not under p.mu.

Map key parsing: keys are `source_path + "|" + variant_id` per the Enqueue contract. Splits on the FIRST `|` via strings.IndexByte (no SplitN allocation per iteration) and passes only the source_path segment to the predicate. The `|` byte is reserved (paths never contain it in practice, and the manifest scanner rejects upserts with malformed paths at the boundary); a defensive missing-pipe entry is left untouched rather than logged — it's never legitimately present.

func (*Pool) Enqueue

func (p *Pool) Enqueue(spec JobSpec) error

Enqueue submits a JobSpec to the worker pool. Non-blocking: if the queue is full, returns ErrQueueFull immediately. Dedup is silent — a duplicate (same source_path + variant_id) returns nil without taking a slot. After Stop, returns ErrPoolClosed.

**Race-safe vs Stop**: pre-fix, Stop() did `close(p.optimizeJobs) / close(p.upscaleJobs)` concurrently with an Enqueue holding `inflight[dedup]` but not yet at the channel send — sending on a closed channel panics (Gemini high + Qodo bug 1 + CodeRabbit echo on PR #109). Fix: the channel-send branch runs INSIDE `p.mu` and Stop() takes the same mutex before close. The mutex serialises the two operations cheaply (the send is non-blocking via the select + default, so the lock window stays in microseconds), and the re-checked `closed` flag inside the lock catches a Stop that won the race for the mutex but hadn't yet closed the channel.

func (*Pool) SetOnJobComplete added in v0.1.3

func (p *Pool) SetOnJobComplete(fn func(path, variantID string, sampleRate, bitsPerSample int, durationSeconds float64, batchID uuid.UUID, completedAt time.Time))

SetOnJobComplete wires (or rewires) the per-job completion callback (see Pool.onJobComplete docstring for ordering invariants). nil disables notification. Set-once at cmd/bridge wiring time; race- safe vs concurrent calls.

PR 3 (v1.3) extended the signature with `batchID`. Existing consumers in cmd/bridge/main.go pass it through to the Coordinator's job-complete handler; pre-v1.3 callers that don't care about batch attribution can ignore the new parameter.

func (*Pool) SetOnJobFailed added in v0.1.3

func (p *Pool) SetOnJobFailed(fn func(path, variantID, errMsg string, durationSeconds float64, batchID uuid.UUID, failedAt time.Time))

SetOnJobFailed wires the per-job failure callback. nil disables. See Pool.onJobFailed docstring for threading invariants. Set-once at cmd/bridge wiring time alongside SetOnJobComplete.

func (*Pool) SetOnStateChange

func (p *Pool) SetOnStateChange(fn func())

SetOnStateChange wires (or rewires) the callback fired after every observable state transition. nil disables notification (back-compat for tests / non-broker deployments). Called once during cmd/bridge wiring after the broker is up; subsequent calls are race-safe but in practice this is set-once.

func (*Pool) Stats

func (p *Pool) Stats() PoolStats

Stats returns the current snapshot. Safe to call concurrently with Enqueue / worker activity.

**QueueLen AND QueueCap are both COMBINED across the two priority channels** so the ratio `QueueLen / QueueCap` stays bounded by 1.0 for monitoring tools that compute a fill-percentage. Pre-fix the snapshot reported a per-channel QueueCap alongside a combined QueueLen, which produced ratios >100% whenever both channels were partially full — caught by Gemini medium on PR #281.

**Per-channel back-pressure is still enforced** at Enqueue time: each channel independently has `p.queueCap` slots, and each independently triggers `ErrQueueFull` when full. The doubling here is presentational — it gives consumers a single combined capacity number to divide against the combined depth.

If a future surface needs per-channel split (e.g. an admin "optimize queue depth" indicator), extend PoolStats with sibling fields rather than retargeting either of the existing combined values — back-compat consumers depend on QueueLen + QueueCap staying ratio-coherent.

func (*Pool) Stop

func (p *Pool) Stop()

Stop signals the workers to drain the queue and exit. Blocks until every in-flight conversion completes (either through success, failure, or sox-process kill on the cancelled context) AND the publisher has drained its remaining buffered events. The Pool can't be reused after Stop.

Idempotent: calling Stop twice is safe (the underlying CancelFunc is itself idempotent).

**Shutdown ordering is load-bearing** — the sequence below prevents BOTH "send on closed channel" panics AND publisher deadlocks:

  1. close(p.optimizeJobs) + close(p.upscaleJobs) under p.mu so any in-flight Enqueue completes its channel send (or its dedup- rollback) before either close() lands. See Enqueue's docstring for the full race trace. Both channels share the same p.mu so ordering between them inside the lock is irrelevant.
  2. stopCancel() so any sox subprocess waiting on the per-job context dies promptly.
  3. p.wg.Wait() — block until every worker goroutine has returned. After this point, no worker can send to stateChangeChan or jobCompleteChan.
  4. close() both publisher channels — safe ONLY because step 3 guaranteed no more sends are possible.
  5. p.publisherWG.Wait() — let the publisher drain its remaining buffered events (especially `upscale.complete` events the iOS path-promotion path depends on), observe both channels closed, and return.

The publisher does NOT exit on stopCtx.Done() — workers blocking- send to jobCompleteChan, and an early publisher exit would deadlock the worker on a buffer-full send.

type PoolStats

type PoolStats struct {
	Workers  int
	QueueCap int
	QueueLen int
	Inflight int
	Enqueued uint64
	Done     uint64
	Failed   uint64
}

Stats returns a snapshot of pool counters for /v1/health observability or tests. Numbers are monotonic per process lifetime; restarting bridge serve resets them.

type Quality

type Quality string

Quality presets map to SoX `rate` flag combinations. We keep the mapping internal so a future `-q` knob on the CLI doesn't bake flag strings into the user-facing surface — operators see "very- high" / "high" / "medium" labels instead.

const (
	QualityVeryHigh Quality = "very-high" // rate -v dither -s — production default
	QualityHigh     Quality = "high"      // rate -h dither -s — for slower hosts
	QualityMedium   Quality = "medium"    // rate -m dither -s — sanity / ad-hoc
)

type ResolverFunc added in v0.1.3

type ResolverFunc func(libraryRel string) (absPath string, err error)

ResolverFunc converts a library-relative path (e.g. `Music/Album/01.flac`) to its absolute filesystem path. Required at JobSpec construction time — `RunSox` consumes the absolute path directly and fails fast on empty input. Wired in cmd/bridge/main.go via a closure around `apiSrv.Resolver().Resolve(...)` so this package stays free of internal/fs.

Returns the absolute path on success. On failure (unknown root, path traversal, IO error) the closure returns an error; Submit silently filters those tracks out of the batch and surfaces the rest, so a single bad path doesn't abort an otherwise-valid folder submission. The filtered count is reflected in the returned `TotalFiles` vs the input track count — operators can reconcile from logs if they care which tracks dropped.

type SoxInfo added in v0.1.7

type SoxInfo struct {
	Path         string
	Version      string   // e.g. "v14.4.2"; "" if the banner couldn't be parsed
	Formats      []string // lowercased AUDIO FILE FORMATS tokens
	FormatsKnown bool     // true iff the format block was found+parsed
	HasFLAC      bool     // Formats contains "flac"
}

SoxInfo is the result of ProbeSox: where sox lives, its version, and — crucially for the bridge's pipeline — whether the build has FLAC support. The bridge forces `-t flac` for every conversion (see SoxArgs), so a FLAC-less sox passes the bare runnable check but fails EVERY job at runtime. FormatsKnown lets callers stay conservative: act on a confirmed FLAC-absence only, never on an unparseable help output.

func ProbeSox added in v0.1.7

func ProbeSox(ctx context.Context) (SoxInfo, error)

ProbeSox locates sox on PATH and inspects it with a SINGLE `sox --help` spawn — the help output carries both the version banner and the "AUDIO FILE FORMATS:" block, so one process call yields everything. The error contract matches the old PrecheckSox exactly (ErrSoxMissing when the binary is absent; a wrapped error on timeout / can't-start) so all existing callers' errors.Is checks keep working.

**Bounded by a 2 s timeout** (PR #108) wrapped around the INCOMING ctx: a parent cancellation (CLI ^C, server shutdown) aborts the spawn early, while the 2 s cap still applies when called via PrecheckSox(Background()) so a wedged PATH wrapper / hung sox can't deadlock startup.

**Locale-pinned** (LC_ALL/LANG/LANGUAGE=C) so a translated help text can't defeat the header match; CombinedOutput because some builds print --help to stderr. A non-zero --help exit is NOT treated as failure (sox --help legitimately exits non-zero on some builds) — only an empty result or a timeout is.

type SubmitResult added in v0.1.3

type SubmitResult struct {
	BatchID            uuid.UUID
	Path               string
	TargetRate         int
	TargetBits         int
	TotalFiles         int
	AlreadyCovered     int
	ProjectedSizeBytes int64
	AvailableBytes     int64
	EnqueuedCount      int
}

SubmitResult is returned by Submit on success. The caller (the HTTP handler) marshals it into the 202 Accepted body.

type ThroughputSnapshot added in v0.1.3

type ThroughputSnapshot struct {
	JobsPerHour float64 `json:"jobsPerHour"`
	EtaSeconds  float64 `json:"etaSeconds"`
	Samples     int     `json:"samples"`
}

ThroughputSnapshot carries the rolling-average derived values the admin dashboard renders. Returned by Throughput().

`JobsPerHour` is the rate computed from the most recent `throughputWindowSize` completions; `EtaSeconds` is a forward projection for the currently-running batches' remaining files at that rate (zero when no batch is active or there's insufficient sample data).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL