Skip to content

Commit 1b4d417

Browse files
committed
audio fixes, mcp changes
1 parent 910da6b commit 1b4d417

File tree

23 files changed

+389
-86
lines changed

23 files changed

+389
-86
lines changed

src/App.jsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,32 +100,41 @@ function AppContent() {
100100
if (savedFlows && savedFlows.flows && savedFlows.flows.length > 0) {
101101
// Convert saved nodes to internal format
102102
// Check if each node is a config node and strip z/x/y if so
103+
// Also filter out runtime-only properties (starting with _) that may have been saved
103104
const internalNodes = (savedFlows.nodes || []).map(node => {
104105
const { id, type, name, z, x, y, wires, streamWires, ...config } = node;
106+
// Filter out runtime-only properties (e.g., _currentValue, _activeButton)
107+
const cleanConfig = Object.fromEntries(
108+
Object.entries(config).filter(([key]) => !key.startsWith('_'))
109+
);
105110
const nodeDef = nodeRegistry.get(type);
106111
const isConfigNode = nodeDef?.category === 'config';
107112
// Config nodes shouldn't have z, x, y - they don't render on canvas
108113
if (isConfigNode) {
109114
return {
110115
_node: { id, type, name: name || '', wires: wires || [] },
111-
...config
116+
...cleanConfig
112117
};
113118
}
114119
return {
115120
_node: {
116121
id, type, name: name || '', z, x: x || 0, y: y || 0, wires: wires || [],
117122
...(streamWires ? { streamWires } : {})
118123
},
119-
...config
124+
...cleanConfig
120125
};
121126
});
122127

123128
// Convert saved config nodes to internal format
124129
const internalConfigNodes = (savedFlows.configNodes || []).map(node => {
125130
const { id, type, name, ...config } = node;
131+
// Filter out runtime-only properties
132+
const cleanConfig = Object.fromEntries(
133+
Object.entries(config).filter(([key]) => !key.startsWith('_'))
134+
);
126135
return {
127136
_node: { id, type, name: name || '' },
128-
...config
137+
...cleanConfig
129138
};
130139
});
131140

src/audio/AudioManager.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,10 +1531,14 @@ class AudioManager {
15311531
/**
15321532
* Handle mainThread calls from runtime nodes
15331533
*/
1534-
handleMainThreadCall(nodeId, action, params) {
1534+
async handleMainThreadCall(nodeId, action, params) {
15351535
switch (action) {
1536-
case 'createAudioNode':
1537-
return this.createNode(nodeId, params.nodeType, params.options);
1536+
case 'createAudioNode': {
1537+
// createNode returns nodeData with non-cloneable AudioNodes
1538+
// Return a simple success indicator instead
1539+
const result = await this.createNode(nodeId, params.nodeType, params.options);
1540+
return result ? { success: true, nodeId } : { success: false };
1541+
}
15381542

15391543
case 'setAudioParam':
15401544
return this.setParam(nodeId, params.param, params.value);
@@ -1650,6 +1654,19 @@ class AudioManager {
16501654
case 'controlStems':
16511655
return this.controlStems(nodeId, params.action, params);
16521656

1657+
case 'getAudioContextState':
1658+
return this.ctx?.state || null;
1659+
1660+
case 'hasActiveSources': {
1661+
// Check if any source nodes are currently playing
1662+
for (const [, nodeData] of this.nodes) {
1663+
if (nodeData.started) {
1664+
return true;
1665+
}
1666+
}
1667+
return false;
1668+
}
1669+
16531670
default:
16541671
logger.warn(`Unknown audio action: ${action}`);
16551672
return null;

src/components/Canvas/AudioPort.jsx

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import { useCallback } from 'react';
33
/**
44
* Audio stream port - RCA jack style with green color
55
* Visual: Circle with center dot (◉) with metallic 3D effect
6+
*
7+
* Nodes can provide custom port rendering via renderStreamPort function.
8+
* If customRender is provided, it receives { index, isOutput, x, y } and should return SVG elements.
9+
* The custom render is placed inside the port group, so coordinates are relative to port position.
610
*/
7-
export function AudioPort({ x, y, isOutput, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave }) {
11+
export function AudioPort({ x, y, isOutput, index, customRender, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave }) {
12+
const hasCustomRender = !!customRender;
813
// Touch handlers that simulate mouse events
914
const handleTouchStart = useCallback((e) => {
1015
if (e.touches.length !== 1) return;
@@ -21,19 +26,21 @@ export function AudioPort({ x, y, isOutput, onMouseDown, onMouseUp, onMouseEnter
2126
onMouseUp?.(fakeEvent);
2227
}, [onMouseUp]);
2328

24-
// Offset to center the port visually - symmetric distance from node body
25-
// Output: start at node edge (x), extend outward. Input: start at x-12, extend into node
26-
const offsetX = isOutput ? x : x - 12;
27-
const gradientId = `audio-port-grad-${isOutput ? 'out' : 'in'}-${x}-${y}`;
29+
// Offset to center the port visually - overlap node body slightly for visual cohesion
30+
// Port is 16px wide, center at 8px. We want ~4px overlap with node edge on both sides.
31+
// Output (right side): x is node right edge, port center should be at x + 4
32+
// Input (left side): x is 0 (node left edge), port center should be at x - 4
33+
const offsetX = isOutput ? x - 4 : x - 12;
34+
const gradientId = `audio-port-grad-${isOutput ? 'out' : 'in'}-${x}-${y}-${hasCustomRender ? 'custom' : 'std'}`;
2835

2936
return (
3037
<g transform={`translate(${offsetX}, ${y - 8})`}>
3138
<defs>
32-
{/* Metallic green gradient for outer ring */}
39+
{/* Metallic green gradient for outer ring - lighter when custom render for better contrast */}
3340
<radialGradient id={gradientId} cx="30%" cy="30%" r="70%">
34-
<stop offset="0%" stopColor="#7dda7d" />
35-
<stop offset="50%" stopColor="#3daa3d" />
36-
<stop offset="100%" stopColor="#1d7a1d" />
41+
<stop offset="0%" stopColor={hasCustomRender ? "#e8f8e8" : "#7dda7d"} />
42+
<stop offset="50%" stopColor={hasCustomRender ? "#b8e8b8" : "#3daa3d"} />
43+
<stop offset="100%" stopColor={hasCustomRender ? "#78c878" : "#1d7a1d"} />
3744
</radialGradient>
3845
</defs>
3946
{/* Larger invisible touch target */}
@@ -61,7 +68,7 @@ export function AudioPort({ x, y, isOutput, onMouseDown, onMouseUp, onMouseEnter
6168
cy={8}
6269
r={7}
6370
fill={`url(#${gradientId})`}
64-
stroke="#1a6a1a"
71+
stroke={hasCustomRender ? "#4a9a4a" : "#1a6a1a"}
6572
strokeWidth={1}
6673
onMouseDown={onMouseDown}
6774
onMouseUp={onMouseUp}
@@ -71,23 +78,27 @@ export function AudioPort({ x, y, isOutput, onMouseDown, onMouseUp, onMouseEnter
7178
onTouchEnd={handleTouchEnd}
7279
style={{ cursor: 'crosshair' }}
7380
/>
74-
{/* Inner ring / hole */}
75-
<circle
76-
cx={8}
77-
cy={8}
78-
r={4}
79-
fill="#0a3a0a"
80-
pointerEvents="none"
81-
/>
82-
{/* Center pin - metallic */}
83-
<circle
84-
className="audio-port-inner"
85-
cx={8}
86-
cy={8}
87-
r={2}
88-
fill="#c0c0c0"
89-
pointerEvents="none"
90-
/>
81+
{/* Inner ring / hole - skip if custom render provides its own content */}
82+
{!hasCustomRender && (
83+
<circle
84+
cx={8}
85+
cy={8}
86+
r={4}
87+
fill="#0a3a0a"
88+
pointerEvents="none"
89+
/>
90+
)}
91+
{/* Center pin - metallic - skip if custom render */}
92+
{!hasCustomRender && (
93+
<circle
94+
className="audio-port-inner"
95+
cx={8}
96+
cy={8}
97+
r={2}
98+
fill="#c0c0c0"
99+
pointerEvents="none"
100+
/>
101+
)}
91102
{/* Highlight */}
92103
<circle
93104
cx={5.5}
@@ -96,6 +107,8 @@ export function AudioPort({ x, y, isOutput, onMouseDown, onMouseUp, onMouseEnter
96107
fill="rgba(255,255,255,0.4)"
97108
pointerEvents="none"
98109
/>
110+
{/* Custom rendering from node definition (icons, labels, etc.) */}
111+
{customRender && customRender({ index, isOutput, x: 8, y: 8 })}
99112
</g>
100113
);
101114
}

src/components/Canvas/NodeShape.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ export function NodeShape({
321321
x={0}
322322
y={yPos}
323323
isOutput={false}
324+
index={i}
325+
customRender={def?.renderStreamPort}
324326
onMouseDown={onStreamPortMouseDown ? (e) => onStreamPortMouseDown(e, i, false) : undefined}
325327
onMouseUp={onStreamPortMouseUp ? (e) => onStreamPortMouseUp(e, i, false) : undefined}
326328
onMouseEnter={onStreamPortMouseEnter ? (e) => onStreamPortMouseEnter(e, i, false) : undefined}
@@ -347,6 +349,8 @@ export function NodeShape({
347349
x={width}
348350
y={yPos}
349351
isOutput={true}
352+
index={i}
353+
customRender={def?.renderStreamPort}
350354
onMouseDown={onStreamPortMouseDown ? (e) => onStreamPortMouseDown(e, i, true) : undefined}
351355
onMouseUp={onStreamPortMouseUp ? (e) => onStreamPortMouseUp(e, i, true) : undefined}
352356
/>

src/components/Canvas/Wire.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,19 @@ export function Wire({ sourceNode, sourcePort, targetNode, targetPort = 0, targe
5858
if (targetNode && !targetPos) {
5959
// Connected to a target node (saved wire)
6060
const targetDef = nodeRegistry.get(targetNode._node.type);
61+
const targetWidth = getNodeWidthWithDef(targetNode, targetDef);
6162
const targetHeight = getNodeHeightWithDef(targetNode, targetDef);
6263
endPos = isStream
63-
? getStreamPortPosition(targetNode, targetPort, false, targetDef, targetHeight)
64-
: getPortPosition(targetNode, targetPort, false, targetHeight, undefined, targetDef);
64+
? getStreamPortPosition(targetNode, targetPort, false, targetDef, targetHeight, targetWidth)
65+
: getPortPosition(targetNode, targetPort, false, targetHeight, targetWidth, targetDef);
6566
} else if (targetNode && targetPos) {
6667
// Temp wire hovering over a valid input - snap to port
6768
const targetDef = nodeRegistry.get(targetNode._node.type);
69+
const targetWidth = getNodeWidthWithDef(targetNode, targetDef);
6870
const targetHeight = getNodeHeightWithDef(targetNode, targetDef);
6971
endPos = isStream
70-
? getStreamPortPosition(targetNode, targetPort, false, targetDef, targetHeight)
71-
: getPortPosition(targetNode, targetPort, false, targetHeight, undefined, targetDef);
72+
? getStreamPortPosition(targetNode, targetPort, false, targetDef, targetHeight, targetWidth)
73+
: getPortPosition(targetNode, targetPort, false, targetHeight, targetWidth, targetDef);
7274
} else if (targetPos) {
7375
// Temp wire following mouse
7476
endPos = targetPos;

src/components/Toolbar/Toolbar.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,16 @@ export function Toolbar() {
6969
dispatch({ type: 'MARK_CLEAN' });
7070

7171
// Save flows to storage (including config nodes)
72+
// Filter out runtime-only properties (starting with _) from config
7273
const flowConfig = {
7374
flows: flowState.flows,
7475
nodes: Object.values(flowState.nodes).map(node => {
7576
const { _node, ...config } = node;
76-
return { ..._node, ...config };
77+
// Filter out runtime-only properties (e.g., _currentValue, _activeButton)
78+
const cleanConfig = Object.fromEntries(
79+
Object.entries(config).filter(([key]) => !key.startsWith('_'))
80+
);
81+
return { ..._node, ...cleanConfig };
7782
}),
7883
configNodes: Object.values(flowState.configNodes).map(node => {
7984
const { _node, users: _users, ...config } = node;

src/context/RuntimeContext.jsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ export function RuntimeProvider({ children }) {
132132
// MediaElementSource actions
133133
'createMediaElementSource', 'mediaElementControl',
134134
// Stems actions
135-
'createStemsNode', 'loadStems', 'controlStems'
135+
'createStemsNode', 'loadStems', 'controlStems',
136+
// Context state
137+
'getAudioContextState',
138+
// Speaker status
139+
'hasActiveSources'
136140
]);
137141

138142
// Generic mainThread request handler (fire-and-forget)
@@ -661,15 +665,23 @@ export function RuntimeProvider({ children }) {
661665
const state = flowStateRef.current;
662666

663667
// Save to persistent storage FIRST (before deploy which may hang on async onInit)
668+
// Filter out runtime-only properties (starting with _) from config
664669
const flowConfig = {
665670
flows: state.flows,
666671
nodes: Object.values(state.nodes).map(node => {
667672
const { _node, ...config } = node;
668-
return { ..._node, ...config };
673+
// Filter out runtime-only properties (e.g., _currentValue, _activeButton)
674+
const cleanConfig = Object.fromEntries(
675+
Object.entries(config).filter(([key]) => !key.startsWith('_'))
676+
);
677+
return { ..._node, ...cleanConfig };
669678
}),
670679
configNodes: Object.values(state.configNodes).map(node => {
671680
const { _node, users: _users, ...config } = node;
672-
return { ..._node, ...config };
681+
const cleanConfig = Object.fromEntries(
682+
Object.entries(config).filter(([key]) => !key.startsWith('_'))
683+
);
684+
return { ..._node, ...cleanConfig };
673685
})
674686
};
675687
logger.log(`[mcp] Saving ${flowConfig.nodes.length} nodes to storage`);

0 commit comments

Comments
 (0)