Please bookmark this page to avoid losing your image tool!

Image Sound Spectrogram Filter Effect Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
// Helper to manage FFT library loading
// Prefixing with _processImage_ to avoid global scope conflicts, ensuring it's unique.
let _processImage_fftJsPromise = null; 
function _processImage_loadFftJs() {
    if (!_processImage_fftJsPromise) {
        _processImage_fftJsPromise = new Promise((resolve, reject) => {
            if (typeof FFT !== 'undefined') { // Check if FFT class from fft.js is already global
                resolve(FFT); // FFT is the constructor from fft.js
                return;
            }
            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/fft.js/4.0.4/fft.js';
            script.onload = () => {
                if (typeof FFT !== 'undefined') {
                    resolve(FFT);
                } else {
                    reject(new Error('FFT library loaded but FFT class not found.'));
                }
            };
            script.onerror = (err) => {
                console.error("Error loading FFT.js for Image Sound Spectrogram Filter:", err);
                reject(new Error('Failed to load FFT.js library from CDN. Please check network connection or CDN status.'));
            };
            document.head.appendChild(script);
        });
    }
    return _processImage_fftJsPromise;
}

// Color mapping data and functions, prefixed for uniqueness.
const _processImage_viridisPoints = [
    { val: 0.0,  rgb: [0.267004, 0.004874, 0.329415] }, // #440154
    { val: 0.25, rgb: [0.229739, 0.322361, 0.548813] }, // #39568C
    { val: 0.5,  rgb: [0.127568, 0.566949, 0.550556] }, // #22908C
    { val: 0.75, rgb: [0.369214, 0.788888, 0.382930] }, // #5FC962
    { val: 1.0,  rgb: [0.993248, 0.906157, 0.143936] }  // #FDE725
];

function _processImage_viridisColorMap(t) {
    t = Math.max(0, Math.min(1, t));
    let p0 = _processImage_viridisPoints[0], p1 = _processImage_viridisPoints[0];

    if (t <= _processImage_viridisPoints[0].val) {
      // Handled by initial p0, p1
    } else if (t >= _processImage_viridisPoints[_processImage_viridisPoints.length - 1].val) {
      p0 = _processImage_viridisPoints[_processImage_viridisPoints.length - 1];
      p1 = _processImage_viridisPoints[_processImage_viridisPoints.length - 1];
    } else {
        for (let i = 0; i < _processImage_viridisPoints.length - 1; i++) {
            if (t >= _processImage_viridisPoints[i].val && t < _processImage_viridisPoints[i+1].val) {
                p0 = _processImage_viridisPoints[i];
                p1 = _processImage_viridisPoints[i+1];
                break;
            }
        }
    }
    
    const T_val = (p1.val - p0.val === 0) ? 0 : (t - p0.val) / (p1.val - p0.val);
    const r = Math.round((p0.rgb[0] * (1 - T_val) + p1.rgb[0] * T_val) * 255);
    const g = Math.round((p0.rgb[1] * (1 - T_val) + p1.rgb[1] * T_val) * 255);
    const b = Math.round((p0.rgb[2] * (1 - T_val) + p1.rgb[2] * T_val) * 255);
    return [r,g,b];
}

function _processImage_jetColorMap(v) {
    v = Math.max(0, Math.min(1, v));
    // Standard Jet colormap: Dark Blue -> Blue -> Cyan -> Yellow -> Red -> Dark Red
    const points = [
        { val: 0.0,   rgb: [0, 0, 0.5] },       // Dark Blue
        { val: 0.125, rgb: [0, 0, 1] },         // Blue
        { val: 0.375, rgb: [0, 1, 1] },         // Cyan
        { val: 0.625, rgb: [1, 1, 0] },         // Yellow
        { val: 0.875, rgb: [1, 0, 0] },         // Red
        { val: 1.0,   rgb: [0.5, 0, 0] }        // Dark Red
    ];

    let p0 = points[0], p1 = points[0];
     if (v <= points[0].val) {
        // Handled by initial p0,p1
    } else if (v >= points[points.length - 1].val) {
        p0 = points[points.length - 1];
        p1 = points[points.length - 1];
    } else {
        for (let i = 0; i < points.length - 1; i++) {
            if (v >= points[i].val && v < points[i+1].val) {
                p0 = points[i];
                p1 = points[i+1];
                break;
            }
        }
    }

    const T_val = (p1.val - p0.val === 0) ? 0 : (v - p0.val) / (p1.val - p0.val);
    const r = Math.round((p0.rgb[0] * (1 - T_val) + p1.rgb[0] * T_val) * 255);
    const g = Math.round((p0.rgb[1] * (1 - T_val) + p1.rgb[1] * T_val) * 255);
    const b = Math.round((p0.rgb[2] * (1 - T_val) + p1.rgb[2] * T_val) * 255);
    return [r, g, b];
}

function _processImage_grayscaleColorMap(v) {
    v = Math.max(0, Math.min(1, v));
    const intensity = Math.round(v * 255);
    return [intensity, intensity, intensity];
}

function _processImage_mapToColor(value, mapName) {
    switch (mapName.toLowerCase()) {
        case "jet":
            return _processImage_jetColorMap(value);
        case "viridis":
            return _processImage_viridisColorMap(value);
        case "grayscale":
        default:
            return _processImage_grayscaleColorMap(value);
    }
}

async function processImage(originalImg, colorMapName = "viridis", logScaleMagnitude = 1, intensityFactor = 1.0, fftWindowFunction = "hann") {
    const FFTConstructor = await _processImage_loadFftJs();

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    // Ensure originalImg is loaded, though Image object implies it is.
    // If originalImg.width/height are 0, it might not be loaded.
    if (!originalImg.width || !originalImg.height) {
        console.error("Original image not loaded or has zero dimensions.");
        // Return an empty canvas or throw error
        canvas.width = 1; canvas.height = 1; 
        return canvas;
    }

    canvas.width = originalImg.width;
    canvas.height = originalImg.height;

    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = originalImg.width;
    tempCanvas.height = originalImg.height;
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
    tempCtx.drawImage(originalImg, 0, 0);
    const imageData = tempCtx.getImageData(0, 0, originalImg.width, originalImg.height);
    const pixels = imageData.data;

    const width = originalImg.width;
    const height = originalImg.height;
    const outputImageData = ctx.createImageData(width, height);
    const outputPixels = outputImageData.data;

    const N_fft = 1 << Math.ceil(Math.log2(height)); // Smallest power of 2 >= height
    const fft = new FFTConstructor(N_fft);
    const complexCoeffs = fft.createComplexArray(); 
    const signal = new Float64Array(N_fft); 

    const windowCoeffs = new Float64Array(height);
    if (height > 0) { // Avoid NaN for height=0, though unlikely
        if (fftWindowFunction === "hann" && height > 1) {
            for (let i = 0; i < height; i++) windowCoeffs[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (height - 1)));
        } else if (fftWindowFunction === "hamming" && height > 1) {
            for (let i = 0; i < height; i++) windowCoeffs[i] = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (height - 1));
        } else { 
            for (let i = 0; i < height; i++) windowCoeffs[i] = 1.0;
        }
    }


    const spectrumMagnitudes = new Float64Array(N_fft / 2 + 1);

    for (let x = 0; x < width; x++) {
        for (let y = 0; y < height; y++) {
            const r_idx = (y * width + x) * 4;
            const r = pixels[r_idx];
            const g = pixels[r_idx + 1];
            const b = pixels[r_idx + 2];
            const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0; // Normalize 0-1
            signal[y] = luminance * windowCoeffs[y];
        }
        for (let y = height; y < N_fft; y++) {
            signal[y] = 0; // Zero-pad
        }

        fft.realTransform(complexCoeffs, signal);

        spectrumMagnitudes[0] = Math.abs(complexCoeffs[0]); // DC
        if (N_fft > 0) { // Check N_fft to avoid issues if height was 0 -> N_fft=0
             for (let k = 1; k < N_fft / 2; k++) {
                spectrumMagnitudes[k] = Math.sqrt(complexCoeffs[2 * k] * complexCoeffs[2 * k] + complexCoeffs[2 * k + 1] * complexCoeffs[2 * k + 1]);
            }
            if (N_fft / 2 > 0) { // Nyquist only if N_fft >= 2
                 spectrumMagnitudes[N_fft / 2] = Math.abs(complexCoeffs[1]); // Nyquist
            }
        }
       

        let minMag = Infinity, maxMag = -Infinity;
        for (let k = 0; k <= N_fft / 2; k++) {
            let mag = spectrumMagnitudes[k] * Math.max(0, intensityFactor); // Ensure positive factor
            if (logScaleMagnitude === 1) {
                mag = Math.log1p(mag); // log(1+mag)
            }
            spectrumMagnitudes[k] = mag;
            if (mag < minMag) minMag = mag;
            if (mag > maxMag) maxMag = mag;
        }
        
        const range = (maxMag > minMag) ? (maxMag - minMag) : 1;

        const num_bins = N_fft / 2 + 1;
        for (let y_out = 0; y_out < height; y_out++) {
            const freq_pos_norm = (height === 1) ? 0 : (height - 1 - y_out) / (height - 1);
            
            const bin_idx_float = freq_pos_norm * (num_bins - 1);
            const bin_idx0 = Math.floor(bin_idx_float);
            const bin_idx1 = Math.min(Math.ceil(bin_idx_float), num_bins - 1); // Clamp idx1
            const frac = bin_idx_float - bin_idx0;

            let mag_interpolated;
            if (num_bins === 1) { // Only DC component if N_fft is small (e.g. 0 or 1)
                 mag_interpolated = spectrumMagnitudes[0];
            } else if (bin_idx0 >= num_bins -1) { // Should effectively be caught by min with num_bins-1 for idx1
                 mag_interpolated = spectrumMagnitudes[num_bins-1];
            } else if (bin_idx0 < 0) { // Should not happen
                 mag_interpolated = spectrumMagnitudes[0];
            } else {
                 mag_interpolated = spectrumMagnitudes[bin_idx0] * (1 - frac) + spectrumMagnitudes[bin_idx1] * frac;
            }
            
            const norm_mag = (mag_interpolated - minMag) / range;
            const color = _processImage_mapToColor(norm_mag, colorMapName);

            const out_idx = (y_out * width + x) * 4;
            outputPixels[out_idx] = color[0];
            outputPixels[out_idx + 1] = color[1];
            outputPixels[out_idx + 2] = color[2];
            outputPixels[out_idx + 3] = 255;
        }
    }

    ctx.putImageData(outputImageData, 0, 0);
    return canvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Sound Spectrogram Filter Effect Tool allows users to transform an image into a sound spectrogram representation. By processing the image data, this tool applies a Fast Fourier Transform (FFT) to visualize sound frequencies and amplitudes, applying various color maps to enhance the display. It is useful for audio analysis, music visualization, and educational purposes, where users can see how sound waves would be represented visually based on the image provided. The tool supports different color mapping options, log scale adjustments, and window functions, providing flexibility for various visualization needs.

Leave a Reply

Your email address will not be published. Required fields are marked *