gradio / js /audio /shared /WaveformControls.svelte
mindmime's picture
Upload folder using huggingface_hub
a03b3ba verified
<script lang="ts">
import { Play, Pause, Forward, Backward, Undo, Trim } from "@gradio/icons";
import { get_skip_rewind_amount } from "../shared/utils";
import type { I18nFormatter } from "@gradio/utils";
import WaveSurfer from "wavesurfer.js";
import RegionsPlugin, {
type Region
} from "wavesurfer.js/dist/plugins/regions.js";
import type { WaveformOptions } from "./types";
import VolumeLevels from "./VolumeLevels.svelte";
import VolumeControl from "./VolumeControl.svelte";
export let waveform: WaveSurfer;
export let audio_duration: number;
export let i18n: I18nFormatter;
export let playing: boolean;
export let showRedo = false;
export let interactive = false;
export let handle_trim_audio: (start: number, end: number) => void;
export let mode = "";
export let container: HTMLDivElement;
export let handle_reset_value: () => void;
export let waveform_options: WaveformOptions = {};
export let trim_region_settings: WaveformOptions = {};
export let show_volume_slider = false;
export let editable = true;
export let trimDuration = 0;
let playbackSpeeds = [0.5, 1, 1.5, 2];
let playbackSpeed = playbackSpeeds[1];
let trimRegion: RegionsPlugin;
let activeRegion: Region | null = null;
let leftRegionHandle: HTMLDivElement | null;
let rightRegionHandle: HTMLDivElement | null;
let activeHandle = "";
let currentVolume = 1;
$: trimRegion = waveform.registerPlugin(RegionsPlugin.create());
$: trimRegion?.on("region-out", (region) => {
region.play();
});
$: trimRegion?.on("region-updated", (region) => {
trimDuration = region.end - region.start;
});
$: trimRegion?.on("region-clicked", (region, e) => {
e.stopPropagation(); // prevent triggering a click on the waveform
activeRegion = region;
region.play();
});
const addTrimRegion = (): void => {
activeRegion = trimRegion.addRegion({
start: audio_duration / 4,
end: audio_duration / 2,
...trim_region_settings
});
trimDuration = activeRegion.end - activeRegion.start;
};
$: if (activeRegion) {
const shadowRoot = container.children[0]!.shadowRoot!;
rightRegionHandle = shadowRoot.querySelector('[data-resize="right"]');
leftRegionHandle = shadowRoot.querySelector('[data-resize="left"]');
if (leftRegionHandle && rightRegionHandle) {
leftRegionHandle.setAttribute("role", "button");
rightRegionHandle.setAttribute("role", "button");
leftRegionHandle?.setAttribute("aria-label", "Drag to adjust start time");
rightRegionHandle?.setAttribute("aria-label", "Drag to adjust end time");
leftRegionHandle?.setAttribute("tabindex", "0");
rightRegionHandle?.setAttribute("tabindex", "0");
leftRegionHandle.addEventListener("focus", () => {
if (trimRegion) activeHandle = "left";
});
rightRegionHandle.addEventListener("focus", () => {
if (trimRegion) activeHandle = "right";
});
}
}
const trimAudio = (): void => {
if (waveform && trimRegion) {
if (activeRegion) {
const start = activeRegion.start;
const end = activeRegion.end;
handle_trim_audio(start, end);
mode = "";
activeRegion = null;
}
}
};
const clearRegions = (): void => {
trimRegion?.getRegions().forEach((region) => {
region.remove();
});
trimRegion?.clearRegions();
};
const toggleTrimmingMode = (): void => {
clearRegions();
if (mode === "edit") {
mode = "";
} else {
mode = "edit";
addTrimRegion();
}
};
const adjustRegionHandles = (handle: string, key: string): void => {
let newStart;
let newEnd;
if (!activeRegion) return;
if (handle === "left") {
if (key === "ArrowLeft") {
newStart = activeRegion.start - 0.05;
newEnd = activeRegion.end;
} else {
newStart = activeRegion.start + 0.05;
newEnd = activeRegion.end;
}
} else {
if (key === "ArrowLeft") {
newStart = activeRegion.start;
newEnd = activeRegion.end - 0.05;
} else {
newStart = activeRegion.start;
newEnd = activeRegion.end + 0.05;
}
}
activeRegion.setOptions({
start: newStart,
end: newEnd
});
trimDuration = activeRegion.end - activeRegion.start;
};
$: trimRegion &&
window.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") {
adjustRegionHandles(activeHandle, "ArrowLeft");
} else if (e.key === "ArrowRight") {
adjustRegionHandles(activeHandle, "ArrowRight");
}
});
</script>
<div class="controls" data-testid="waveform-controls">
<div class="control-wrapper">
<button
class="action icon volume"
style:color={show_volume_slider
? "var(--color-accent)"
: "var(--neutral-400)"}
aria-label="Adjust volume"
on:click={() => (show_volume_slider = !show_volume_slider)}
>
<VolumeLevels {currentVolume} />
</button>
{#if show_volume_slider}
<VolumeControl bind:currentVolume bind:show_volume_slider {waveform} />
{/if}
<button
class:hidden={show_volume_slider}
class="playback icon"
aria-label={`Adjust playback speed to ${
playbackSpeeds[
(playbackSpeeds.indexOf(playbackSpeed) + 1) % playbackSpeeds.length
]
}x`}
on:click={() => {
playbackSpeed =
playbackSpeeds[
(playbackSpeeds.indexOf(playbackSpeed) + 1) % playbackSpeeds.length
];
waveform.setPlaybackRate(playbackSpeed);
}}
>
<span>{playbackSpeed}x</span>
</button>
</div>
<div class="play-pause-wrapper">
<button
class="rewind icon"
aria-label={`Skip backwards by ${get_skip_rewind_amount(
audio_duration,
waveform_options.skip_length
)} seconds`}
on:click={() =>
waveform.skip(
get_skip_rewind_amount(audio_duration, waveform_options.skip_length) *
-1
)}
>
<Backward />
</button>
<button
class="play-pause-button icon"
on:click={() => waveform.playPause()}
aria-label={playing ? i18n("audio.pause") : i18n("audio.play")}
>
{#if playing}
<Pause />
{:else}
<Play />
{/if}
</button>
<button
class="skip icon"
aria-label="Skip forward by {get_skip_rewind_amount(
audio_duration,
waveform_options.skip_length
)} seconds"
on:click={() =>
waveform.skip(
get_skip_rewind_amount(audio_duration, waveform_options.skip_length)
)}
>
<Forward />
</button>
</div>
<div class="settings-wrapper">
{#if editable && interactive}
{#if showRedo && mode === ""}
<button
class="action icon"
aria-label="Reset audio"
on:click={() => {
handle_reset_value();
clearRegions();
mode = "";
}}
>
<Undo />
</button>
{/if}
{#if mode === ""}
<button
class="action icon"
aria-label="Trim audio to selection"
on:click={toggleTrimmingMode}
>
<Trim />
</button>
{:else}
<button class="text-button" on:click={trimAudio}>Trim</button>
<button class="text-button" on:click={toggleTrimmingMode}>Cancel</button
>
{/if}
{/if}
</div>
</div>
<style>
.settings-wrapper {
display: flex;
justify-self: self-end;
align-items: center;
}
.text-button {
border: 1px solid var(--neutral-400);
border-radius: var(--radius-sm);
font-weight: 300;
font-size: var(--size-3);
text-align: center;
color: var(--neutral-400);
height: var(--size-5);
font-weight: bold;
padding: 0 5px;
margin-left: 5px;
}
.text-button:hover,
.text-button:focus {
color: var(--color-accent);
border-color: var(--color-accent);
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
margin-top: 5px;
align-items: center;
position: relative;
}
.hidden {
display: none;
}
.control-wrapper {
display: flex;
justify-self: self-start;
align-items: center;
justify-content: space-between;
}
@media (max-width: 375px) {
.controls {
display: flex;
flex-wrap: wrap;
}
.controls * {
margin: var(--spacing-sm);
}
.controls .text-button {
margin-left: 0;
}
}
.action {
width: var(--size-5);
width: var(--size-5);
color: var(--neutral-400);
margin-left: var(--spacing-md);
}
.icon:hover,
.icon:focus {
color: var(--color-accent);
}
.play-pause-wrapper {
display: flex;
justify-self: center;
}
.playback {
border: 1px solid var(--neutral-400);
border-radius: var(--radius-sm);
width: 5.5ch;
font-weight: 300;
font-size: var(--size-3);
text-align: center;
color: var(--neutral-400);
height: var(--size-5);
font-weight: bold;
}
.playback:hover,
.playback:focus {
color: var(--color-accent);
border-color: var(--color-accent);
}
.rewind,
.skip {
margin: 0 10px;
color: var(--neutral-400);
}
.play-pause-button {
width: var(--size-8);
width: var(--size-8);
display: flex;
align-items: center;
justify-content: center;
color: var(--neutral-400);
fill: var(--neutral-400);
}
.volume {
position: relative;
display: flex;
justify-content: center;
margin-right: var(--spacing-xl);
}
</style>