Skip to main content
Glama
williamzujkowski

Strudel MCP Server

detect_key

Identifies musical keys in audio to support pattern creation and music generation within the Strudel.cc environment.

Instructions

Key detection

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • MCP tool registration defining the 'detect_key' tool with name, description, and empty input schema (no parameters required).
    {
      name: 'detect_key',
      description: 'Key detection',
      inputSchema: { type: 'object', properties: {} }
    },
  • MCP server handler for 'detect_key' tool: checks initialization, calls controller.detectKey(), processes KeyAnalysis result into standardized output with confidence, alternatives, and error handling.
    case 'detect_key':
      if (!this.isInitialized) {
        return 'Browser not initialized. Run init first.';
      }
      try {
        const keyAnalysis = await this.controller.detectKey();
        if (!keyAnalysis || keyAnalysis.confidence < 0.1) {
          return {
            key: 'Unknown',
            scale: 'unknown',
            confidence: 0,
            message: 'No clear key detected. Ensure audio is playing and has tonal content.'
          };
        }
    
        const result: any = {
          key: keyAnalysis.key,
          scale: keyAnalysis.scale,
          confidence: Math.round(keyAnalysis.confidence * 100) / 100,
          message: `Detected ${keyAnalysis.key} ${keyAnalysis.scale} with ${Math.round(keyAnalysis.confidence * 100)}% confidence`
        };
    
        // Include alternatives if available and confidence is moderate
        if (keyAnalysis.alternatives && keyAnalysis.alternatives.length > 0) {
          result.alternatives = keyAnalysis.alternatives.map((alt: any) => ({
            key: alt.key,
            scale: alt.scale,
            confidence: Math.round(alt.confidence * 100) / 100
          }));
        }
    
        return result;
      } catch (error: any) {
        return {
          key: 'Unknown',
          scale: 'unknown',
          confidence: 0,
          error: error.message || 'Key detection failed'
        };
      }
  • Delegation helper in StrudelController: forwards detectKey call to AudioAnalyzer instance with the browser page.
    async detectKey(): Promise<KeyAnalysis | null> {
      if (!this._page) throw new Error('Browser not initialized. Run init tool first.');
    
      return await this.analyzer.detectKey(this._page);
    }
  • Core key detection handler: extracts chroma vector from audio analyzer data, correlates with scale profiles using cosine similarity and Krumhansl-Schmuckler algorithm, applies tonal biases, computes confidence and alternatives.
    async detectKey(page: Page): Promise<KeyAnalysis | null> {
      // Get analyzer object from browser
      const analyzer = await page.evaluate(() => {
        return (window as any).strudelAudioAnalyzer;
      });
    
      if (!analyzer || !analyzer.isConnected) {
        throw new Error('Audio analyzer not connected');
      }
    
      let chroma: number[];
    
      // Check if this is a mock with pre-calculated chroma vector (for testing)
      if (typeof analyzer.analyze === 'function') {
        const analysis = analyzer.analyze();
        if (analysis?.features?.chromaVector) {
          chroma = analysis.features.chromaVector;
        } else {
          // No mock data, extract from FFT
          if (!analyzer.dataArray) {
            throw new Error('Invalid audio data');
          }
          const fftData = new Uint8Array(analyzer.dataArray);
          chroma = this.extractChroma(fftData);
        }
      } else {
        // No analyze function, extract from FFT
        if (!analyzer.dataArray) {
          throw new Error('Invalid audio data');
        }
        const fftData = new Uint8Array(analyzer.dataArray);
        chroma = this.extractChroma(fftData);
      }
    
      // Check for sufficient energy
      const totalEnergy = chroma.reduce((sum, val) => sum + val, 0);
      if (totalEnergy < 0.1) {
        return { key: 'C', scale: 'major', confidence: 0.1 };
      }
    
      // Correlate with all key/scale combinations
      const scores: Array<{ key: string; scale: string; score: number }> = [];
    
      for (const scale of Object.keys(this.SCALE_PROFILES)) {
        // Normalize profile to sum to 1
        const rawProfile = this.SCALE_PROFILES[scale];
        const profileSum = rawProfile.reduce((a, b) => a + b, 0);
        const profile = rawProfile.map(v => v / profileSum);
    
        for (let tonic = 0; tonic < 12; tonic++) {
          // Rotate chroma to align with profile
          // Put the tonic at position 0 to match the profile structure
          const rotatedChroma = new Array(12);
          for (let i = 0; i < 12; i++) {
            rotatedChroma[i] = chroma[(i + tonic) % 12];
          }
    
          // Use cosine similarity for correlation
          const correlation = this.cosineSimilarity(rotatedChroma, profile);
    
          scores.push({
            key: this.PITCH_CLASSES[tonic],
            scale,
            score: correlation
          });
        }
      }
    
      // Find the top 3 loudest pitch classes - any could be the tonic
      const chromaWithIndices = chroma.map((v, i) => ({ value: v, index: i }));
      chromaWithIndices.sort((a, b) => b.value - a.value);
      const topPitches = chromaWithIndices.slice(0, 3).map(x => this.PITCH_CLASSES[x.index]);
    
      // Apply bias boosts to resolve ambiguous cases
      for (const s of scores) {
        // Boost keys that match one of the top 3 loudest pitches
        // (any of these could plausibly be the tonic)
        const pitchBoost = topPitches.indexOf(s.key);
        if (pitchBoost >= 0) {
          // Slightly favor 2nd pitch to handle dominant/mediant being louder than tonic
          const boosts = [1.075, 1.075, 1.075];
          s.score *= boosts[pitchBoost];
        }
        // Boost for common scales
        if (s.scale === 'major') {
          s.score *= 1.03;  // 3% boost for major scales (most common)
        } else if (s.scale === 'dorian') {
          s.score *= 1.015;  // 1.5% boost for dorian (common modal scale)
        }
      }
    
      // Sort by score (after applying biases)
      scores.sort((a, b) => b.score - a.score);
    
      // Calculate confidence
      const best = scores[0];
      const secondBest = scores[1];
      // Confidence based on score strength (cosine similarity 0-1) and separation
      // Increased separation weight to better differentiate close matches
      const strength = best.score;
      const separation = Math.min(1, Math.max(0, (best.score - secondBest.score) * 10));
      const confidence = Math.min(1, strength * 0.75 + separation * 0.25);
    
      return {
        key: best.key,
        scale: best.scale as any,
        confidence,
        alternatives: scores.slice(1, 4).map(s => ({
          key: s.key,
          scale: s.scale,
          confidence: Math.max(0, s.score)
        }))
      };
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/williamzujkowski/strudel-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server