🎯 Library Strategy

FlowState includes a curated library of royalty-free samples. Users get instant access to quality sounds without legal concerns.

🎁
Starter Library: 10,000+ government/public domain sound effects plus curated free samples from trusted sources. Users can also upload their own samples.

πŸ“Š Sample Sources

Source Samples License Quality
Government Sound Library 10,000+ Public Domain Variable
Freesound.org (curated) 2,000+ CC0 Good
NASA Sound Library 500+ Public Domain Unique
BBC Sound Effects 16,000+ Remix allowed Excellent
Custom Recorded 500+ Original Excellent

πŸ—‚οΈ Sample Categories

Category Subcategories Est. Count
Drums Kicks, Snares, Hats, Claps, Percussion, Loops 3,000
Bass 808s, Sub Bass, Synth Bass, Acoustic 500
Melodic Keys, Strings, Pads, Leads, Plucks 1,500
Vocals Chops, Ad-libs, FX, Chants 800
FX Risers, Impacts, Transitions, Textures 1,200
Foley Ambient, Nature, Urban, Mechanical 3,000

πŸ” AI-Powered Search

Samples are indexed with semantic embeddings for natural language search.

Search Examples

Query Results
"dark trap snare" Filtered snares with dark/distorted character
"something like Travis Scott" Reverby 808s, ambient pads, auto-tune style
"punchy kick 90bpm" Tempo-matched kicks with transient impact
"ethereal melody F minor" Key-matched melodic samples

Search Implementation

// sample-search.ts
interface Sample {
  id: string;
  name: string;
  category: string;
  tags: string[];
  bpm: number | null;
  key: string | null;
  duration: number;
  waveformPeaks: number[];
  embedding: number[];  // 768-dim BGE embedding
  url: string;
}

async function searchSamples(query: string, filters?: SearchFilters) {
  // Generate embedding for query
  const queryEmbedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
    text: query
  });

  // Search Vectorize index
  const results = await env.SAMPLES_INDEX.query(queryEmbedding.data[0], {
    topK: 20,
    filter: buildFilter(filters)
  });

  // Fetch full sample metadata
  const samples = await Promise.all(
    results.matches.map(m => env.DB.prepare(
      'SELECT * FROM samples WHERE id = ?'
    ).bind(m.id).first())
  );

  return samples;
}

function buildFilter(filters?: SearchFilters): VectorizeFilter {
  const conditions: any = {};

  if (filters?.category) {
    conditions.category = { $eq: filters.category };
  }
  if (filters?.bpmRange) {
    conditions.bpm = {
      $gte: filters.bpmRange[0],
      $lte: filters.bpmRange[1]
    };
  }
  if (filters?.key) {
    conditions.key = { $eq: filters.key };
  }

  return conditions;
}
πŸ“Š Sample Metadata Schema
-- D1 Schema
CREATE TABLE samples (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  filename TEXT NOT NULL,
  category TEXT NOT NULL,
  subcategory TEXT,
  tags TEXT,  -- JSON array

  -- Audio properties
  duration_ms INTEGER NOT NULL,
  bpm REAL,
  key TEXT,
  sample_rate INTEGER DEFAULT 44100,
  bit_depth INTEGER DEFAULT 16,
  channels INTEGER DEFAULT 2,

  -- Analysis
  loudness_lufs REAL,
  peak_db REAL,
  spectral_centroid REAL,

  -- Display
  waveform_peaks TEXT,  -- JSON array for visualization
  color TEXT,  -- Category color hint

  -- Metadata
  source TEXT,  -- e.g., "government", "freesound", "user"
  license TEXT,
  attribution TEXT,

  -- Storage
  r2_key TEXT NOT NULL,
  file_size INTEGER,

  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_samples_category ON samples(category);
CREATE INDEX idx_samples_bpm ON samples(bpm);
CREATE INDEX idx_samples_key ON samples(key);
🎡 Audio Analysis Pipeline

Samples are analyzed on upload to extract metadata automatically.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Upload β”‚ β”‚ Analyze β”‚ β”‚ Embed β”‚ β”‚ Index β”‚ β”‚ Sample │───▢│ Audio │───▢│ Tags │───▢│ Store β”‚ β”‚ (R2) β”‚ β”‚ (Worker) β”‚ β”‚ (AI) β”‚ β”‚ (D1+Vec) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Analysis Functions

Analysis Method Output
BPM Detection Essentia.js / Web Audio Float (e.g., 140.5)
Key Detection Chroma analysis String (e.g., "F#m")
Loudness LUFS calculation Float (e.g., -14.2)
Waveform Peaks Downsampled max values Array[100]
Category AI classification String (e.g., "drums/kick")
Embedding BGE text encoder Float[768]
// analyze-sample.ts
async function analyzeSample(audioBuffer: ArrayBuffer): Promise<SampleAnalysis> {
  const audioContext = new OfflineAudioContext(2, 44100 * 30, 44100);
  const buffer = await audioContext.decodeAudioData(audioBuffer);

  // Extract features
  const channelData = buffer.getChannelData(0);

  return {
    duration_ms: Math.round(buffer.duration * 1000),
    sample_rate: buffer.sampleRate,
    channels: buffer.numberOfChannels,
    waveform_peaks: computePeaks(channelData, 100),
    loudness_lufs: calculateLUFS(channelData, buffer.sampleRate),
    peak_db: calculatePeakDb(channelData),
    bpm: await detectBPM(channelData, buffer.sampleRate),
    key: await detectKey(channelData, buffer.sampleRate)
  };
}

function computePeaks(data: Float32Array, numPeaks: number): number[] {
  const peaks: number[] = [];
  const samplesPerPeak = Math.floor(data.length / numPeaks);

  for (let i = 0; i < numPeaks; i++) {
    let max = 0;
    const start = i * samplesPerPeak;
    for (let j = start; j < start + samplesPerPeak && j < data.length; j++) {
      max = Math.max(max, Math.abs(data[j]));
    }
    peaks.push(max);
  }

  return peaks;
}
πŸ“¦ Storage Architecture
Storage Content Access Pattern
R2 (samples/) Audio files (WAV/MP3) CDN cached, range requests
R2 (waveforms/) Pre-rendered waveform images CDN cached
D1 Sample metadata Filtered queries
Vectorize Semantic embeddings Similarity search

R2 Key Structure

samples/
  library/           # Built-in samples
    drums/
      kicks/
        808-deep-01.wav
        808-punchy-01.wav
      snares/
      hats/
    melodic/
    fx/
  user/              # User uploads
    {user_id}/
      {sample_id}.wav
  temp/              # Processing queue
    {job_id}.wav
πŸŽ›οΈ Sample Browser UI

The sample browser provides multiple ways to find sounds.

UI Components

  • Search Bar: Natural language + filters
  • Category Tree: Hierarchical navigation
  • Tag Cloud: Quick tag filtering
  • Waveform Preview: Visual + hover-to-play
  • Drag-to-Timeline: Direct placement
  • Favorites: Quick access list
  • Recent: Recently used samples

Keyboard Shortcuts

Shortcut Action
Space Preview selected sample
Enter Add to current track
↑/↓ Navigate list
Cmd/Ctrl + F Focus search
F Toggle favorite
πŸ‘€ User Uploads

Users can upload their own samples to their personal library.

Tier Storage Limit Max File Size
Free 100MB 10MB
Pro 5GB 50MB
Enterprise 50GB 200MB

Upload Flow

// upload.ts
async function uploadSample(file: File, userId: string) {
  // Validate
  if (!['audio/wav', 'audio/mp3', 'audio/mpeg'].includes(file.type)) {
    throw new Error('Unsupported format');
  }

  // Check quota
  const usage = await getUserStorageUsage(userId);
  if (usage + file.size > getUserStorageLimit(userId)) {
    throw new Error('Storage quota exceeded');
  }

  // Generate ID and key
  const sampleId = crypto.randomUUID();
  const r2Key = `samples/user/${userId}/${sampleId}.wav`;

  // Upload to R2
  await env.SAMPLES_BUCKET.put(r2Key, file.stream());

  // Analyze async
  await env.ANALYSIS_QUEUE.send({
    sampleId,
    r2Key,
    userId
  });

  return { sampleId, status: 'processing' };
}
πŸ’° Cost Breakdown
Component Cost (10K users)
R2 Storage (library) $1.50 (100GB @ $0.015/GB)
R2 Storage (user uploads) $7.50 (500GB)
Vectorize (5M vectors) $0 (free tier)
D1 Database $0 (free tier)
Workers AI (embeddings) $2.00
Total ~$11/mo
πŸ’‘
R2 Advantage: Zero egress fees mean sample streaming is essentially free regardless of how many times samples are previewed.