mixedkey

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 5 Imported by: 0

README

mixedkey

Go Reference

Pure Go musical-key detection from mono PCM. Uses only the Go standard library — no cgo, no external DSP libraries, no bundled native binaries.

mixedkey is the harmonic-mixing companion to a tempo estimator such as tactus: tactus tells you the BPM, mixedkey tells you the key. Both take the same mono-PCM-in contract and hand you back one musical number.

Scope

mixedkey does exactly one thing: mono PCM → musical key. It does not decode audio files. You decode with whatever you already use, mix to mono, and hand mixedkey the samples.

This separation keeps the library dependency-free and trivial to test: the entire contract is a slice of float64 in, a Key out.

Install

go get github.com/cabbagekobe/mixedkey

Requires Go 1.24 or later.

API

// Detect returns the single best key using the default Aarden–Essen profile.
func Detect(mono []float64, sampleRate int) (Key, error)
func DetectWith(mono []float64, sampleRate int, opt Options) (Key, error)

// DetectCandidates returns [primary, relative, dominant, subdominant] —
// the primary estimate and its most common confusions, each scored.
// DetectAllCandidates returns all 24 keys ranked by Support.
func DetectCandidates(mono []float64, sampleRate int) ([]Candidate, error)
func DetectCandidatesWith(mono []float64, sampleRate int, opt Options) ([]Candidate, error)
func DetectAllCandidates(mono []float64, sampleRate int) ([]Candidate, error)
func DetectAllCandidatesWith(mono []float64, sampleRate int, opt Options) ([]Candidate, error)

// Key is a tonic pitch class plus a mode. Its String form is Camelot.
type Key struct {
    Tonic PitchClass // 0 = C, 1 = C#/Db, …, 11 = B
    Mode  Mode       // Major | Minor
}
func (k Key) Camelot() string // "8A" (A minor), "8B" (C major)
func (k Key) String() string  // = Camelot()

type Candidate struct {
    Key        Key
    Support    float64 // correlation relative to the primary (primary = 1.0)
    Confidence float64 // absolute chroma-to-profile correlation in [0, 1]
}

// Options selects the correlation profile. The zero value uses
// ProfileAardenEssen (also: ProfileKrumhansl, ProfileTemperley,
// ProfileBellmanBudge).
type Options struct {
    Profile Profile
}

// ErrTooShort: input too short to analyse. ErrNoKey: long enough but no
// detectable key (a flat or silent chroma).
var ErrTooShort = errors.New("mixedkey: audio too short to detect")
var ErrNoKey = errors.New("mixedkey: no detectable key")
  • Input samples are expected in the range [-1, 1].
  • Stereo sources must be mixed to mono by the caller ((L+R)/2).
  • The result's canonical string form is Camelot notation, for DJ harmonic mixing.

Example

package main

import (
    "fmt"

    "github.com/cabbagekobe/mixedkey"
)

func main() {
    // mono holds decoded PCM in [-1, 1]; sampleRate is its rate in Hz.
    var mono []float64
    sampleRate := 44100

    key, err := mixedkey.Detect(mono, sampleRate)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Key: %s\n", key.Camelot()) // e.g. "8A"

    // The relative major/minor is the classic confusion. Surface the
    // alternates and let the user confirm rather than trusting one answer.
    cands, _ := mixedkey.DetectCandidates(mono, sampleRate)
    fmt.Printf("primary %s (conf %.2f), relative %s\n",
        cands[0].Key.Camelot(), cands[0].Confidence, cands[1].Key.Camelot())
}

Algorithm

Built from scratch with the standard library (including a self-contained radix-2 FFT):

  1. Chromagram — resample to a fixed internal rate, take a Hann-windowed short-time Fourier transform, and sum each magnitude bin in the musical band (~C2–C7) into one of twelve pitch classes. Each frame is L1-normalised (so loud sections don't dominate) and the mapping is re-centred by the track's global tuning offset (so a master tuned off A=440 doesn't smear across pitch classes). Summed over every frame, this yields a 12-bin chroma vector: how much energy each pitch class carries across the track.
  2. Profile correlation — correlate the chroma against each of the 24 major and minor key profiles (the Aarden–Essen corpus weights by default; Krumhansl, Temperley, and Bellman–Budge are selectable via Options), rotated to each tonic. Pearson's r scores how well each key's expected pitch-class distribution matches the audio.
  3. Pick the best — the key with the highest correlation wins; that correlation doubles as an absolute Confidence in [0, 1], and the alternates are ranked by their correlation relative to the winner's.

The output is Camelot wheel notation (8A, 8B, …), where keys with the same number, adjacent numbers, or the same number with the other letter are harmonically compatible.

License

MIT. See LICENSE. The algorithm is an independent, clean-room implementation built from standard methods (chromagram extraction, Krumhansl–Schmuckler profile correlation); no GPL code is included. The Krumhansl–Kessler profile values are published experimental data.

Documentation

Overview

Package mixedkey detects the musical key of audio from mono PCM using only the Go standard library — no cgo, no external DSP dependencies.

Scope

mixedkey does one thing: it maps a mono PCM signal to a musical key. It deliberately does not decode audio files. Callers are expected to decode their container of choice, mix to mono ((L+R)/2 for stereo), normalise samples to the range [-1, 1], and pass the resulting slice together with the sample rate.

The result is a Key — a tonic pitch class and a mode — whose canonical string form is Camelot wheel notation (e.g. "8A" for A minor, "8B" for C major), the notation DJs use for harmonic mixing. mixedkey is the key-detection companion to a tempo estimator such as tactus (github.com/cabbagekobe/tactus): the same mono-PCM-in contract, a different musical attribute out.

Entry points

All return ErrTooShort when the input is too short to analyse, and ErrNoKey when it is long enough but carries no detectable key (a flat or silent chroma). Each Candidate reports a Confidence in [0, 1] a caller can gate on.

Algorithm

The detector builds a 12-bin chroma vector (pitch-class profile) from the audio: a short-time Fourier transform (via a self-contained radix-2 FFT) whose magnitude bins, over a musical frequency band, are summed into the twelve pitch classes across every frame. Each frame is L1-normalised first, so loud sections do not dominate, and the mapping is re-centred by the track's average deviation from equal temperament, so a master tuned off A=440 does not smear into neighbouring pitch classes. The chroma reflects which pitch classes carry the most energy in the track.

That vector is correlated against each of the 24 major and minor key profiles — the Aarden–Essen corpus weights by default, rotated to each tonic — and the key with the highest Pearson correlation wins. Other published profiles (Krumhansl, Temperley, Bellman–Budge) are selectable via Options. The correlation doubles as an absolute Confidence, and the alternates are ranked by their correlation relative to the winner's.

Concurrency

All functions are pure and re-entrant: they read only their arguments and allocate their own scratch buffers, so independent calls are safe to run concurrently.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoKey = errors.New("mixedkey: no detectable key")

ErrNoKey is returned when the input is long enough but carries no detectable key: a flat or silent chroma with no pitch class standing out, so no profile correlates with it. Callers may treat this differently from ErrTooShort (e.g. flag an atonal or percussion-only track rather than skipping it).

View Source
var ErrTooShort = errors.New("mixedkey: audio too short to detect")

ErrTooShort is returned when the input has too few samples to yield a single analysis frame — its chroma vector cannot be built.

Functions

This section is empty.

Types

type Candidate

type Candidate struct {
	Key        Key
	Support    float64
	Confidence float64
}

Candidate is one key hypothesis.

Support is this candidate's correlation relative to the primary estimate (whose Support is 1.0) — useful for ranking alternates against each other. Confidence is the absolute fit of this key: the chroma-to-profile correlation clamped to [0, 1]. A caller can gate on the primary's Confidence to decide whether to trust the detection or flag the track for review.

func DetectAllCandidates

func DetectAllCandidates(mono []float64, sampleRate int) ([]Candidate, error)

DetectAllCandidates is like DetectCandidates but returns every key the detector considered. See DetectAllCandidatesWith.

func DetectAllCandidatesWith

func DetectAllCandidatesWith(mono []float64, sampleRate int, opt Options) ([]Candidate, error)

DetectAllCandidatesWith returns all 24 keys (12 tonics × major and minor) ordered by descending Support, with the primary estimate first. Unlike DetectCandidatesWith (which curates the common confusions), this exposes the full ranking for a richer "pick the key" UI. Support is relative to the primary and Confidence is absolute; see Candidate.

func DetectCandidates

func DetectCandidates(mono []float64, sampleRate int) ([]Candidate, error)

DetectCandidates is like Detect but also returns the key's principal alternates. See DetectCandidatesWith.

func DetectCandidatesWith

func DetectCandidatesWith(mono []float64, sampleRate int, opt Options) ([]Candidate, error)

DetectCandidatesWith returns the primary key estimate followed by its three most common confusions, ordered [primary, relative, dominant, subdominant]. The relative key shares the primary's pitch-class content (the classic major/minor ambiguity); the dominant and subdominant differ from it by a single note. Each carries its Support relative to the primary and its absolute Confidence, so a caller can offer a "did it pick the right one?" control rather than trusting a single answer where the tonal centre is ambiguous.

type Key

type Key struct {
	Tonic PitchClass
	Mode  Mode
}

Key is a musical key: a tonic pitch class together with a mode. Its canonical string form is Camelot notation (see Camelot and String), the wheel used for DJ harmonic mixing.

func Detect

func Detect(mono []float64, sampleRate int) (Key, error)

Detect detects the musical key of mono PCM (samples in the range [-1, 1]) sampled at sampleRate Hz, using the default Aarden–Essen profile. Stereo sources must be mixed to mono by the caller ((L+R)/2). It returns ErrTooShort when the input is too short to analyse and ErrNoKey when it is long enough but carries no detectable key.

func DetectWith

func DetectWith(mono []float64, sampleRate int, opt Options) (Key, error)

DetectWith is like Detect but uses the profile selected by opt (see Options). It builds a 12-bin chroma vector from the audio and picks the key whose pitch-class profile correlates best with it.

func (Key) Camelot

func (k Key) Camelot() string

Camelot returns the key in Camelot wheel notation: a number 1–12 and a letter, "A" for minor keys and "B" for major keys (e.g. "8A" for A minor, "8B" for C major). Keys with the same number and adjacent numbers are harmonically compatible, which is what the notation is for.

func (Key) String

func (k Key) String() string

String returns the key's Camelot notation (see Camelot).

type Mode

type Mode int

Mode is the mode of a musical key: major or minor.

const (
	// Major is the major mode (Ionian).
	Major Mode = iota
	// Minor is the natural minor mode (Aeolian).
	Minor
)

The two supported modes.

type Options

type Options struct {
	Profile Profile
}

Options selects the pitch-class profile used for correlation. The zero value uses ProfileAardenEssen, which gave the best harmonically- compatible agreement on a real multi-key corpus; ProfileBellmanBudge and ProfileTemperley edge it out on exact match. Profile is a single global setting — like choosing a detector, not a per-track hint.

type PitchClass

type PitchClass int

PitchClass is a pitch class in [0, 12) with C = 0, C#/Db = 1, …, B = 11.

type Profile

type Profile int

Profile selects the pair of pitch-class templates (one major, one minor) that a chroma vector is correlated against to pick a key. It is a single global setting on Options; the zero value is ProfileKrumhansl.

const (
	// ProfileAardenEssen uses the Aarden–Essen profiles, fitted to the
	// large Essen folk-song corpus. It is the default (the zero value)
	// because on a 273-track multi-key rekordbox-labelled corpus it gave
	// the best harmonically-compatible agreement (85%: exact, relative, or
	// an adjacent Camelot key) — its errors tend to be a neighbouring key
	// that still mixes, which suits harmonic mixing. For maximum exact
	// agreement instead, ProfileBellmanBudge and ProfileTemperley score a
	// few points higher on exact match.
	ProfileAardenEssen Profile = iota

	// ProfileKrumhansl uses the Krumhansl–Kessler key profiles derived
	// from the classic probe-tone experiments. Well-documented, but the
	// weakest of the four on recorded popular music (~49% exact).
	ProfileKrumhansl

	// ProfileTemperley uses the Temperley–Kostka–Payne profiles, fitted to
	// a corpus of common-practice music. Strong on exact match (~62%).
	ProfileTemperley

	// ProfileBellmanBudge uses the Bellman–Budge profiles, fitted to a
	// corpus of common-practice harmony. Best on exact match (~64%) on the
	// reference corpus.
	ProfileBellmanBudge
)

Jump to

Keyboard shortcuts

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