First version of freqscope
This commit is contained in:
@ -122,11 +122,11 @@ export interface OscilloscopeConfig {
|
|||||||
thickness: number;
|
thickness: number;
|
||||||
fftSize: number; // multiples of 256
|
fftSize: number; // multiples of 256
|
||||||
orientation: "horizontal" | "vertical";
|
orientation: "horizontal" | "vertical";
|
||||||
is3D: boolean;
|
mode: "3D" | "scope" | "freqscope";
|
||||||
size: number;
|
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.
|
* Initializes and runs an oscilloscope using an AnalyzerNode.
|
||||||
@ -140,12 +140,47 @@ export const runOscilloscope = (
|
|||||||
let config = app.osc;
|
let config = app.osc;
|
||||||
let analyzer = getAnalyser(config.fftSize);
|
let analyzer = getAnalyser(config.fftSize);
|
||||||
let dataArray = new Float32Array(analyzer.frequencyBinCount);
|
let dataArray = new Float32Array(analyzer.frequencyBinCount);
|
||||||
|
let freqDataArray = new Uint8Array(analyzer.frequencyBinCount);
|
||||||
const canvasCtx = canvas.getContext("2d")!;
|
const canvasCtx = canvas.getContext("2d")!;
|
||||||
const WIDTH = canvas.width;
|
const WIDTH = canvas.width;
|
||||||
const HEIGHT = canvas.height;
|
const HEIGHT = canvas.height;
|
||||||
let lastDrawTime = 0;
|
let lastDrawTime = 0;
|
||||||
let frameInterval = 1000 / 30;
|
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() {
|
function draw() {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
@ -166,8 +201,7 @@ export const runOscilloscope = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
analyzer.getFloatTimeDomainData(dataArray);
|
analyzer.getFloatTimeDomainData(dataArray);
|
||||||
canvasCtx.globalCompositeOperation = 'source-over';
|
canvasCtx.globalCompositeOperation = "source-over";
|
||||||
|
|
||||||
|
|
||||||
canvasCtx.fillStyle = "rgba(0, 0, 0, 0)";
|
canvasCtx.fillStyle = "rgba(0, 0, 0, 0)";
|
||||||
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
|
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||||
@ -184,23 +218,26 @@ export const runOscilloscope = (
|
|||||||
} else {
|
} else {
|
||||||
canvasCtx.strokeStyle = app.osc.color;
|
canvasCtx.strokeStyle = app.osc.color;
|
||||||
}
|
}
|
||||||
const remainingRefreshTime = app.clock.time_position.pulse % app.osc.refresh;
|
const remainingRefreshTime =
|
||||||
const opacityRatio = 1 - (remainingRefreshTime / app.osc.refresh);
|
app.clock.time_position.pulse % app.osc.refresh;
|
||||||
|
const opacityRatio = 1 - remainingRefreshTime / app.osc.refresh;
|
||||||
canvasCtx.globalAlpha = opacityRatio;
|
canvasCtx.globalAlpha = opacityRatio;
|
||||||
canvasCtx.beginPath();
|
canvasCtx.beginPath();
|
||||||
|
|
||||||
|
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
for (let i = 1; i < dataArray.length; ++i) {
|
for (let i = 1; i < dataArray.length; ++i) {
|
||||||
let currentType = null;
|
let currentType = null;
|
||||||
if (dataArray[i] >= 0 && dataArray[i - 1] < 0) {
|
if (dataArray[i] >= 0 && dataArray[i - 1] < 0) {
|
||||||
currentType = 'negToPos';
|
currentType = "negToPos";
|
||||||
} else if (dataArray[i] < 0 && dataArray[i - 1] >= 0) {
|
} else if (dataArray[i] < 0 && dataArray[i - 1] >= 0) {
|
||||||
currentType = 'posToNeg';
|
currentType = "posToNeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentType) {
|
if (currentType) {
|
||||||
if (lastZeroCrossingType === null || currentType === lastZeroCrossingType) {
|
if (
|
||||||
|
lastZeroCrossingType === null ||
|
||||||
|
currentType === lastZeroCrossingType
|
||||||
|
) {
|
||||||
startIndex = i;
|
startIndex = i;
|
||||||
lastZeroCrossingType = currentType;
|
lastZeroCrossingType = currentType;
|
||||||
break;
|
break;
|
||||||
@ -208,14 +245,18 @@ export const runOscilloscope = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (app.osc.mode === "freqscope") {
|
||||||
if (app.osc.is3D) {
|
drawFrequencyScope();
|
||||||
|
} else if (app.osc.mode === "3D") {
|
||||||
for (let i = startIndex; i < dataArray.length; i += 2) {
|
for (let i = startIndex; i < dataArray.length; i += 2) {
|
||||||
const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4;
|
const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4;
|
||||||
const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4;
|
const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4;
|
||||||
i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y);
|
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 sliceWidth = (WIDTH * 1.0) / dataArray.length;
|
||||||
const yOffset = HEIGHT / 4;
|
const yOffset = HEIGHT / 4;
|
||||||
let x = 0;
|
let x = 0;
|
||||||
@ -226,7 +267,7 @@ export const runOscilloscope = (
|
|||||||
x += sliceWidth;
|
x += sliceWidth;
|
||||||
}
|
}
|
||||||
canvasCtx.lineTo(WIDTH, yOffset);
|
canvasCtx.lineTo(WIDTH, yOffset);
|
||||||
} else {
|
} else if (app.osc.mode === "scope" && app.osc.orientation === "vertical") {
|
||||||
const sliceHeight = (HEIGHT * 1.0) / dataArray.length;
|
const sliceHeight = (HEIGHT * 1.0) / dataArray.length;
|
||||||
const xOffset = WIDTH / 4;
|
const xOffset = WIDTH / 4;
|
||||||
let y = 0;
|
let y = 0;
|
||||||
@ -243,6 +284,5 @@ export const runOscilloscope = (
|
|||||||
canvasCtx.globalAlpha = 1.0;
|
canvasCtx.globalAlpha = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
draw();
|
draw();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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.
|
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(
|
${makeExample(
|
||||||
"Oscilloscope configuration",
|
"Oscilloscope configuration",
|
||||||
`
|
`
|
||||||
scope({
|
scope({
|
||||||
enabled: true, // off by default
|
enabled: true, // off by default
|
||||||
color: "#fdba74", // any valid CSS color or "random"
|
color: "#fdba74", // any valid CSS color or "random"
|
||||||
thickness: 4, // stroke thickness
|
thickness: 4, // stroke thickness
|
||||||
fftSize: 256, // multiples of 128
|
fftSize: 256, // multiples of 128
|
||||||
orientation: "horizontal", // "vertical" or "horizontal"
|
orientation: "horizontal", // "vertical" or "horizontal"
|
||||||
is3D: false, // 3D oscilloscope
|
mode: "scope" | "3D" | "freqscope", // scope mode
|
||||||
size: 1, // size of the oscilloscope
|
size: 1, // size of the oscilloscope
|
||||||
refresh: 1 // refresh rate (in pulses)
|
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 :)
|
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 :)
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export class Editor {
|
|||||||
refresh: 1,
|
refresh: 1,
|
||||||
fftSize: 1024,
|
fftSize: 1024,
|
||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
is3D: false,
|
mode: "scope",
|
||||||
size: 1,
|
size: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user