First version of freqscope

This commit is contained in:
2023-11-14 17:04:59 +01:00
parent 02fc13803f
commit 15bfd6c5eb
3 changed files with 82 additions and 21 deletions

View File

@ -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();
};

View File

@ -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 <ic>scope()</ic> 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 :)

View File

@ -76,7 +76,7 @@ export class Editor {
refresh: 1,
fftSize: 1024,
orientation: "horizontal",
is3D: false,
mode: "scope",
size: 1,
};