Skip to content
DocumentationAPI Reference

Audio Analysis

Analyze tracks with audio features, tempo, key, and detailed segment data

Spotify provides detailed audio analysis for every track in its catalog. This includes high-level audio features like energy and danceability, as well as low-level audio analysis with beat, bar, and section information.

Audio Features

High-level descriptors like energy, danceability, and valence. Quick to fetch, great for filtering and recommendations.

Audio Analysis

Detailed low-level analysis including beats, bars, sections, segments, and tatums. Useful for visualizations and DJ apps.


Audio features are high-level attributes that describe a track’s musical characteristics on a scale of 0.0 to 1.0 (except for tempo, key, loudness, and duration).

FeatureRangeDescription
acousticness0.0–1.0Confidence the track is acoustic
danceability0.0–1.0How suitable for dancing based on tempo, rhythm stability, beat strength
energy0.0–1.0Perceptual measure of intensity and activity
instrumentalness0.0–1.0Predicts whether a track contains no vocals
liveness0.0–1.0Presence of an audience in the recording
speechiness0.0–1.0Presence of spoken words
valence0.0–1.0Musical positiveness (happy vs sad)
tempoBPMEstimated tempo in beats per minute
key0–11Pitch class (0=C, 1=C#, 2=D, etc.)
mode0 or 1Modality (0=minor, 1=major)
loudnessdBOverall loudness in decibels
time_signature3–7Estimated time signature
duration_msmsDuration in milliseconds
import Spotted from "spotted-ts";
const client = new Spotted();
const features = await client.audioFeatures.retrieve("11dFghVXANMlKmJXsNCbNl");
console.log(`
Danceability: ${(features.danceability * 100).toFixed(0)}%
Energy: ${(features.energy * 100).toFixed(0)}%
Valence: ${(features.valence * 100).toFixed(0)}%
Tempo: ${features.tempo.toFixed(0)} BPM
Key: ${getKeyName(features.key, features.mode)}
`);
function getKeyName(key, mode) {
const keys = [
"C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B",
];
const modeName = mode === 1 ? "major" : "minor";
return `${keys[key]} ${modeName}`;
}
import Spotted from "spotted-ts";
const client = new Spotted();
const trackIds = [
"11dFghVXANMlKmJXsNCbNl",
"4iV5W9uYEdYUVa79Axb7Rh",
"1301WleyT98MSxVHPZCA6M",
];
// Max 100 tracks per request
const { audio_features } = await client.audioFeatures.bulkRetrieve({
ids: trackIds.join(","),
});
audio_features.forEach((features) => {
console.log(
`${features.id}: Energy=${features.energy}, Tempo=${features.tempo}`,
);
});

Audio analysis provides detailed structural information about a track, including its sections, beats, bars, and segments.

import Spotted from "spotted-ts";
const client = new Spotted();
const analysis = await client.audioAnalysis.retrieve("11dFghVXANMlKmJXsNCbNl");
console.log(`
Track duration: ${analysis.track.duration}s
Sections: ${analysis.sections.length}
Beats: ${analysis.beats.length}
Bars: ${analysis.bars.length}
Segments: ${analysis.segments.length}
Tatums: ${analysis.tatums.length}
`);

Sections are large divisions of a song (verse, chorus, bridge).

analysis.sections.forEach((section, i) => {
console.log(`
Section ${i + 1}:
Start: ${section.start.toFixed(2)}s
Duration: ${section.duration.toFixed(2)}s
Tempo: ${section.tempo.toFixed(0)} BPM
Key: ${section.key}
Mode: ${section.mode === 1 ? "major" : "minor"}
Loudness: ${section.loudness.toFixed(1)} dB
`);
});

import Spotted from "spotted-ts";
const client = new Spotted();
async function analyzePlaylistMood(playlistId) {
// Get playlist tracks using async iterator
const trackIds = [];
for await (const item of client.playlists.tracks.list(playlistId)) {
if (item.track) {
trackIds.push(item.track.id);
}
}
// Get audio features for all tracks (in batches of 100)
const features = [];
for (let i = 0; i < trackIds.length; i += 100) {
const batch = trackIds.slice(i, i + 100);
const { audio_features } = await client.audioFeatures.bulkRetrieve({
ids: batch.join(","),
});
features.push(...audio_features.filter(Boolean));
}
// Calculate averages
const avg = (arr, key) =>
arr.reduce((sum, f) => sum + f[key], 0) / arr.length;
const analysis = {
trackCount: features.length,
averages: {
danceability: avg(features, "danceability"),
energy: avg(features, "energy"),
valence: avg(features, "valence"),
acousticness: avg(features, "acousticness"),
instrumentalness: avg(features, "instrumentalness"),
tempo: avg(features, "tempo"),
},
};
// Determine mood
const { valence, energy } = analysis.averages;
if (valence > 0.6 && energy > 0.6) {
analysis.mood = "Happy/Energetic";
} else if (valence > 0.6 && energy <= 0.6) {
analysis.mood = "Happy/Chill";
} else if (valence <= 0.4 && energy > 0.6) {
analysis.mood = "Angry/Intense";
} else if (valence <= 0.4 && energy <= 0.4) {
analysis.mood = "Sad/Melancholic";
} else {
analysis.mood = "Neutral";
}
return analysis;
}
const playlistAnalysis = await analyzePlaylistMood("37i9dQZF1DXcBWIGoYBM5M");
console.log(`
Playlist Mood: ${playlistAnalysis.mood}
Average Energy: ${(playlistAnalysis.averages.energy * 100).toFixed(0)}%
Average Valence: ${(playlistAnalysis.averages.valence * 100).toFixed(0)}%
Average Tempo: ${playlistAnalysis.averages.tempo.toFixed(0)} BPM
`);
import Spotted from "spotted-ts";
const client = new Spotted();
async function findTracksByBPM(trackIds, minBPM, maxBPM) {
const features = [];
for (let i = 0; i < trackIds.length; i += 100) {
const batch = trackIds.slice(i, i + 100);
const { audio_features } = await client.audioFeatures.bulkRetrieve({
ids: batch.join(","),
});
features.push(...audio_features.filter(Boolean));
}
return features.filter((f) => f.tempo >= minBPM && f.tempo <= maxBPM);
}
// Find tracks between 120-130 BPM (good for running)
const runningTracks = await findTracksByBPM(savedTrackIds, 120, 130);
import Spotted from "spotted-ts";
const client = new Spotted();
async function sortPlaylistByEnergy(playlistId, ascending = true) {
// Get playlist tracks using async iterator
const tracks = [];
for await (const item of client.playlists.tracks.list(playlistId)) {
if (item.track) {
tracks.push(item.track);
}
}
const trackIds = tracks.map((t) => t.id);
// Get audio features (in batches of 100)
const allFeatures = [];
for (let i = 0; i < trackIds.length; i += 100) {
const batch = trackIds.slice(i, i + 100);
const { audio_features } = await client.audioFeatures.bulkRetrieve({
ids: batch.join(","),
});
allFeatures.push(...audio_features);
}
// Combine and sort
const combined = tracks.map((track, i) => ({
track,
features: allFeatures[i],
}));
combined.sort((a, b) => {
const energyA = a.features?.energy || 0;
const energyB = b.features?.energy || 0;
return ascending ? energyA - energyB : energyB - energyA;
});
return combined;
}
// Sort from low to high energy (good for winding down)
const sorted = await sortPlaylistByEnergy("playlist123", true);
import Spotted from "spotted-ts";
const client = new Spotted({ bearerToken: userAccessToken });
async function createEnergyCurvePlaylist(sourcePlaylistId, userId) {
// Get and analyze source tracks
const sorted = await sortPlaylistByEnergy(sourcePlaylistId, true);
// Create energy curve: start medium, build up, peak, come down
const trackCount = sorted.length;
const curve = [];
// Divide tracks into energy buckets
const low = sorted.slice(0, Math.floor(trackCount * 0.33));
const medium = sorted.slice(
Math.floor(trackCount * 0.33),
Math.floor(trackCount * 0.66),
);
const high = sorted.slice(Math.floor(trackCount * 0.66));
// Build curve: medium -> high -> peak -> medium -> low
const sections = [
medium.slice(0, 3),
high.slice(0, 4),
high.slice(-3).reverse(),
medium.slice(-3),
low.slice(-3),
];
sections.forEach((section) => curve.push(...section));
// Create new playlist
const playlist = await client.users.playlists.create(userId, {
name: "Energy Curve Mix",
description: "Tracks arranged in an energy arc",
});
// Add tracks
const uris = curve.map((item) => item.track.uri);
await client.playlists.tracks.add(playlist.id, { uris });
return playlist;
}

import Spotted from "spotted-ts";
const client = new Spotted();
async function analyzeArtistSound(artistId) {
// Get artist's top tracks
const { tracks } = await client.artists.getTopTracks(artistId, {
market: "US",
});
const trackIds = tracks.map((t) => t.id);
// Get audio features
const { audio_features } = await client.audioFeatures.bulkRetrieve({
ids: trackIds.join(","),
});
// Calculate artist's signature sound
const avg = (arr, key) =>
arr.filter(Boolean).reduce((sum, f) => sum + f[key], 0) / arr.length;
const signature = {
energy: avg(audio_features, "energy"),
danceability: avg(audio_features, "danceability"),
valence: avg(audio_features, "valence"),
acousticness: avg(audio_features, "acousticness"),
instrumentalness: avg(audio_features, "instrumentalness"),
tempo: avg(audio_features, "tempo"),
};
// Find most common key
const keyCounts = {};
audio_features.filter(Boolean).forEach((f) => {
const keyStr = `${f.key}-${f.mode}`;
keyCounts[keyStr] = (keyCounts[keyStr] || 0) + 1;
});
const mostCommonKey = Object.entries(keyCounts).sort(
(a, b) => b[1] - a[1],
)[0];
return {
signature,
mostCommonKey: mostCommonKey
? getKeyName(
parseInt(mostCommonKey[0].split("-")[0]),
parseInt(mostCommonKey[0].split("-")[1]),
)
: "Unknown",
topTracks: tracks.map((t) => t.name),
};
}
const artistAnalysis = await analyzeArtistSound("06HL4z0CvFAxyc27GXpf02");
console.log(`Artist Sound Signature:`, artistAnalysis.signature);
async function compareArtists(artistId1, artistId2) {
const [artist1, artist2] = await Promise.all([
analyzeArtistSound(artistId1),
analyzeArtistSound(artistId2),
]);
const features = [
"energy",
"danceability",
"valence",
"acousticness",
"tempo",
];
const comparison = {};
features.forEach((feature) => {
const diff = artist1.signature[feature] - artist2.signature[feature];
comparison[feature] = {
artist1: artist1.signature[feature],
artist2: artist2.signature[feature],
difference: Math.abs(diff),
winner: diff > 0 ? "Artist 1" : "Artist 2",
};
});
// Calculate similarity score (0-100)
const similarity =
100 -
features.reduce((sum, f) => {
const maxDiff = f === "tempo" ? 200 : 1; // Normalize tempo
return sum + (Math.abs(comparison[f].difference) / maxDiff) * 20;
}, 0);
return {
comparison,
similarity: Math.max(0, similarity),
};
}
import Spotted from "spotted-ts";
const client = new Spotted();
async function analyzeAlbumJourney(albumId) {
// Get album tracks using async iterator
const tracks = [];
for await (const track of client.albums.listTracks(albumId)) {
tracks.push(track);
}
const trackIds = tracks.map((t) => t.id);
// Get audio features
const { audio_features } = await client.audioFeatures.bulkRetrieve({
ids: trackIds.join(","),
});
// Analyze journey
const journey = tracks.map((track, i) => ({
position: i + 1,
name: track.name,
energy: audio_features[i]?.energy || 0,
valence: audio_features[i]?.valence || 0,
tempo: audio_features[i]?.tempo || 0,
}));
// Find peaks and valleys
const energies = journey.map((t) => t.energy);
const peakIndex = energies.indexOf(Math.max(...energies));
const valleyIndex = energies.indexOf(Math.min(...energies));
// Calculate energy arc
const firstHalf = energies.slice(0, Math.ceil(energies.length / 2));
const secondHalf = energies.slice(Math.ceil(energies.length / 2));
const firstHalfAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
const secondHalfAvg =
secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
let arc;
if (secondHalfAvg > firstHalfAvg + 0.1) {
arc = "Building (energy increases throughout)";
} else if (firstHalfAvg > secondHalfAvg + 0.1) {
arc = "Fading (energy decreases throughout)";
} else if (peakIndex > 2 && peakIndex < energies.length - 2) {
arc = "Peak in middle (classic arc)";
} else {
arc = "Consistent energy";
}
return {
journey,
peakTrack: journey[peakIndex],
calmestTrack: journey[valleyIndex],
energyArc: arc,
};
}
const albumJourney = await analyzeAlbumJourney("4LH4d3cOWNNsVw41Gqt2kv");
console.log(`Album energy arc: ${albumJourney.energyArc}`);
console.log(`Peak: ${albumJourney.peakTrack.name}`);

import Spotted from "spotted-ts";
class AudioVisualizer {
constructor(client) {
this.client = client;
this.currentAnalysis = null;
this.currentTrackId = null;
}
async loadTrack(trackId) {
if (trackId === this.currentTrackId) return this.currentAnalysis;
const [features, analysis] = await Promise.all([
this.client.audioFeatures.retrieve(trackId),
this.client.audioAnalysis.retrieve(trackId),
]);
this.currentTrackId = trackId;
this.currentAnalysis = {
features,
analysis,
duration: analysis.track.duration,
};
return this.currentAnalysis;
}
getDataAtTime(timeSeconds) {
if (!this.currentAnalysis) return null;
const { analysis } = this.currentAnalysis;
// Find current section
const section = analysis.sections.find(
(s) => timeSeconds >= s.start && timeSeconds < s.start + s.duration,
);
// Find current beat
const beat = analysis.beats.find(
(b) => timeSeconds >= b.start && timeSeconds < b.start + b.duration,
);
// Find current segment
const segment = analysis.segments.find(
(s) => timeSeconds >= s.start && timeSeconds < s.start + s.duration,
);
// Calculate beat progress (0-1)
const beatProgress = beat ? (timeSeconds - beat.start) / beat.duration : 0;
return {
section: section
? {
loudness: section.loudness,
tempo: section.tempo,
key: section.key,
mode: section.mode,
}
: null,
beat: {
active: beat !== undefined,
progress: beatProgress,
confidence: beat?.confidence || 0,
},
segment: segment
? {
loudness: segment.loudness_max,
pitches: segment.pitches,
timbre: segment.timbre,
}
: null,
};
}
// Get all beats for timeline visualization
getBeatTimeline() {
if (!this.currentAnalysis) return [];
return this.currentAnalysis.analysis.beats.map((beat) => ({
time: beat.start,
duration: beat.duration,
confidence: beat.confidence,
}));
}
// Get section markers
getSectionMarkers() {
if (!this.currentAnalysis) return [];
return this.currentAnalysis.analysis.sections.map((section, i) => ({
index: i,
start: section.start,
duration: section.duration,
loudness: section.loudness,
tempo: section.tempo,
}));
}
}
// Usage with playback
const client = new Spotted();
const visualizer = new AudioVisualizer(client);
await visualizer.loadTrack("11dFghVXANMlKmJXsNCbNl");
// In your animation loop
function updateVisualization(currentTimeSeconds) {
const data = visualizer.getDataAtTime(currentTimeSeconds);
if (data?.beat.active && data.beat.progress < 0.1) {
// Flash on beat
console.log("BEAT!");
}
// Use segment pitches for frequency visualization
if (data?.segment) {
const dominantPitch = data.segment.pitches.indexOf(
Math.max(...data.segment.pitches),
);
console.log(`Dominant pitch class: ${dominantPitch}`);
}
}

import Spotted from "spotted-ts";
class FeatureBasedRecommender {
constructor(client) {
this.client = client;
}
async findSimilarByFeatures(trackId, options = {}) {
const { tolerance = 0.15, limit = 20, seedGenres = [] } = options;
// Get source track features
const sourceFeatures = await this.client.audioFeatures.retrieve(trackId);
// Build recommendation request with feature targets
const recs = await this.client.recommendations.list({
seed_tracks: trackId,
seed_genres:
seedGenres.length > 0 ? seedGenres.slice(0, 4).join(",") : undefined,
limit,
target_energy: sourceFeatures.energy,
target_danceability: sourceFeatures.danceability,
target_valence: sourceFeatures.valence,
min_energy: Math.max(0, sourceFeatures.energy - tolerance),
max_energy: Math.min(1, sourceFeatures.energy + tolerance),
min_danceability: Math.max(0, sourceFeatures.danceability - tolerance),
max_danceability: Math.min(1, sourceFeatures.danceability + tolerance),
min_valence: Math.max(0, sourceFeatures.valence - tolerance),
max_valence: Math.min(1, sourceFeatures.valence + tolerance),
target_tempo: sourceFeatures.tempo,
min_tempo: sourceFeatures.tempo - 10,
max_tempo: sourceFeatures.tempo + 10,
});
return recs.tracks;
}
async createMixTransition(fromTrackId, toTrackId, steps = 5) {
// Get features of both tracks
const [fromFeatures, toFeatures] = await Promise.all([
this.client.audioFeatures.retrieve(fromTrackId),
this.client.audioFeatures.retrieve(toTrackId),
]);
// Calculate intermediate targets
const transitions = [];
for (let i = 1; i <= steps; i++) {
const progress = i / (steps + 1);
const targetFeatures = {
energy:
fromFeatures.energy +
(toFeatures.energy - fromFeatures.energy) * progress,
danceability:
fromFeatures.danceability +
(toFeatures.danceability - fromFeatures.danceability) * progress,
valence:
fromFeatures.valence +
(toFeatures.valence - fromFeatures.valence) * progress,
tempo:
fromFeatures.tempo +
(toFeatures.tempo - fromFeatures.tempo) * progress,
};
const recs = await this.client.recommendations.list({
seed_tracks: `${fromTrackId},${toTrackId}`,
limit: 3,
target_energy: targetFeatures.energy,
target_danceability: targetFeatures.danceability,
target_valence: targetFeatures.valence,
target_tempo: targetFeatures.tempo,
});
transitions.push(...recs.tracks.slice(0, 1));
}
return transitions;
}
}
// Usage
const client = new Spotted();
const recommender = new FeatureBasedRecommender(client);
// Find similar tracks
const similar = await recommender.findSimilarByFeatures(
"11dFghVXANMlKmJXsNCbNl",
{ tolerance: 0.1 },
);
// Create smooth transition between two tracks
const transition = await recommender.createMixTransition(
"4iV5W9uYEdYUVa79Axb7Rh", // From
"1301WleyT98MSxVHPZCA6M", // To
3, // Number of transition tracks
);

import Spotted from "spotted-ts";
class DJHelper {
constructor(client) {
this.client = client;
// Camelot wheel for harmonic mixing
this.camelotWheel = {
"0-1": "8B",
"0-0": "5A", // C major / C minor
"1-1": "3B",
"1-0": "12A", // C# major / C# minor
"2-1": "10B",
"2-0": "7A", // D major / D minor
"3-1": "5B",
"3-0": "2A", // D# major / D# minor
"4-1": "12B",
"4-0": "9A", // E major / E minor
"5-1": "7B",
"5-0": "4A", // F major / F minor
"6-1": "2B",
"6-0": "11A", // F# major / F# minor
"7-1": "9B",
"7-0": "6A", // G major / G minor
"8-1": "4B",
"8-0": "1A", // G# major / G# minor
"9-1": "11B",
"9-0": "8A", // A major / A minor
"10-1": "6B",
"10-0": "3A", // A# major / A# minor
"11-1": "1B",
"11-0": "10A", // B major / B minor
};
}
getCamelotKey(key, mode) {
return this.camelotWheel[`${key}-${mode}`] || "?";
}
getCompatibleKeys(camelotKey) {
const num = parseInt(camelotKey);
const letter = camelotKey.slice(-1);
// Compatible: same key, +1, -1, and relative major/minor
return [
camelotKey, // Same
`${(num % 12) + 1 || 12}${letter}`, // +1
`${((num - 2 + 12) % 12) + 1}${letter}`, // -1
`${num}${letter === "A" ? "B" : "A"}`, // Relative
];
}
async analyzeForMixing(trackIds) {
const { audio_features } = await this.client.audioFeatures.bulkRetrieve({
ids: trackIds.join(","),
});
return audio_features.filter(Boolean).map((f) => ({
id: f.id,
tempo: f.tempo,
key: f.key,
mode: f.mode,
camelotKey: this.getCamelotKey(f.key, f.mode),
energy: f.energy,
}));
}
findMixableTrack(currentTrack, candidates) {
const currentCamelot = this.getCamelotKey(
currentTrack.key,
currentTrack.mode,
);
const compatibleKeys = this.getCompatibleKeys(currentCamelot);
return candidates
.filter((track) => {
const trackCamelot = this.getCamelotKey(track.key, track.mode);
// Key compatibility
const keyCompatible = compatibleKeys.includes(trackCamelot);
// Tempo compatibility (within 5% or half/double time)
const tempoRatio = track.tempo / currentTrack.tempo;
const tempoCompatible =
(tempoRatio > 0.95 && tempoRatio < 1.05) ||
(tempoRatio > 0.48 && tempoRatio < 0.52) ||
(tempoRatio > 1.95 && tempoRatio < 2.05);
return keyCompatible && tempoCompatible;
})
.sort((a, b) => {
// Sort by closest tempo match
const aRatio = Math.abs(1 - a.tempo / currentTrack.tempo);
const bRatio = Math.abs(1 - b.tempo / currentTrack.tempo);
return aRatio - bRatio;
});
}
}
// Usage
const client = new Spotted();
const dj = new DJHelper(client);
// Analyze playlist for mixing
const analyzed = await dj.analyzeForMixing(playlistTrackIds);
// Find what to play next
const currentTrack = analyzed[0];
const nextOptions = dj.findMixableTrack(currentTrack, analyzed.slice(1));
console.log(`Current: ${currentTrack.camelotKey} @ ${currentTrack.tempo} BPM`);
console.log("Compatible next tracks:");
nextOptions.slice(0, 5).forEach((track) => {
console.log(` ${track.camelotKey} @ ${track.tempo} BPM`);
});