gradio/js/audio/interactive/Audio.svelte

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

366 lines
8.1 KiB
Svelte
Raw Normal View History

2022-03-07 21:36:49 +08:00
<script context="module" lang="ts">
import type { FileData } from "@gradio/upload";
import { BaseButton } from "@gradio/button/static";
2022-03-07 21:36:49 +08:00
export interface AudioData extends FileData {
crop_min?: number;
crop_max?: number;
}
</script>
2022-02-23 19:17:41 +08:00
2022-03-07 21:36:49 +08:00
<script lang="ts">
import { onDestroy, createEventDispatcher } from "svelte";
2022-02-23 19:17:41 +08:00
import { Upload, ModifyUpload } from "@gradio/upload";
import { BlockLabel } from "@gradio/atoms";
import { Music } from "@gradio/icons";
import Audio from "../shared/Audio.svelte";
// @ts-ignore
2022-02-23 19:17:41 +08:00
import Range from "svelte-range-slider-pips";
import { _ } from "svelte-i18n";
2022-02-23 19:17:41 +08:00
import type { IBlobEvent, IMediaRecorder } from "extendable-media-recorder";
2022-02-25 20:24:32 +08:00
export let value: null | { name: string; data: string } = null;
export let label: string;
export let show_label = true;
export let name = "";
2022-03-07 21:36:49 +08:00
export let source: "microphone" | "upload" | "none";
export let pending = false;
export let streaming = false;
export let autoplay = false;
export let show_edit_button = true;
2022-02-23 19:17:41 +08:00
2022-03-17 00:34:30 +08:00
// TODO: make use of this
// export let type: "normal" | "numpy" = "normal";
2022-03-17 00:34:30 +08:00
2022-02-23 19:17:41 +08:00
let recording = false;
let recorder: IMediaRecorder;
2022-02-23 19:17:41 +08:00
let mode = "";
let header: Uint8Array | undefined = undefined;
let pending_stream: Uint8Array[] = [];
let submit_pending_stream_on_pending_end = false;
let player: HTMLAudioElement;
2022-02-23 19:17:41 +08:00
let inited = false;
let crop_values: [number, number] = [0, 100];
const STREAM_TIMESLICE = 500;
const NUM_HEADER_BYTES = 44;
let audio_chunks: Blob[] = [];
let module_promises: [
Promise<typeof import("extendable-media-recorder")>,
Promise<typeof import("extendable-media-recorder-wav-encoder")>
];
function get_modules(): void {
module_promises = [
import("extendable-media-recorder"),
import("extendable-media-recorder-wav-encoder"),
];
}
if (streaming) {
get_modules();
}
2022-02-23 19:17:41 +08:00
2022-03-07 21:36:49 +08:00
const dispatch = createEventDispatcher<{
change: AudioData | null;
stream: AudioData;
edit: never;
play: never;
pause: never;
stop: never;
end: never;
drag: boolean;
error: string;
upload: FileData;
clear: never;
start_recording: never;
stop_recording: never;
2022-03-07 21:36:49 +08:00
}>();
2022-02-23 19:17:41 +08:00
function blob_to_data_url(blob: Blob): Promise<string> {
return new Promise((fulfill, reject) => {
let reader = new FileReader();
reader.onerror = reject;
reader.onload = () => fulfill(reader.result as string);
2022-02-23 19:17:41 +08:00
reader.readAsDataURL(blob);
});
}
const dispatch_blob = async (
blobs: Uint8Array[] | Blob[],
event: "stream" | "change" | "stop_recording"
): Promise<void> => {
let _audio_blob = new Blob(blobs, { type: "audio/wav" });
value = {
data: await blob_to_data_url(_audio_blob),
name: "audio.wav",
};
dispatch(event, value);
};
async function prepare_audio(): Promise<void> {
let stream: MediaStream | null;
try {
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (err) {
if (!navigator.mediaDevices) {
dispatch("error", $_("audio.no_device_support"));
return;
}
if (err instanceof DOMException && err.name == "NotAllowedError") {
dispatch("error", $_("audio.allow_recording_access"));
return;
}
throw err;
}
if (stream == null) return;
if (streaming) {
const [{ MediaRecorder, register }, { connect }] = await Promise.all(
module_promises
);
2022-02-23 19:17:41 +08:00
await register(await connect());
2022-02-23 19:17:41 +08:00
recorder = new MediaRecorder(stream, { mimeType: "audio/wav" });
recorder.addEventListener("dataavailable", handle_chunk);
} else {
recorder = new MediaRecorder(stream);
recorder.addEventListener("dataavailable", (event) => {
audio_chunks.push(event.data);
});
recorder.addEventListener("stop", async () => {
recording = false;
await dispatch_blob(audio_chunks, "change");
await dispatch_blob(audio_chunks, "stop_recording");
audio_chunks = [];
});
}
2022-07-20 02:37:07 +08:00
inited = true;
2022-02-23 19:17:41 +08:00
}
async function handle_chunk(event: IBlobEvent): Promise<void> {
let buffer = await event.data.arrayBuffer();
let payload = new Uint8Array(buffer);
if (!header) {
header = new Uint8Array(buffer.slice(0, NUM_HEADER_BYTES));
payload = new Uint8Array(buffer.slice(NUM_HEADER_BYTES));
}
if (pending) {
pending_stream.push(payload);
} else {
let blobParts = [header].concat(pending_stream, [payload]);
dispatch_blob(blobParts, "stream");
pending_stream = [];
}
}
$: if (submit_pending_stream_on_pending_end && pending === false) {
submit_pending_stream_on_pending_end = false;
if (header && pending_stream) {
let blobParts: Uint8Array[] = [header].concat(pending_stream);
pending_stream = [];
dispatch_blob(blobParts, "stream");
}
}
2022-02-23 19:17:41 +08:00
async function record(): Promise<void> {
if (!navigator.mediaDevices) {
dispatch("error", $_("audio.no_device_support"));
return;
}
recording = true;
2023-06-08 11:54:02 +08:00
dispatch("start_recording");
if (!inited) await prepare_audio();
header = undefined;
if (streaming) {
recorder.start(STREAM_TIMESLICE);
} else {
recorder.start();
}
2022-02-23 19:17:41 +08:00
}
onDestroy(() => {
2022-02-25 20:24:32 +08:00
if (recorder && recorder.state !== "inactive") {
2022-02-23 19:17:41 +08:00
recorder.stop();
}
});
function stop(): void {
2022-02-23 19:17:41 +08:00
recorder.stop();
if (streaming) {
recording = false;
dispatch("stop_recording");
if (pending) {
submit_pending_stream_on_pending_end = true;
}
}
}
2022-02-23 19:17:41 +08:00
function clear(): void {
dispatch("change", null);
dispatch("clear");
2022-02-23 19:17:41 +08:00
mode = "";
2022-02-25 20:24:32 +08:00
value = null;
2022-02-23 19:17:41 +08:00
}
function handle_change({
detail: { values },
2022-02-23 19:17:41 +08:00
}: {
detail: { values: [number, number] };
}): void {
if (!value) return;
2022-02-23 19:17:41 +08:00
dispatch("change", {
2022-02-25 20:24:32 +08:00
data: value.data,
2022-02-23 19:17:41 +08:00
name,
crop_min: values[0],
crop_max: values[1],
2022-02-23 19:17:41 +08:00
});
2022-03-17 00:34:30 +08:00
dispatch("edit");
2022-02-23 19:17:41 +08:00
}
2022-02-25 23:12:22 +08:00
function handle_load({
detail,
2022-02-25 23:12:22 +08:00
}: {
detail: {
data: string;
name: string;
size: number;
is_example: boolean;
};
}): void {
2022-02-25 23:12:22 +08:00
value = detail;
dispatch("change", { data: detail.data, name: detail.name });
dispatch("upload", detail);
2022-02-25 23:12:22 +08:00
}
function handle_ended(): void {
2023-06-08 11:54:02 +08:00
dispatch("stop");
dispatch("end");
}
export let dragging = false;
$: dispatch("drag", dragging);
2022-02-23 19:17:41 +08:00
</script>
Python backend to theming (#2931) * add theme + theme atoms * audio * buttons * chatbot * forms * start file * complete file * fixup workbench * gallery * highlighted text * label * json * upload * 3d model * atoms * chart * md + html * image * plot + build * table * tabs * tooltip * upload * tweaks * tweaks + more tooling * tweaks to padding/ lineheight * app components _ start api docs * format, more api docs * finish api docs * interpretation * todos * tweaks + cleanup * tweaks + cleanup * revert range tweaks * fix notebooks * fix test * remove tw * cleanup + login * fix gitignore * fix types * run css script * fix progress + tweaks * update demos * add css build to static check workflow * tweak ci * fix tests * tweak markdown * tweak chatbot + file * fix tabs * tweak tabs * cleanup * fix api docs * fix example gallery * add gradient to toast * fix min height for interfaces * revert tab changes * update notebooks * changes * changes * change * changes * changes * changes * changes * changes * changes * changes * changes * change * changes * changes * changes * changes * changes * changes * changes * fix * changes * changes * changes * changes * changes * changes * undo radius * undo radius * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * change * undo * Add absolute imports * mock theme in tests * clean * changes * changes --------- Co-authored-by: pngwn <hello@pngwn.io> Co-authored-by: freddyaboulton <alfonsoboulton@gmail.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
2023-03-07 04:52:31 +08:00
<BlockLabel
{show_label}
Icon={Music}
float={source === "upload" && value === null}
label={label || $_("audio.audio")}
Python backend to theming (#2931) * add theme + theme atoms * audio * buttons * chatbot * forms * start file * complete file * fixup workbench * gallery * highlighted text * label * json * upload * 3d model * atoms * chart * md + html * image * plot + build * table * tabs * tooltip * upload * tweaks * tweaks + more tooling * tweaks to padding/ lineheight * app components _ start api docs * format, more api docs * finish api docs * interpretation * todos * tweaks + cleanup * tweaks + cleanup * revert range tweaks * fix notebooks * fix test * remove tw * cleanup + login * fix gitignore * fix types * run css script * fix progress + tweaks * update demos * add css build to static check workflow * tweak ci * fix tests * tweak markdown * tweak chatbot + file * fix tabs * tweak tabs * cleanup * fix api docs * fix example gallery * add gradient to toast * fix min height for interfaces * revert tab changes * update notebooks * changes * changes * change * changes * changes * changes * changes * changes * changes * changes * changes * change * changes * changes * changes * changes * changes * changes * changes * fix * changes * changes * changes * changes * changes * changes * undo radius * undo radius * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * change * undo * Add absolute imports * mock theme in tests * clean * changes * changes --------- Co-authored-by: pngwn <hello@pngwn.io> Co-authored-by: freddyaboulton <alfonsoboulton@gmail.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
2023-03-07 04:52:31 +08:00
/>
{#if value === null || streaming}
{#if source === "microphone"}
<div class="mic-wrap">
{#if recording}
<BaseButton size="sm" on:click={stop}>
<span class="record-icon">
<span class="pinger" />
<span class="dot" />
</span>
{$_("audio.stop_recording")}
</BaseButton>
{:else}
<BaseButton size="sm" on:click={record}>
<span class="record-icon">
<span class="dot" />
</span>
{$_("audio.record_from_microphone")}
</BaseButton>
{/if}
</div>
{:else if source === "upload"}
<!-- explicitly listed out audio mimetypes due to iOS bug not recognizing audio/* -->
<Upload
filetype="audio/aac,audio/midi,audio/mpeg,audio/ogg,audio/wav,audio/x-wav,audio/opus,audio/webm,audio/flac,audio/vnd.rn-realaudio,audio/x-ms-wma,audio/x-aiff,audio/amr,audio/*"
on:load={handle_load}
bind:dragging
>
<slot />
</Upload>
{/if}
{:else}
<ModifyUpload
on:clear={clear}
on:edit={() => (mode = "edit")}
editable={show_edit_button}
Python backend to theming (#2931) * add theme + theme atoms * audio * buttons * chatbot * forms * start file * complete file * fixup workbench * gallery * highlighted text * label * json * upload * 3d model * atoms * chart * md + html * image * plot + build * table * tabs * tooltip * upload * tweaks * tweaks + more tooling * tweaks to padding/ lineheight * app components _ start api docs * format, more api docs * finish api docs * interpretation * todos * tweaks + cleanup * tweaks + cleanup * revert range tweaks * fix notebooks * fix test * remove tw * cleanup + login * fix gitignore * fix types * run css script * fix progress + tweaks * update demos * add css build to static check workflow * tweak ci * fix tests * tweak markdown * tweak chatbot + file * fix tabs * tweak tabs * cleanup * fix api docs * fix example gallery * add gradient to toast * fix min height for interfaces * revert tab changes * update notebooks * changes * changes * change * changes * changes * changes * changes * changes * changes * changes * changes * change * changes * changes * changes * changes * changes * changes * changes * fix * changes * changes * changes * changes * changes * changes * undo radius * undo radius * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * changes * change * undo * Add absolute imports * mock theme in tests * clean * changes * changes --------- Co-authored-by: pngwn <hello@pngwn.io> Co-authored-by: freddyaboulton <alfonsoboulton@gmail.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
2023-03-07 04:52:31 +08:00
absolute={true}
/>
<div class="container">
<Audio
controls
{autoplay}
{crop_values}
bind:node={player}
preload="metadata"
src={value?.data}
on:play
on:pause
on:ended={handle_ended}
data-testid={`${label}-audio`}
/>
</div>
{#if mode === "edit" && player?.duration}
<Range
bind:values={crop_values}
range
min={0}
max={100}
step={1}
on:change={handle_change}
2022-02-23 19:17:41 +08:00
/>
{/if}
{/if}
<style>
.mic-wrap {
padding: var(--size-2);
}
.record-icon {
display: flex;
position: relative;
margin-right: var(--size-2);
width: 6px;
height: 6px;
}
.dot {
display: inline-flex;
position: relative;
border-radius: var(--radius-full);
background: var(--color-red-500);
width: 6px;
height: 6px;
}
.pinger {
display: inline-flex;
position: absolute;
opacity: 0.9;
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
border-radius: var(--radius-full);
background: var(--color-red-500);
width: var(--size-full);
height: var(--size-full);
}
@keyframes ping {
75%,
100% {
transform: scale(2);
opacity: 0;
}
}
</style>