Documentation/Source example
Example source
samples/vis/Shader - Spectrogram.js
Referenced by:vis.CreateAsync
include('docs/Flags.js');
window.DrawMode = 1;
// JSplitter vis sample: Spectrogram | custom HLSL.
// Source: scrolling FFT history; render options affect only the presentation.
// Uses the built-in default grayscale palette and 50 px/s scrolling geometry.
// JSplitter vis shader sample: Spectrogram. All source settings are API defaults.
// The native SpectrogramSource calculates FFT using Nuttall windowing, maps
// frequencies and dB values, and retains the rolling history. JavaScript does
// not allocate or upload FFT arrays; all texture updates happen in Draw().
//
// HLSL inputs for source.type = "spectrogram":
// t0 / s0 HistoryTexture / HistorySampler: 2D R32F texture.
// .r = normalized spectral level, 0 at minDb and 1 at maxDb.
// Each row represents a frequency range (top = high frequencies),
// each column is 20 ms of displayed history (50 pixels/second).
// Columns form a physical circular buffer, not chronological order.
// b0 VisConstants (16 floats):
// viewportSize.xy = Draw width/height, in pixels.
// time = elapsed seconds since shader effect creation.
// sourceCount = 1 (one spectrogram source).
// textureWidth / textureHeight = t0 dimensions in pixels.
// nextColumn = physical index where the next column will be written.
// validColumns = number of populated columns, 0..textureWidth.
// scrollPixels = fractional time phase for a 50 px/s scroll.
// Remaining constants are reserved/zero for this source.
// input.sceneSpacePos.xy = local position in Draw, in pixels.
// Mapping: full history => wrap nextColumn + screenX to textureWidth.
// partial history => blank on the left, filled data right-aligned.
// IMPORTANT: Use Load() for exact R32F texels, without filtering across the
// physical ring seam. No display history is copied to JS every frame.
vis.UpdateIntervalMs = 16; // Default: 16 ms; allowed: unsigned integer 1..4294967295 ms.
const backgroundColor = 0xff202020;
const spectrogramShader = `
Texture2D<float> HistoryTexture : register(t0);
SamplerState HistorySampler : register(s0);
cbuffer VisConstants : register(b0)
{
float2 viewportSize; // Draw width and height in pixels.
float time; // Seconds since effect creation.
float sourceCount; // 1 for Spectrogram.
float textureWidth; // Width of the circular history texture.
float textureHeight; // Frequency rows in the texture.
float nextColumn; // Physical index of the next write.
float validColumns; // Number of columns currently filled.
float scrollPixels; // Fractional time phase (Nearest mode).
float reserved0;
float reserved1;
float reserved2;
float4 reservedColor;
};
struct VS_OUTPUT
{
float4 clipSpacePos : SV_POSITION;
float4 sceneSpacePos : SCENE_POS;
};
float4 main(VS_OUTPUT input) : SV_Target
{
int width = max((int)textureWidth, 1);
int height = max((int)textureHeight, 1);
int valid = clamp((int)validColumns, 0, width);
int next = clamp((int)nextColumn, 0, width - 1);
float2 pos = input.sceneSpacePos.xy;
int row = clamp((int)floor(pos.y / max(viewportSize.y, 1.0) * height), 0, height - 1);
float screenX = pos.x / max(viewportSize.x, 1.0) * width - 0.5;
int columnX = clamp((int)floor(screenX + max(scrollPixels, 0.0) + 0.5), 0, width - 1);
// The left edge is empty until the history has been populated.
if (columnX < width - valid)
return float4(32.0 / 255.0, 32.0 / 255.0, 32.0 / 255.0, 1.0);
// Both indices are in [0, width), so one subtraction wraps the ring.
int wrappedX = next + columnX;
if (wrappedX >= width) wrappedX -= width;
int physicalX = valid < width ? columnX - (width - valid) : wrappedX;
float level = saturate(HistoryTexture.Load(int3(physicalX, row, 0)).r);
// Match the built-in grayscale palette byte-for-byte: 0x20 -> 0xC2.
float shadeByte = floor(32.0 + level * (194.0 - 32.0) + 0.5);
float shade = shadeByte / 255.0;
return float4(shade, shade, shade, 1.0);
}
`;
const source = {
channel: "mix", // Default: "mix"; also accepts a vis.AudioFormat channel name or unsigned PCM index.
fftSize: 4096, // Default: 4096. Power of two, 256..16384; allowed: power of two, 256..16384.
columnIntervalMs: 16, // Default: 16 ms. FFT history publication interval; allowed: integer 8..250 ms.
minFrequency: 50, // Default: 50 Hz; allowed: 0 < value < maxFrequency.
maxFrequency: 20000, // Default: 20000 Hz; allowed: value > minFrequency.
scale: "log", // Default: "log"; allowed: "log" or "linear".
minDb: -78, // Default: -78 dB (mapped to level 0); allowed: -160 <= value < maxDb.
maxDb: 0, // Default: 0 dB (mapped to level 1); allowed: minDb < value <= +24.
gain: 1 // Default: 1. Linear gain before dB conversion; allowed: > 0.
};
let spectrogram = null;
vis.CreateAsync(VisSourceType.Spectrogram, source, { shader: spectrogramShader })
.then(visualization => {
spectrogram = visualization;
window.Repaint();
})
.catch(error => console.log("Spectrogram shader compilation failed: " + error));
function on_paint(gr) {
const w = window.Width, h = window.Height;
gr.FillSolidRect(0, 0, w, h, backgroundColor);
if (spectrogram && w > 0 && h > 0)
spectrogram.Draw(gr, 0, 0, w, h);
}
setInterval(() => window.Repaint(), 16.6667);