Skip to content

Instantly share code, notes, and snippets.

@leostera
Created August 23, 2022 10:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leostera/fc98ea8b72fef8d724c5a4fbf49c81b0 to your computer and use it in GitHub Desktop.
Save leostera/fc98ea8b72fef8d724c5a4fbf49c81b0 to your computer and use it in GitHub Desktop.
import React, { useState, useCallback, useMemo} from 'react';
import ReactFlow, {
ReactFlowProvider,
useNodesState,
useEdgesState,
addEdge,
useReactFlow,
Handle, Position,
} from 'react-flow-renderer';
import './main.css';
const AudioContext = window.AudioContext || window.webkitAudioContext;
window.audioCtx = null
// Audio Graph
const NODES = {};
const EDGES = {};
function Oscilloscope({ width, height, audioAnalyser }) {
let canvasRef = React.useRef(null);
let [tick, setTick] = React.useState(0);
React.useLayoutEffect(() => {
let timerId
const animate = () => {
setTick(t => t + 1)
timerId = requestAnimationFrame(animate)
}
timerId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(timerId)
})
React.useEffect(() => {
const bufferLength = audioAnalyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
audioAnalyser.getByteFrequencyData(dataArray);
const canvas = canvasRef.current
const canvasCtx = canvas.getContext('2d')
canvasCtx.fillStyle = 'rgb(0, 0, 0)';
canvasCtx.fillRect(0, 0, width, height);
var barWidth = (width / bufferLength) * 2.5;
var barHeight;
var x = 0;
for(var i = 0; i < bufferLength; i++) {
barHeight = dataArray[i];
canvasCtx.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';
canvasCtx.fillRect(x,height-barHeight/2,barWidth,barHeight/2);
x += barWidth + 1;
}
}, [tick])
return <canvas ref={canvasRef} width={width} height={height} />
}
function AudioSourceNode(props) {
NODES[props.id] = NODES[props.id] || {};
let node = NODES[props.id]
let [devices, setDevices] = React.useState([]);
audioAnalyser.minDecibels = -90;
audioAnalyser.maxDecibels = 10;
audioAnalyser.smoothingTimeConstant = 0.85;
audioAnalyser.fftSize = 256;
React.useEffect(() => {
if (devices.length === 0) {
navigator.mediaDevices.enumerateDevices().then( devices => {
let audioDevices = devices.filter(d => d.kind === "audioinput").reduce((acc, d) => {
acc[d.deviceId] = d;
return acc
}, {});
setDevices(_ => audioDevices)
})
}
}, [setDevices]);
const onChange = useCallback((evt) => {
let deviceId = evt.target.value;
navigator.mediaDevices.getUserMedia({ audio: { deviceId } })
.then(stream => {
let tracks = stream.getAudioTracks();
let source = audioCtx.createMediaStreamSource(stream);
let gainNode = audioCtx.createGain();
source.connect(audioAnalyser);
audioAnalyser.connect(audioCtx.destination);
console.log("stream", tracks[0].enabled)
console.log("source", source)
})
node.deviceId = deviceId;
console.log(node);
}, [devices]);
return (
<div>
<span htmlFor="device">Device: </span>
<select id="text" name="device" onChange={onChange}>
{ Object.entries(devices).map(([id, dev]) => {
return <option key={id} value={id}> {dev.label} </option>
}) }
</select>
<br />
<Oscilloscope width={400} height={120} audioAnalyser={audioAnalyser} />
<Handle type="source" position={Position.Bottom} id="a" />
<Handle type="source" position={Position.Bottom} id="b" />
</div>
);
}
const flowKey = 'example-flow';
const getNodeId = () => `randomnode_${+new Date()}`;
const initialNodes = [
{ id: '1', type: 'AudioSourceNode', data: { label: 'Node 1' }, position: { x: 100, y: 100 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 500 } },
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
const SaveRestore = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [rfInstance, setRfInstance] = useState(null);
const { setViewport } = useReactFlow();
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const onSave = useCallback(() => {
if (rfInstance) {
const flow = rfInstance.toObject();
localStorage.setItem(flowKey, JSON.stringify(flow));
}
}, [rfInstance]);
const onRestore = useCallback(() => {
const restoreFlow = async () => {
const flow = JSON.parse(localStorage.getItem(flowKey));
if (flow) {
const { x = 0, y = 0, zoom = 1 } = flow.viewport;
setNodes(flow.nodes || []);
setEdges(flow.edges || []);
setViewport({ x, y, zoom });
}
};
restoreFlow();
}, [setNodes, setViewport]);
const onAdd = useCallback(() => {
const newNode = {
id: getNodeId(),
data: { label: 'Added node' },
position: {
x: Math.random() * window.innerWidth - 100,
y: Math.random() * window.innerHeight,
},
};
setNodes((nds) => nds.concat(newNode));
}, [setNodes]);
const nodeTypes = useMemo(() => ({ AudioSourceNode, }), []);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={setRfInstance}
nodeTypes={nodeTypes}
>
<div className="save__controls">
<button onClick={onSave}>save</button>
<button onClick={onRestore}>restore</button>
<button onClick={onAdd}>add node</button>
</div>
</ReactFlow>
);
};
import * as ReactDOM from 'react-dom';
let started = false;
window.addEventListener("click", () => {
if (started) return;
started = true;
window.audioCtx = audioCtx || new AudioContext();
window.audioAnalyser = audioCtx.createAnalyser();
ReactDOM.render(
<ReactFlowProvider>
<SaveRestore />
</ReactFlowProvider>,
document.getElementById("app"));
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment