diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c74d395cb..baa4d6c17 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -23,6 +23,7 @@ jobs:
       STAGING_CLOUDFRONT_DISTRIBUTION_ID: E2ELTBTA2OFPY2
       REVIEW_CLOUDFRONT_DISTRIBUTION_ID: E3267W09ZJHQG9
       REACT_APP_FOUNDATION_BUILD: ${{ github.repository_owner == 'microbit-foundation' }}
+      CI: false
 
     steps:
       # Note: This workflow disables deployment steps and micro:bit branding installation on forks.
diff --git a/package-lock.json b/package-lock.json
index 225f778d7..e629fd6cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -47,6 +47,7 @@
         "lzma": "^2.3.2",
         "marked": "^4.0.15",
         "mobile-drag-drop": "^2.3.0-rc.2",
+        "perlin-noise": "^0.0.1",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
         "react-icons": "^4.8.0",
@@ -15851,6 +15852,11 @@
       "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
       "dev": true
     },
+    "node_modules/perlin-noise": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/perlin-noise/-/perlin-noise-0.0.1.tgz",
+      "integrity": "sha512-33wNN1FN7jZPF0ISkSF8BLag71wjBWzrpzd/m00iFsxtIhKeZ8VaKBQtzPX3TBegK9GYPXwGzR3oJp9v2T7QuQ=="
+    },
     "node_modules/picocolors": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
diff --git a/package.json b/package.json
index b7e3739bb..591c857da 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
     "lzma": "^2.3.2",
     "marked": "^4.0.15",
     "mobile-drag-drop": "^2.3.0-rc.2",
+    "perlin-noise": "^0.0.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-icons": "^4.8.0",
diff --git a/src/editor/codemirror/CodeMirror.tsx b/src/editor/codemirror/CodeMirror.tsx
index 31d155dd1..15b4c4c5e 100644
--- a/src/editor/codemirror/CodeMirror.tsx
+++ b/src/editor/codemirror/CodeMirror.tsx
@@ -12,7 +12,15 @@ import {
   lineNumbers,
   ViewUpdate,
 } from "@codemirror/view";
-import { useEffect, useMemo, useRef } from "react";
+import React, {
+  ReactNode,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { createPortal } from "react-dom";
 import { useIntl } from "react-intl";
 import { lineNumFromUint8Array } from "../../common/text-util";
 import useActionFeedback from "../../common/use-action-feedback";
@@ -40,6 +48,7 @@ import { languageServer } from "./language-server/view";
 import { lintGutter } from "./lint/lint";
 import { codeStructure } from "./structure-highlighting";
 import themeExtensions from "./themeExtensions";
+import { reactWidgetExtension } from "./helper-widgets/reactWidgetExtension";
 
 interface CodeMirrorProps {
   className?: string;
@@ -52,6 +61,20 @@ interface CodeMirrorProps {
   parameterHelpOption: ParameterHelpOption;
 }
 
+interface PortalContent {
+  dom: HTMLElement;
+  content: ReactNode;
+}
+
+/**
+ * Creates a React portal for a CodeMirror dom element (e.g. for a widget) and
+ * returns a clean-up function to call when the widget is destroyed.
+ */
+export type PortalFactory = (
+  dom: HTMLElement,
+  content: ReactNode
+) => () => void;
+
 /**
  * A React component for CodeMirror 6.
  *
@@ -100,6 +123,29 @@ const CodeMirror = ({
     [fontSize, codeStructureOption, parameterHelpOption]
   );
 
+  const [portals, setPortals] = useState<PortalContent[]>([]);
+  const portalFactory: PortalFactory = useCallback((dom, content) => {
+    setPortals((portals) => {
+      let found = false;
+      let updated = portals.map((p) => {
+        if (p.dom === dom) {
+          found = true;
+          return {
+            dom,
+            content,
+          };
+        }
+        return p;
+      });
+      if (!found) {
+        updated = [...portals, { dom, content }];
+      }
+      return updated;
+    });
+
+    return () => setPortals((portals) => portals.filter((p) => p.dom !== dom));
+  }, []);
+
   useEffect(() => {
     const initializing = !viewRef.current;
     if (initializing) {
@@ -113,11 +159,13 @@ const CodeMirror = ({
           logPastedLineCount(logging, update);
         }
       });
+
       const state = EditorState.create({
         doc: defaultValue,
         extensions: [
           notify,
           editorConfig,
+          reactWidgetExtension(portalFactory),
           // Extension requires external state.
           dndSupport({ sessionSettings, setSessionSettings }),
           // Extensions only relevant for editing:
@@ -172,6 +220,9 @@ const CodeMirror = ({
     parameterHelpOption,
     uri,
     apiReferenceMap,
+    portals,
+    portalFactory,
+    setPortals,
   ]);
   useEffect(() => {
     // Do this separately as we don't want to destroy the view whenever options needed for initialization change.
@@ -260,13 +311,16 @@ const CodeMirror = ({
   }, [routerState, setRouterState]);
 
   return (
-    <section
-      data-testid="editor"
-      aria-label={intl.formatMessage({ id: "code-editor" })}
-      style={{ height: "100%" }}
-      className={className}
-      ref={elementRef}
-    />
+    <>
+      <section
+        data-testid="editor"
+        aria-label={intl.formatMessage({ id: "code-editor" })}
+        style={{ height: "100%" }}
+        className={className}
+        ref={elementRef}
+      />
+      {portals.map(({ content, dom }) => createPortal(content, dom))}
+    </>
   );
 };
 
diff --git a/src/editor/codemirror/helper-widgets/openWidgets.tsx b/src/editor/codemirror/helper-widgets/openWidgets.tsx
new file mode 100644
index 000000000..fcb392471
--- /dev/null
+++ b/src/editor/codemirror/helper-widgets/openWidgets.tsx
@@ -0,0 +1,82 @@
+import { Button, Center, HStack } from "@chakra-ui/react";
+import { StateEffect } from "@codemirror/state";
+import { EditorView } from "@codemirror/view";
+import { useCallback } from "react";
+
+export const openWidgetEffect = StateEffect.define<number>();
+export const OpenReactComponent = ({
+  loc,
+  view,
+}: {
+  loc: number;
+  view: EditorView;
+}) => {
+  const handleClick = useCallback(() => {
+    view.dispatch({
+      effects: [openWidgetEffect.of(loc)],
+    });
+  }, [loc, view]);
+  return (
+      <Button onClick={handleClick} size="xs">Open</Button>
+  );
+};
+
+
+function createSoundWavePath(): string {
+  let pathData = 'M0,12'; 
+
+  const totalPoints = 18; 
+
+    
+    const stepSize = 24 / totalPoints;
+
+    for (let i = 0; i < totalPoints; i++) {
+      const x = i * stepSize;
+      const angle = (x / totalPoints) * 3 * Math.PI;
+
+      const heightVariation = Math.cos(angle) * 6;
+      const y1 = 12 + heightVariation; 
+      const y2 = 12 - heightVariation; 
+      
+      pathData += ` M${x},${y1} L${x},${y2}`;
+  }
+
+  return pathData;
+}
+
+export const OpenSoundComponent = ({
+  loc,
+  view,
+}: {
+  loc: number;
+  view: EditorView;
+}) => {
+
+  
+
+  const handleClick = useCallback(() => {
+    view.dispatch({
+      effects: [openWidgetEffect.of(loc)],
+    });
+  }, [loc, view]);
+
+  const soundWavePath = createSoundWavePath();
+
+  return (
+      <Button onClick={handleClick} size="sm" height="25px" marginBottom="3px" marginLeft="5px" style={{ padding: '3px 3px' }}>
+          <svg
+              width="20" 
+              height="18" 
+              viewBox="0 0 24 24"
+              fill="none"
+          >
+              <path
+                  d={soundWavePath}
+                  stroke="green" 
+                  strokeWidth="1" 
+                  fill="none" 
+              />
+          </svg>
+      </Button>
+  );
+};
\ No newline at end of file
diff --git a/src/editor/codemirror/helper-widgets/reactWidgetExtension.tsx b/src/editor/codemirror/helper-widgets/reactWidgetExtension.tsx
new file mode 100644
index 000000000..2480b27e0
--- /dev/null
+++ b/src/editor/codemirror/helper-widgets/reactWidgetExtension.tsx
@@ -0,0 +1,158 @@
+import { EditorState, Extension, StateField } from "@codemirror/state";
+import {
+  Decoration,
+  DecorationSet,
+  EditorView,
+  WidgetType,
+} from "@codemirror/view";
+import { syntaxTree } from "@codemirror/language";
+import { PortalFactory } from "../CodeMirror";
+import React from "react";
+import { createWidget } from "./widgetArgParser";
+import { openWidgetEffect } from "./openWidgets";
+import { ValidateComponentArgs } from "./widgetArgParser";
+
+export interface WidgetProps {
+  // Note: always an array, can be singleton
+  args: any[];
+  // Ranges of where to insert each argument
+  ranges: { from: number; to: number }[];
+  // Type of each argument, can be checked in widget to determine if it is editable
+  types: string[];
+  // Where to insert the changed values
+  from: number;
+  to: number;
+}
+
+/**
+ * This widget will have its contents rendered by the code in CodeMirror.tsx
+ * which it communicates with via the portal factory.
+ */
+class Widget extends WidgetType {
+  private portalCleanup: (() => void) | undefined;
+
+  constructor(
+    private component: React.ComponentType<any>,
+    private props: WidgetProps,
+    private open: React.ComponentType<any>,
+    private inline: boolean,
+    private createPortal: PortalFactory
+  ) {
+    super();
+  }
+
+  eq(other: WidgetType): boolean {
+    const them = other as Widget;
+    let args1 = this.props.args;
+    let args2 = them.props.args;
+    let eqArgs =
+      args1.length === args2.length &&
+      args1.every((element, index) => element === args2[index]);
+
+    return (
+      them.component === this.component &&
+      them.props.to === this.props.to &&
+      eqArgs &&
+      them.inline === this.inline
+    );
+  }
+
+  updateDOM(dom: HTMLElement, view: EditorView): boolean {
+    dom.style.display = this.inline ? "inline-block" : "unset";
+    this.portalCleanup = this.createPortal(dom, this.toComponent(view));
+    return true;
+  }
+
+  private toComponent(view: EditorView) {
+    if (this.inline) {
+      return <this.open loc={this.props.to} view={view} />;
+    }
+    return <this.component props={this.props} view={view} />;
+  }
+
+  toDOM(view: EditorView) {
+    const dom = document.createElement("div");
+
+    if (
+      this.inline &&
+      !ValidateComponentArgs(this.component, this.props.args, this.props.types)
+    ) {
+      return dom;
+    }
+    dom.style.display = this.inline ? "inline-block" : "unset";
+    this.portalCleanup = this.createPortal(dom, this.toComponent(view));
+    return dom;
+  }
+
+  destroy(dom: HTMLElement): void {
+    if (this.portalCleanup) {
+      this.portalCleanup();
+    }
+  }
+
+  ignoreEvent() {
+    return true;
+  }
+}
+
+// Iterates through the syntax tree, finding occurences of SoundEffect ArgList, and places widget there
+export const reactWidgetExtension = (
+  createPortal: PortalFactory
+): Extension => {
+  const decorate = (state: EditorState) => {
+    let widgets: any[] = [];
+
+    syntaxTree(state).iterate({
+      enter: (ref) => {
+        // Found an ArgList, parent will be a CallExpression
+        if (ref.name === "ArgList" && ref.node.parent) {
+          // Match CallExpression name to our widgets
+          let name = state.doc.sliceString(ref.node.parent.from, ref.from);
+          let widget = createWidget(name, state, ref.node);
+          if (widget) {
+            let deco = Decoration.widget({
+              widget: new Widget(
+                widget.comp,
+                widget.props,
+                widget.open,
+                widget.props.to !== openWidgetLoc,
+                createPortal
+              ),
+              side: 1,
+            });
+            widgets.push(deco.range(ref.to));
+          }
+        }
+      },
+    });
+
+    return Decoration.set(widgets);
+  };
+
+  let openWidgetLoc = -1;
+  const stateField = StateField.define<DecorationSet>({
+    create(state) {
+      return decorate(state);
+    },
+    update(widgets, transaction) {
+      // check for open/close button pressed
+      for (let effect of transaction.effects) {
+        if (effect.is(openWidgetEffect)) {
+          openWidgetLoc = effect.value;
+          return decorate(transaction.state);
+        }
+      }
+      // else check for other doc edits
+      if (transaction.docChanged) {
+        // update openWidgetLoc if changes moves it
+        openWidgetLoc = transaction.changes.mapPos(openWidgetLoc);
+        return decorate(transaction.state);
+      }
+      return widgets.map(transaction.changes);
+    },
+    provide(field) {
+      return EditorView.decorations.from(field);
+    },
+  });
+  return [stateField];
+};
diff --git a/src/editor/codemirror/helper-widgets/setPixelWidget.tsx b/src/editor/codemirror/helper-widgets/setPixelWidget.tsx
new file mode 100644
index 000000000..f61a93202
--- /dev/null
+++ b/src/editor/codemirror/helper-widgets/setPixelWidget.tsx
@@ -0,0 +1,211 @@
+import React from "react";
+import {
+  Box,
+  Button,
+  Slider,
+  SliderTrack,
+  SliderFilledTrack,
+  SliderThumb,
+} from "@chakra-ui/react";
+import { EditorView } from "@codemirror/view";
+import { WidgetProps } from "./reactWidgetExtension";
+import { openWidgetEffect } from "./openWidgets";
+
+interface Pixel {
+  x: number;
+  y: number;
+  brightness: number;
+}
+
+interface MicrobitSinglePixelGridProps {
+  onPixelClick: (pixel: Pixel) => void;
+  initialPixel: Pixel | null;
+  onCloseClick: () => void;
+}
+const MicrobitSinglePixelGrid: React.FC<MicrobitSinglePixelGridProps> = ({
+  onPixelClick,
+  initialPixel,
+  onCloseClick,
+}) => {
+  const { x, y, brightness } = initialPixel ?? { x: 0, y: 0, brightness: 9 };
+
+  const handlePixelClick = (x: number, y: number) => {
+    const newPixel: Pixel = { x, y, brightness };
+    onPixelClick(newPixel);
+  };
+
+  const handleSliderChange = (value: number) => {
+    const updatedPixel: Pixel = { x, y, brightness: value };
+    onPixelClick(updatedPixel);
+  };
+
+  const calculateColor = () => {
+    const red = brightness * 25.5;
+    return `rgb(${red}, 0, 0)`;
+  };
+
+  return (
+    <div>
+    <Box ml="10px" style={{ marginRight: "4px" }}>
+        <Button size="xs" onClick={onCloseClick} bg="white">
+          X
+        </Button>
+    </Box>
+    <Box // TODO: copy to allow other widgets to access bg and close
+      display="flex"
+      flexDirection="row"
+      justifyContent="flex-start"
+      width="250px"
+      background="snon"
+      border='1px solid lightgray'
+      boxShadow='0 0 10px 5px rgba(173, 216, 230, 0.7)'
+    >
+      <Box>
+        <Box
+          bg="white"
+          p="10px"
+          borderRadius="0px"
+          border="1px solid black"
+          style={{ marginLeft: "15px", marginTop: "15px", marginBottom: "15px" }}
+        >
+          {[...Array(5)].map((_, gridY) => (
+            <Box key={gridY} display="flex">
+              {[...Array(5)].map((_, gridX) => (
+                <Box key={gridX} display="flex" mr="0px">
+                  <Button
+                    height="32px"
+                    width="30px"
+                    p={0}
+                    borderRadius={0}
+                    bgColor={
+                      gridX === x && gridY === y
+                        ? `rgba(255, 0, 0, ${brightness / 9})`
+                        : "rgba(255, 255, 255, 0)"
+                    }
+                    border={
+                      gridX === x && gridY === y
+                        ? "2px solid black"
+                        : "1px solid black"
+                    }
+                    _hover={{
+                      bgColor:
+                        gridX === x && gridY === y
+                          ? `rgba(255, 0, 0, ${brightness / 9})`
+                          : "rgba(255, 255, 255, 0.5)",
+                    }}
+                    onClick={() => handlePixelClick(gridX, gridY)}
+                  />
+                </Box>
+              ))}
+            </Box>
+          ))}
+        </Box>
+      </Box>
+      <Box ml="10px" style={{ marginTop: "15px" }}>
+        <Slider
+          aria-label="brightness"
+          defaultValue={brightness}
+          min={0}
+          max={9}
+          step={1}
+          height="182px"
+          orientation="vertical"
+          _focus={{ boxShadow: "none" }}
+          _active={{ bgColor: "transparent" }}
+          onChange={handleSliderChange}
+        >
+          <SliderTrack>
+            <SliderFilledTrack bg={calculateColor()} />
+          </SliderTrack>
+          <SliderThumb />
+        </Slider>
+      </Box>
+    </Box>
+    </div>
+  );
+};
+
+export const MicrobitSinglePixelComponent = ({
+  props,
+  view,
+}: {
+  props: WidgetProps;
+  view: EditorView;
+}) => {
+  let args = props.args;
+  let ranges = props.ranges;
+  let types = props.types;
+  let from = props.from;
+  let to = props.to;
+
+  const selectedPixel = parseArgs(args, types);
+
+  const handleCloseClick = () => {
+    view.dispatch({
+      effects: [openWidgetEffect.of(-1)],
+    });
+  };
+
+  const handleSelectPixel = (pixel: Pixel) => {
+    const { x, y, brightness } = pixel;
+    if (ranges.length === 3) {
+      view.dispatch({
+        changes: [
+          {
+            from: ranges[0].from,
+            to: ranges[0].to,
+            insert: `${x}`,
+          },
+          {
+            from: ranges[1].from,
+            to: ranges[1].to,
+            insert: `${y}`,
+          },
+          {
+            from: ranges[2].from,
+            to: ranges[2].to,
+            insert: `${brightness}`,
+          },
+        ],
+        effects: [openWidgetEffect.of(to)],
+      });
+    } else {
+      let vals = `${x},${y},${brightness}`;
+      view.dispatch({
+        changes: [
+          {
+            from: from + 1,
+            to: to - 1,
+            insert: vals,
+          },
+        ],
+        effects: [openWidgetEffect.of(vals.length + from + 2)],
+      });
+    }
+  };
+
+  return (
+    <MicrobitSinglePixelGrid
+      onPixelClick={handleSelectPixel}
+      initialPixel={selectedPixel}
+      onCloseClick={handleCloseClick}
+    />
+  );
+};
+
+const parseArgs = (args: string[], types: string[]): Pixel => {
+  const parsedArgs: number[] = [];
+  for (let i = 0; i < args.length; i++) {
+    let arg = args[i];
+    if (types[i] === "Number") {
+      parsedArgs.push(parseInt(arg));
+    } else {
+      parsedArgs.push(0);
+    }
+  }
+  // Replace missing arguments with 0
+  while (parsedArgs.length < 3) {
+    parsedArgs.push(0);
+  }
+  return { x: parsedArgs[0], y: parsedArgs[1], brightness: parsedArgs[2] };
+};
diff --git a/src/editor/codemirror/helper-widgets/showImageWidget.tsx b/src/editor/codemirror/helper-widgets/showImageWidget.tsx
new file mode 100644
index 000000000..484747ebb
--- /dev/null
+++ b/src/editor/codemirror/helper-widgets/showImageWidget.tsx
@@ -0,0 +1,222 @@
+import {
+  Box,
+  Button,
+  Slider,
+  SliderTrack,
+  SliderFilledTrack,
+  SliderThumb,
+} from "@chakra-ui/react";
+import React, { useState } from "react";
+import { WidgetProps } from "./reactWidgetExtension";
+import { EditorView } from "@codemirror/view";
+import { openWidgetEffect } from "./openWidgets";
+
+interface MultiMicrobitGridProps {
+  selectedPixels: number[][];
+  onCloseClick: () => void;
+  onPixelChange: (x: number, y: number, brightness: number) => void;
+}
+
+const MicrobitMultiplePixelsGrid: React.FC<MultiMicrobitGridProps> = ({
+  selectedPixels,
+  onCloseClick,
+  onPixelChange,
+}) => {
+  const [currentBrightness, setCurrentBrightness] = useState<number>(5);
+  const [selectedPixel, setSelectedPixel] = useState<{
+    x: number;
+    y: number;
+  } | null>(null);
+
+  const handlePixelClick = (x: number, y: number) => {
+    setSelectedPixel({ x, y });
+    onPixelChange(x, y, currentBrightness);
+  };
+
+  const handleBrightnessChange = (brightness: number) => {
+    setCurrentBrightness(brightness);
+    if (selectedPixel) {
+      onPixelChange(selectedPixel.x, selectedPixel.y, brightness);
+    }
+  };
+
+  const calculateColor = () => {
+    const red = currentBrightness * 25.5;
+    return `rgb(${red}, 0, 0)`;
+  };
+
+  return (
+    <Box
+      display="flex"
+      flexDirection="row"
+      justifyContent="flex-start"
+      bg="lightgray"
+    >
+      <Box ml="10px" style={{ marginRight: "4px" }}>
+        <Button size="xs" onClick={onCloseClick} bg="white">
+          X
+        </Button>
+      </Box>
+      <Box>
+        <Box
+          bg="black"
+          p="10px"
+          borderRadius="5px"
+          style={{ marginTop: "15px" }}
+        >
+          {selectedPixels.map((row, y) => (
+            <Box key={y} display="flex">
+              {row.map((brightness, x) => (
+                <Box key={x} display="flex" mr="2px">
+                  <Button
+                    size="xs"
+                    h="15px"
+                    w="15px"
+                    p={0}
+                    borderRadius={0}
+                    border={
+                      selectedPixel?.x === x && selectedPixel.y === y
+                        ? "2px solid white"
+                        : "0.5px solid white"
+                    }
+                    bgColor={`rgba(255, 0, 0, ${brightness / 9})`}
+                    _hover={{
+                      bgColor:
+                        brightness > 0
+                          ? `rgba(255, 100, 100, ${selectedPixels[y][x] / 9})`
+                          : "rgba(255, 255, 255, 0.5)",
+                    }}
+                    onClick={() => handlePixelClick(x, y)}
+                  />
+                </Box>
+              ))}
+            </Box>
+          ))}
+        </Box>
+      </Box>
+      <Box ml="10px" style={{ marginTop: "15px" }}>
+        <Slider
+          aria-label="brightness"
+          value={currentBrightness}
+          min={0}
+          max={9}
+          step={1}
+          orientation="vertical"
+          _focus={{ boxShadow: "none" }}
+          _active={{ bgColor: "transparent" }}
+          onChange={(value) => handleBrightnessChange(value)}
+        >
+          <SliderTrack>
+            <SliderFilledTrack bg={calculateColor()} />
+          </SliderTrack>
+          <SliderThumb />
+        </Slider>
+      </Box>
+    </Box>
+  );
+};
+
+export const MicrobitMultiplePixelComponent = ({
+  props,
+  view,
+}: {
+  props: WidgetProps;
+  view: EditorView;
+}) => {
+  let args = props.args;
+  let ranges = props.ranges;
+  let types = props.types;
+  let from = props.from;
+  let to = props.to;
+
+  const initialSelectedPixels = parseArgs(args);
+  const [selectedPixels, setSelectedPixels] = useState<number[][]>(
+    initialSelectedPixels
+  );
+
+  const handlePixelChange = (x: number, y: number, brightness: number) => {
+    const updatedPixels = [...selectedPixels];
+    updatedPixels[y][x] = brightness;
+    setSelectedPixels(updatedPixels);
+    updateView();
+  };
+
+  const updateView = () => {
+    let insertion = pixelsToString(selectedPixels);
+    console.log(insertion);
+    if (ranges.length === 1) {
+      view.dispatch({
+        changes: {
+          from: ranges[0].from,
+          to: ranges[0].to,
+          insert: insertion,
+        },
+        effects: [openWidgetEffect.of(insertion.length + from + 2)],
+      });
+    } else {
+      view.dispatch({
+        changes: [
+          {
+            from: from + 1,
+            to: to - 1,
+            insert: insertion,
+          },
+        ],
+        effects: [openWidgetEffect.of(insertion.length + from + 2)],
+      });
+    }
+  };
+
+  const handleCloseClick = () => {
+    view.dispatch({
+      effects: [openWidgetEffect.of(-1)],
+    });
+  };
+
+  return (
+    <MicrobitMultiplePixelsGrid
+      selectedPixels={selectedPixels}
+      onPixelChange={handlePixelChange}
+      onCloseClick={handleCloseClick}
+    />
+  );
+};
+
+const parseArgs = (args: string[]): number[][] => {
+  const defaultPixels = Array.from({ length: 5 }, () => Array(5).fill(0));
+  // If args is empty, return a 5x5 array filled with zeros
+  if (args.length === 0) {
+    return defaultPixels;
+  }
+  if (args.length !== 1) {
+    return defaultPixels;
+  }
+  const argString = args[0].replace(/"/g, "");
+  const rows = argString.split(":");
+  if (rows.length !== 5) {
+    return defaultPixels;
+  }
+  const numbers: number[][] = [];
+  for (let row of rows) {
+    row = row.trim();
+    if (!/^\d{5}$/.test(row)) {
+      return defaultPixels;
+    }
+    const rowNumbers = row.split("").map(Number);
+    numbers.push(rowNumbers);
+  }
+  return numbers;
+};
+
+function pixelsToString(pixels: number[][]): string {
+  let outputString = '"';
+  for (let y = 0; y < 5; y++) {
+    for (let x = 0; x < 5; x++) {
+      outputString += pixels[y][x].toString();
+    }
+    outputString += ":";
+  }
+  outputString = outputString.slice(0, -1);
+  outputString += '"';
+  return outputString;
+}
diff --git a/src/editor/codemirror/helper-widgets/soundWidget.tsx b/src/editor/codemirror/helper-widgets/soundWidget.tsx
new file mode 100644
index 000000000..f999f19e3
--- /dev/null
+++ b/src/editor/codemirror/helper-widgets/soundWidget.tsx
@@ -0,0 +1,556 @@
+import { Box, Button, HStack } from "@chakra-ui/react";
+import { EditorView } from "@codemirror/view";
+import React, { useState } from "react";
+import { openWidgetEffect } from "./openWidgets";
+import { WidgetProps } from "./reactWidgetExtension";
+
+type FixedLengthArray = [
+  number,
+  number,
+  number,
+  number,
+  number,
+  string,
+  string
+];
+
+interface SliderProps {
+  min: number;
+  max: number;
+  step: number;
+  value: number;
+  onChange: (value: number) => void;
+  sliderStyle?: React.CSSProperties;
+  label: string;
+  vertical: boolean;
+  colour: string;
+}
+
+const startVolProps: Omit<SliderProps, "value" | "onChange"> = {
+  min: 0,
+  max: 255,
+  step: 1,
+  sliderStyle: {
+    width: "100%", // Adjust the width of the slider
+    height: "100px", // Adjust the height of the slider
+    backgroundColor: "lightgray", // Change the background color of the slider
+    borderRadius: "10px", // Apply rounded corners to the slider track
+    border: "none", // Remove the border of the slider track
+    outline: "none", // Remove the outline when focused
+  },
+  label: "Start Vol",
+  vertical: true,
+  colour: "red",
+};
+
+const endFrequencySliderProps: Omit<SliderProps, "value" | "onChange"> = {
+  min: 0,
+  max: 9999,
+  step: 1,
+  sliderStyle: {
+    width: "100%",
+    height: "100px",
+    backgroundColor: "lightgray",
+    borderRadius: "10px",
+    border: "none",
+    outline: "none",
+  },
+  label: "End Freq",
+  vertical: true,
+  colour: "green",
+};
+
+const startFrequencySliderProps: Omit<SliderProps, "value" | "onChange"> = {
+  min: 0,
+  max: 9999,
+  step: 1,
+  sliderStyle: {
+    width: "200%", // Adjust the width of the slider
+    height: "100px", // Adjust the height of the slider
+    backgroundColor: "lightgray", // Change the background color of the slider
+    borderRadius: "10px", // Apply rounded corners to the slider track
+    border: "none", // Remove the border of the slider track
+    outline: "none", // Remove the outline when focused
+  },
+  label: "Start Freq",
+  vertical: true,
+  colour: "blue",
+};
+
+const endVolProps: Omit<SliderProps, "value" | "onChange"> = {
+  min: 0,
+  max: 255,
+  step: 1,
+  sliderStyle: {
+    width: "200%", // Adjust the width of the slider
+    height: "100px", // Adjust the height of the slider
+    backgroundColor: "lightgray", // Change the background color of the slider
+    borderRadius: "10px", // Apply rounded corners to the slider track
+    border: "none", // Remove the border of the slider track
+    outline: "none", // Remove the outline when focused
+  },
+  label: "End Vol",
+  vertical: true,
+  colour: "black",
+};
+
+const Slider: React.FC<SliderProps & { vertical?: boolean; colour: string }> =
+  ({
+    min,
+    max,
+    step,
+    value,
+    onChange,
+    sliderStyle,
+    label,
+    vertical,
+    colour,
+  }) => {
+    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+      const newValue = parseFloat(event.target.value);
+      onChange(newValue);
+    };
+
+    return (
+      <div
+        style={{
+          position: "relative",
+          height: "80px",
+          width: "45px",
+          display: "flex",
+          flexDirection: "column",
+          alignItems: "center",
+        }}
+      >
+        <input
+          type="range"
+          min={min}
+          max={max}
+          step={step}
+          value={value}
+          onChange={handleChange}
+          style={{
+            position: "absolute",
+            width: "115px", // Width of the slider
+            height: "40px", // Height of the slider
+            transform: "rotate(-90deg)", // Rotate the slider to vertical orientation
+            accentColor: colour,
+            bottom: "0%",
+          }}
+        />
+        <div
+          style={{
+            position: "absolute",
+            left: "calc(100% - 15px)", // Position the label to the right of the slider
+            bottom: `${((value - min) / (max - min)) * 100}%`, // Calculate the position based on value
+            transform: "translateY(50%)", // Center the label vertically with the thumb
+            fontSize: "13px", // Font size of the label
+          }}
+        >
+          {value}
+        </div>
+        <div
+          style={{ marginTop: "120px", textAlign: "center", fontSize: "11px" }}
+        >
+          <b>{label}</b>
+        </div>
+      </div>
+    );
+  };
+
+const TripleSliderWidget: React.FC<{
+  freqStartProps: Omit<SliderProps, "value" | "onChange">;
+  freqEndProps: Omit<SliderProps, "value" | "onChange">;
+  volStartProps: Omit<SliderProps, "value" | "onChange">;
+  volEndprops: Omit<SliderProps, "value" | "onChange">;
+  props: WidgetProps;
+  view: EditorView;
+}> = ({
+  freqStartProps,
+  freqEndProps,
+  volStartProps,
+  volEndprops,
+  props,
+  view,
+}) => {
+  let args = props.args;
+  let ranges = props.ranges;
+  let types = props.types;
+  let from = props.from;
+  let to = props.to;
+
+  //parse args
+
+  let argsToBeUsed: FixedLengthArray = [200, 500, 2000, 50, 50, "sine", "None"]; // default args
+  let count = 0;
+  for (let i = 2; i < args.length; i += 3) {
+    //Update default args with user args where they exist
+    argsToBeUsed[count] = args[i];
+    let arg = args[i];
+    console.log("arg: ", arg);
+    count += 1;
+  }
+
+  console.log("args", argsToBeUsed);
+
+  const startFreq = Math.min(argsToBeUsed[0], 9999);
+  const endFreq = Math.min(argsToBeUsed[1], 9999);
+  const startVol = Math.min(argsToBeUsed[3], 255);
+  const endVol = Math.min(argsToBeUsed[4], 9999);
+
+  const [waveType, setWaveType] = useState("sine");
+
+  const waveformOptions = ["None", "Vibrato", "Tremolo", "Warble"];
+  const textBoxValue = Number(argsToBeUsed[2]);
+
+  const updateView = (change: Partial<ParsedArgs>) => {
+    let insertion = statesToString({
+      startFreq,
+      endFreq,
+      duration: textBoxValue,
+      startVol,
+      endVol,
+      ...change,
+    });
+    console.log(insertion);
+    if (ranges.length === 1) {
+      view.dispatch({
+        changes: {
+          from: ranges[0].from,
+          to: ranges[0].to,
+          insert: insertion,
+        },
+        effects: [openWidgetEffect.of(insertion.length + from + 2)],
+      });
+    } else {
+      view.dispatch({
+        changes: [
+          {
+            from: from + 1,
+            to: to - 1,
+            insert: insertion,
+          },
+        ],
+        effects: [openWidgetEffect.of(insertion.length + from + 2)],
+      });
+    }
+  };
+
+  const handleTextInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    //const newValue = e.target.value;
+    //setTextBoxValue(Number(newValue));
+    updateView({});
+  };
+
+  const handleSlider1Change = (value: number) => {
+    //freqStartProps.onChange(value);
+    //setInitialFrequency(value);
+    updateView({
+      startFreq: value,
+    });
+  };
+
+  const handleSlider2Change = (value: number) => {
+    //freqEndProps.onChange(value);
+    //setEndFrequency(value); //
+    updateView({});
+  };
+
+  const handleSlider3Change = (value: number) => {
+    //freqStartProps.onChange(value);
+    //setStartAmplitude(value);
+    updateView({});
+  };
+
+  const handleSlider4Change = (value: number) => {
+    //freqStartProps.onChange(value);
+    //setEndAmplitude(value);
+    updateView({});
+  };
+
+  const handleWaveTypeChange = (value: string) => {
+    setWaveType(value);
+  };
+
+  const generateWavePath = () => {
+    const waveLength = 400; // Width of the box
+    const pathData = [];
+
+    const frequencyDifference = endFreq - startFreq;
+    const amplitudeDifference = endVol - startVol;
+
+    // Loop through the wave's width to generate the path
+    for (let x = 0; x <= waveLength; x++) {
+      const currentFrequency =
+        (startFreq + (frequencyDifference * x) / waveLength) / 100;
+      const currentAmplitude =
+        (startVol + (amplitudeDifference * x) / waveLength) / 2.2;
+      const period = waveLength / currentFrequency;
+
+      // Calculate the y-coordinate based on the current frequency and amplitude
+      let y = 0;
+      switch (waveType) {
+        case "sine":
+          y = 65 + currentAmplitude * Math.sin((x / period) * 2 * Math.PI);
+          break;
+        case "square":
+          y =
+            x % period < period / 2
+              ? 65 + currentAmplitude
+              : 65 - currentAmplitude;
+          break;
+        case "sawtooth":
+          y =
+            65 +
+            currentAmplitude -
+            ((x % period) / period) * (2 * currentAmplitude);
+          break;
+        case "triangle":
+          const tPeriod = x % period;
+          y =
+            tPeriod < period / 2
+              ? 65 + ((2 * currentAmplitude) / period) * tPeriod
+              : 65 - ((2 * currentAmplitude) / period) * (tPeriod - period / 2);
+          break;
+        case "noisy":
+          // Generate noisy wave based on sine wave and random noise
+          const baseWave =
+            65 + currentAmplitude * Math.sin((x / period) * 2 * Math.PI);
+          const randomNoise = Math.random() * 2 - 1;
+          y = baseWave + randomNoise * (currentAmplitude * 0.3);
+          break;
+      }
+
+      // Add the point to the path data
+      pathData.push(`${x},${y}`);
+    }
+
+    // Join the path data points to create the path
+    return `M${pathData.join(" ")}`;
+  };
+
+  return (
+    <div>
+      <div
+        style={{
+          display: "flex",
+          justifyContent: "flex-start",
+          backgroundColor: "snow",
+          width: "575px",
+          height: "150px",
+          border: "1px solid lightgray",
+          boxShadow: "0 0 10px 5px rgba(173, 216, 230, 0.7)",
+          zIndex: 10,
+        }}
+      >
+        {/* Vertical Slider 1 */}
+        <div
+          style={{
+            marginLeft: "6px",
+            marginRight: "20px",
+            height: "100px",
+            marginTop: "9px",
+          }}
+        >
+          <Slider
+            {...freqStartProps}
+            value={startFreq}
+            onChange={handleSlider1Change}
+            vertical
+          />
+        </div>
+        {/* Vertical Slider 2 */}
+        <div style={{ marginRight: "20px", height: "100px", marginTop: "9px" }}>
+          <Slider
+            {...freqEndProps}
+            // TODO: for this and all the following sliders we need value to come from the parsed args above
+            //       and the handleXXXChange functions need to be updated to pass the relevant change to updateView
+            value={0}
+            onChange={handleSlider2Change}
+            vertical
+          />
+        </div>
+        {/* Vertical Slider 3 */}
+        <div style={{ marginRight: "20px", height: "100px", marginTop: "9px" }}>
+          <Slider
+            {...volStartProps}
+            value={0}
+            onChange={handleSlider3Change}
+            vertical
+          />
+        </div>
+        {/* Vertical Slider 4 */}
+        <div style={{ marginRight: "25px", height: "100px", marginTop: "9px" }}>
+          <Slider
+            {...volEndprops}
+            value={0}
+            onChange={handleSlider4Change}
+            vertical
+          />
+        </div>
+
+        <div style={{ marginRight: "10px", height: "100px", fontSize: "12px" }}>
+          {/* waveform type selection */}
+          <label
+            style={{ display: "block", marginBottom: "5px", marginTop: "7px" }}
+          >
+            <b>Waveform:</b>
+          </label>
+          <select onChange={(e) => handleWaveTypeChange(e.target.value)}>
+            <option value="sine">Sine</option>
+            <option value="square">Square</option>
+            <option value="sawtooth">Sawtooth</option>
+            <option value="triangle">Triangle</option>
+            <option value="noisy">Noisy</option>
+          </select>
+
+          {/* fx type selection */}
+
+          <label
+            style={{ display: "block", marginBottom: "5px", marginTop: "10px" }}
+          >
+            <b>Effects:</b>
+          </label>
+          <select onChange={(e) => handleWaveTypeChange(e.target.value)}>
+            <option value="sine">None</option>
+            <option value="square">Vibrato</option>
+            <option value="sawtooth">Tremelo</option>
+            <option value="triangle">Warble</option>
+          </select>
+
+          {/* Duration selctor */}
+
+          <label
+            style={{ display: "block", marginBottom: "5px", marginTop: "10px" }}
+          >
+            <b>Duration(ms):</b>
+          </label>
+          {/* Input field with associated datalist */}
+          <input
+            type="text"
+            value={textBoxValue}
+            onChange={handleTextInputChange} // Handle the selected or typed-in value
+            defaultValue="2000"
+            style={{ width: "75px" }}
+          />
+        </div>
+        {/* Waveform box */}
+        <div
+          style={{
+            width: "200px",
+            height: "130px",
+            backgroundColor: "linen",
+            marginTop: "9px",
+            marginLeft: "5px",
+          }}
+        >
+          <svg width="100%" height="100%">
+            <path d={generateWavePath()} stroke="black" fill="none" />
+            <line
+              x1="0%" // Start of the line
+              y1="50%" // Vertically center the line
+              x2="100%" // End of the line
+              y2="50%" // Keep the line horizontal
+              stroke="gray" // Line color
+              strokeWidth="0.5" // Line thickness
+            />
+          </svg>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export const SoundComponent = ({
+  props,
+  view,
+}: {
+  props: WidgetProps;
+  view: EditorView;
+}) => {
+  let args = props.args;
+  let ranges = props.ranges;
+  let types = props.types;
+  let from = props.from;
+  let to = props.to;
+
+  //for future reference add a aclose button
+  const handleCloseClick = () => {
+    view.dispatch({
+      effects: [openWidgetEffect.of(-1)],
+    });
+  };
+
+  const updateView = () => {
+    let insertion = "test";
+    console.log(insertion);
+    if (ranges.length === 1) {
+      view.dispatch({
+        changes: {
+          from: ranges[0].from,
+          to: ranges[0].to,
+          insert: insertion,
+        },
+        effects: [openWidgetEffect.of(insertion.length + from + 2)],
+      });
+    } else {
+      view.dispatch({
+        changes: [
+          {
+            from: from + 1,
+            to: to - 1,
+            insert: insertion,
+          },
+        ],
+        effects: [openWidgetEffect.of(insertion.length + from + 2)],
+      });
+    }
+  };
+
+  return (
+    <HStack fontFamily="body" spacing={5} py={3} zIndex={10}>
+      <Box ml="10px" style={{ marginRight: "4px" }}>
+        <Button size="xs" onClick={handleCloseClick} bg="white">
+          X
+        </Button>
+      </Box>
+      <TripleSliderWidget
+        freqStartProps={startFrequencySliderProps}
+        freqEndProps={endFrequencySliderProps}
+        volStartProps={startVolProps}
+        volEndprops={endVolProps}
+        props={props}
+        view={view}
+      />
+    </HStack>
+  );
+};
+
+//(startFreq: number, endFreq: Number, duration: Number, startVol: number, endVol: Number, waveform: string, fx: string)
+
+interface ParsedArgs {
+  startFreq: number;
+  endFreq: number;
+  duration: number;
+  startVol: number;
+  endVol: number;
+}
+
+function statesToString({
+  startFreq,
+  endFreq,
+  duration,
+  startVol,
+  endVol,
+}: ParsedArgs): string {
+  return (
+    `\n` +
+    `        freq_start=${startFreq},\n` +
+    `        freq_end=${endFreq},\n` +
+    `        duration=${duration},\n` +
+    `        vol_start=${startVol},\n` +
+    `        vol_end=${endVol},\n` +
+    `        waveform=SoundEffect.FX_WARBLE,\n` +
+    `        fx=SoundEffect.FX_VIBRATO`
+  );
+}
diff --git a/src/editor/codemirror/helper-widgets/widgetArgParser.tsx b/src/editor/codemirror/helper-widgets/widgetArgParser.tsx
new file mode 100644
index 000000000..176cd8b6e
--- /dev/null
+++ b/src/editor/codemirror/helper-widgets/widgetArgParser.tsx
@@ -0,0 +1,142 @@
+import { EditorState } from "@codemirror/state";
+import { SyntaxNode } from "@lezer/common";
+import { WidgetProps } from "./reactWidgetExtension";
+import { MicrobitSinglePixelComponent } from "./setPixelWidget";
+import { MicrobitMultiplePixelComponent } from "./showImageWidget";
+import { SoundComponent } from "./soundWidget";
+import { OpenReactComponent, OpenSoundComponent } from "./openWidgets";
+
+export interface CompProps {
+  comp: React.ComponentType<any>;
+  props: WidgetProps;
+  open: React.ComponentType<any>
+}
+
+export function createWidget(
+  name: string,
+  state: EditorState,
+  node: SyntaxNode
+): CompProps | null {
+  let children = getChildNodes(node);
+  let ranges = getRanges(children);
+  let args = getArgs(state, ranges);
+  let types = getTypes(children);
+  let component: React.ComponentType<any> | null = null;
+
+  switch (name) {
+    case "display.set_pixel":
+      component = MicrobitSinglePixelComponent;
+      break;
+    case "Image":
+      component = MicrobitMultiplePixelComponent;
+      break;
+    case "audio.SoundEffect":
+    case "SoundEffect":
+      component = SoundComponent;
+      break;
+    default:
+      // No widget implemented for this function
+      // console.log("No widget implemented for this function: " + name);
+      return null;
+  }
+  if (component) {
+    return {
+      comp: component,
+      props: {
+        args: args,
+        ranges: ranges,
+        types: types,
+        from: node.from,
+        to: node.to,
+      },
+      open: OpenButtonDesign(component, args, types)
+    };
+  }
+  return null;
+}
+
+// Gets all child nodes of a CallExpression, no typechecking
+function getChildNodes(node: SyntaxNode): SyntaxNode[] {
+  let child = node.firstChild?.nextSibling;
+  let children = [];
+  while (child && child.name !== ")") {
+    if (child.name !== "," && child.name !== "Comment") children.push(child);
+    child = child.nextSibling;
+  }
+  return children;
+}
+
+// Gets ranges for insertion into arguments
+function getRanges(nodes: SyntaxNode[]): { from: number; to: number }[] {
+  let ranges: { from: number; to: number }[] = [];
+  nodes.forEach(function (value) {
+    ranges.push({ from: value.from, to: value.to });
+  });
+  return ranges;
+}
+
+// Gets arguments as string
+function getArgs(
+  state: EditorState,
+  ranges: { from: number; to: number }[]
+): string[] {
+  let args: string[] = [];
+  ranges.forEach(function (value) {
+    args.push(state.doc.sliceString(value.from, value.to));
+  });
+  return args;
+}
+
+// Gets types of each arg to determine if it is editable
+function getTypes(nodes: SyntaxNode[]): string[] {
+  let types: string[] = [];
+  nodes.forEach(function (value) {
+    types.push(value.name);
+  });
+  return types;
+}
+
+function OpenButtonDesign(
+  name: React.ComponentType<any>,
+  args: string[],
+  types: string[]
+): React.ComponentType<any> {
+  switch (name) {
+    case MicrobitMultiplePixelComponent:
+      return OpenReactComponent;
+    case MicrobitSinglePixelComponent:
+      return OpenReactComponent;
+    case SoundComponent:
+      return OpenSoundComponent;
+    default:
+      // shouldnt be called so just null
+      return OpenReactComponent;
+  }
+}
+
+export function ValidateComponentArgs(
+  name: React.ComponentType<any>,
+  args: string[],
+  types: string[]
+): boolean {
+  switch (name) {
+    case MicrobitMultiplePixelComponent:
+      return true;
+    case MicrobitSinglePixelComponent:
+      // If more than 3 arguments, don't open
+      if (args.length > 3) {
+        return false;
+      }
+      // If some arguments are not numbers or empty, don't open
+      for (let i = 0; i < args.length; i++) {
+        if (types[i] !== "Number" && args[i] !== ",") {
+          return false;
+        }
+      }
+      return true;
+    case SoundComponent:
+      return true;
+    default:
+      return false;
+  }
+}