From 15bfd6c5eb74d4cc16bf2b4edaf9f165d3879042 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Tue, 14 Nov 2023 17:04:59 +0100 Subject: [PATCH 1/2] First version of freqscope --- src/AudioVisualisation.ts | 70 ++++++++++++++++++++++++------- src/documentation/oscilloscope.ts | 31 +++++++++++--- src/main.ts | 2 +- 3 files changed, 82 insertions(+), 21 deletions(-) diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index f13add5..61d9c76 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -122,11 +122,11 @@ export interface OscilloscopeConfig { thickness: number; fftSize: number; // multiples of 256 orientation: "horizontal" | "vertical"; - is3D: boolean; + mode: "3D" | "scope" | "freqscope"; size: number; } -let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg' +let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg' /** * Initializes and runs an oscilloscope using an AnalyzerNode. @@ -140,12 +140,47 @@ export const runOscilloscope = ( let config = app.osc; let analyzer = getAnalyser(config.fftSize); let dataArray = new Float32Array(analyzer.frequencyBinCount); + let freqDataArray = new Uint8Array(analyzer.frequencyBinCount); const canvasCtx = canvas.getContext("2d")!; const WIDTH = canvas.width; const HEIGHT = canvas.height; let lastDrawTime = 0; let frameInterval = 1000 / 30; + function drawFrequencyScope() { + analyzer.getByteFrequencyData(freqDataArray); + canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + + // Ensure the number of bars does not exceed the frequency data length + const numBars = freqDataArray.length; + const barWidth = WIDTH / numBars; + let barHeight; + let x = 0; + + for (let i = 0; i < numBars; i++) { + // Calculate index in the frequency data array using logarithmic scale + const logIndex = Math.floor( + Math.pow(i / numBars, 2) * freqDataArray.length + ); + barHeight = Math.floor( + freqDataArray[logIndex] * ((HEIGHT / 256) * app.osc.size) + ); + + // Color configuration + if (app.osc.color === "random") { + canvasCtx.fillStyle = `hsl(${Math.random() * 360}, 100%, 50%)`; + } else { + const gradient = canvasCtx.createLinearGradient(0, 0, WIDTH / 2, 0); + gradient.addColorStop(0, app.osc.color || `rgb(255, 255, 255)`); + gradient.addColorStop(1, `rgb(${barHeight + 50},50,50)`); + canvasCtx.fillStyle = gradient; + } + + canvasCtx.fillRect(x, (HEIGHT - barHeight) / 2, barWidth + 1, barHeight); + + x += barWidth; + } + } function draw() { const currentTime = Date.now(); @@ -166,8 +201,7 @@ export const runOscilloscope = ( } analyzer.getFloatTimeDomainData(dataArray); - canvasCtx.globalCompositeOperation = 'source-over'; - + canvasCtx.globalCompositeOperation = "source-over"; canvasCtx.fillStyle = "rgba(0, 0, 0, 0)"; canvasCtx.fillRect(0, 0, WIDTH, HEIGHT); @@ -184,23 +218,26 @@ export const runOscilloscope = ( } else { canvasCtx.strokeStyle = app.osc.color; } - const remainingRefreshTime = app.clock.time_position.pulse % app.osc.refresh; - const opacityRatio = 1 - (remainingRefreshTime / app.osc.refresh); + const remainingRefreshTime = + app.clock.time_position.pulse % app.osc.refresh; + const opacityRatio = 1 - remainingRefreshTime / app.osc.refresh; canvasCtx.globalAlpha = opacityRatio; canvasCtx.beginPath(); - let startIndex = 0; for (let i = 1; i < dataArray.length; ++i) { let currentType = null; if (dataArray[i] >= 0 && dataArray[i - 1] < 0) { - currentType = 'negToPos'; + currentType = "negToPos"; } else if (dataArray[i] < 0 && dataArray[i - 1] >= 0) { - currentType = 'posToNeg'; + currentType = "posToNeg"; } if (currentType) { - if (lastZeroCrossingType === null || currentType === lastZeroCrossingType) { + if ( + lastZeroCrossingType === null || + currentType === lastZeroCrossingType + ) { startIndex = i; lastZeroCrossingType = currentType; break; @@ -208,14 +245,18 @@ export const runOscilloscope = ( } } - - if (app.osc.is3D) { + if (app.osc.mode === "freqscope") { + drawFrequencyScope(); + } else if (app.osc.mode === "3D") { for (let i = startIndex; i < dataArray.length; i += 2) { const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4; const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4; i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); } - } else if (app.osc.orientation === "horizontal") { + } else if ( + app.osc.mode === "scope" && + app.osc.orientation === "horizontal" + ) { const sliceWidth = (WIDTH * 1.0) / dataArray.length; const yOffset = HEIGHT / 4; let x = 0; @@ -226,7 +267,7 @@ export const runOscilloscope = ( x += sliceWidth; } canvasCtx.lineTo(WIDTH, yOffset); - } else { + } else if (app.osc.mode === "scope" && app.osc.orientation === "vertical") { const sliceHeight = (HEIGHT * 1.0) / dataArray.length; const xOffset = WIDTH / 4; let y = 0; @@ -243,6 +284,5 @@ export const runOscilloscope = ( canvasCtx.globalAlpha = 1.0; } - draw(); }; diff --git a/src/documentation/oscilloscope.ts b/src/documentation/oscilloscope.ts index 2d5ea5a..d1103d9 100644 --- a/src/documentation/oscilloscope.ts +++ b/src/documentation/oscilloscope.ts @@ -8,21 +8,42 @@ export const oscilloscope = (application: Editor): string => { You can turn on the oscilloscope to generate interesting visuals or to inspect audio. Use the scope() function to turn it on and off. The oscilloscope is off by default. ${makeExample( - "Oscilloscope configuration", - ` + "Oscilloscope configuration", + ` scope({ enabled: true, // off by default color: "#fdba74", // any valid CSS color or "random" thickness: 4, // stroke thickness fftSize: 256, // multiples of 128 orientation: "horizontal", // "vertical" or "horizontal" - is3D: false, // 3D oscilloscope + mode: "scope" | "3D" | "freqscope", // scope mode size: 1, // size of the oscilloscope refresh: 1 // refresh rate (in pulses) }) `, - true - )} + true +)} + +${makeExample( + "Demo with multiple scope mode", + ` +rhythm(.5, [4,5].dur(4*3, 4*1), 8)::sound('fhardkick').out() +beat(0.25)::sound('square').freq([ + [250, 250/2, 250/4].pick(), + [250, 250/2, 250/4].beat() / 2 * 4, + ]) + .fmi([1,2,3,4].bar()).fmh(fill()? 0 : 4) + .lpf(100+usine(1/4)*1200).lpad(4, 0, .5) + .room(0.5).size(8).vib(0.5).vibmod(0.125) + .ad(0, .125).out() +beat(2)::sound('fsoftsnare').shape(0.5).out() +scope({enabled: true, thickness: 8, + mode: ['freqscope', 'scope', '3D'].beat(), + color: ['purple', 'green', 'random'].beat(), + size: 0.5, fftSize: 2048}) + `, + true +)} Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :) diff --git a/src/main.ts b/src/main.ts index 8b6faa8..ee0aa95 100644 --- a/src/main.ts +++ b/src/main.ts @@ -76,7 +76,7 @@ export class Editor { refresh: 1, fftSize: 1024, orientation: "horizontal", - is3D: false, + mode: "scope", size: 1, }; From b070dd9fff8f6c94b5862df2b1dabad0394b526b Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Tue, 14 Nov 2023 23:20:29 +0100 Subject: [PATCH 2/2] Better Frequency scope and visualisations --- src/AudioVisualisation.ts | 94 ++++++++++++++++++++++--------- src/documentation/oscilloscope.ts | 2 + src/main.ts | 2 + 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 61d9c76..c3ce67a 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -123,6 +123,8 @@ export interface OscilloscopeConfig { fftSize: number; // multiples of 256 orientation: "horizontal" | "vertical"; mode: "3D" | "scope" | "freqscope"; + offsetX: number; + offsetY: number; size: number; } @@ -142,47 +144,76 @@ export const runOscilloscope = ( let dataArray = new Float32Array(analyzer.frequencyBinCount); let freqDataArray = new Uint8Array(analyzer.frequencyBinCount); const canvasCtx = canvas.getContext("2d")!; - const WIDTH = canvas.width; - const HEIGHT = canvas.height; let lastDrawTime = 0; let frameInterval = 1000 / 30; - function drawFrequencyScope() { + function drawFrequencyScope( + width: number, + height: number, + offset_height: number, + offset_width: number + ) { + // Existing setup code... + analyzer.fftSize = app.osc.fftSize * 4; analyzer.getByteFrequencyData(freqDataArray); - canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + canvasCtx.clearRect(0, 0, width, height); - // Ensure the number of bars does not exceed the frequency data length - const numBars = freqDataArray.length; - const barWidth = WIDTH / numBars; + const numBars = Math.min( + freqDataArray.length, + app.osc.orientation === "horizontal" ? width : height + ); + const barWidth = + app.osc.orientation === "horizontal" ? width / numBars : height / numBars; let barHeight; - let x = 0; + let x = 0, + y = 0; for (let i = 0; i < numBars; i++) { - // Calculate index in the frequency data array using logarithmic scale - const logIndex = Math.floor( - Math.pow(i / numBars, 2) * freqDataArray.length - ); barHeight = Math.floor( - freqDataArray[logIndex] * ((HEIGHT / 256) * app.osc.size) + freqDataArray[i] * ((height / 256) * app.osc.size) ); - // Color configuration - if (app.osc.color === "random") { - canvasCtx.fillStyle = `hsl(${Math.random() * 360}, 100%, 50%)`; + // Create gradient based on orientation + let gradient; + if (app.osc.orientation === "horizontal") { + gradient = canvasCtx.createLinearGradient(0, 0, width / 2, 0); } else { - const gradient = canvasCtx.createLinearGradient(0, 0, WIDTH / 2, 0); - gradient.addColorStop(0, app.osc.color || `rgb(255, 255, 255)`); - gradient.addColorStop(1, `rgb(${barHeight + 50},50,50)`); - canvasCtx.fillStyle = gradient; + gradient = canvasCtx.createLinearGradient(0, 0, 0, height / 2); } + gradient.addColorStop(0, app.osc.color || `rgb(255, 255, 255)`); + gradient.addColorStop(1, `rgb(${barHeight + 50},50,50)`); + canvasCtx.fillStyle = gradient; - canvasCtx.fillRect(x, (HEIGHT - barHeight) / 2, barWidth + 1, barHeight); - - x += barWidth; + if (app.osc.orientation === "horizontal") { + canvasCtx.fillRect( + x + offset_width, // Apply horizontal offset here + (height - barHeight) / 2 + offset_height, // Apply vertical offset here + barWidth + 1, + barHeight + ); + x += barWidth; + } else { + canvasCtx.fillRect( + (width - barHeight) / 2 + offset_width, // Apply horizontal offset here + y + offset_height, // Apply vertical offset here + barHeight, + barWidth + 1 + ); + y += barWidth; + } } } function draw() { + // Update the canvas position on each cycle + const WIDTH = canvas.width; + const HEIGHT = canvas.height; + const OFFSET_WIDTH = app.osc.offsetX; + const OFFSET_HEIGHT = app.osc.offsetY; + + // Apply an offset to the canvas! + canvasCtx.setTransform(1, 0, 0, 1, OFFSET_WIDTH, OFFSET_HEIGHT); + const currentTime = Date.now(); requestAnimationFrame(draw); if (currentTime - lastDrawTime < frameInterval) { @@ -191,7 +222,12 @@ export const runOscilloscope = ( lastDrawTime = currentTime; if (!app.osc.enabled) { - canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + canvasCtx.clearRect( + -OFFSET_WIDTH, + -OFFSET_HEIGHT, + WIDTH + 2 * OFFSET_WIDTH, + HEIGHT + 2 * OFFSET_HEIGHT + ); return; } @@ -206,9 +242,13 @@ export const runOscilloscope = ( canvasCtx.fillStyle = "rgba(0, 0, 0, 0)"; canvasCtx.fillRect(0, 0, WIDTH, HEIGHT); if (app.clock.time_position.pulse % app.osc.refresh == 0) { - canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + canvasCtx.clearRect( + -OFFSET_WIDTH, + -OFFSET_HEIGHT, + WIDTH + 2 * OFFSET_WIDTH, + HEIGHT + 2 * OFFSET_HEIGHT + ); } - canvasCtx.lineWidth = app.osc.thickness; if (app.osc.color === "random") { @@ -246,7 +286,7 @@ export const runOscilloscope = ( } if (app.osc.mode === "freqscope") { - drawFrequencyScope(); + drawFrequencyScope(WIDTH, HEIGHT, OFFSET_HEIGHT, OFFSET_WIDTH); } else if (app.osc.mode === "3D") { for (let i = startIndex; i < dataArray.length; i += 2) { const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4; diff --git a/src/documentation/oscilloscope.ts b/src/documentation/oscilloscope.ts index d1103d9..4c7eea0 100644 --- a/src/documentation/oscilloscope.ts +++ b/src/documentation/oscilloscope.ts @@ -14,6 +14,8 @@ scope({ enabled: true, // off by default color: "#fdba74", // any valid CSS color or "random" thickness: 4, // stroke thickness + offsetY: 0, // Horizontal offset + offsetX: 0, // Vertical offset fftSize: 256, // multiples of 128 orientation: "horizontal", // "vertical" or "horizontal" mode: "scope" | "3D" | "freqscope", // scope mode diff --git a/src/main.ts b/src/main.ts index ee0aa95..45904ac 100644 --- a/src/main.ts +++ b/src/main.ts @@ -76,6 +76,8 @@ export class Editor { refresh: 1, fftSize: 1024, orientation: "horizontal", + offsetX: 0, + offsetY: 0, mode: "scope", size: 1, };