Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
amiika committed Nov 16, 2023
2 parents 9c362d6 + b070dd9 commit 76145a0
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 26 deletions.
120 changes: 100 additions & 20 deletions src/AudioVisualisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ export interface OscilloscopeConfig {
thickness: number;
fftSize: number; // multiples of 256
orientation: "horizontal" | "vertical";
is3D: boolean;
mode: "3D" | "scope" | "freqscope";
offsetX: number;
offsetY: 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.
Expand All @@ -140,14 +142,78 @@ 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(
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);

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,
y = 0;

for (let i = 0; i < numBars; i++) {
barHeight = Math.floor(
freqDataArray[i] * ((height / 256) * app.osc.size)
);

// Create gradient based on orientation
let gradient;
if (app.osc.orientation === "horizontal") {
gradient = canvasCtx.createLinearGradient(0, 0, width / 2, 0);
} else {
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;

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) {
Expand All @@ -156,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;
}

Expand All @@ -166,15 +237,18 @@ 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);
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") {
Expand All @@ -184,38 +258,45 @@ 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;
}
}
}


if (app.osc.is3D) {
if (app.osc.mode === "freqscope") {
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;
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;
Expand All @@ -226,7 +307,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;
Expand All @@ -243,6 +324,5 @@ export const runOscilloscope = (
canvasCtx.globalAlpha = 1.0;
}


draw();
};
33 changes: 28 additions & 5 deletions src/documentation/oscilloscope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,44 @@ 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
offsetY: 0, // Horizontal offset
offsetX: 0, // Vertical offset
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 :)
Expand Down
4 changes: 3 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export class Editor {
refresh: 1,
fftSize: 1024,
orientation: "horizontal",
is3D: false,
offsetX: 0,
offsetY: 0,
mode: "scope",
size: 1,
};

Expand Down

0 comments on commit 76145a0

Please sign in to comment.