diff --git a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx
index 282b7432..fecef837 100644
--- a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx
+++ b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx
@@ -3,6 +3,7 @@ import {
ThreejsArtifactViewer,
isThreejsArtifact,
} from "./ThreejsArtifactViewer"
+import { WaveSurferArtifactViewer } from "./WaveSurferArtifactViewer"
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"
import { CardMedia, Box } from "@mui/material"
@@ -43,9 +44,12 @@ export const ArtifactCardMedia: FC<{
alignItems: "center",
}}
>
-
+
)
} else if (artifact.mimetype.startsWith("image")) {
diff --git a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx
new file mode 100644
index 00000000..74177602
--- /dev/null
+++ b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx
@@ -0,0 +1,72 @@
+import React, { useCallback, useEffect, useState, useRef } from "react"
+import WaveSurfer from "wavesurfer.js"
+import { Box } from "@mui/material"
+
+interface WaveSurferArtifactViewerProps {
+ height: number
+ waveColor: string
+ progressColor: string
+ url: string
+}
+
+const useWavesurfer = (
+ containerRef: React.MutableRefObject,
+ options: WaveSurferArtifactViewerProps
+) => {
+ const [wavesurfer, setWavesurfer] = useState(null)
+
+ useEffect(() => {
+ if (!containerRef.current) return
+
+ const ws = WaveSurfer.create({
+ ...options,
+ container: containerRef.current,
+ })
+
+ setWavesurfer(ws)
+
+ return () => {
+ ws.destroy()
+ }
+ }, [containerRef])
+
+ return wavesurfer
+}
+
+// Create a React component of wavesurfer.
+export const WaveSurferArtifactViewer: React.FC<
+ WaveSurferArtifactViewerProps
+> = (props) => {
+ const containerRef = useRef(null!)
+ const [isPlaying, setIsPlaying] = useState(false)
+ const wavesurfer = useWavesurfer(containerRef, props)
+
+ const onPlayClick = useCallback(() => {
+ if (!wavesurfer) return
+ wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play()
+ }, [wavesurfer])
+
+ useEffect(() => {
+ if (!wavesurfer) return
+
+ setIsPlaying(false)
+
+ const subscriptions = [
+ wavesurfer.on("play", () => setIsPlaying(true)),
+ wavesurfer.on("pause", () => setIsPlaying(false)),
+ ]
+
+ return () => {
+ subscriptions.forEach((unsub) => unsub())
+ }
+ }, [wavesurfer])
+
+ return (
+
+
+
+
+ )
+}
diff --git a/package-lock.json b/package-lock.json
index 409d4225..05c79bb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,7 +32,8 @@
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
- "three": "^0.155.0"
+ "three": "^0.155.0",
+ "wavesurfer.js": "^7.4.12"
},
"devDependencies": {
"@babel/core": "^7.14.3",
@@ -14824,6 +14825,11 @@
"node": ">=10.13.0"
}
},
+ "node_modules/wavesurfer.js": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz",
+ "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g=="
+ },
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
@@ -25938,6 +25944,11 @@
"graceful-fs": "^4.1.2"
}
},
+ "wavesurfer.js": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz",
+ "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g=="
+ },
"web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
diff --git a/package.json b/package.json
index 3e624c56..9e6e59e9 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,8 @@
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
- "three": "^0.155.0"
+ "three": "^0.155.0",
+ "wavesurfer.js": "^7.4.12"
},
"devDependencies": {
"@babel/core": "^7.14.3",