Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add strudel fft to hydra #310

Merged
merged 12 commits into from
Jan 1, 2025
52 changes: 47 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ run Flok in secure mode by passing the `--secure` parameter:

```sh
npx flok-web@latest --secure
```
```

#### Note about remote users (not LAN)

Expand Down Expand Up @@ -159,7 +159,7 @@ object, like this:

#### Sardine

Use `flok-repl` with the `-t sardine` parameter. In order to make it work,
Use `flok-repl` with the `-t sardine` parameter. In order to make it work,
the `sardine` REPL must be included to your PATH. It should already be the
case if you followed a regular install.

Expand Down Expand Up @@ -217,14 +217,56 @@ installing and using it.

#### Hydra

[Hydra](https://hydra.ojack.xyz/) is a video synth and coding environment, inspired in
analog video synthesis, that runs directly in the browser and is already included in
[Hydra](https://hydra.ojack.xyz/) is a video synth and coding environment, inspired in
analog video synthesis, that runs directly in the browser and is already included in
the web App. You don't need to install anything as it runs on the browser. Just use
the `hydra` target to execute Hydra code.

You can also use [p5.js](https://p5js.org/) within a `hydra` target, like you would in
You can also use [p5.js](https://p5js.org/) within a `hydra` target, like you would in
the official Hydra editor.

##### `fft()` function

The `fft()` function is a special function that allows you to get the FFT data
from web targets.

**Note: Only Strudel is supported at the moment.**

**You can disable the FFT visualizer in the display settings. This might help with performance.**

```ts
fft(index: number,
buckets: number = 8,
options?: { min?: number; max?: number, scale?: number, analyzerId?: string }): number
```

Parameters:
- `index: number` : The index of the bucket to return the value from.
- `buckets: number`: The number of buckets to combine the underlying FFT data
too. Defaults to 8.
- `options?: { min?: number; max?: number, scale?: number }`:
- `min?: number`: Minimum clamp value of the underlying data. Defaults to
-150.
- `max?: number`: Maximum clamp value of the underlying data. Defaults to 0.
- `scale?: number`: Scale of the output. Defaults to 1 (so the output is
from 0 to 1)
- `analyzerId?: string`: Which Strudel analyser to listen to. Defaults to
`flok-master`, which is also automatically added to all strudel patterns.
Can be used to route different patterns to different parts of the hydra
visualiser

Example:
```js
solid(() => fft(0,1), 0)
.mask(shape(5,.05))
.rotate(() => 50 * fft(0, 40)) // we need to supply a function
// for the parameter, for it to update automaticaly.
```

**Caveat**: Because of how we setup the analyze node on Strudel, every Strudel pane
needs a re-eval after the Hydra code decides that we need to get the fft data.
This does not happen automatically, manual re-eval is necessary.

#### Mercury

[Mercury](https://github.com/tmhglnd/mercury) is a minimal and human readable
Expand Down
20 changes: 20 additions & 0 deletions packages/web/src/components/display-settings-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,26 @@ export default function DisplaySettingsDialog({
}
/>
</div>
<div className="">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Use FFT for visualization
</Label>
<input
id="showCanvas"
type="checkbox"
checked={unsavedSettings.enableFft ?? true}
className="w-5"
onChange={(e) =>
sanitizeAndSetUnsavedSettings({
...unsavedSettings,
enableFft: e.target.checked,
})
}
/>
</div>
<p>You need to reload the page to apply changes to fft</p>
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/lib/display-settings.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export interface DisplaySettings {
canvasPixelSize: number;
showCanvas: boolean;
enableFft: boolean;
}

export const defaultDisplaySettings: DisplaySettings = {
canvasPixelSize: 1,
showCanvas: true,
enableFft: true,
}

export function sanitizeDisplaySettings(settings: DisplaySettings): DisplaySettings {
Expand All @@ -16,6 +18,7 @@ export function sanitizeDisplaySettings(settings: DisplaySettings): DisplaySetti
// canvas; should be low enough
const maxPixelSize = 50;


return {
...settings,
canvasPixelSize: Math.max(
Expand Down
49 changes: 49 additions & 0 deletions packages/web/src/lib/hydra-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import Hydra from "hydra-synth";
import { isWebglSupported } from "@/lib/webgl-detector.js";
import {DisplaySettings} from "@/lib/display-settings.ts";

declare global {
interface Window {
global: Window;
src: Function;
H: Function;
P5: Function;
fft: (index: number, buckets: number) => number;
}
}

Expand All @@ -19,19 +21,27 @@ export class HydraWrapper {
protected _hydra: any;
protected _onError: ErrorHandler;
protected _onWarning: ErrorHandler;
protected _displaySettings: DisplaySettings;

constructor({
canvas,
onError,
onWarning,
displaySettings,
}: {
canvas: HTMLCanvasElement;
onError?: ErrorHandler;
onWarning?: ErrorHandler;
displaySettings: DisplaySettings;
}) {
this._canvas = canvas;
this._onError = onError || (() => {});
this._onWarning = onWarning || (() => {});
this._displaySettings = displaySettings;
}

setDisplaySettings(displaySettings: DisplaySettings) {
this._displaySettings = displaySettings;
}

async initialize() {
Expand Down Expand Up @@ -64,6 +74,45 @@ export class HydraWrapper {

window.H = this._hydra;

const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max);


// Enables Hydra to use Strudel frequency data
// with `.scrollX(() => fft(1,0)` it will influence the x-axis, according to the fft data
// first number is the index of the bucket, second is the number of buckets to aggregate the number too
window.fft = (index: number, buckets: number = 8, options?: { min?: number; max?: number, scale?: number, analyzerId?: string }) => {
const analyzerId = options?.analyzerId ?? "flok-master"
const min = options?.min ?? -150;
const scale = options?.scale ?? 1
const max = options?.max ?? 0

// Strudel is not initialized yet, so we just return a default value
if(window.strudel == undefined) return .5;

// If display settings are not enabled, we just return a default value
if(!(this._displaySettings.enableFft ?? true)) return .5;

// Enable auto-analyze
window.strudel.enableAutoAnalyze = true;

// If the analyzerId is not defined, we just return a default value
if(window.strudel.webaudio.analysers[analyzerId] == undefined) {
return .5
}

const freq = window.strudel.webaudio.getAnalyzerData("frequency", analyzerId) as Array<number>;
const bucketSize = (freq.length) / buckets

// inspired from https://github.com/tidalcycles/strudel/blob/a7728e3d81fb7a0a2dff9f2f4bd9e313ddf138cd/packages/webaudio/scope.mjs#L53
const normalized = freq.map((it: number) => {
const norm = clamp((it - min) / (max - min), 0, 1);
return norm * scale;
})

return normalized.slice(bucketSize * index, bucketSize * (index + 1))
.reduce((a, b) => a + b, 0) / bucketSize
}

this.initialized = true;
console.log("Hydra initialized");
}
Expand Down
24 changes: 21 additions & 3 deletions packages/web/src/lib/strudel-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ export class StrudelWrapper {
protected _docPatterns: any;
protected _audioInitialized: boolean;
protected framer?: any;
protected webaudio?: any;

enableAutoAnalyze = false;
hapAnalyzeSnippet = `
all(x =>
x.fmap(hap => {
if(hap.analyze == undefined) {
hap.analyze = 'flok-master';
}
return hap
})
)
`;

constructor({
onError,
Expand All @@ -49,6 +62,9 @@ export class StrudelWrapper {

async importModules() {
// import desired modules and add them to the eval scope

this.webaudio = await import("@strudel/webaudio");

await evalScope(
import("@strudel/core"),
import("@strudel/midi"),
Expand All @@ -57,7 +73,7 @@ export class StrudelWrapper {
import("@strudel/osc"),
import("@strudel/serial"),
import("@strudel/soundfonts"),
import("@strudel/webaudio"),
this.webaudio,
controls
);
try {
Expand Down Expand Up @@ -145,12 +161,14 @@ export class StrudelWrapper {
}
}


async tryEval(msg: EvalMessage) {
if (!this.initialized) await this.initialize();
try {
const { body: code, docId } = msg;
const {body: code, docId} = msg;
// little hack that injects the docId at the end of the code to make it available in afterEval
const pattern = await this._repl.evaluate(`${code}//${docId}`);
// also add ann analyser node to all patterns, for fft data in hydra
const pattern = await this._repl.evaluate(`${code}\n${this.enableAutoAnalyze ? this.hapAnalyzeSnippet : ""}\n//${docId}`);
if (pattern) {
this._docPatterns[docId] = pattern.docId(docId); // docId is needed for highlighting
const allPatterns = stack(...Object.values(this._docPatterns));
Expand Down
6 changes: 6 additions & 0 deletions packages/web/src/routes/frames/hydra.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function Component() {
onWarning: (msg) => {
sendToast("warning", "Hydra warning", msg);
},
displaySettings: displaySettings,
});

await hydra.initialize();
Expand All @@ -56,9 +57,14 @@ export function Component() {
useAnimationFrame(
useCallback(() => {
window.m = window.parent?.mercury?.m;
window.strudel = window.parent?.strudel?.strudel;
}, [])
);

useEffect(() => {
instance?.setDisplaySettings(displaySettings);
}, [displaySettings]);

useEvalHandler(
useCallback(
(msg) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/routes/frames/strudel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export function Component() {
}
};

window.strudel = instance;

window.addEventListener("message", handleWindowMessage);

return () => {
Expand Down
Loading