> ## Documentation Index
> Fetch the complete documentation index at: https://numpyts.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Signal Processing with FFT

> Build signals, compute Fourier transforms, filter frequencies, and apply convolution using numpy-ts.

export const Playground = ({code: singleCode = "", label = "", height = null, startingHeight = null, showImportHeader = false, showCopyButton = false, showTiming = true}) => {
  const NUMPY_TS_CDN_VERSION = "1.3.0";
  const CDN_URLS = {
    numpyTs: `https://cdn.jsdelivr.net/npm/numpy-ts@${NUMPY_TS_CDN_VERSION}/dist/numpy-ts.browser.js`,
    prismCore: "https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js",
    prismTS: "https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-typescript.min.js",
    prismCSSLight: "https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css",
    prismCSSDark: "https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-tomorrow.min.css"
  };
  const SHARED_FONT = {
    fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace",
    fontSize: "13px",
    lineHeight: "1.5",
    tabSize: 2
  };
  const THEME_COLORS = {
    light: {
      editorBg: '#f5f5f5',
      editorText: '#24292e',
      editorBorder: '#d1d5da',
      toolbarBg: '#ffffff',
      outputBg: '#fafbfc',
      outputText: '#24292e',
      selectBg: '#ffffff',
      selectText: '#24292e',
      selectBorder: '#d1d5da',
      caretColor: '#000000',
      placeholderText: '#6a737d',
      resizeHandleBg: '#d1d5da',
      resizeHandleHoverBg: '#959da5'
    },
    dark: {
      editorBg: '#1e1e1e',
      editorText: '#d4d4d4',
      editorBorder: '#333333',
      toolbarBg: '#161616',
      outputBg: '#1a1a1a',
      outputText: '#d4d4d4',
      selectBg: '#2a2a2a',
      selectText: '#cccccc',
      selectBorder: '#444444',
      caretColor: '#ffffff',
      placeholderText: '#666666',
      resizeHandleBg: '#333333',
      resizeHandleHoverBg: '#555555'
    }
  };
  const BASE_MIN_HEIGHT = 100;
  const MAX_HEIGHT = 800;
  const IMPORT_HEADER_TEXT = `import * as np from 'numpy-ts';\n\n`;
  const parseHeightPx = value => {
    if (value == null) return null;
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string") {
      const parsed = parseFloat(value);
      if (Number.isFinite(parsed)) return parsed;
    }
    return null;
  };
  const minHeightPx = BASE_MIN_HEIGHT;
  const clampHeight = h => Math.max(minHeightPx, Math.min(MAX_HEIGHT, h));
  const estimateSingleCodeHeight = (script, includeImportHeader) => {
    const normalized = (script || "").replace(/\r\n/g, "\n");
    const lineCount = normalized.length > 0 ? normalized.split("\n").length : 1;
    const fontSizePx = parseFloat(SHARED_FONT.fontSize) || 13;
    const lineHeightRaw = parseFloat(SHARED_FONT.lineHeight);
    const lineHeightPx = (Number.isFinite(lineHeightRaw) ? lineHeightRaw : 1.5) * fontSizePx;
    const topPadding = includeImportHeader ? 16 + 3 * fontSizePx : 16;
    const bottomPadding = 16;
    return Math.ceil(topPadding + bottomPadding + lineCount * lineHeightPx + 4);
  };
  function loadScript(src) {
    return new Promise((resolve, reject) => {
      const existing = document.querySelector(`script[src="${src}"]`);
      if (existing) {
        if (existing.dataset.loaded === 'true') {
          resolve();
          return;
        }
        existing.addEventListener('load', resolve, {
          once: true
        });
        existing.addEventListener('error', reject, {
          once: true
        });
        return;
      }
      const s = document.createElement("script");
      s.src = src;
      s.onload = () => {
        s.dataset.loaded = 'true';
        resolve();
      };
      s.onerror = reject;
      document.head.appendChild(s);
    });
  }
  function loadCSS(href, removeOldId = null) {
    if (document.querySelector(`link[href="${href}"]`)) return;
    if (removeOldId) {
      const old = document.querySelector(`link[data-prism-theme="${removeOldId}"]`);
      if (old) old.remove();
    }
    const l = document.createElement("link");
    l.rel = "stylesheet";
    l.href = href;
    if (removeOldId) l.setAttribute("data-prism-theme", removeOldId);
    document.head.appendChild(l);
  }
  const startHeightPx = parseHeightPx(startingHeight) ?? parseHeightPx(height);
  const initialEditorHeight = startHeightPx != null ? clampHeight(startHeightPx) : clampHeight(estimateSingleCodeHeight(singleCode, showImportHeader));
  const [code, setCode] = useState(singleCode);
  const [output, setOutput] = useState("");
  const [loaded, setLoaded] = useState(false);
  const [loadError, setLoadError] = useState(null);
  const [running, setRunning] = useState(false);
  const [isDarkMode, setIsDarkMode] = useState(() => document.documentElement.classList.contains('dark'));
  const [editorHeight, setEditorHeight] = useState(initialEditorHeight);
  const [isResizing, setIsResizing] = useState(false);
  const [copied, setCopied] = useState(false);
  const [timing, setTiming] = useState(null);
  const [copyHover, setCopyHover] = useState(false);
  const [scrollbarWidth, setScrollbarWidth] = useState(0);
  const [toolbarNarrow, setToolbarNarrow] = useState(false);
  const textareaRef = useRef(null);
  const preRef = useRef(null);
  const codeRef = useRef(null);
  const toolbarRef = useRef(null);
  const resizeStartY = useRef(null);
  const resizeStartHeight = useRef(null);
  const runSeqRef = useRef(0);
  const npRef = useRef(null);
  const colors = THEME_COLORS[isDarkMode ? 'dark' : 'light'];
  const copyTimeoutRef = useRef(null);
  useEffect(() => {
    let cancelled = false;
    async function load() {
      try {
        loadCSS(isDarkMode ? CDN_URLS.prismCSSDark : CDN_URLS.prismCSSLight, "prism");
        const dynamicImport = new Function("url", "return import(url)");
        const npModule = await dynamicImport(CDN_URLS.numpyTs);
        npRef.current = npModule.default ?? npModule;
        await loadScript(CDN_URLS.prismCore);
        await loadScript(CDN_URLS.prismTS);
        if (!cancelled) {
          console.info(`[Playground] Loaded numpy-ts v${NUMPY_TS_CDN_VERSION} from CDN.`);
          setLoaded(true);
        }
      } catch (e) {
        if (!cancelled) setLoadError("Failed to load dependencies. Check your connection.");
      }
    }
    load();
    return () => {
      cancelled = true;
    };
  }, []);
  const syncScroll = useCallback(() => {
    if (!textareaRef.current || !codeRef.current) return;
    const ta = textareaRef.current;
    codeRef.current.style.transform = `translate(${-ta.scrollLeft}px, ${-ta.scrollTop}px)`;
  }, []);
  useEffect(() => {
    const observer = new MutationObserver(() => {
      const isDark = document.documentElement.classList.contains('dark');
      setIsDarkMode(isDark);
      loadCSS(isDark ? CDN_URLS.prismCSSDark : CDN_URLS.prismCSSLight, "prism");
      if (loaded && window.Prism) {
        setTimeout(() => {
          window.Prism.highlightAll();
          requestAnimationFrame(syncScroll);
        }, 50);
      }
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => observer.disconnect();
  }, [loaded, syncScroll]);
  useEffect(() => {
    if (loaded && window.Prism) {
      window.Prism.highlightAll();
      requestAnimationFrame(syncScroll);
    }
  }, [code, loaded, syncScroll]);
  const handleScroll = useCallback(() => {
    syncScroll();
  }, [syncScroll]);
  const handleKeyDown = useCallback(e => {
    const ta = e.target;
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    const hasSelection = start !== end;
    if (e.key === "Tab") {
      e.preventDefault();
      const newVal = ta.value.substring(0, start) + "  " + ta.value.substring(end);
      setCode(newVal);
      requestAnimationFrame(() => {
        ta.selectionStart = ta.selectionEnd = start + 2;
      });
      return;
    }
    if (e.key === "Enter") {
      e.preventDefault();
      const lineStart = ta.value.lastIndexOf("\n", start - 1) + 1;
      const lineTextUpToCursor = ta.value.substring(lineStart, start);
      const indent = (lineTextUpToCursor.match(/^[ \t]*/) || [""])[0];
      const insert = `\n${indent}`;
      const newVal = ta.value.substring(0, start) + insert + ta.value.substring(end);
      const nextPos = start + insert.length;
      setCode(newVal);
      requestAnimationFrame(() => {
        ta.selectionStart = ta.selectionEnd = nextPos;
      });
      return;
    }
    if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === "a") {
      e.preventDefault();
      ta.focus();
      ta.selectionStart = 0;
      ta.selectionEnd = ta.value.length;
      return;
    }
    if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === "c") {
      e.preventDefault();
      const prevStart = ta.selectionStart;
      const prevEnd = ta.selectionEnd;
      let copyStart = prevStart;
      let copyEnd = prevEnd;
      if (!hasSelection) {
        const lineStart = ta.value.lastIndexOf("\n", start - 1) + 1;
        const lineEndRaw = ta.value.indexOf("\n", start);
        const lineEnd = lineEndRaw === -1 ? ta.value.length : lineEndRaw;
        copyStart = lineStart;
        copyEnd = lineEnd;
      }
      ta.focus();
      ta.selectionStart = copyStart;
      ta.selectionEnd = copyEnd;
      let copied = false;
      try {
        copied = typeof document.execCommand === "function" ? document.execCommand("copy") : false;
      } catch {
        copied = false;
      }
      if (!copied && navigator.clipboard?.writeText) {
        void navigator.clipboard.writeText(ta.value.substring(copyStart, copyEnd)).catch(() => {});
      }
      requestAnimationFrame(() => {
        ta.selectionStart = prevStart;
        ta.selectionEnd = prevEnd;
      });
      return;
    }
    if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && (e.key === "/" || e.code === "Slash")) {
      e.preventDefault();
      const lineStart = ta.value.lastIndexOf("\n", start - 1) + 1;
      const endForLineCalc = hasSelection && ta.value[end - 1] === "\n" ? end - 1 : end;
      const lineEndRaw = ta.value.indexOf("\n", endForLineCalc);
      const lineEnd = lineEndRaw === -1 ? ta.value.length : lineEndRaw;
      const block = ta.value.substring(lineStart, lineEnd);
      const lines = block.split("\n");
      const isCommentedLine = line => (/^(\s*)\/\//).test(line);
      const nonEmptyLines = lines.filter(line => line.trim().length > 0);
      const shouldUncomment = nonEmptyLines.length > 0 && nonEmptyLines.every(isCommentedLine);
      const toggleLine = line => {
        if (line.trim().length === 0) return line;
        if (shouldUncomment) return line.replace(/^(\s*)\/\/ ?/, "$1");
        const match = line.match(/^(\s*)/);
        const indent = match ? match[1] : "";
        return `${indent}// ${line.slice(indent.length)}`;
      };
      const updatedLines = lines.map(toggleLine);
      const replacedBlock = updatedLines.join("\n");
      const newVal = ta.value.substring(0, lineStart) + replacedBlock + ta.value.substring(lineEnd);
      setCode(newVal);
      requestAnimationFrame(() => {
        if (hasSelection) {
          ta.selectionStart = lineStart;
          ta.selectionEnd = lineStart + replacedBlock.length;
          return;
        }
        const line = lines[0] || "";
        const cursorCol = start - lineStart;
        let newCursorCol = cursorCol;
        if (line.trim().length > 0) {
          if (shouldUncomment) {
            const uncommentMatch = line.match(/^(\s*)\/\/ ?/);
            if (uncommentMatch) {
              const indentLen = uncommentMatch[1].length;
              const removedLen = uncommentMatch[0].length - indentLen;
              if (cursorCol > indentLen) {
                newCursorCol = Math.max(indentLen, cursorCol - removedLen);
              }
            }
          } else {
            const indentLen = (line.match(/^(\s*)/) || [""])[0].length;
            if (cursorCol > indentLen) {
              newCursorCol = cursorCol + 3;
            }
          }
        }
        const newCursor = lineStart + newCursorCol;
        ta.selectionStart = ta.selectionEnd = newCursor;
      });
    }
  }, []);
  const handleResizeStart = useCallback(e => {
    e.preventDefault();
    setIsResizing(true);
    resizeStartY.current = e.clientY;
    resizeStartHeight.current = editorHeight;
  }, [editorHeight]);
  useEffect(() => {
    if (!isResizing) return;
    const handleMouseMove = e => {
      const delta = e.clientY - resizeStartY.current;
      const newHeight = Math.max(minHeightPx, Math.min(MAX_HEIGHT, resizeStartHeight.current + delta));
      setEditorHeight(newHeight);
    };
    const handleMouseUp = () => {
      setIsResizing(false);
    };
    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isResizing, minHeightPx]);
  useEffect(() => {
    if (startHeightPx != null) {
      setEditorHeight(clampHeight(startHeightPx));
    } else {
      setEditorHeight(clampHeight(estimateSingleCodeHeight(singleCode, showImportHeader)));
    }
  }, [startHeightPx, singleCode, showImportHeader]);
  const run = useCallback(async () => {
    if (!loaded || !npRef.current) return;
    const runId = runSeqRef.current + 1;
    runSeqRef.current = runId;
    setRunning(true);
    setTiming(null);
    const logs = [];
    const origLog = console.log;
    const origError = console.error;
    const origWarn = console.warn;
    const fmt = (...args) => args.map(a => {
      if (a == null) return String(a);
      if (typeof a === "object" && typeof a.toString === "function" && a.toString !== Object.prototype.toString) return a.toString();
      if (typeof a === "object") {
        try {
          return JSON.stringify(a);
        } catch {
          return String(a);
        }
      }
      return String(a);
    }).join(" ");
    console.log = (...args) => logs.push(fmt(...args));
    console.error = (...args) => logs.push("Error: " + fmt(...args));
    console.warn = (...args) => logs.push("Warning: " + fmt(...args));
    let hasError = false;
    const shouldRunAsync = (/\bawait\b/).test(code);
    const t0 = performance.now();
    try {
      let result;
      if (shouldRunAsync) {
        const executeAsync = new Function("np", `"use strict"; return (async () => {\n${code}\n})();`);
        result = await executeAsync(npRef.current);
      } else {
        const executeSync = new Function("np", code);
        result = executeSync(npRef.current);
      }
      if (result !== undefined) {
        logs.push(typeof result === "object" && typeof result?.toString === "function" && result.toString !== Object.prototype.toString ? result.toString() : String(result));
      }
    } catch (e) {
      hasError = true;
      logs.push("Error: " + (e?.message || String(e)));
    } finally {
      console.log = origLog;
      console.error = origError;
      console.warn = origWarn;
    }
    const elapsed = performance.now() - t0;
    if (runId !== runSeqRef.current) return;
    setOutput(logs.join("\n"));
    if (showTiming && !hasError) {
      const formatted = elapsed < 0.15 ? "< 0.10 ms" : elapsed < 1 ? elapsed.toFixed(2) + " ms" : elapsed < 1000 ? elapsed.toFixed(1) + " ms" : (elapsed / 1000).toFixed(2) + " s";
      setTiming(formatted);
    }
    setRunning(false);
  }, [loaded, code, showTiming]);
  const handleCopy = useCallback(async () => {
    const textToCopy = `${showImportHeader ? IMPORT_HEADER_TEXT : ""}${code}`;
    try {
      await navigator.clipboard.writeText(textToCopy);
      setCopied(true);
      if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
      copyTimeoutRef.current = setTimeout(() => setCopied(false), 1200);
    } catch {
      setCopied(false);
    }
  }, [code, showImportHeader]);
  useEffect(() => {
    return () => {
      if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
    };
  }, []);
  useEffect(() => {
    const measureScrollbar = () => {
      if (!textareaRef.current) return;
      const el = textareaRef.current;
      const width = Math.max(el.offsetWidth - el.clientWidth, 0);
      setScrollbarWidth(width);
    };
    measureScrollbar();
    window.addEventListener('resize', measureScrollbar);
    return () => window.removeEventListener('resize', measureScrollbar);
  }, [editorHeight, code, showImportHeader]);
  useEffect(() => {
    if (!toolbarRef.current) return;
    const ro = new ResizeObserver(([entry]) => {
      setToolbarNarrow(entry.contentRect.width < 380);
    });
    ro.observe(toolbarRef.current);
    return () => ro.disconnect();
  }, []);
  if (loadError) {
    return <div style={{
      padding: "20px",
      color: "#e55",
      background: colors.outputBg,
      borderRadius: "8px",
      fontSize: "14px"
    }}>
        {loadError}
      </div>;
  }
  const editorRadius = "8px 8px 0 0";
  const editorContentRadius = "8px 8px 0 0";
  const editorWrap = {
    position: "relative",
    height: `${editorHeight}px`,
    overflow: "hidden"
  };
  const editorBorderOverlay = {
    position: "absolute",
    inset: 0,
    borderRadius: editorRadius,
    border: `1px solid ${colors.editorBorder}`,
    borderBottom: "none",
    pointerEvents: "none",
    zIndex: 4
  };
  const copyWrapStyle = {
    position: "absolute",
    top: "8px",
    right: `${10 + scrollbarWidth}px`,
    zIndex: 3,
    display: "inline-flex",
    alignItems: "center",
    gap: "6px"
  };
  const copyTooltipVisible = copied || copyHover;
  const copyTooltipStyle = {
    fontSize: "11px",
    lineHeight: 1,
    color: "#ffffff",
    background: "#3b82f6",
    border: "1px solid #3b82f6",
    borderRadius: "6px",
    padding: "4px 8px",
    opacity: copyTooltipVisible ? 1 : 0,
    transform: copyTooltipVisible ? "translateY(0)" : "translateY(2px)",
    transition: "opacity 0.15s, transform 0.15s",
    pointerEvents: "none",
    whiteSpace: "nowrap"
  };
  const copyBtnStyle = {
    width: "24px",
    height: "24px",
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    borderRadius: "6px",
    border: "none",
    background: "transparent",
    color: isDarkMode ? "rgba(255,255,255,0.4)" : "#9ca3af",
    cursor: "pointer",
    padding: 0,
    lineHeight: 1
  };
  const sharedTextStyle = {
    margin: 0,
    border: 0,
    padding: "16px",
    whiteSpace: "pre",
    overflowWrap: "normal",
    wordBreak: "normal",
    verticalAlign: "baseline",
    textRendering: "auto"
  };
  const preStyle = {
    ...SHARED_FONT,
    ...sharedTextStyle,
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    background: colors.editorBg,
    overflow: "hidden",
    pointerEvents: "none",
    padding: 0,
    borderRadius: editorContentRadius
  };
  const codeStyle = {
    display: "block",
    ...SHARED_FONT,
    background: "transparent",
    padding: "16px",
    margin: 0,
    border: 0,
    whiteSpace: "pre",
    overflowWrap: "normal",
    wordBreak: "normal",
    lineHeight: "inherit",
    willChange: "transform"
  };
  const textareaStyle = {
    ...SHARED_FONT,
    ...sharedTextStyle,
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    paddingTop: showImportHeader ? "calc(16px + 3em)" : "16px",
    background: "transparent",
    color: "transparent",
    caretColor: colors.caretColor,
    outline: "none",
    resize: "none",
    overflow: "auto",
    WebkitTextFillColor: "transparent",
    zIndex: 1,
    width: "100%",
    height: "100%",
    borderRadius: editorContentRadius
  };
  const resizeHandle = {
    height: "4px",
    background: isResizing ? colors.resizeHandleHoverBg : colors.resizeHandleBg,
    cursor: "ns-resize",
    transition: "background 0.15s",
    borderLeft: `1px solid ${colors.editorBorder}`,
    borderRight: `1px solid ${colors.editorBorder}`
  };
  const toolbarStyle = {
    display: "flex",
    alignItems: "center",
    gap: "8px",
    padding: "8px 12px",
    background: colors.toolbarBg,
    borderLeft: `1px solid ${colors.editorBorder}`,
    borderRight: `1px solid ${colors.editorBorder}`,
    flexWrap: "wrap"
  };
  const btnStyle = {
    display: "inline-flex",
    alignItems: "center",
    gap: "6px",
    padding: "6px 16px",
    background: "#3179C7",
    color: "#fff",
    border: "none",
    borderRadius: "6px",
    fontSize: "13px",
    fontWeight: 500,
    cursor: loaded ? "pointer" : "not-allowed",
    opacity: loaded ? 1 : 0.5,
    transition: "background 0.15s"
  };
  const selectStyle = {
    padding: "6px 10px",
    background: colors.selectBg,
    color: colors.selectText,
    border: `1px solid ${colors.selectBorder}`,
    borderRadius: "6px",
    fontSize: "13px",
    cursor: "pointer",
    outline: "none"
  };
  const badgeStyle = {
    marginLeft: "auto",
    fontSize: "11px",
    color: colors.placeholderText,
    display: "flex",
    alignItems: "center",
    gap: "4px",
    whiteSpace: "nowrap"
  };
  const outputStyle = {
    margin: 0,
    padding: "16px",
    minHeight: "60px",
    maxHeight: "240px",
    overflow: "auto",
    ...SHARED_FONT,
    fontSize: "12.5px",
    background: colors.outputBg,
    color: colors.outputText,
    borderRadius: "0 0 8px 8px",
    border: `1px solid ${colors.editorBorder}`,
    borderTop: "none",
    whiteSpace: "pre-wrap",
    overflowWrap: "break-word"
  };
  const renderedCode = `${showImportHeader ? IMPORT_HEADER_TEXT : ""}${code}`;
  const runSpinnerStyle = {
    animation: "playgroundSpin 0.9s linear infinite",
    display: "inline-block"
  };
  return <div>
      <style>{`
        @keyframes playgroundSpin {
          0% { transform: rotate(0deg); }
          100% { transform: rotate(360deg); }
        }
        @keyframes playgroundPulse {
          0%, 100% { opacity: 0.45; transform: scale(1); }
          50% { opacity: 1; transform: scale(1.12); }
        }
        code[class*="language-"] {
          white-space: pre !important;
          word-break: normal !important;
          overflow-wrap: normal !important;
        }
      `}</style>
      {label ? <div style={{
    marginTop: "8px",
    marginBottom: "8px",
    fontSize: "14px",
    fontWeight: 500,
    color: colors.editorText
  }}>
          {label}
        </div> : null}
      <div style={editorWrap}>
        <div style={editorBorderOverlay} aria-hidden="true" />
        {showCopyButton ? <div style={copyWrapStyle}>
            <span style={copyTooltipStyle}>{copied ? "Copied" : "Copy"}</span>
            <button type="button" onClick={handleCopy} style={copyBtnStyle} onMouseEnter={e => {
    setCopyHover(true);
    e.currentTarget.style.color = isDarkMode ? "rgba(255,255,255,0.6)" : "#6b7280";
  }} onMouseLeave={e => {
    setCopyHover(false);
    e.currentTarget.style.color = isDarkMode ? "rgba(255,255,255,0.4)" : "#9ca3af";
  }} aria-label="Copy code">
              {copied ? <svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                  <path d="M4.5 10.5L8.2 14.2L15.5 6.8" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                </svg> : <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                  <path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
                  <path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
                </svg>}
            </button>
          </div> : null}
        <pre ref={preRef} style={preStyle}>
          <code ref={codeRef} className="language-typescript" style={codeStyle}>{renderedCode}</code>
        </pre>
        <textarea ref={textareaRef} value={code} onChange={e => setCode(e.target.value)} onScroll={handleScroll} onKeyDown={handleKeyDown} style={textareaStyle} spellCheck={false} wrap="off" autoCapitalize="off" autoCorrect="off" aria-label="Code editor" />
      </div>
      <div style={resizeHandle} onMouseDown={handleResizeStart} onMouseEnter={e => !isResizing && (e.target.style.background = colors.resizeHandleHoverBg)} onMouseLeave={e => !isResizing && (e.target.style.background = colors.resizeHandleBg)} aria-label="Resize editor" role="separator" />
      <div ref={toolbarRef} style={toolbarStyle}>
        <button onClick={run} disabled={!loaded} style={btnStyle} onMouseEnter={e => loaded && (e.target.style.background = "#2668b0")} onMouseLeave={e => loaded && (e.target.style.background = "#3179C7")}>
          {running ? <><span style={runSpinnerStyle}>{"\u21BB"}</span> Run</> : <><span>{"\u25B6"}</span> Run</>}
        </button>
        {timing && !running && <span style={{
    fontSize: "12px",
    color: "#4caf50",
    fontWeight: 500,
    whiteSpace: "nowrap"
  }}>
            {toolbarNarrow ? timing : `Completed in ${timing}`}
          </span>}
        <span style={badgeStyle}>
          {loaded ? <><span style={{
    color: "#4caf50",
    display: "inline-block",
    animation: "playgroundPulse 2.2s ease-in-out infinite",
    transformOrigin: "center"
  }}>{"\u25CF"}</span> Runs in your browser</> : <><span style={{
    animation: "spin 1s linear infinite",
    display: "inline-block"
  }}>{"\u21BB"}</span> Loading numpy-ts...</>}
        </span>
      </div>
      <pre style={outputStyle}>
        {output || <span style={{
    color: colors.placeholderText,
    fontStyle: "italic"
  }}>Click Run to execute the code above</span>}
      </pre>
    </div>;
};

## Creating a composite signal

Build a signal by summing sine waves at different frequencies. This is the starting point for most frequency-domain analysis.

<Playground
  code={String.raw`// Sampling parameters
const sampleRate = 256;  // Hz
const duration = 1;      // seconds
const N = sampleRate * duration;

// Time vector: 256 samples over 1 second
const t = np.linspace(0, duration, N);

// Create a composite signal: 5 Hz + 20 Hz + 50 Hz
const freq1 = 5, freq2 = 20, freq3 = 50;

const wave1 = t.multiply(2 * Math.PI * freq1).sin();
const wave2 = t.multiply(2 * Math.PI * freq2).sin().multiply(0.5);
const wave3 = t.multiply(2 * Math.PI * freq3).sin().multiply(0.3);
const signal = wave1.add(wave2).add(wave3);

console.log('Signal shape:', signal.shape);
console.log('Signal dtype:', signal.dtype, '\n');

// Verify: signal should oscillate between roughly -1.8 and +1.8
console.log('Min value:', Number(np.min(signal)).toFixed(4));
console.log('Max value:', Number(np.max(signal)).toFixed(4));
console.log('Mean (should be ~0):', Number(np.mean(signal)).toFixed(4), '\n');

// First few samples (signal starts near 0 since sin(0)=0)
console.log('First 5 samples:', signal.slice('0:5'));`}
  showImportHeader={true}
  showCopyButton={true}
/>

***

## Computing the FFT and finding frequency components

The FFT converts a time-domain signal into its frequency-domain representation, revealing which frequencies are present.

<Playground
  code={String.raw`// Assume signal and sampleRate from above
const N = 256;
const sampleRate = 256;

const t = np.linspace(0, 1, N);
const wave1 = t.multiply(2 * Math.PI * 5).sin();
const wave2 = t.multiply(2 * Math.PI * 20).sin().multiply(0.5);
const signal = wave1.add(wave2);

// Compute the FFT
const spectrum = np.fft.fft(signal);
console.log('Spectrum shape:', spectrum.shape);
console.log('Spectrum dtype:', spectrum.dtype, '\n');

// Get the frequency bins
const freqs = np.fft.fftfreq(N, 1 / sampleRate);

// Compute the magnitude (absolute value of complex spectrum)
const magnitude = np.absolute(spectrum);

// Only the first half is meaningful for real signals (positive frequencies)
const halfN = Math.floor(N / 2);
const posFreqs = freqs.slice('0:' + halfN);
const posMag = magnitude.slice('0:' + halfN);

// Normalize magnitude
const normalizedMag = np.divide(posMag, N / 2);

// Find the top 2 peaks by sorting magnitudes
const sorted = np.argsort(np.multiply(normalizedMag, -1));
const i0 = Number(sorted.get([0]));
const i1 = Number(sorted.get([1]));

console.log('Peak 1: ' + Number(posFreqs.get([i0])) + ' Hz (amplitude: ' + Number(normalizedMag.get([i0])).toFixed(3) + ')');
console.log('Peak 2: ' + Number(posFreqs.get([i1])) + ' Hz (amplitude: ' + Number(normalizedMag.get([i1])).toFixed(3) + ')');
console.log('\nExpected: 5 Hz (amplitude ~1.0) and 20 Hz (amplitude ~0.5)');`}
  showImportHeader={true}
  showCopyButton={true}
/>

***

## Filtering frequencies and inverse FFT

Remove unwanted frequencies by zeroing out their components in the frequency domain, then transform back to the time domain.

<Playground
  code={String.raw`const N = 256;
const sampleRate = 256;
const t = np.linspace(0, 1, N);

// Noisy signal: 10 Hz clean signal + 60 Hz noise
const clean = np.sin(np.multiply(t, 2 * Math.PI * 10));
const noise = np.multiply(np.sin(np.multiply(t, 2 * Math.PI * 60)), 0.8);
const noisy = np.add(clean, noise);

// Transform to frequency domain
const spectrum = np.fft.fft(noisy);
const freqs = np.fft.fftfreq(N, 1 / sampleRate);

// Create a low-pass filter: zero out frequencies above 30 Hz
const absFreqs = np.absolute(freqs);
const mask = np.less_equal(absFreqs, 30);  // true where |freq| <= 30 Hz

// Apply the filter by multiplying spectrum with the mask
const filtered = np.multiply(spectrum, mask);

// Transform back to time domain
const recovered = np.fft.ifft(filtered);

// The result is complex, take the real part
const recoveredReal = np.real(recovered);

// Validate: recovered signal should closely match the original clean signal
const error = np.subtract(recoveredReal, clean);
const maxErr = Number(np.max(np.absolute(error)));
const meanErr = Number(np.mean(np.absolute(error)));
console.log('Filtered signal shape:', recoveredReal.shape);
console.log('Mean absolute error:', meanErr.toFixed(6));
console.log('Max absolute error:', maxErr.toFixed(6), '(Gibbs ringing from sharp cutoff)', '\n');

// The 60 Hz noise is gone — compare std dev before and after
const noisyStd = Number(np.std(np.subtract(noisy, clean)));
const recoveredStd = Number(np.std(error));
console.log('Noise std (before filter):', noisyStd.toFixed(4));
console.log('Residual std (after filter):', recoveredStd.toFixed(4));`}
  showImportHeader={true}
  showCopyButton={true}
/>

***

## Using fftfreq and fftshift

`fftfreq` returns the frequency bins for each FFT output element. `fftshift` rearranges the output so that the zero-frequency component is at the center, which is the standard convention for visualization.

<Playground
  code={String.raw`const N = 8;
const sampleSpacing = 1 / 8;  // 8 Hz sample rate

// fftfreq returns frequencies in "standard" order:
// [0, positive..., negative...] (Hz)
const freqs = np.fft.fftfreq(N, sampleSpacing);
console.log('fftfreq:', freqs, '\n');

// fftshift centers the zero frequency:
// [negative..., 0, positive...] — standard for visualization
const shiftedFreqs = np.fft.fftshift(freqs);
console.log('fftshift:', shiftedFreqs, '\n');

// For real-valued signals, rfftfreq returns only the positive frequencies
const rfreqs = np.fft.rfftfreq(N, sampleSpacing);
console.log('rfftfreq:', rfreqs, '\n');

// Use ifftshift to undo the shift before calling ifft
const unshifted = np.fft.ifftshift(shiftedFreqs);
console.log('ifftshift round-trip matches:', np.allclose(unshifted, freqs));`}
  showImportHeader={true}
  showCopyButton={true}
/>

***

## Convolution with a kernel

Convolution is fundamental to signal processing -- it is used for smoothing, differentiation, edge detection, and more. numpy-ts provides `convolve` for 1-D convolution.

<Playground
  code={String.raw`// Create a noisy step signal
const step = np.concatenate([np.zeros([50]), np.ones([50])]);
np.random.seed(42);
const noisy = np.add(step, np.multiply(np.random.normal(0, 1, [100]), 0.3));

// Moving average kernel (smoothing filter)
const kernelSize = 7;
const smoothKernel = np.full([kernelSize], 1.0 / kernelSize);

// Apply convolution
const smoothed = np.convolve(noisy, smoothKernel, 'same');

// Smoothing reduces noise — compare std dev before and after
const noisyStd = Number(np.std(np.subtract(noisy, step))).toFixed(3);
const smoothStd = Number(np.std(np.subtract(smoothed, step))).toFixed(3);
console.log('Noise std (before smoothing):', noisyStd);
console.log('Noise std (after smoothing):', smoothStd, '\n');

// Differentiation kernel (first derivative approximation)
const diffKernel = np.array([-1, 0, 1]);
const derivative = np.convolve(step, diffKernel, 'same');

// The derivative should spike at the step transition (index ~50)
const absDeriv = derivative.absolute();
const peakIdx = Number(np.argmax(absDeriv));
console.log('Derivative peak at index:', peakIdx, '(step transition is at 50)');
console.log('Derivative peak magnitude:', Number(absDeriv.get([peakIdx])), '\n');

// Edge detection kernel (Laplacian approximation)
const edgeKernel = np.array([1, -2, 1]);
const edges = np.convolve(step, edgeKernel, 'same');
const edgePeak = Number(np.argmax(np.absolute(edges)));
console.log('Edge detected at index:', edgePeak);`}
  showImportHeader={true}
  showCopyButton={true}
/>

<Note>
  The `'same'` mode returns output with the same length as the input, `'full'` returns the full convolution (length M+N-1), and `'valid'` returns only the part computed without zero-padded edges.
</Note>

***

## Real FFT for real-valued signals

When your input is purely real (no imaginary component), `rfft` is more efficient than `fft` because it exploits the conjugate symmetry of the spectrum.

<Playground
  code={String.raw`const N = 1024;
const t = np.linspace(0, 1, N);
const signal = np.sin(np.multiply(t, 2 * Math.PI * 50));

// rfft is ~2x faster and returns only positive frequencies
const rfftResult = np.fft.rfft(signal);
console.log('Input length:', N);
console.log('rfft output length:', rfftResult.shape[0], '(= N/2 + 1 =', N/2 + 1 + ')', '\n');

// Corresponding frequencies
const rfreqs = np.fft.rfftfreq(N, 1.0 / N);
console.log('Frequency bins:', rfreqs.shape[0], '\n');

// The peak should be at 50 Hz
const mag = np.absolute(rfftResult);
const peakIdx = Number(np.argmax(mag));
console.log('Peak frequency:', Number(rfreqs.get([peakIdx])), 'Hz (expected: 50)', '\n');

// Inverse: irfft recovers the original real signal
const recovered = np.fft.irfft(rfftResult);
console.log('Recovered shape:', recovered.shape);
console.log('Round-trip matches original:', np.allclose(recovered, signal));`}
  showImportHeader={true}
  showCopyButton={true}
/>
