Audio Features
High-level descriptors like energy, danceability, and valence. Quick to fetch, great for filtering and recommendations.
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).
| Feature | Range | Description |
|---|---|---|
| acousticness | 0.0–1.0 | Confidence the track is acoustic |
| danceability | 0.0–1.0 | How suitable for dancing based on tempo, rhythm stability, beat strength |
| energy | 0.0–1.0 | Perceptual measure of intensity and activity |
| instrumentalness | 0.0–1.0 | Predicts whether a track contains no vocals |
| liveness | 0.0–1.0 | Presence of an audience in the recording |
| speechiness | 0.0–1.0 | Presence of spoken words |
| valence | 0.0–1.0 | Musical positiveness (happy vs sad) |
| tempo | BPM | Estimated tempo in beats per minute |
| key | 0–11 | Pitch class (0=C, 1=C#, 2=D, etc.) |
| mode | 0 or 1 | Modality (0=minor, 1=major) |
| loudness | dB | Overall loudness in decibels |
| time_signature | 3–7 | Estimated time signature |
| duration_ms | ms | Duration 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 requestconst { 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 `);});Beats are the basic time unit of a piece.
// Get beats per minute distributionconst beatDurations = analysis.beats.map((b) => b.duration);const avgBeatDuration = beatDurations.reduce((a, b) => a + b, 0) / beatDurations.length;const calculatedBPM = 60 / avgBeatDuration;
console.log(`Calculated BPM: ${calculatedBPM.toFixed(1)}`);Segments are sound entities (notes, chords) with pitch and timbre data.
// Segments contain pitch and timbre vectorsconst segment = analysis.segments[0];
console.log(` Start: ${segment.start}s Duration: ${segment.duration}s Loudness: ${segment.loudness_max} dB Pitches: ${segment.pitches.map((p) => p.toFixed(2)).join(", ")} Timbre: ${segment.timbre.slice(0, 3).map((t) => t.toFixed(1)).join(", ")}...`);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 playbackconst client = new Spotted();const visualizer = new AudioVisualizer(client);await visualizer.loadTrack("11dFghVXANMlKmJXsNCbNl");
// In your animation loopfunction 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; }}
// Usageconst client = new Spotted();const recommender = new FeatureBasedRecommender(client);
// Find similar tracksconst similar = await recommender.findSimilarByFeatures( "11dFghVXANMlKmJXsNCbNl", { tolerance: 0.1 },);
// Create smooth transition between two tracksconst 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; }); }}
// Usageconst client = new Spotted();const dj = new DJHelper(client);
// Analyze playlist for mixingconst analyzed = await dj.analyzeForMixing(playlistTrackIds);
// Find what to play nextconst 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`);});