From f86f2c3e3331248448c2043930569f200fb6f5a6 Mon Sep 17 00:00:00 2001 From: kgalvin Date: Thu, 9 Nov 2023 11:15:36 -0800 Subject: [PATCH] convert negative radians into degrees --- src/components/ContourAudioControls.tsx | 142 ++++++++++++++++++++++++ src/components/ContourAudioEngine.tsx | 139 +++++++++++++++++++++++ src/components/FractalPlayer.tsx | 129 ++++++++++++++------- src/utils/MarchingSquares.js | 64 ++++------- 4 files changed, 391 insertions(+), 83 deletions(-) create mode 100644 src/components/ContourAudioControls.tsx create mode 100644 src/components/ContourAudioEngine.tsx diff --git a/src/components/ContourAudioControls.tsx b/src/components/ContourAudioControls.tsx new file mode 100644 index 0000000..b335744 --- /dev/null +++ b/src/components/ContourAudioControls.tsx @@ -0,0 +1,142 @@ +import ContourAudioEngine from "@/components/ContourAudioEngine"; +import WebRenderer from "@elemaudio/web-renderer"; +import dynamic from "next/dynamic"; +import React, {useEffect, useState} from "react"; +import styled from "styled-components"; +import {AudioParamsType, KnobRow} from "@/components/FractalPlayer"; + +type ContourAudioControlsProps = { + rowIndex: number; + fractalRow: number[]; + audioContext: AudioContext | null; + core: WebRenderer; + playing: boolean; +} + +const ContourAudioControls: React.FC = ( + { + rowIndex, + fractalRow, + audioContext, + core, + playing, + }) => { + + const [volume, setVolume] = useState(0); + const [threshold, setThreshold] = useState(0.09); + const [lowest, setLowest] = useState(200); + const [highest, setHighest] = useState(6000); + const [smoothing, setSmoothing] = useState(0.02); + const [audioParams, setAudioParams] = useState({ + volume: 0, + threshold: 0, + highest: 0, + lowest: 0, + smoothing: 0, + }); + + useEffect(() => { + setAudioParams({volume, lowest, highest, threshold, smoothing}); + }, [volume, lowest, highest, threshold, smoothing, setAudioParams]); + + return (<> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const Knob = dynamic(() => import("el-vis-audio").then((mod) => mod.KnobParamLabel), + {ssr: false} +) + +const ControlKnob = styled.div` + margin: -0.5rem 0.4rem 0 0.4rem; +`; + +export default ContourAudioControls; \ No newline at end of file diff --git a/src/components/ContourAudioEngine.tsx b/src/components/ContourAudioEngine.tsx new file mode 100644 index 0000000..1386c9d --- /dev/null +++ b/src/components/ContourAudioEngine.tsx @@ -0,0 +1,139 @@ +import {AudioParamsType} from "@/components/FractalPlayer"; +import dynamic from "next/dynamic"; +import React, {useEffect, useState} from "react"; +import {el, NodeRepr_t} from "@elemaudio/core"; +import WebRenderer from "@elemaudio/web-renderer"; +import styled from "styled-components"; + +const OscilloscopeSpectrogram = dynamic(() => import('el-vis-audio').then((mod) => mod.OscilloscopeSpectrogram), {ssr: false}); + +require("events").EventEmitter.defaultMaxListeners = 0; + +type AudioEngineProps = { + rowIndex: number; + fractalRow: number[]; + audioContext: AudioContext | null; + core: WebRenderer; + playing: boolean; + audioParams: AudioParamsType; +} + +const AudioEngine: React.FC = ({ + rowIndex, + fractalRow, + audioContext, + core, + playing, + audioParams: {volume, lowest, highest, threshold, smoothing} + }) => { + + const [audioVizData, setAudioVizData] = useState(); + const [fftVizData, setFftVizData] = useState(); + const [ampScale, setAmpScale] = useState(0); + + useEffect(() => { + return () => { + if (audioContext) { + audioContext.suspend(); + } + } + }, [audioContext]); + + useEffect(() => { + if (audioContext) { + const SignalSynth = (signal: NodeRepr_t) => { + if (playing && signal && core) { + let synth = el.mul( + signal, + el.sm(el.const({key: `main-amp`, value: volume})) + ) as NodeRepr_t; + synth = el.scope({name: `scope-contour`}, synth); + synth = el.fft({name: `fft-contour`}, synth); + core.render(synth, synth); + } + }; + + const Resynth = () => { + let accum = 0; + const linearRange = Math.log10(highest) - Math.log10(lowest); + const linearInterval = linearRange / fractalRow.length; + + const allVoices = [...Array(fractalRow.length)].map((_, i) => { + const amplitude = fractalRow[i] > threshold ? fractalRow[i] : 0; + const key = `osc-freq-${i}`; + const linearFreq = Math.log10(lowest) + (i * linearInterval); + const freq = 10 ** linearFreq; + + if (freq < audioContext.sampleRate / 2) { + accum += amplitude; + const freqSignal = el.const({key, value: freq}); + const ampSignal = el.const({key: `osc-amp-${i}`, value: amplitude}); + const smoothFreqSignal = el.smooth(el.tau2pole(smoothing), freqSignal); + const smoothAmpSignal = el.smooth(el.tau2pole(smoothing), ampSignal); + return el.mul(el.cycle(smoothFreqSignal), smoothAmpSignal); + } else { + return el.const({key, value: 0}); + } + }); + + const addMany = (ins: NodeRepr_t[]): NodeRepr_t => { + return el.add(...ins) as NodeRepr_t; + }; + const rowMult = accum !== 0 ? 1 / accum : 0; + setAmpScale(rowMult); + const rowAmp = el.const({key: "row-gain", value: rowMult}); + const smoothRowAmp = el.smooth(el.tau2pole(smoothing), rowAmp); + return el.mul(addMany(allVoices as NodeRepr_t[]), smoothRowAmp) as NodeRepr_t; + } + + if (playing) { + audioContext.resume(); + if (fractalRow?.length > 0) SignalSynth(Resynth()); + } else { + audioContext.suspend(); + } + } + }, [playing, volume, lowest, highest, threshold, fractalRow, audioContext, core, smoothing]); + + core?.on("scope", function (e) { + if (e.source === `scope-contour`) { + e.data.length && setAudioVizData(e.data[0]); + } + }); + + core?.on("fft", function (e) { + if (e.source === `fft-contour`) { + setFftVizData(e.data.real); + } + }); + + return ( + + + +




+
+
+ ); +}; + +const FlexColumn = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding-left: 0.5rem; +`; + +const StyledOscilloscopeSpectrogram = styled.div` + outline: 1px solid #000000; + font-size: 1.5rem; + width: 156px; + height: 38px; + cursor: pointer; +`; +export default AudioEngine; \ No newline at end of file diff --git a/src/components/FractalPlayer.tsx b/src/components/FractalPlayer.tsx index 9eaeac8..b1baa70 100644 --- a/src/components/FractalPlayer.tsx +++ b/src/components/FractalPlayer.tsx @@ -3,6 +3,7 @@ import ColoringAlgorithm from "@/components/ColoringAlgorithm"; import PlayheadDataControls from "@/PlayheadDataControls"; import PlayheadOSCControls from "@/components/PlayheadOSCControls"; import PlayheadAudioControls from "@/components/PlayheadAudioControls"; +import ContourAudioControls from "@/components/ContourAudioControls"; import PlayheadData from "@/components/PlayheadData"; import Playheads from "@/components/Playheads"; import Transport from "@/components/Transport"; @@ -83,6 +84,7 @@ const FractalPlayer: React.FC = ({ const [rowIndex, setRowIndex] = useState(0); const [fractalLoop, setFractalLoop] = useState(true); const [tolerance, setTolerance] = useState(0); + const [contour, setContour] = useState<{ angle: number; duration: number; }[]>([]); const fractalCanvasRef = useRef(null); const fractalContourCanvasRef = useRef(null); @@ -102,7 +104,11 @@ const FractalPlayer: React.FC = ({ if (fractalContourCanvasRef.current) { canvasCtxRef.current = fractalContourCanvasRef.current.getContext('2d'); let ctx = canvasCtxRef.current; - if (ctx && rawFractalData.length) new MarchingSquares(ctx, {inputValues: rawFractalData, tolerance, cx, cy}); + if (ctx && rawFractalData.length) { + const newContour = new MarchingSquares(ctx, {inputValues: rawFractalData, tolerance, cx, cy}); + console.log('newContour', newContour.soundControlList); + setContour(newContour.soundControlList); + } } console.log("marching squares"); } @@ -119,13 +125,25 @@ const FractalPlayer: React.FC = ({ useEffect(() => { if (fractalTransport === 'play') { setPlaying(true); - playFractal(); + if (fractal === 'julia' && showContour) { + playContour(); + } else { + playFractal(); + } } else if (fractalTransport === 'stop') { setPlaying(false); - stopFractal(); + if (fractal === 'julia' && showContour) { + // stopContour(); + } else { + stopFractal(); + } } else if (fractalTransport === 'pause') { setPlaying(false); - pauseFractal(); + if (fractal === 'julia' && showContour) { + // pauseContour(); + } else { + pauseFractal(); + } } else if (fractalTransport === 'replay') { setFractalTransport('play'); } @@ -201,6 +219,18 @@ const FractalPlayer: React.FC = ({ } }; + const playContour = async () => { + for (let i = 0; i < contour.length; i++) { + setTimeout(function () { + + // play sine wave and adjust freq according to the countour[i].angle + + + }, (fractalSpeed * contour[i].duration)); + } + }; + + const setJuliaComplexNumberByClick = useCallback((e: any) => { setComplex(e); }, [size, size, fractalWindow]); @@ -276,14 +306,15 @@ const FractalPlayer: React.FC = ({ loop={fractalLoop} setLoop={setFractalLoop} /> - {playType === 'osc' ? ( - - ) : (<> + {!showContour && ( + (playType === 'osc') ? ( + + ) : ( = ({ playing={playing} setPlayType={setPlayType} /> - + ) )} = ({ {fractal === 'julia' && ( - - setShowContour(!showContour)} selected={showContour} width={"8rem"} - height={"3rem"} - color={'#0066FF'}> - {showContour ? "Hide" : "Show"} Contour Tracer - - {showContour && - <> - - - - -

cx: {cx}
- cy: {cy}
- download link -

- } -
+ <> + + setShowContour(!showContour)} selected={showContour} width={"8rem"} + height={"3rem"} + color={'#0066FF'}> + {showContour ? "Hide" : "Show"} Contour Tracer + + {showContour && + <> + +

cx: {cx}
+ cy: {cy}
+ download link +

+ } +
+ {showContour && ( + + + + + )} + )} @@ -486,6 +528,7 @@ export const Label = styled.label` `; export const KnobRow = styled.div` + display: flex; margin: 0.5rem 0 0 0; display: flex; flex-direction: row; diff --git a/src/utils/MarchingSquares.js b/src/utils/MarchingSquares.js index 76b55a8..d1faa3d 100644 --- a/src/utils/MarchingSquares.js +++ b/src/utils/MarchingSquares.js @@ -10,6 +10,7 @@ export class MarchingSquares { this.contour = []; this.contourX = []; this.contourY = []; + this.soundControlList = []; this.startingPoint = { x: 0, y: Math.floor(this.inputValues?.length / 2) @@ -24,28 +25,28 @@ export class MarchingSquares { const {nextPoint, pointToWrite} = this.traceContour(tracePoint.x, tracePoint.y); - if (!nextPoint || (nextPoint.y > this.inputValues.length || nextPoint.x > this.inputValues[0].length || nextPoint.x < 0 || nextPoint.y < 0)) whileFlag = true; + if (!nextPoint || (nextPoint.y > this.inputValues.length - 1 || nextPoint.x > this.inputValues[0].length - 1 || nextPoint.x < 0 || nextPoint.y < 0)) whileFlag = true; if (JSON.stringify(pointToWrite) === JSON.stringify(this.contour[0])) whileFlag = true; - if (pointToWrite !== null) { - this.contour.push(pointToWrite); - let newX = (pointToWrite.x / (this.inputValues[0].length / 2)) - 1.0; - this.contourX.push(newX); - let newY = (pointToWrite.y / (this.inputValues.length / 2)) - 1.0; - this.contourY.push(newY); + if (!whileFlag) { + if (pointToWrite !== null) { + this.contour.push(pointToWrite); + let newX = (pointToWrite.x / (this.inputValues[0].length / 2)) - 1.0; + this.contourX.push(newX); + let newY = (pointToWrite.y / (this.inputValues.length / 2)) - 1.0; + this.contourY.push(newY); + } + tracePoint = nextPoint; } - tracePoint = nextPoint; } let simplifiedX = []; let simplifiedY = []; const simplified = simplify(this.contour, this.tolerance, true); - const directionList = this.calculateDirectionList(simplified); simplified.forEach((point, index) => { simplifiedX.push((point.x / (this.inputValues[0].length / 2)) - 1.0); simplifiedY.push((point.y / (this.inputValues.length / 2)) - 1.0); }); - let audioContext = new AudioContext(); let audioBuffer = audioContext.createBuffer(2, simplified.length, 44100); let xArray = audioBuffer.getChannelData(0); @@ -69,10 +70,6 @@ export class MarchingSquares { } }); - /*console.log( - "initialized MarchingSquares class for", - args - );*/ this.ctx?.clearRect(0, 0, this.inputValues.length, this.inputValues.length); this.generateContour(); } @@ -82,55 +79,42 @@ export class MarchingSquares { ctx.lineTo(to.x, to.y); } - calculateDirectionList(coords) { - let directions = []; - coords.forEach((coord, index) => { - if (index === 0) { - directions.push(this.calcDir(coords[coords.length - 1], coord, coords[index + 1])); - } else if (index <= coords.length - 2) { - directions.push(this.calcDir(coords[index - 1], coord, coords[index + 1])); - } else if (index === coords.length - 1) { - directions.push(this.calcDir(coords[index - 1], coord, coords[0])); - } - }); - return directions; + calcLength(a, b) { + return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); } calcDir(a, b, c) { - return (Math.atan2(c.y - b.y, c.x - b.x) - Math.atan2(a.y - b.y, a.x - b.x)) * 180 / Math.PI; + return (Math.atan2(c.y - b.y, c.x - b.x) - Math.atan2(a.y - b.y, a.x - b.x)); } drawContour(ctx, pointList) { + let controlList = []; ctx.beginPath(); ctx.lineWidth = 1; - console.log('pointList: ', pointList.length); pointList.forEach((point, index) => { let a, c; let b = point; if (index === 0) { a = pointList[pointList.length - 1]; - c = pointList[index + 1]; - } else if (index <= pointList.length - 2) { + c = pointList[1]; + } else if (index < pointList.length - 1) { a = pointList[index - 1]; c = pointList[index + 1]; } else if (index === pointList.length - 1) { - a = pointList[index - 1]; + a = pointList[pointList.length - 2]; c = pointList[0]; } - const angle = this.calcDir(a, b, c); - const shade = this.getColorFromAngle(angle); + const radians = this.calcDir(a, b, c); + const angle = 180 / Math.PI * (radians < 0 ? radians + (2 * Math.PI) : radians); + console.log('a: ', a, " b: ", b, " c: ", c); + controlList.push({angle, duration: this.calcLength(b, c)}); ctx.beginPath(); ctx.lineWidth = 1; - ctx.strokeStyle = `hsl(${shade}, 100%, 50%)`; + ctx.strokeStyle = `hsl(${angle}, 100%, 50%)`; this.line(ctx, b, c); ctx.stroke(); }); - - } - - getColorFromAngle(angle) { - //return angle < 0 ? angle / 2 + 180 : angle / 2; - return angle + 360 / 2; + this.soundControlList = controlList; } traceContour(x, y) {