├── public ├── favicon.ico ├── pop_sample.jpg ├── pop_sample.mp3 ├── rap_sample.jpg ├── rap_sample.mp3 └── about │ ├── astronaut.gif │ ├── funky_sax.gif │ ├── funky_sax.mp3 │ ├── funky_sax.png │ ├── hand_drawn.mp3 │ ├── arabic_gospel.mp3 │ ├── typing_to_jazz.mp3 │ ├── fourier_transform.png │ ├── img2img_example.png │ ├── spectrogram_label.png │ ├── funky_sax_to_piano.mp3 │ ├── funky_sax_to_piano.png │ ├── web_app_screenshot.png │ ├── hand_drawn_spectrogram.png │ ├── mambo_but_from_jamaica.mp3 │ ├── mambo_but_from_jamaica.png │ ├── techno_to_jamaican_rap.mp3 │ ├── happy_cows_interpolation.gif │ ├── acoustic_folk_fiddle_solo.mp3 │ ├── acoustic_folk_fiddle_solo.png │ ├── latent_space_interpolation.png │ ├── sunrise_dj_set_to_hard_synth.mp3 │ ├── church_bells_to_electronic_beats.mp3 │ ├── rock_and_roll_electric_guitar_solo.mp3 │ ├── rock_and_roll_electric_guitar_solo.png │ ├── fantasy_ballad_to_teen_boy_pop_star.mp3 │ ├── newyourkduststorm_goldenhourmountain.mp3 │ └── detroit_rap_to_jazz_denoising_0_6_seed_50.mp3 ├── .eslintrc.json ├── postcss.config.js ├── styles ├── globals.css └── Home.module.css ├── next.config.js ├── pages ├── _app.tsx ├── api │ ├── server.js │ └── baseten.js ├── index.tsx └── about.tsx ├── components ├── FallingBehindWarning.tsx ├── about │ ├── CaptionedImage.tsx │ └── ToApp.tsx ├── ImagePlane.tsx ├── ThreeCanvas.tsx ├── RotatingBox.tsx ├── Pause.tsx ├── HeightMapImage.tsx ├── PageHead.tsx ├── DebugView.tsx ├── SpectrogramViewer.tsx ├── AudioPlayer.tsx ├── Info.tsx ├── ModelInference.tsx ├── PromptEntry.tsx ├── Settings.tsx ├── Share.tsx └── PromptPanel.tsx ├── .gitignore ├── tsconfig.json ├── tailwind.config.js ├── package.json ├── LICENSE ├── types.ts ├── README.md ├── shaders.js ├── samplePrompts.ts └── external └── unmute.js /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/pop_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/pop_sample.jpg -------------------------------------------------------------------------------- /public/pop_sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/pop_sample.mp3 -------------------------------------------------------------------------------- /public/rap_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/rap_sample.jpg -------------------------------------------------------------------------------- /public/rap_sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/rap_sample.mp3 -------------------------------------------------------------------------------- /public/about/astronaut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/astronaut.gif -------------------------------------------------------------------------------- /public/about/funky_sax.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/funky_sax.gif -------------------------------------------------------------------------------- /public/about/funky_sax.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/funky_sax.mp3 -------------------------------------------------------------------------------- /public/about/funky_sax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/funky_sax.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { "react/no-unescaped-entities": 0 } 4 | } 5 | -------------------------------------------------------------------------------- /public/about/hand_drawn.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/hand_drawn.mp3 -------------------------------------------------------------------------------- /public/about/arabic_gospel.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/arabic_gospel.mp3 -------------------------------------------------------------------------------- /public/about/typing_to_jazz.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/typing_to_jazz.mp3 -------------------------------------------------------------------------------- /public/about/fourier_transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/fourier_transform.png -------------------------------------------------------------------------------- /public/about/img2img_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/img2img_example.png -------------------------------------------------------------------------------- /public/about/spectrogram_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/spectrogram_label.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/about/funky_sax_to_piano.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/funky_sax_to_piano.mp3 -------------------------------------------------------------------------------- /public/about/funky_sax_to_piano.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/funky_sax_to_piano.png -------------------------------------------------------------------------------- /public/about/web_app_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/web_app_screenshot.png -------------------------------------------------------------------------------- /public/about/hand_drawn_spectrogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/hand_drawn_spectrogram.png -------------------------------------------------------------------------------- /public/about/mambo_but_from_jamaica.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/mambo_but_from_jamaica.mp3 -------------------------------------------------------------------------------- /public/about/mambo_but_from_jamaica.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/mambo_but_from_jamaica.png -------------------------------------------------------------------------------- /public/about/techno_to_jamaican_rap.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/techno_to_jamaican_rap.mp3 -------------------------------------------------------------------------------- /public/about/happy_cows_interpolation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/happy_cows_interpolation.gif -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | a { 6 | @apply text-gray-700 underline 7 | } 8 | -------------------------------------------------------------------------------- /public/about/acoustic_folk_fiddle_solo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/acoustic_folk_fiddle_solo.mp3 -------------------------------------------------------------------------------- /public/about/acoustic_folk_fiddle_solo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/acoustic_folk_fiddle_solo.png -------------------------------------------------------------------------------- /public/about/latent_space_interpolation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/latent_space_interpolation.png -------------------------------------------------------------------------------- /public/about/sunrise_dj_set_to_hard_synth.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/sunrise_dj_set_to_hard_synth.mp3 -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /public/about/church_bells_to_electronic_beats.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/church_bells_to_electronic_beats.mp3 -------------------------------------------------------------------------------- /public/about/rock_and_roll_electric_guitar_solo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/rock_and_roll_electric_guitar_solo.mp3 -------------------------------------------------------------------------------- /public/about/rock_and_roll_electric_guitar_solo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/rock_and_roll_electric_guitar_solo.png -------------------------------------------------------------------------------- /public/about/fantasy_ballad_to_teen_boy_pop_star.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/fantasy_ballad_to_teen_boy_pop_star.mp3 -------------------------------------------------------------------------------- /public/about/newyourkduststorm_goldenhourmountain.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/newyourkduststorm_goldenhourmountain.mp3 -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | background: #0a2342; 4 | color: white; 5 | } 6 | 7 | .main { 8 | min-height: 100vh; 9 | } 10 | -------------------------------------------------------------------------------- /public/about/detroit_rap_to_jazz_denoising_0_6_seed_50.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riffusion/riffusion-app-hobby/HEAD/public/about/detroit_rap_to_jazz_denoising_0_6_seed_50.mp3 -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /components/FallingBehindWarning.tsx: -------------------------------------------------------------------------------- 1 | export default function FallingBehindWarning() { 2 | return ( 3 |
7 | 8 | Uh oh! Servers are behind, scaling up... 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /pages/api/server.js: -------------------------------------------------------------------------------- 1 | export default async function handler(req, res) { 2 | const headers = { 3 | "Content-Type": "application/json", 4 | "Access-Control-Allow-Origin": "*", 5 | }; 6 | 7 | const response = await fetch(process.env.RIFFUSION_FLASK_URL, { 8 | method: "POST", 9 | headers: headers, 10 | body: req.body, 11 | signal: AbortSignal.timeout(60000), 12 | }); 13 | 14 | const data = await response.json(); 15 | res.status(200).json({ data }); 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .env 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /components/about/CaptionedImage.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface CaptionedImageProps { 4 | image_url: string; 5 | caption: string; 6 | marginLeft?: number; 7 | } 8 | 9 | export default function CaptionedImage({ 10 | image_url, 11 | caption, 12 | marginLeft = 16, 13 | }: CaptionedImageProps) { 14 | return ( 15 |
16 |

{caption}

17 | {caption} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /pages/api/baseten.js: -------------------------------------------------------------------------------- 1 | export default async function handler(req, res) { 2 | let headers = { 3 | "Content-Type": "application/json", 4 | "Access-Control-Allow-Origin": "*", 5 | "Authorization": `Api-Key ${process.env.RIFFUSION_BASETEN_API_KEY}` 6 | }; 7 | 8 | // This code is no longer active in favor of blueprint method 9 | const response = await fetch(process.env.RIFFUSION_BASETEN_URL, { 10 | method: "POST", 11 | headers: headers, 12 | body: req.body, 13 | signal: AbortSignal.timeout(20000), 14 | }); 15 | 16 | const data = await response.json(); 17 | res.status(200).json({ data }); 18 | } 19 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | const defaultTheme = require('tailwindcss/defaultTheme'); 4 | 5 | module.exports = { 6 | content: [ 7 | './pages/**/*.{js,ts,jsx,tsx}', 8 | './components/**/*.{js,ts,jsx,tsx}', 9 | './app/**/*.{js,ts,jsx,tsx}', 10 | ], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | 'sans': [ 15 | '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen', 16 | 'Ubuntu', 'Cantarell', 'Fira Sans', 17 | ...defaultTheme.fontFamily.sans 18 | ], 19 | }, 20 | }, 21 | }, 22 | plugins: [ require("daisyui") ], 23 | } 24 | -------------------------------------------------------------------------------- /components/ImagePlane.tsx: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import React, { Suspense } from "react"; 3 | import { useLoader } from "@react-three/fiber"; 4 | 5 | interface ImagePlaneProps { 6 | url: string; 7 | height: number; 8 | duration: number; 9 | } 10 | 11 | /** 12 | * Draw an image on a plane geometry. 13 | */ 14 | export default function ImagePlane(props: ImagePlaneProps) { 15 | const texture = useLoader(THREE.TextureLoader, props.url); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/ThreeCanvas.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@react-three/fiber"; 2 | 3 | import { InferenceResult } from "../types"; 4 | import SpectrogramViewer from "./SpectrogramViewer"; 5 | import { PerspectiveCamera } from "@react-three/drei"; 6 | 7 | interface CanvasProps { 8 | paused: boolean; 9 | inferenceResults: InferenceResult[]; 10 | getTime: () => number; 11 | } 12 | 13 | /** 14 | * React three fiber canvas with spectrogram drawing. 15 | */ 16 | export default function ThreeCanvas({ 17 | paused, 18 | inferenceResults, 19 | getTime, 20 | }: CanvasProps) { 21 | return ( 22 | 23 | 24 | 25 | 31 | 32 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riffusion", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.5.0", 13 | "@react-three/drei": "^9.41.2", 14 | "@react-three/fiber": "^8.9.1", 15 | "daisyui": "^2.43.0", 16 | "@vercel/analytics": "^0.1.6", 17 | "eslint": "8.27.0", 18 | "eslint-config-next": "13.0.4", 19 | "next": "13.0.4", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "react-icons": "^4.6.0", 23 | "styled-components": "^5.3.6", 24 | "three": "^0.146.0", 25 | "tone": "^14.7.77", 26 | "usehooks-ts": "^2.9.1" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^18.11.9", 30 | "@types/react": "^18.0.25", 31 | "@types/styled-components": "^5.1.26", 32 | "@types/three": "^0.146.0", 33 | "autoprefixer": "^10.4.13", 34 | "postcss": "^8.4.19", 35 | "tailwindcss": "^3.2.4", 36 | "typescript": "^4.9.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Hayk Martiros and Seth Forsgren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 6 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 13 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 15 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /components/RotatingBox.tsx: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { useRef, useState } from "react"; 3 | import { useFrame, GroupProps } from "@react-three/fiber"; 4 | import { RoundedBox } from "@react-three/drei"; 5 | 6 | /** 7 | * Component to draw rotating boxes. 8 | */ 9 | export default function RotatingBox(props: JSX.IntrinsicElements["mesh"]) { 10 | const mesh = useRef(null); 11 | 12 | // Track whether the boxes are hovered over and clicked 13 | const [hovered, setHover] = useState(false); 14 | const [active, setActive] = useState(false); 15 | 16 | // Rotate the meshes every animation frame 17 | useFrame(() => { 18 | mesh.current.rotation.y += 0.01; 19 | mesh.current.rotation.x += 0.01; 20 | }); 21 | 22 | return ( 23 | setActive(!active)} 32 | onPointerOver={() => setHover(true)} 33 | onPointerOut={() => setHover(false)} 34 | > 35 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export interface PromptInput { 2 | prompt: string; 3 | seed?: number; 4 | denoising?: number; 5 | guidance?: number; 6 | 7 | // promptsInput is assigned a transitionCounter equal to the result.counter upon first being played 8 | transitionCounter?: number; 9 | } 10 | 11 | export interface InferenceInput { 12 | alpha: number; 13 | num_inference_steps?: number; 14 | seed_image_id?: string; 15 | mask_image_id?: string; 16 | 17 | start: PromptInput; 18 | end: PromptInput; 19 | } 20 | 21 | export interface InferenceResult { 22 | input: InferenceInput; 23 | 24 | counter: number; 25 | 26 | // Binary played status (true = played or playing, false = not played) 27 | played: boolean; 28 | 29 | // URL of the image 30 | image: string; 31 | 32 | // URL of the audio 33 | audio: string; 34 | 35 | // Duration of the audio in seconds 36 | duration_s: number; 37 | } 38 | 39 | // High-level state of the app's inference call 40 | export enum AppState { 41 | UNINITIALIZED = "UNINITIALIZED", 42 | SAME_PROMPT = "SAME_PROMPT", 43 | TRANSITION = "TRANSITION", 44 | } 45 | 46 | // High-level state of the actively playing audio 47 | export enum PlayingState { 48 | UNINITIALIZED = "UNINITIALIZED", 49 | SAME_PROMPT = "SAME_PROMPT", 50 | TRANSITION = "TRANSITION", 51 | } 52 | -------------------------------------------------------------------------------- /components/Pause.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { FiPause, FiPlay } from "react-icons/fi"; 3 | 4 | interface PauseProps { 5 | paused: boolean; 6 | setPaused: (value: boolean) => void; 7 | } 8 | 9 | export default function Pause({ 10 | paused, 11 | setPaused, 12 | }: PauseProps) { 13 | 14 | // Print the state into the console 15 | useEffect(() => { 16 | if (paused) { 17 | console.log("Pause"); 18 | } else { 19 | console.log("Play"); 20 | } 21 | }, [paused]); 22 | 23 | var classNameCondition = "" 24 | if (paused) { 25 | classNameCondition="animate-pulse fixed z-20 top-4 right-4 md:top-8 md:right-8 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-white text-2xl bg-red-500 hover:bg-red-600 ring-4 ring-red-700 focus:outline-none hover:drop-shadow-2xl" 26 | } else { 27 | classNameCondition="fixed z-20 top-4 right-4 md:top-8 md:right-8 bg-slate-100 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-sky-900 text-2xl hover:text-white hover:bg-sky-600 hover:drop-shadow-2xl" 28 | } 29 | 30 | return ( 31 | <> 32 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /components/about/ToApp.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export default function ToApp() { 4 | const [showButton, setShowButton] = useState(true); 5 | 6 | useEffect(() => { 7 | const handleScroll = () => { 8 | if (window.scrollY < 2) { 9 | setShowButton(true); 10 | } else { 11 | setShowButton(false); 12 | } 13 | }; 14 | 15 | window.addEventListener("scroll", handleScroll); 16 | return () => window.removeEventListener("scroll", handleScroll); 17 | }, []); 18 | 19 | return ( 20 | <> 21 | 28 | 29 | {showButton && ( 30 |
31 | Try it! 32 | 38 |
39 | )} 40 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /components/HeightMapImage.tsx: -------------------------------------------------------------------------------- 1 | import { DoubleSide, RepeatWrapping, sRGBEncoding } from "three"; 2 | import { 3 | useTexture, 4 | } from "@react-three/drei"; 5 | 6 | import { vertexShader, fragmentShader } from "../shaders"; 7 | 8 | interface HeightMapImageProps { 9 | url: string; 10 | position: [number, number, number]; 11 | rotation: [number, number, number]; 12 | scale: [number, number, number]; 13 | } 14 | 15 | export default function HeightMapImage(props: HeightMapImageProps) { 16 | const url = props.url; 17 | 18 | // Load the heightmap image 19 | const heightMap = useTexture(url); 20 | heightMap.wrapS = RepeatWrapping; 21 | heightMap.wrapT = RepeatWrapping; 22 | 23 | // Load the texture map 24 | const textureMap = useTexture(url); 25 | textureMap.wrapS = RepeatWrapping; 26 | textureMap.wrapT = RepeatWrapping; 27 | 28 | return ( 29 | 34 | {/* TODO hayk reduce */} 35 | 36 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/PageHead.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | export default function PageHead() { 4 | return ( 5 | 6 | Riffusion 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Riffusion App 2 | 3 | :no_entry: This project is no longer actively maintained. 4 | 5 | Riffusion is an app for real-time music generation with stable diffusion. 6 | 7 | This repository contains the interactive web app that powers the website. 8 | 9 | It is built with Next.js, React, Typescript, three.js, Tailwind, and Vercel. 10 | 11 | ## Run 12 | 13 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 14 | 15 | First, make sure you have Node v18 or greater installed using `node --version`. 16 | 17 | Install packages: 18 | 19 | ```bash 20 | npm install 21 | ``` 22 | 23 | Run the development server: 24 | 25 | ```bash 26 | npm run dev 27 | # or 28 | yarn dev 29 | ``` 30 | 31 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the app. 32 | 33 | The app home is at `pages/index.js`. The page auto-updates as you edit the file. The about page is at `pages/about.tsx`. 34 | 35 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 36 | 37 | ## Inference Server 38 | 39 | To actually generate model outputs, we need a model backend that responds to inference requests via API. If you have a large GPU that can run stable diffusion in under five seconds, clone and run the instructions in the [inference server](https://github.com/hmartiro/riffusion-inference) to run the Flask app. 40 | 41 | You will need to add a `.env.local` file in the root of this repository specifying the URL of the inference server: 42 | 43 | ``` 44 | RIFFUSION_FLASK_URL=http://127.0.0.1:3013/run_inference/ 45 | ``` 46 | 47 | ## Citation 48 | 49 | If you build on this work, please cite it as follows: 50 | 51 | ``` 52 | @article{Forsgren_Martiros_2022, 53 | author = {Forsgren, Seth* and Martiros, Hayk*}, 54 | title = {{Riffusion - Stable diffusion for real-time music generation}}, 55 | url = {https://riffusion.com/about}, 56 | year = {2022} 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /shaders.js: -------------------------------------------------------------------------------- 1 | export const vertexShader = ` 2 | // Uniforms are data that are shared between shaders 3 | // The contain data that are uniform across the entire frame. 4 | // The heightmap and scaling constant for each point are uniforms in this respect. 5 | 6 | // A uniform to contain the heightmap image 7 | uniform sampler2D bumpTexture; 8 | // A uniform to contain the scaling constant 9 | uniform float bumpScale; 10 | 11 | // Varyings are variables whose values are decided in the vertext shader 12 | // But whose values are then needed in the fragment shader 13 | 14 | // A variable to store the height of the point 15 | varying float vAmount; 16 | // The UV mapping coordinates of a vertex 17 | varying vec2 vUV; 18 | 19 | void main() 20 | { 21 | // The "coordinates" in UV mapping representation 22 | vUV = uv; 23 | 24 | // The heightmap data at those coordinates 25 | vec4 bumpData = texture2D(bumpTexture, uv); 26 | 27 | // height map is grayscale, so it doesn't matter if you use r, g, or b. 28 | vAmount = bumpData.r; 29 | 30 | // move the position along the normal 31 | vec3 newPosition = position + normal * bumpScale * vAmount; 32 | 33 | // Compute the position of the vertex using a standard formula 34 | gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); 35 | } 36 | `; 37 | 38 | export const fragmentShader = ` 39 | // A uniform for the terrain texture image 40 | uniform sampler2D terrainTexture; 41 | 42 | // Get the varyings from the vertex shader 43 | varying vec2 vUV; 44 | // vAmount isn't really used, but could be if necessary 45 | varying float vAmount; 46 | 47 | uniform lowp float shadows; 48 | uniform lowp float highlights; 49 | 50 | const mediump vec3 luminanceWeighting = vec3(0.3, 0.3, 0.3); 51 | 52 | void main() 53 | { 54 | // Get the color of the fragment from the texture map 55 | // at that coordinate in the UV mapping 56 | vec4 source = texture2D(terrainTexture, vUV); 57 | 58 | float x = vUV.x; 59 | float y = vUV.y; 60 | 61 | float r = source.r - 0.05 - 0.0 * sin(x * 3.14 * 0.5); 62 | 63 | float g = source.g - 0.05 - 0.05 * sin(y * 10.0); 64 | float b = source.b + 0.1 - 0.05 * sin(y * 10.0); 65 | 66 | gl_FragColor = vec4(r, g, b, 1.0); 67 | } 68 | 69 | `; 70 | -------------------------------------------------------------------------------- /samplePrompts.ts: -------------------------------------------------------------------------------- 1 | // List of interesting prompts to randomly sample. 2 | export const samplePrompts = [ 3 | "a folksy blues song from the mississippi delta around 1910", 4 | "acoustic folk violin jam", 5 | "ancient chinese hymn", 6 | "anger rap", 7 | "arabic gospel vocals", 8 | "bongos on a havana street", 9 | "bossa nova with distorted guitar", 10 | "bubblegum eurodance", 11 | "church bells", 12 | "classical italian tenor operatic pop", 13 | "copacabana beach", 14 | "deep, smooth synthwave with a dream-like atmosphere", 15 | "emotional disco", 16 | "funk bassline with a jazzy saxophone", 17 | "ibiza at 3am", 18 | "jamaican dancehall vocals", 19 | "jazzy clarinet with maracas", 20 | "jazzy rapping from paris", 21 | "k-pop boy group", 22 | "latent space vaporwave", 23 | "lo-fi beat for the holidays", 24 | "mambo but from Kenya", 25 | "piano concerto in A minor", 26 | "post-teen pop talent show winner", 27 | "psychedelic nepalese trance", 28 | "rock and roll electric guitar solo", 29 | "scott joplin style ragtime piano", 30 | "smooth tropical dance jazz", 31 | "swing jazz trumpet", 32 | "techno DJ and a country fiddle", 33 | "typing", 34 | ]; 35 | 36 | export const rollTheDicePrompts = [ 37 | "(caribbean:1.5) (jazz:0.3)", 38 | "alarm clock", 39 | "bird calls", 40 | "baroque new wave pop", 41 | "Berlin and Paris electronic fused together with acoustic tunes", 42 | "breathing", 43 | "brazilian Forró dance", 44 | "british soul dance", 45 | "chainsaw funk", 46 | "church organ with a harpsichord", 47 | "classic rock mellow gold progressive", 48 | "classical flute", 49 | "cowbell ballad", 50 | "funky human music", 51 | "hip hop vocals piano cowbell", 52 | "interdimensional cable", 53 | "jamaican ska rap", 54 | "laughing", 55 | "lucky disco punk", 56 | "new orleans blues", 57 | "piano funk", 58 | "pop r&b urban contemporary", 59 | "reggae fusion", 60 | "Shepard tone", 61 | "smelly clown", 62 | "square dancing in wyoming", 63 | "tropical deep sea", 64 | "tropical electro house moombahton", 65 | "tropical german dance house", 66 | "uk permanent wave pop", 67 | "water drops", 68 | "west coast rap vocals", 69 | ]; 70 | 71 | export const initialSeedImageMap = { 72 | "og_beat": [3, 738973, 674, 745234, 808, 231, 3324, 323984, 123, 51209, 123, 51209, 6754, 8730], 73 | "agile": [808, 231, 3324, 323984], 74 | "marim": [123, 51209, 6754, 8730], 75 | "motorway": [8730, 323984, 745234], 76 | "vibes": [4205, 94, 78530] 77 | } 78 | 79 | export const initialSeeds = [ 80 | "og_beat", 81 | // "agile", 82 | // "marim", 83 | // "motorway", 84 | // "vibes" 85 | ] 86 | -------------------------------------------------------------------------------- /components/DebugView.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from "@headlessui/react"; 2 | import { useState } from "react"; 3 | import { ImStatsBars } from "react-icons/im"; 4 | import styled from "styled-components"; 5 | 6 | import { InferenceResult, PromptInput } from "../types"; 7 | 8 | interface DebugViewProps { 9 | promptInputs: PromptInput[]; 10 | inferenceResults: InferenceResult[]; 11 | nowPlayingResult: InferenceResult; 12 | open: boolean; 13 | setOpen: (open: boolean) => void; 14 | } 15 | 16 | const ModalContainer = styled.div` 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | width: 100vw; 21 | height: 100vh; 22 | background: rgba(0, 0, 0, 0.5); 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | `; 27 | 28 | export default function DebugView({ 29 | promptInputs, 30 | inferenceResults, 31 | nowPlayingResult, 32 | open, 33 | setOpen, 34 | }: DebugViewProps) { 35 | return ( 36 | <> 37 | setOpen(false)} 40 | as="div" 41 | className="fixed inset-0 z-30" 42 | key="debug-dialog" 43 | > 44 | 45 |
46 |
47 | 48 |

49 | Prompt Inputs 50 |

51 |
52 | {promptInputs.map((promptInput) => ( 53 |
54 | "{promptInput.prompt}" - {promptInput.transitionCounter} 55 |
56 | ))} 57 |
58 |

59 | Inference Results 60 |

61 |
62 |
    63 | {inferenceResults.map((result) => ( 64 |
  • 72 | #{result.counter} - Alpha{" "} 73 | {result.input.alpha.toFixed(2)} from (" 74 | {result.input.start.prompt}", {result.input.start.seed}) 75 | to (" 76 | {result.input.end.prompt}", {result.input.end.seed}) 77 |
  • 78 | ))} 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /components/SpectrogramViewer.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { useFrame, useThree } from "@react-three/fiber"; 3 | import { Box } from "@react-three/drei"; 4 | 5 | import { InferenceResult } from "../types"; 6 | import HeightMapImage from "./HeightMapImage"; 7 | import ImagePlane from "./ImagePlane"; 8 | 9 | // import { Effects } from "@react-three/drei"; 10 | // import { ShaderPass, VerticalTiltShiftShader} from "three-stdlib"; 11 | 12 | // extend({ ShaderPass }); 13 | 14 | // Fun shaders: 15 | // RGBShiftShader 16 | // VerticalBlurShader 17 | // VerticalTiltShiftShader 18 | 19 | interface SpectrogramViewerProps { 20 | paused: boolean; 21 | inferenceResults: InferenceResult[]; 22 | getTime: () => number; 23 | use_height_map?: boolean; 24 | } 25 | 26 | /** 27 | * Spectrogram drawing code. 28 | */ 29 | export default function SpectrogramViewer({ 30 | paused, 31 | inferenceResults, 32 | getTime, 33 | use_height_map = true, 34 | }: SpectrogramViewerProps) { 35 | const { camera } = useThree(); 36 | 37 | const playheadRef = useRef(null); 38 | 39 | // Move the camera based on the clock 40 | useFrame(() => { 41 | const time = getTime(); 42 | 43 | const velocity = -1.0; // [m/s] 44 | const position = velocity * time; // [m] 45 | 46 | camera.position.y = position; 47 | playheadRef.current.position.y = camera.position.y; 48 | }); 49 | 50 | const playbarShift = 3.6; // [m] 51 | 52 | // NOTE: this is a hacky way to constrict image and box width to fit in the window for responsiveness 53 | // if window is between 768px and 1350px, this scales the image to fit using a scaler 54 | const imageScaler = window.innerHeight / 3.40; 55 | const boxScaler = window.innerHeight / 3.76; 56 | const spectrogramImageScale = window.innerWidth > 767 && window.innerWidth < 1350 ? window.innerWidth / imageScaler : 5; 57 | const spectrogramBoxScale = window.innerWidth > 767 && window.innerWidth < 1350 ? window.innerWidth / boxScaler : 5.5; 58 | 59 | return ( 60 | 61 | {/* 68 | 69 | */} 70 | 71 | {inferenceResults.map((result: InferenceResult, index: number) => { 72 | const duration_s = result.duration_s; 73 | const position = duration_s * (-0.55 - result.counter) + playbarShift; 74 | 75 | if (use_height_map) { 76 | return ( 77 | 84 | ); 85 | } else { 86 | return ( 87 | 93 | ); 94 | } 95 | })} 96 | 97 | {/* Playhead as as transparent red box. */} 98 | {/* TODO(hayk): Synchronize this better with the audio. */} 99 | 100 | 105 | 106 | 107 | 108 | 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /components/AudioPlayer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import * as Tone from "tone"; 4 | 5 | import { InferenceResult } from "../types"; 6 | 7 | interface AudioPlayerProps { 8 | paused: boolean; 9 | inferenceResults: InferenceResult[]; 10 | nowPlayingCallback: (result: InferenceResult, playerTime: number) => void; 11 | playerIsBehindCallback: (isBehind: boolean) => void; 12 | useCompressor: boolean; 13 | } 14 | 15 | /** 16 | * Audio player with Tone.js 17 | * 18 | * TODO(hayk): Look into https://github.com/E-Kuerschner/useAudioPlayer as an alternative. 19 | */ 20 | export default function AudioPlayer({ 21 | paused, 22 | inferenceResults, 23 | nowPlayingCallback, 24 | playerIsBehindCallback, 25 | useCompressor, 26 | }: AudioPlayerProps) { 27 | const [tonePlayer, setTonePlayer] = useState(null); 28 | 29 | const [numClipsPlayed, setNumClipsPlayed] = useState(0); 30 | const [prevNumClipsPlayed, setPrevNumClipsPlayed] = useState(0); 31 | 32 | const [resultCounter, setResultCounter] = useState(0); 33 | 34 | // On load, create a player synced to the tone transport 35 | useEffect(() => { 36 | if (tonePlayer) { 37 | return; 38 | } 39 | 40 | if (inferenceResults.length === 0) { 41 | return; 42 | } 43 | 44 | const result = inferenceResults[0]; 45 | 46 | const player = new Tone.Player(result.audio, () => { 47 | player.loop = true; 48 | player.sync().start(0); 49 | 50 | // Set up a callback to increment numClipsPlayed at the edge of each clip 51 | const bufferLength = player.sampleTime * player.buffer.length; 52 | 53 | // TODO(hayk): Set this callback up to vary each time using duration_s 54 | Tone.Transport.scheduleRepeat((time) => { 55 | setNumClipsPlayed((n) => n + 1); 56 | }, bufferLength); 57 | 58 | setTonePlayer(player); 59 | 60 | // Make further load callbacks do nothing. 61 | player.buffer.onload = () => {}; 62 | }); 63 | 64 | if (useCompressor) { 65 | const compressor = new Tone.Compressor(-30, 3).toDestination(); 66 | player.connect(compressor); 67 | } else { 68 | player.toDestination(); 69 | } 70 | }, [tonePlayer, inferenceResults, useCompressor]); 71 | 72 | // On play/pause button, play/pause the audio with the tone transport 73 | useEffect(() => { 74 | if (!paused) { 75 | if (Tone.context.state == "suspended") { 76 | Tone.context.resume(); 77 | } 78 | 79 | if (tonePlayer) { 80 | Tone.Transport.start(); 81 | } 82 | } else { 83 | if (tonePlayer) { 84 | Tone.Transport.pause(); 85 | } 86 | } 87 | }, [paused, tonePlayer]); 88 | 89 | // If there is a new clip, switch to it 90 | useEffect(() => { 91 | if (numClipsPlayed == prevNumClipsPlayed) { 92 | return; 93 | } 94 | 95 | const maxResultCounter = Math.max( 96 | ...inferenceResults.map((r) => r.counter) 97 | ); 98 | 99 | if (maxResultCounter < resultCounter) { 100 | console.info( 101 | "No new result available, looping previous clip", 102 | resultCounter, 103 | maxResultCounter 104 | ); 105 | playerIsBehindCallback(true); 106 | return; 107 | } 108 | 109 | playerIsBehindCallback(false); 110 | 111 | const result = inferenceResults.find( 112 | (r: InferenceResult) => r.counter == resultCounter 113 | ); 114 | 115 | setResultCounter((c) => c + 1); 116 | 117 | tonePlayer.load(result.audio).then(() => { 118 | // Re-jigger the transport so it stops playing old buffers. It seems like this doesn't 119 | // introduce a gap, but watch out for that. 120 | Tone.Transport.pause(); 121 | if (!paused) { 122 | Tone.Transport.start(); 123 | } 124 | 125 | const playerTime = Tone.Transport.seconds; 126 | nowPlayingCallback(result, playerTime); 127 | }); 128 | 129 | setPrevNumClipsPlayed(numClipsPlayed); 130 | }, [ 131 | numClipsPlayed, 132 | prevNumClipsPlayed, 133 | resultCounter, 134 | inferenceResults, 135 | paused, 136 | nowPlayingCallback, 137 | playerIsBehindCallback, 138 | tonePlayer, 139 | ]); 140 | 141 | return null; 142 | } 143 | -------------------------------------------------------------------------------- /components/Info.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { Fragment, useState } from "react"; 3 | import { FiInfo } from "react-icons/fi"; 4 | import styled, { css } from "styled-components"; 5 | 6 | const ModalContainer = styled.div` 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width: 100vw; 11 | height: 100vh; 12 | background: rgba(0, 0, 0, 0.5); 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | `; 17 | 18 | export default function Info() { 19 | const [open, setOpen] = useState(false); 20 | 21 | var classNameCondition = "" 22 | if (open) { 23 | classNameCondition = "fixed z-20 top-44 right-4 md:top-48 md:right-8 bg-sky-400 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-white text-2xl hover:bg-sky-500 hover:drop-shadow-2xl" 24 | } else { 25 | classNameCondition = "fixed z-20 top-44 right-4 md:top-48 md:right-8 bg-slate-100 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-sky-900 text-2xl hover:text-white hover:bg-sky-600 hover:drop-shadow-2xl" 26 | } 27 | 28 | return ( 29 | <> 30 | 37 | 38 | 39 | setOpen(false)} 43 | > 44 |
45 | 54 | 55 | 56 | 57 | 63 | 72 | 73 |
74 | 78 | Welcome to Riffusion 79 | 80 |
81 |

82 | Riffusion generates endless new jams from any text prompt. Try typing in your favorite artist or genre, and you'll hear the music gradually transform.

83 |

84 | The diffusion model first creates images from your prompt, and then converts them into music. Learn more about surfing the latent space of sound below.

85 |

86 |
87 | 88 |
89 | 90 | 101 | 102 | 109 | 110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | 118 | ); 119 | }; -------------------------------------------------------------------------------- /components/ModelInference.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useCallback, useEffect, useState } from "react"; 3 | 4 | import { 5 | AppState, 6 | InferenceInput, 7 | InferenceResult, 8 | PromptInput, 9 | } from "../types"; 10 | 11 | interface ModelInferenceProps { 12 | alpha: number; 13 | alphaRollover: boolean; 14 | seed: number; 15 | appState: AppState; 16 | promptInputs: PromptInput[]; 17 | nowPlayingResult: InferenceResult; 18 | newResultCallback: (input: InferenceInput, result: InferenceResult) => void; 19 | useBaseten: boolean; 20 | denoising: number; 21 | seedImageId: string; 22 | } 23 | 24 | /** 25 | * Calls the server to run model inference. 26 | * 27 | * 28 | */ 29 | export default function ModelInference({ 30 | alpha, 31 | alphaRollover, 32 | seed, 33 | appState, 34 | promptInputs, 35 | nowPlayingResult, 36 | newResultCallback, 37 | useBaseten, 38 | denoising, 39 | seedImageId, 40 | }: ModelInferenceProps) { 41 | // Create parameters for the inference request 42 | const [guidance, setGuidance] = useState(7.0); 43 | const [numInferenceSteps, setNumInferenceSteps] = useState(50); 44 | const [maskImageId, setMaskImageId] = useState(null); 45 | 46 | const [initializedUrlParams, setInitializedUrlParams] = useState(false); 47 | const [numRequestsMade, setNumRequestsMade] = useState(0); 48 | const [numResponsesReceived, setNumResponsesReceived] = useState(0); 49 | 50 | useEffect(() => { 51 | console.log("Using baseten: ", useBaseten); 52 | }, [useBaseten]); 53 | 54 | // Set initial params from URL query strings 55 | const router = useRouter(); 56 | useEffect(() => { 57 | if (router.query.guidance) { 58 | setGuidance(parseFloat(router.query.guidance as string)); 59 | } 60 | 61 | if (router.query.numInferenceSteps) { 62 | setNumInferenceSteps(parseInt(router.query.numInferenceSteps as string)); 63 | } 64 | 65 | if (router.query.maskImageId) { 66 | if (router.query.maskImageId === "none") { 67 | setMaskImageId(""); 68 | } else { 69 | setMaskImageId(router.query.maskImageId as string); 70 | } 71 | } 72 | 73 | setInitializedUrlParams(true); 74 | }, [router.query]); 75 | 76 | // Memoized function to kick off an inference request 77 | const runInference = useCallback( 78 | async ( 79 | alpha: number, 80 | seed: number, 81 | appState: AppState, 82 | promptInputs: PromptInput[] 83 | ) => { 84 | const startPrompt = promptInputs[promptInputs.length - 3].prompt; 85 | const endPrompt = promptInputs[promptInputs.length - 2].prompt; 86 | 87 | const transitioning = appState == AppState.TRANSITION; 88 | 89 | const inferenceInput = { 90 | alpha: alpha, 91 | num_inference_steps: numInferenceSteps, 92 | seed_image_id: seedImageId, 93 | mask_image_id: maskImageId, 94 | start: { 95 | prompt: startPrompt, 96 | seed: seed, 97 | denoising: denoising, 98 | guidance: guidance, 99 | }, 100 | end: { 101 | prompt: transitioning ? endPrompt : startPrompt, 102 | seed: transitioning ? seed : seed + 1, 103 | denoising: denoising, 104 | guidance: guidance, 105 | }, 106 | }; 107 | 108 | console.log(`Inference #${numRequestsMade}: `, { 109 | alpha: alpha, 110 | prompt_a: inferenceInput.start.prompt, 111 | seed_a: inferenceInput.start.seed, 112 | prompt_b: inferenceInput.end.prompt, 113 | seed_b: inferenceInput.end.seed, 114 | appState: appState, 115 | }); 116 | 117 | setNumRequestsMade((n) => n + 1); 118 | 119 | // Customize for baseten 120 | const apiHandler = useBaseten 121 | ? process.env.NEXT_PUBLIC_RIFFUSION_BASETEN_GC_URL 122 | : "/api/server"; 123 | const payload = useBaseten 124 | ? { worklet_input: inferenceInput } 125 | : inferenceInput; 126 | 127 | // NOTE(hayk): Clean this up with server.js, there's something fishy going on. 128 | const headers = useBaseten ? { "Content-Type": "application/json" } : {}; 129 | 130 | const response = await fetch(apiHandler, { 131 | method: "POST", 132 | body: JSON.stringify(payload), 133 | headers: headers, 134 | }); 135 | 136 | const data = await response.json(); 137 | 138 | console.log(`Got result #${numResponsesReceived}`); 139 | 140 | if (useBaseten) { 141 | if (data?.output) { 142 | newResultCallback(inferenceInput, data.output); 143 | } else { 144 | console.error("Baseten call failed: ", data); 145 | } 146 | } else { 147 | // Note, data is currently wrapped in a data field 148 | newResultCallback(inferenceInput, data.data); 149 | } 150 | 151 | setNumResponsesReceived((n) => n + 1); 152 | }, 153 | [ 154 | denoising, 155 | guidance, 156 | maskImageId, 157 | numInferenceSteps, 158 | seedImageId, 159 | newResultCallback, 160 | numRequestsMade, 161 | numResponsesReceived, 162 | useBaseten, 163 | ] 164 | ); 165 | 166 | // Kick off inference requests 167 | useEffect(() => { 168 | // Make sure things are initialized properly 169 | if ( 170 | !initializedUrlParams || 171 | appState == AppState.UNINITIALIZED || 172 | promptInputs.length == 0 173 | ) { 174 | return; 175 | } 176 | 177 | // Wait for alpha rollover to resolve. 178 | if (alphaRollover) { 179 | return; 180 | } 181 | 182 | if (numRequestsMade == 0) { 183 | // Kick off the first request 184 | runInference(alpha, seed, appState, promptInputs); 185 | } else if (numRequestsMade == numResponsesReceived) { 186 | // Otherwise buffer ahead a few from where the audio player currently is 187 | // TODO(hayk): Replace this with better buffer management 188 | 189 | const nowPlayingCounter = nowPlayingResult ? nowPlayingResult.counter : 0; 190 | const numAhead = numRequestsMade - nowPlayingCounter; 191 | 192 | if (numAhead < 3) { 193 | runInference(alpha, seed, appState, promptInputs); 194 | } 195 | } 196 | }, [ 197 | initializedUrlParams, 198 | alpha, 199 | alphaRollover, 200 | seed, 201 | appState, 202 | promptInputs, 203 | nowPlayingResult, 204 | numRequestsMade, 205 | numResponsesReceived, 206 | runInference, 207 | ]); 208 | 209 | return null; 210 | } 211 | -------------------------------------------------------------------------------- /components/PromptEntry.tsx: -------------------------------------------------------------------------------- 1 | import { InferenceInput, InferenceResult, PlayingState } from "../types"; 2 | import { IoMdClose } from "react-icons/io"; 3 | import Pause from "./Pause"; 4 | 5 | interface PromptEntryProps { 6 | prompt: string; 7 | index: number; 8 | className: string; 9 | playingState: PlayingState; 10 | resetCallback: () => void; 11 | inferenceResults: InferenceResult[]; 12 | nowPlayingResult: InferenceResult; 13 | setPaused: (value: boolean) => void; 14 | } 15 | 16 | export default function PromptEntry({ 17 | prompt, 18 | index, 19 | className, 20 | playingState, 21 | resetCallback, 22 | inferenceResults, 23 | nowPlayingResult, 24 | setPaused 25 | }: PromptEntryProps) { 26 | 27 | const getPromptCopy = (prompt: string) => { 28 | switch (playingState) { 29 | case PlayingState.UNINITIALIZED: 30 | case PlayingState.SAME_PROMPT: 31 | switch (index) { 32 | case 0: 33 | return ( 34 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 35 |

{prompt}

36 |
37 | ); 38 | case 1: 39 | return ( 40 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 41 |

{prompt}

42 |
43 | ); 44 | case 2: 45 | // active prompt 46 | if (prompt == " " || prompt == "") { 47 | return {""}; 48 | } else { 49 | return ( 50 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 51 |

{prompt}

52 |
53 | ) 54 | } 55 | case 3: 56 | if (prompt == " " || prompt == "") { 57 | return

...

58 | } else { 59 | return ( 60 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 61 |

{prompt}

62 |
63 | ) 64 | } 65 | case 4: 66 | if (prompt == " " || prompt == "") { 67 | return

UP NEXT: Anything you want

; 68 | } else { 69 | return ( 70 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 71 |

UP NEXT: {prompt}

72 |
73 | ) 74 | } 75 | default: { 76 | console.log("UNHANDLED default"); 77 | return

{prompt}

; 78 | } 79 | } 80 | case PlayingState.TRANSITION: 81 | switch (index) { 82 | case 0: 83 | return ( 84 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 85 |

{prompt}

86 |
87 | ); 88 | case 1: 89 | return ( 90 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 91 |

{prompt}

92 |
93 | ); 94 | case 2: 95 | return ( 96 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 97 |

{prompt}

98 |
99 | ) 100 | case 3: 101 | if (prompt == " " || prompt == "") { 102 | return

-enter prompt-

; 103 | } else { 104 | return ( 105 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 106 |

{prompt}

107 |
108 | ) 109 | } 110 | case 4: 111 | if (prompt == " " || prompt == "") { 112 | return

...

; 113 | } else { 114 | return ( 115 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 116 |

{prompt}

117 |
118 | ) 119 | } 120 | case 5: 121 | if (prompt == " " || prompt == "") { 122 | return

UP NEXT: Anything you want

; 123 | } else { 124 | return ( 125 |
{ jumpToPrompt(prompt, inferenceResults, setPaused, nowPlayingResult) }} > 126 |

UP NEXT: {prompt}

127 |
128 | ) 129 | } 130 | default: { 131 | console.log("UNHANDLED default"); 132 | return

{prompt}

; 133 | } 134 | } 135 | } 136 | }; 137 | 138 | return ( 139 |
140 | {getPromptCopy(prompt)} 141 | 142 | {/* TODO(hayk): Re-enable this when it's working. */} 143 | {/* {index == 2 ? ( 144 | { 147 | resetCallback(); 148 | }} 149 | /> 150 | ) : null} */} 151 |
152 | ); 153 | } 154 | 155 | export function jumpToPrompt(prompt: String, inferenceResults: InferenceResult[], setPaused: (value: boolean) => void, nowPlayingResult?: InferenceResult) { 156 | 157 | // Pause player since this function will open new tab that user will interact with 158 | setPaused(true) 159 | 160 | let firstTimePromptAppears = -1; 161 | for (let i = 0; i < inferenceResults.length; i++) { 162 | if (inferenceResults[i].input.start.prompt === prompt) { 163 | firstTimePromptAppears = i; 164 | break; 165 | } 166 | } 167 | if (firstTimePromptAppears == -1) { 168 | let url = generateLinkToUpcomingPrompt(prompt, nowPlayingResult) 169 | window.location.href = url; 170 | } 171 | else { 172 | let url = generateLinkToPreviousInput(inferenceResults[firstTimePromptAppears].input) 173 | window.location.href = url; 174 | } 175 | } 176 | 177 | export function generateLinkToUpcomingPrompt(prompt, nowPlayingResult?: InferenceResult) { 178 | 179 | var promptString = "&prompt=" + prompt; 180 | promptString = promptString.replace(/ /g, "+"); 181 | 182 | if (nowPlayingResult != null) { 183 | var denoisingString = "&denoising=" + nowPlayingResult.input.start.denoising; 184 | var seedImageIdString = "&seedImageId=" + nowPlayingResult.input.seed_image_id; 185 | } else { 186 | denoisingString = ""; 187 | seedImageIdString = ""; 188 | } 189 | 190 | var baseUrl = window.location.origin + "/?"; 191 | var url = baseUrl + promptString + denoisingString + seedImageIdString; 192 | return url; 193 | } 194 | 195 | // Todo: DRY this and share functions 196 | export function generateLinkToPreviousInput(selectedInput: InferenceInput) { 197 | var prompt; 198 | var seed; 199 | var denoising; 200 | var maskImageId; 201 | var seedImageId; 202 | var guidance; 203 | var numInferenceSteps; 204 | var alphaVelocity; 205 | 206 | prompt = selectedInput.start.prompt; 207 | seed = selectedInput.start.seed; 208 | denoising = selectedInput.start.denoising; 209 | maskImageId = selectedInput.mask_image_id; 210 | seedImageId = selectedInput.seed_image_id; 211 | 212 | var baseUrl = window.location.origin + "/?"; 213 | 214 | if (prompt != null) { 215 | var promptString = "&prompt=" + prompt; 216 | } else { 217 | promptString = ""; 218 | } 219 | if (seed != null) { 220 | var seedString = "&seed=" + seed; 221 | } else { 222 | seedString = ""; 223 | } 224 | if (denoising != null) { 225 | var denoisingString = "&denoising=" + denoising; 226 | } else { 227 | denoisingString = ""; 228 | } 229 | if (maskImageId != null) { 230 | var maskImageIdString = "&maskImageId=" + maskImageId; 231 | } else { 232 | maskImageIdString = ""; 233 | } 234 | if (seedImageId != null) { 235 | var seedImageIdString = "&seedImageId=" + seedImageId; 236 | } else { 237 | seedImageIdString = ""; 238 | } 239 | if (guidance != null) { 240 | var guidanceString = "&guidance=" + guidance; 241 | } else { 242 | guidanceString = ""; 243 | } 244 | if (numInferenceSteps != null) { 245 | var numInferenceStepsString = "&numInferenceSteps=" + numInferenceSteps; 246 | } else { 247 | numInferenceStepsString = ""; 248 | } 249 | if (alphaVelocity != null) { 250 | var alphaVelocityString = "&alphaVelocity=" + alphaVelocity; 251 | } else { 252 | alphaVelocityString = ""; 253 | } 254 | 255 | // Format strings to have + in place of spaces for ease of sharing, note this is only necessary for prompts currently 256 | promptString = promptString.replace(/ /g, "+"); 257 | 258 | // create url string with the variables above combined 259 | var shareUrl = 260 | baseUrl + 261 | promptString + 262 | seedString + 263 | denoisingString + 264 | maskImageIdString + 265 | seedImageIdString + 266 | guidanceString + 267 | numInferenceStepsString + 268 | alphaVelocityString; 269 | 270 | return shareUrl; 271 | } -------------------------------------------------------------------------------- /components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { Fragment, useState } from "react"; 3 | import { FiSettings } from "react-icons/fi"; 4 | import { ImStatsBars } from "react-icons/im"; 5 | import styled from "styled-components"; 6 | import { InferenceResult, PromptInput } from "../types"; 7 | import DebugView from "./DebugView"; 8 | 9 | const ModalContainer = styled.div` 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 100vw; 14 | height: 100vh; 15 | background: rgba(0, 0, 0, 0.5); 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | `; 20 | 21 | interface DebugViewProps { 22 | promptInputs: PromptInput[]; 23 | inferenceResults: InferenceResult[]; 24 | nowPlayingResult: InferenceResult; 25 | denoising: number; 26 | setDenoising: (denoising: number) => void; 27 | seedImage: string; 28 | setSeedImage: (seedImage: string) => void; 29 | } 30 | 31 | export default function Settings({ 32 | promptInputs, 33 | inferenceResults, 34 | nowPlayingResult, 35 | denoising, 36 | setDenoising, 37 | seedImage, 38 | setSeedImage, 39 | }: DebugViewProps) { 40 | const [open, setOpen] = useState(false); 41 | 42 | var classNameCondition = ""; 43 | if (open) { 44 | classNameCondition = 45 | "fixed z-20 top-44 right-4 md:top-48 md:right-8 bg-sky-400 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-white text-2xl hover:bg-sky-500 hover:drop-shadow-2xl"; 46 | } else { 47 | classNameCondition = 48 | "fixed z-20 top-44 right-4 md:top-48 md:right-8 bg-slate-100 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-sky-900 text-2xl hover:text-white hover:bg-sky-600 hover:drop-shadow-2xl"; 49 | } 50 | 51 | return ( 52 | <> 53 | 60 | 61 | 62 | setOpen(false)} 66 | > 67 |
68 | 77 | 78 | 79 | 80 | 86 | 95 | 96 |
97 | 101 | Settings 102 | 103 |
104 |

105 | Riffusion generates music from text prompts. Try your 106 | favorite styles, instruments like saxophone or violin, 107 | modifiers like arabic or jamaican, genres like jazz or 108 | gospel, sounds like church bells or rain, or any 109 | combination. Play with the settings below to explore the 110 | latent space of sound. 111 |

112 | 113 | {/* */} 114 | 115 | {SeedImageSelector(seedImage, setSeedImage)} 116 | 117 | {DenoisingSelector(denoising, setDenoising)} 118 | 119 | {DebugButton( 120 | promptInputs, 121 | inferenceResults, 122 | nowPlayingResult 123 | )} 124 |
125 | 126 |
127 | 138 | 139 | 148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | 156 | ); 157 | } 158 | 159 | export function SeedImageSelector( 160 | seedImage: string, 161 | setSeedImage: (seedImage: string) => void 162 | ) { 163 | let selectOptions = [ 164 | ["OG Beat", "og_beat"], 165 | ["Agile", "agile"], 166 | ["Marim", "marim"], 167 | ["Motorway", "motorway"], 168 | ["Vibes", "vibes"], 169 | ]; 170 | 171 | let matchedOption = selectOptions.find((x) => x[1] === seedImage); 172 | if (matchedOption === undefined) { 173 | matchedOption = [`Custom (${seedImage})`, seedImage]; 174 | selectOptions.push(matchedOption); 175 | } 176 | 177 | return ( 178 |
179 | 182 | 199 |

200 | Used as the base for img2img diffusion. This keeps your riff on beat and 201 | impacts melodic patterns. 202 |

203 |
204 | ); 205 | } 206 | 207 | export function DenoisingSelector( 208 | denoising: number, 209 | setDenoising: (d: number) => void 210 | ) { 211 | let selectOptions = [ 212 | ["Keep it on beat (0.75)", 0.75], 213 | ["Get a little crazy (0.8)", 0.8], 214 | ["I'm feeling lucky (0.85)", 0.85], 215 | ["What is tempo? (0.95)", 0.95], 216 | ]; 217 | 218 | let matchedOption = selectOptions.find((x) => x[1] === denoising); 219 | if (matchedOption === undefined) { 220 | matchedOption = [`Custom (${denoising})`, denoising]; 221 | selectOptions.push(matchedOption); 222 | } 223 | 224 | return ( 225 |
226 | 229 | 246 |

247 | The higher the denoising, the more creative the output, and the more 248 | likely you are to get off beat. 249 |

250 |
251 | ); 252 | } 253 | 254 | export function DebugButton(promptInputs, inferenceResults, nowPlayingResult) { 255 | const [debugOpen, debugSetOpen] = useState(false); 256 | 257 | let buttonClassName = ""; 258 | if (debugOpen) { 259 | buttonClassName = 260 | "fixed z-20 top-4 right-6 bg-sky-400 w-10 h-10 rounded-full flex justify-center items-center text-white text-xl hover:bg-sky-500 hover:drop-shadow-2xl"; 261 | } else { 262 | buttonClassName = 263 | "fixed z-20 top-4 right-6 bg-sky-100 w-10 h-10 rounded-full flex justify-center items-center text-sky-900 text-xl hover:text-white hover:bg-sky-500 hover:drop-shadow-2xl"; 264 | } 265 | 266 | return ( 267 | <> 268 | 278 | 279 | 287 | 288 | ); 289 | } 290 | -------------------------------------------------------------------------------- /components/Share.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { Fragment, useCallback, useState } from "react"; 3 | import { FiShare } from "react-icons/fi"; 4 | import { GrTwitter, GrReddit } from "react-icons/gr"; 5 | import styled from "styled-components"; 6 | 7 | import { InferenceResult } from "../types"; 8 | 9 | interface ShareProps { 10 | inferenceResults: InferenceResult[]; 11 | nowPlayingResult: InferenceResult; 12 | } 13 | 14 | const ModalContainer = styled.div` 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100vw; 19 | height: 100vh; 20 | background: rgba(0, 0, 0, 0.5); 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | `; 25 | 26 | export default function Share({ 27 | inferenceResults, 28 | nowPlayingResult, 29 | }: ShareProps) { 30 | const [open, setOpen] = useState(false); 31 | 32 | var classNameCondition = ""; 33 | if (open) { 34 | classNameCondition = 35 | "fixed z-20 top-24 right-4 md:top-28 md:right-8 bg-sky-400 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-white text-2xl hover:bg-sky-500 hover:drop-shadow-2xl"; 36 | } else { 37 | classNameCondition = 38 | "fixed z-20 top-24 right-4 md:top-28 md:right-8 bg-slate-100 w-14 h-14 rounded-full drop-shadow-lg flex justify-center items-center text-sky-900 text-2xl hover:text-white hover:bg-sky-600 hover:drop-shadow-2xl"; 39 | } 40 | 41 | // function to copy link to moment in song to the clipboard 42 | function copyLinkToClipboard(secondsAgo: number) { 43 | // use generateLink to generate the link 44 | const link = generateLink(secondsAgo); 45 | 46 | navigator.clipboard.writeText(link); 47 | } 48 | 49 | function getActiveResult() { 50 | if (!nowPlayingResult) { 51 | if (inferenceResults.length == 0) { 52 | return null; 53 | } 54 | return inferenceResults[0]; 55 | } else { 56 | return nowPlayingResult; 57 | } 58 | } 59 | 60 | // function to generate a link to a the moment in the song based on the played clips, input variable is how many seconds ago 61 | const generateLink = useCallback( 62 | (secondsAgo: number) => { 63 | var prompt; 64 | var seed; 65 | var denoising; 66 | var maskImageId; 67 | var seedImageId; 68 | var guidance; 69 | var numInferenceSteps; 70 | var alphaVelocity; 71 | 72 | const result = nowPlayingResult ? nowPlayingResult : inferenceResults[0]; 73 | 74 | if (!result) { 75 | return window.location.href; 76 | } else { 77 | var selectedInput: InferenceResult["input"]; 78 | if (secondsAgo == 0) { 79 | selectedInput = result.input; 80 | } else { 81 | var selectedCounter = result.counter - secondsAgo / 5; 82 | selectedInput = inferenceResults.find( 83 | (result) => result.counter == selectedCounter 84 | )?.input; 85 | 86 | if (!selectedInput) { 87 | // TODO: ideally don't show the button in this case... 88 | return window.location.href; 89 | } 90 | } 91 | 92 | // TODO: Consider only including in the link the things that are different from the default values 93 | prompt = selectedInput.start.prompt; 94 | seed = selectedInput.start.seed; 95 | denoising = selectedInput.start.denoising; 96 | maskImageId = selectedInput.mask_image_id; 97 | seedImageId = result.input.seed_image_id; 98 | 99 | // TODO, selectively add these based on whether we give user option to change them 100 | // guidance = result.input.guidance 101 | // numInferenceSteps = result.input.num_inference_steps 102 | // alphaVelocity = result.input.alpha_velocity 103 | } 104 | 105 | var baseUrl = window.location.origin + "/?"; 106 | 107 | if (prompt != null) { 108 | var promptString = "&prompt=" + prompt; 109 | } else { 110 | promptString = ""; 111 | } 112 | if (seed != null) { 113 | var seedString = "&seed=" + seed; 114 | } else { 115 | seedString = ""; 116 | } 117 | if (denoising != null) { 118 | var denoisingString = "&denoising=" + denoising; 119 | } else { 120 | denoisingString = ""; 121 | } 122 | if (maskImageId != null) { 123 | var maskImageIdString = "&maskImageId=" + maskImageId; 124 | } else { 125 | maskImageIdString = ""; 126 | } 127 | if (seedImageId != null) { 128 | var seedImageIdString = "&seedImageId=" + seedImageId; 129 | } else { 130 | seedImageIdString = ""; 131 | } 132 | if (guidance != null) { 133 | var guidanceString = "&guidance=" + guidance; 134 | } else { 135 | guidanceString = ""; 136 | } 137 | if (numInferenceSteps != null) { 138 | var numInferenceStepsString = "&numInferenceSteps=" + numInferenceSteps; 139 | } else { 140 | numInferenceStepsString = ""; 141 | } 142 | if (alphaVelocity != null) { 143 | var alphaVelocityString = "&alphaVelocity=" + alphaVelocity; 144 | } else { 145 | alphaVelocityString = ""; 146 | } 147 | 148 | // Format strings to have + in place of spaces for ease of sharing, note this is only necessary for prompts currently 149 | promptString = promptString.replace(/ /g, "+"); 150 | 151 | // create url string with the variables above combined 152 | var shareUrl = 153 | baseUrl + 154 | promptString + 155 | seedString + 156 | denoisingString + 157 | maskImageIdString + 158 | seedImageIdString + 159 | guidanceString + 160 | numInferenceStepsString + 161 | alphaVelocityString; 162 | 163 | return shareUrl; 164 | }, 165 | [nowPlayingResult, inferenceResults] 166 | ); 167 | 168 | const getRedditLink = useCallback(() => { 169 | if (inferenceResults.length == 0) { 170 | return null; 171 | } 172 | 173 | const result = nowPlayingResult ? nowPlayingResult : inferenceResults[0]; 174 | 175 | const encodedPrompt = encodeURIComponent(result.input.start.prompt); 176 | const encodedUrl = encodeURIComponent(generateLink(0)); 177 | return `https://www.reddit.com/r/riffusion/submit?title=Check+out+this+%23riffusion+prompt:+${encodeURI( 178 | '"' 179 | )}${encodedPrompt}${encodeURI('"')}&url=${encodedUrl}`; 180 | }, [nowPlayingResult, inferenceResults, generateLink]); 181 | 182 | const getTwitterLink = useCallback(() => { 183 | if (inferenceResults.length == 0) { 184 | return null; 185 | } 186 | 187 | const result = nowPlayingResult ? nowPlayingResult : inferenceResults[0]; 188 | 189 | const encodedPrompt = encodeURIComponent(result.input.start.prompt); 190 | const encodedUrl = encodeURIComponent(generateLink(0)); 191 | 192 | return `https://twitter.com/intent/tweet?&text=Check+out+this+%23riffusion+prompt:+${encodeURI( 193 | '"' 194 | )}${encodedPrompt}${encodeURI('"')}${encodeURI("\n\n")}${encodedUrl}`; 195 | }, [nowPlayingResult, inferenceResults, generateLink]); 196 | 197 | return ( 198 | <> 199 | 206 | 207 | 208 | setOpen(false)} 212 | > 213 |
214 | 223 | 224 | 225 | 226 | 232 | 241 | 242 |
243 | 247 |
248 | Share your riff 249 | 250 | { 253 | window.open(getTwitterLink(), "_blank"); 254 | }} 255 | /> 256 | { 259 | window.open(getRedditLink(), "_blank"); 260 | }} 261 | /> 262 |
263 |
264 |
265 | {!getActiveResult() && ( 266 |
267 |
268 |
269 | )} 270 | {getActiveResult() && ( 271 | share image 276 | )} 277 |
278 | 279 |
280 | 287 |
288 |
289 | 299 | 300 | 310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 | 318 | ); 319 | } 320 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { unmute } from "../external/unmute"; 3 | import { useCallback, useEffect, useState } from "react"; 4 | import * as Tone from "tone"; 5 | 6 | import AudioPlayer from "../components/AudioPlayer"; 7 | import FallingBehindWarning from "../components/FallingBehindWarning"; 8 | import PageHead from "../components/PageHead"; 9 | import Share from "../components/Share"; 10 | import Settings from "../components/Settings"; 11 | import ModelInference from "../components/ModelInference"; 12 | import Pause from "../components/Pause"; 13 | import PromptPanel from "../components/PromptPanel"; 14 | import ThreeCanvas from "../components/ThreeCanvas"; 15 | 16 | import { samplePrompts, initialSeeds, initialSeedImageMap } from "../samplePrompts"; 17 | 18 | import { 19 | AppState, 20 | InferenceInput, 21 | InferenceResult, 22 | PromptInput, 23 | } from "../types"; 24 | 25 | function getRandomFromArray(arr: any[], n: number) { 26 | var result = new Array(n), 27 | len = arr.length, 28 | taken = new Array(len); 29 | if (n > len) 30 | throw new RangeError("getRandom: more elements taken than available"); 31 | while (n--) { 32 | var x = Math.floor(Math.random() * len); 33 | result[n] = arr[x in taken ? taken[x] : x]; 34 | taken[x] = --len in taken ? taken[len] : len; 35 | } 36 | return result; 37 | } 38 | 39 | export default function Home() { 40 | // High-level state of the prompts 41 | const [appState, setAppState] = useState(AppState.UNINITIALIZED); 42 | 43 | // Whether playback is paused 44 | const [paused, setPaused] = useState(true); 45 | 46 | // Current interpolation parameters 47 | const [alpha, setAlpha] = useState(0.0); 48 | const [alphaRollover, setAlphaRollover] = useState(false); 49 | const [alphaVelocity, setAlphaVelocity] = useState(0.25); 50 | 51 | // Settings 52 | const [denoising, setDenoising] = useState(0.75); 53 | const [seedImageId, setSeedImageId] = useState( 54 | initialSeeds[Math.floor(Math.random() * initialSeeds.length)] 55 | ); 56 | const [seed, setSeed] = useState( 57 | initialSeedImageMap[seedImageId][ 58 | Math.floor(Math.random() * initialSeedImageMap[seedImageId].length) 59 | ] 60 | ); 61 | 62 | // Prompts shown on screen and maintained by the prompt panel 63 | const [promptInputs, setPromptInputs] = useState([]); 64 | 65 | // Model execution results 66 | const [inferenceResults, setInferenceResults] = useState( 67 | [] 68 | ); 69 | 70 | // Currently playing result, from the audio player 71 | const [nowPlayingResult, setNowPlayingResult] = 72 | useState(null); 73 | 74 | // Set the initial seed from the URL if available 75 | const router = useRouter(); 76 | useEffect(() => { 77 | // Make sure params are ready 78 | if (!router.isReady) { 79 | return; 80 | } 81 | 82 | if (router.query.alphaVelocity) { 83 | setAlphaVelocity(parseFloat(router.query.alphaVelocity as string)); 84 | } 85 | 86 | if (router.query.seed) { 87 | setSeed(parseInt(router.query.seed as string)); 88 | } 89 | 90 | // Set prompt inputs here so that they are empty and incorrect information isn't shown 91 | // until URL params have a chance to be read. 92 | let initPromptInputs = [ 93 | { prompt: "" }, 94 | { prompt: "" }, 95 | { prompt: "" }, 96 | { prompt: "" }, 97 | { prompt: "" }, 98 | { prompt: "" }, 99 | ]; 100 | 101 | // Set random initial prompts 102 | const samples = getRandomFromArray(samplePrompts, 4); 103 | for (let i = 0; i < 4; i++) { 104 | initPromptInputs[i].prompt = samples[i]; 105 | } 106 | if (router.query.prompt) { 107 | initPromptInputs[3].prompt = router.query.prompt as string; 108 | } 109 | setPromptInputs(initPromptInputs); 110 | 111 | if (router.query.denoising) { 112 | setDenoising(parseFloat(router.query.denoising as string)); 113 | } 114 | 115 | if (router.query.seedImageId) { 116 | setSeedImageId(router.query.seedImageId as string); 117 | } 118 | }, [router.isReady, router.query]); 119 | 120 | // Set the app state based on the prompt inputs array 121 | useEffect(() => { 122 | if (!alphaRollover) { 123 | return; 124 | } 125 | setAlphaRollover(false); 126 | 127 | const upNextPrompt = promptInputs[promptInputs.length - 1].prompt; 128 | const endPrompt = promptInputs[promptInputs.length - 2].prompt; 129 | 130 | let newAppState = appState; 131 | 132 | if (appState == AppState.SAME_PROMPT) { 133 | if (upNextPrompt) { 134 | newAppState = AppState.TRANSITION; 135 | 136 | // swap the last two prompts to bring the upNextPrompt into the next inference call 137 | setPromptInputs((prevPromptInputs) => { 138 | const newPromptInputs = [...prevPromptInputs]; 139 | newPromptInputs[newPromptInputs.length - 1] = { 140 | prompt: endPrompt, 141 | }; 142 | newPromptInputs[newPromptInputs.length - 2] = { 143 | prompt: upNextPrompt, 144 | }; 145 | return newPromptInputs; 146 | }); 147 | } 148 | setSeed(seed + 1); 149 | } else if (appState == AppState.TRANSITION) { 150 | setPromptInputs([...promptInputs, { prompt: "" }]); 151 | 152 | if (upNextPrompt) { 153 | newAppState = AppState.TRANSITION; 154 | } else { 155 | newAppState = AppState.SAME_PROMPT; 156 | } 157 | } 158 | 159 | if (newAppState != appState) { 160 | setAppState(newAppState); 161 | } 162 | }, [promptInputs, alpha, alphaRollover, appState, seed]); 163 | 164 | // On any app state change, reset alpha 165 | useEffect(() => { 166 | console.log("App State: ", appState); 167 | 168 | if (appState == AppState.UNINITIALIZED) { 169 | setAppState(AppState.SAME_PROMPT); 170 | } else { 171 | setAlpha(0.25); 172 | } 173 | }, [appState]); 174 | 175 | // What to do when a new inference result is available 176 | const newResultCallback = useCallback( 177 | (input: InferenceInput, result: InferenceResult) => { 178 | setInferenceResults((prevResults: InferenceResult[]) => { 179 | const maxResultCounter = Math.max(...prevResults.map((r) => r.counter)); 180 | 181 | const lastResult = prevResults.find( 182 | (r) => r.counter == maxResultCounter 183 | ); 184 | 185 | const newCounter = lastResult ? lastResult.counter + 1 : 0; 186 | 187 | result.counter = newCounter; 188 | result.input = input; 189 | result.played = false; 190 | 191 | let newAlpha = alpha + alphaVelocity; 192 | if (newAlpha > 1 + 1e-3) { 193 | newAlpha = newAlpha - 1; 194 | setAlphaRollover(true); 195 | } 196 | setAlpha(newAlpha); 197 | 198 | return [...prevResults, result]; 199 | }); 200 | }, 201 | [alpha, alphaVelocity] 202 | ); 203 | 204 | // State to handle the timeout for the player to not hog GPU forever. If you are 205 | // in SAME_PROMPT for this long, it will pause the player and bring up an alert. 206 | const timeoutIncrement = 600.0; 207 | const [timeoutPlayerTime, setTimeoutPlayerTime] = useState(timeoutIncrement); 208 | 209 | const nowPlayingCallback = useCallback( 210 | (result: InferenceResult, playerTime: number) => { 211 | console.log( 212 | "Now playing result ", 213 | result.counter, 214 | ", player time is ", 215 | playerTime 216 | ); 217 | 218 | setNowPlayingResult(result); 219 | 220 | // find the first promptInput that matches the result.input.end.prompt and set it's transitionCounter to the result.counter if not already set 221 | setPromptInputs((prevPromptInputs) => { 222 | const newPromptInputs = [...prevPromptInputs]; 223 | const promptInputIndex = newPromptInputs.findIndex( 224 | (p) => p.prompt == result.input.end.prompt 225 | ); 226 | if (promptInputIndex >= 0) { 227 | if (newPromptInputs[promptInputIndex].transitionCounter == null) { 228 | newPromptInputs[promptInputIndex].transitionCounter = 229 | result.counter; 230 | } 231 | } 232 | return newPromptInputs; 233 | }); 234 | 235 | // set played state for the result to true 236 | setInferenceResults((prevResults: InferenceResult[]) => { 237 | return prevResults.map((r) => { 238 | if (r.counter == result.counter) { 239 | r.played = true; 240 | } 241 | return r; 242 | }); 243 | }); 244 | 245 | // Extend the timeout if we're transitioning 246 | if (appState == AppState.TRANSITION) { 247 | setTimeoutPlayerTime(playerTime + timeoutIncrement); 248 | } 249 | 250 | // If we've hit the timeout, pause and increment the timeout 251 | if (playerTime > timeoutPlayerTime) { 252 | setTimeoutPlayerTime(playerTime + timeoutIncrement); 253 | setPaused(true); 254 | 255 | setTimeout(() => { 256 | if (confirm("Are you still riffing?")) { 257 | setPaused(false); 258 | } 259 | }, 100); 260 | } 261 | }, 262 | [timeoutPlayerTime, appState] 263 | ); 264 | 265 | // Track from the audio player whether we're behind on having new inference results, 266 | // in order to display an alert. 267 | const [playerIsBehind, setPlayerIsBehind] = useState(false); 268 | const playerIsBehindCallback = (isBehind: boolean) => { 269 | setPlayerIsBehind(isBehind); 270 | }; 271 | 272 | // TODO(hayk): This is not done yet but it's supposed to clear out future state and prompts 273 | // and allow the user to immediately start a new prompt. 274 | const resetCallback = () => { 275 | console.log("RESET"); 276 | 277 | setPromptInputs([ 278 | promptInputs[0], 279 | promptInputs[1], 280 | promptInputs[2], 281 | { prompt: "" }, 282 | { prompt: "" }, 283 | { prompt: "" }, 284 | ]); 285 | 286 | if (nowPlayingResult == null) { 287 | setInferenceResults([]); 288 | } 289 | 290 | // const counter = nowPlayingResult ? nowPlayingResult.counter : -1; 291 | // setInferenceResults(inferenceResults.filter((r) => r.counter <= counter)); 292 | // setNowPlayingResult(null); 293 | }; 294 | 295 | useEffect(() => { 296 | unmute(Tone.context.rawContext, true, false); 297 | }, []); 298 | 299 | return ( 300 | <> 301 | 302 | 303 |
304 | {/* Note, top bg section is used to maintain color in background on ios notch devices */} 305 |
306 |
307 |
308 | 309 |
310 |
311 |
312 |
window.open("/about", "_blank")} 315 | > 316 | [RIFFUSION] 317 |
318 |
319 |
320 | 321 | {playerIsBehind ? : null} 322 | 323 |
324 | Tone.Transport.seconds} 327 | inferenceResults={inferenceResults} 328 | /> 329 |
330 | 331 | 343 | 344 | 351 | 352 | { 358 | const newPromptInputs = [...promptInputs]; 359 | newPromptInputs[index].prompt = prompt; 360 | setPromptInputs(newPromptInputs); 361 | }} 362 | setPaused={setPaused} 363 | resetCallback={resetCallback} 364 | /> 365 | 366 | 367 | 368 | 372 | 373 | 382 |
383 | 384 | ); 385 | } 386 | -------------------------------------------------------------------------------- /components/PromptPanel.tsx: -------------------------------------------------------------------------------- 1 | import PromptEntry from "./PromptEntry"; 2 | 3 | import { AppState, PlayingState, InferenceResult, PromptInput } from "../types"; 4 | import { useRef } from "react"; 5 | import { samplePrompts, rollTheDicePrompts } from "../samplePrompts"; 6 | 7 | interface PromptPanelProps { 8 | prompts: PromptInput[]; 9 | inferenceResults: InferenceResult[]; 10 | nowPlayingResult: InferenceResult; 11 | appState: AppState; 12 | changePrompt: (prompt: string, index: number) => void; 13 | resetCallback: () => void; 14 | setPaused: (paused: boolean) => void; 15 | } 16 | 17 | export default function PromptPanel({ 18 | prompts, 19 | inferenceResults, 20 | nowPlayingResult, 21 | appState, 22 | changePrompt, 23 | resetCallback, 24 | setPaused, 25 | }: PromptPanelProps) { 26 | const inputPrompt = useRef(null); 27 | 28 | var playingState: PlayingState; 29 | 30 | const getDisplayPrompts = () => { 31 | var displayPrompts = []; 32 | 33 | if (nowPlayingResult == null) { 34 | playingState = PlayingState.UNINITIALIZED; 35 | } else if ( 36 | nowPlayingResult.input.start.prompt == nowPlayingResult.input.end.prompt 37 | ) { 38 | playingState = PlayingState.SAME_PROMPT; 39 | } else { 40 | playingState = PlayingState.TRANSITION; 41 | } 42 | 43 | // Add the last 4 prompts from playedResults 44 | // filter inferenceResults to only include results that have been played 45 | const playedResults = inferenceResults.filter((r) => r.played); 46 | 47 | // filter playedResults to include only results where alpha is 0.25 (the first alpha of a new transition) 48 | const playedResultsAlpha = playedResults.filter( 49 | (r) => r.input.alpha == 0.25 50 | ); 51 | 52 | // filter playedResultsAlpha to include only results where playedResultsAlpha.input.start.prompt is not the same as playedResultsAlpha.input.end.prompt 53 | const playedResultsAlphaTransition = playedResultsAlpha.filter( 54 | (r) => r.input.start.prompt != r.input.end.prompt 55 | ); 56 | 57 | // select the last 4 results 58 | const lastPlayedResultsAlphaTransition = 59 | playedResultsAlphaTransition.slice(-4); 60 | 61 | // add the most recent end prompts to the displayPrompts 62 | lastPlayedResultsAlphaTransition.forEach((result) => { 63 | displayPrompts.push({ prompt: result.input.end.prompt }); 64 | }); 65 | 66 | // Handle the case where there are less than 4 played results (i.e. the initial state) 67 | if (displayPrompts.length < 4) { 68 | const promptsToAdd = prompts.slice(displayPrompts.length, 4); 69 | displayPrompts = [...promptsToAdd, ...displayPrompts]; 70 | } 71 | 72 | // Add in the upNext and staged prompts 73 | // select the last 2 prompts from prompts 74 | const lastPrompts = prompts.slice(-2); 75 | 76 | // make a copy of the lastPrompts with new pointers 77 | const lastPromptsCopy = lastPrompts.map((p) => ({ ...p })); 78 | 79 | // if any of the lastPrompts have a transitionCounter, replace them with "" because they are already represented in the displayPrompts 80 | lastPromptsCopy.forEach((prompt, index) => { 81 | if (prompt.transitionCounter != null) { 82 | lastPromptsCopy[index].prompt = ""; 83 | lastPromptsCopy[index].transitionCounter = null; 84 | } 85 | }); 86 | 87 | // add the prompts to the displayPrompts 88 | lastPromptsCopy.forEach((p) => { 89 | displayPrompts.push(p); 90 | }); 91 | 92 | // if playingState == PlayingState.SAME_PROMPT or playingState == PlayingState.UNINITIALIZED, remove the first prompt from displayPrompts 93 | if ( 94 | playingState == PlayingState.SAME_PROMPT || 95 | playingState == PlayingState.UNINITIALIZED 96 | ) { 97 | displayPrompts.shift(); 98 | } 99 | 100 | return displayPrompts; 101 | }; 102 | 103 | const getPromptEntryClassName = (index: number) => { 104 | switch (playingState) { 105 | case PlayingState.UNINITIALIZED: 106 | return promptEntryClassNames_5_0[index]; 107 | case PlayingState.SAME_PROMPT: 108 | if (appState != AppState.TRANSITION) { 109 | return promptEntryClassNames_5_0[index]; 110 | } else { 111 | switch (nowPlayingResult.input.alpha) { 112 | case 0: 113 | return promptEntryClassNames_5_0[index]; 114 | case 0.25: 115 | return promptEntryClassNames_5_25[index]; 116 | case 0.5: 117 | return promptEntryClassNames_5_50[index]; 118 | case 0.75: 119 | return promptEntryClassNames_5_75[index]; 120 | case 1: 121 | return promptEntryClassNames_5_1[index]; 122 | } 123 | } 124 | case PlayingState.TRANSITION: 125 | switch (nowPlayingResult.input.alpha) { 126 | case 0: 127 | return promptEntryClassNames_6_0[index]; 128 | case 0.25: 129 | return promptEntryClassNames_6_25[index]; 130 | case 0.5: 131 | return promptEntryClassNames_6_50[index]; 132 | case 0.75: 133 | return promptEntryClassNames_6_75[index]; 134 | case 1: 135 | return promptEntryClassNames_6_1[index]; 136 | } 137 | default: 138 | // These states are reached if alpha is greater than 1 but the new inference is not ready 139 | if (appState != AppState.TRANSITION) { 140 | return promptEntryClassNames_5_0[index]; 141 | } else if (playingState == PlayingState.SAME_PROMPT) { 142 | return promptEntryClassNames_5_1[index]; 143 | } else { 144 | return promptEntryClassNames_6_1[index]; 145 | } 146 | } 147 | }; 148 | 149 | const rollTheDice = () => { 150 | const prompts = [...samplePrompts, ...rollTheDicePrompts]; 151 | 152 | const selectedPrompt = prompts[Math.floor(Math.random() * prompts.length)]; 153 | 154 | inputPrompt.current.value = selectedPrompt; 155 | }; 156 | 157 | return ( 158 | <> 159 |
160 |
161 |
162 | {getDisplayPrompts().map((prompt, index) => ( 163 | 174 | ))} 175 |
176 | 177 | {/* // Form trims spaces, and only submits if the remaining prompt is more than 0 characters */} 178 |
{ 181 | e.preventDefault(); 182 | const prompt = e.currentTarget.prompt.value; 183 | const trimmedPrompt = prompt.trimStart(); 184 | if (trimmedPrompt.length > 0) { 185 | changePrompt(trimmedPrompt, prompts.length - 1); 186 | inputPrompt.current.value = ""; 187 | } else { 188 | inputPrompt.current.value = ""; 189 | } 190 | }} 191 | > 192 | 204 |
205 |
206 |
207 | 215 |
216 |
217 |
218 |
219 | 220 | ); 221 | } 222 | 223 | export function refreshPage() { 224 | window.location.reload(); 225 | } 226 | 227 | const promptEntryClassNameDict = { 228 | 0: "font-extralight text-xs text-gray-500 text-opacity-20", 229 | 1: "font-extralight text-xs text-gray-400 text-opacity-20", 230 | 2: "font-extralight text-sm text-gray-300 text-opacity-30", 231 | 3: "font-extralight text-sm text-gray-200 text-opacity-30", 232 | 4: "font-light text-sm text-gray-200 text-opacity-40", 233 | 5: "font-light text-base text-gray-200 text-opacity-40", 234 | 6: "font-light text-base text-gray-100 text-opacity-50", 235 | 7: "font-light text-base text-gray-100 text-opacity-50", // starter for 0 236 | 237 | 8: "font-light text-base text-gray-100 text-opacity-50", 238 | 9: "font-light text-lg text-gray-100 text-opacity-50", 239 | 10: "font-light text-lg text-gray-100 text-opacity-60", 240 | 11: "font-normal text-lg text-gray-100 text-opacity-60", 241 | 12: "font-normal text-xl text-gray-100 text-opacity-60", 242 | 13: "font-normal text-xl text-gray-100 text-opacity-70", 243 | 14: "font-normal text-xl text-gray-100 text-opacity-70", 244 | 15: "font-normal text-xl text-gray-100 text-opacity-70", // starter for 1 245 | 246 | 16: "font-medium text-2xl text-gray-100 text-opacity-80", // 0% 247 | 17: "font-medium text-3xl text-gray-100 text-opacity-90", // 25% 248 | 18: "font-semibold text-4xl text-white", // 50% 249 | 19: "font-bold text-4xl text-white", // 75% 250 | 20: "font-bold text-5xl text-white", 251 | 21: "font-bold text-5xl text-white", 252 | 22: "font-bold text-5xl text-white", 253 | 23: "font-bold text-5xl text-white", // starter for 2 "start" 254 | 255 | 24: "font-bold text-5xl text-white", 256 | 25: "font-bold text-4xl text-white", // 75% 257 | 26: "font-semibold text-4xl text-white", // 50% 258 | 27: "font-medium text-3xl text-gray-100 text-opacity-90", // 25% 259 | 28: "font-normal text-2xl text-gray-100 text-opacity-80", 260 | 29: "font-normal text-l text-gray-100 text-opacity-70", 261 | 30: "font-normal text-l text-gray-100 text-opacity-70", 262 | 31: "font-normal text-l text-gray-100 text-opacity-70", // starter for 3 "end" 263 | 264 | 32: "font-normal text-base text-gray-100 text-opacity-70", 265 | 33: "font-normal text-base text-gray-100 text-opacity-60", 266 | 34: "font-normal text-base text-gray-100 text-opacity-60", 267 | 35: "font-normal text-base text-gray-100 text-opacity-60", // starter for 4 when "staging" 268 | 269 | 36: "font-normal text-base text-gray-100 text-opacity-60", // starter for 4 and 5 "Up Next" 270 | }; 271 | 272 | // ClassNames below 273 | // Note that 6_0 and 5_25 are never reached in current stucture 274 | 275 | const promptEntryClassNames_5_0 = { 276 | 0: promptEntryClassNameDict[7], 277 | 1: promptEntryClassNameDict[15], 278 | 2: promptEntryClassNameDict[23], // This is the start and end prompt 279 | 3: promptEntryClassNameDict[31], // This is the staged prompt 280 | 4: promptEntryClassNameDict[36], // This is the UP NEXT prompt 281 | }; 282 | 283 | const promptEntryClassNames_5_25 = { 284 | // This is not reached unless user has poor connection or delayed server response 285 | 0: promptEntryClassNameDict[7], 286 | 1: promptEntryClassNameDict[15], 287 | 2: promptEntryClassNameDict[23], // This is the start and end prompt 288 | 3: promptEntryClassNameDict[31], // This is the staged prompt 289 | 4: promptEntryClassNameDict[36], // This is the UP NEXT prompt 290 | }; 291 | 292 | const promptEntryClassNames_5_50 = { 293 | 0: promptEntryClassNameDict[6], 294 | 1: promptEntryClassNameDict[14], 295 | 2: promptEntryClassNameDict[22], 296 | 3: promptEntryClassNameDict[30], 297 | 4: promptEntryClassNameDict[36], 298 | }; 299 | 300 | const promptEntryClassNames_5_75 = { 301 | 0: promptEntryClassNameDict[5], 302 | 1: promptEntryClassNameDict[13], 303 | 2: promptEntryClassNameDict[21], 304 | 3: promptEntryClassNameDict[29], 305 | 4: promptEntryClassNameDict[36], 306 | }; 307 | 308 | const promptEntryClassNames_5_1 = { 309 | 0: promptEntryClassNameDict[4], 310 | 1: promptEntryClassNameDict[12], 311 | 2: promptEntryClassNameDict[20], 312 | 3: promptEntryClassNameDict[28], 313 | 4: promptEntryClassNameDict[36], 314 | }; 315 | 316 | const promptEntryClassNames_6_0 = { 317 | // This is not reached unless user has poor connection or delayed server response 318 | 0: promptEntryClassNameDict[3], 319 | 1: promptEntryClassNameDict[11], 320 | 2: promptEntryClassNameDict[19], 321 | 3: promptEntryClassNameDict[27], 322 | 4: promptEntryClassNameDict[35], 323 | 5: promptEntryClassNameDict[36], 324 | }; 325 | 326 | const promptEntryClassNames_6_25 = { 327 | 0: promptEntryClassNameDict[3], 328 | 1: promptEntryClassNameDict[11], 329 | 2: promptEntryClassNameDict[19], 330 | 3: promptEntryClassNameDict[27], 331 | 4: promptEntryClassNameDict[35], 332 | 5: promptEntryClassNameDict[36], 333 | }; 334 | 335 | const promptEntryClassNames_6_50 = { 336 | 0: promptEntryClassNameDict[2], 337 | 1: promptEntryClassNameDict[10], 338 | 2: promptEntryClassNameDict[18], 339 | 3: promptEntryClassNameDict[26], 340 | 4: promptEntryClassNameDict[34], 341 | 5: promptEntryClassNameDict[36], 342 | }; 343 | 344 | const promptEntryClassNames_6_75 = { 345 | 0: promptEntryClassNameDict[1], 346 | 1: promptEntryClassNameDict[9], 347 | 2: promptEntryClassNameDict[17], 348 | 3: promptEntryClassNameDict[25], 349 | 4: promptEntryClassNameDict[33], 350 | 5: promptEntryClassNameDict[36], 351 | }; 352 | 353 | const promptEntryClassNames_6_1 = { 354 | 0: promptEntryClassNameDict[0], 355 | 1: promptEntryClassNameDict[8], 356 | 2: promptEntryClassNameDict[16], 357 | 3: promptEntryClassNameDict[24], 358 | 4: promptEntryClassNameDict[32], 359 | 5: promptEntryClassNameDict[36], 360 | }; 361 | -------------------------------------------------------------------------------- /external/unmute.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * @file unmute.ts 4 | * Source: https://github.com/swevans/unmute 5 | * @author Spencer Evans evans.spencer@gmail.com 6 | * 7 | * unmute is a disgusting hack that helps.. 8 | * 1) automatically resume web audio contexts on user interaction 9 | * 2) automatically pause and resume web audio when the page is hidden. 10 | * 3) ios only: web audio play on the media channel rather than the ringer channel 11 | * 4) ios only: disable the media playback widget and airplay when: 12 | * 13 | * WebAudio is automatically resumed by checking context state and resuming whenever possible. 14 | * 15 | * WebAudio pausing is accomplished by watching the page visilibility API. When on iOS, page focus 16 | * is also used to determine if the page is in the foreground because Apple's page vis api implementation is buggy. 17 | * 18 | * iOS Only: Forcing WebAudio onto the media channel (instead of the ringer channel) works by playing 19 | * a short, high-quality, silent html audio track continuously when web audio is playing. 20 | * 21 | * iOS Only: Hiding the media playback widgets on iOS is accomplished by completely nuking the silent 22 | * html audio track whenever the app isn't in the foreground. 23 | * 24 | * iOS detection is done by looking at the user agent for iPhone, iPod, iPad. This detects the phones fine, but 25 | * apple likes to pretend their new iPads are computers (lol right..). Newer iPads are detected by finding 26 | * mac osx in the user agent and then checking for touch support by testing navigator.maxTouchPoints > 0. 27 | * 28 | * This is all really gross and apple should really fix their janky browser. 29 | * This code isn't optimized in any fashion, it is just whipped up to help someone out on stack overflow, its just meant as an example. 30 | */ 31 | /** 32 | * Enables unmute. 33 | * @param context A reference to the web audio context to "unmute". 34 | * @param allowBackgroundPlayback Optional. Default false. Allows audio to continue to play in the background. This is not recommended because it will burn battery and display playback controls on the iOS lockscreen. 35 | * @param forceIOSBehavior Optional. Default false. Forces behavior to that which is on iOS. This *could* be used in the event the iOS detection fails (which it shouldn't). It is strongly recommended NOT to use this. 36 | * @returns An object containing a dispose function which can be used to dispose of the unmute controller. 37 | */ 38 | export function unmute(context, allowBackgroundPlayback, forceIOSBehavior) { 39 | if (allowBackgroundPlayback === void 0) { 40 | allowBackgroundPlayback = false; 41 | } 42 | if (forceIOSBehavior === void 0) { 43 | forceIOSBehavior = false; 44 | } 45 | //#region Helpers 46 | // Determine the page visibility api 47 | var pageVisibilityAPI; 48 | if (document.hidden !== undefined) 49 | pageVisibilityAPI = { 50 | hidden: "hidden", 51 | visibilitychange: "visibilitychange", 52 | }; 53 | else if (document.webkitHidden !== undefined) 54 | pageVisibilityAPI = { 55 | hidden: "webkitHidden", 56 | visibilitychange: "webkitvisibilitychange", 57 | }; 58 | else if (document.mozHidden !== undefined) 59 | pageVisibilityAPI = { 60 | hidden: "mozHidden", 61 | visibilitychange: "mozvisibilitychange", 62 | }; 63 | else if (document.msHidden !== undefined) 64 | pageVisibilityAPI = { 65 | hidden: "msHidden", 66 | visibilitychange: "msvisibilitychange", 67 | }; 68 | // Helpers to add/remove a bunch of event listeners 69 | function addEventListeners(target, events, handler, capture, passive) { 70 | for (var i = 0; i < events.length; ++i) 71 | target.addEventListener(events[i], handler, { 72 | capture: capture, 73 | passive: passive, 74 | }); 75 | } 76 | function removeEventListeners(target, events, handler, capture, passive) { 77 | for (var i = 0; i < events.length; ++i) 78 | target.removeEventListener(events[i], handler, { 79 | capture: capture, 80 | passive: passive, 81 | }); 82 | } 83 | /** 84 | * Helper no-operation function to ignore promises safely 85 | */ 86 | function noop() {} 87 | //#endregion 88 | //#region iOS Detection 89 | var ua = navigator.userAgent.toLowerCase(); 90 | var isIOS = 91 | forceIOSBehavior || 92 | (ua.indexOf("iphone") >= 0 && ua.indexOf("like iphone") < 0) || 93 | (ua.indexOf("ipad") >= 0 && ua.indexOf("like ipad") < 0) || 94 | (ua.indexOf("ipod") >= 0 && ua.indexOf("like ipod") < 0) || 95 | (ua.indexOf("mac os x") >= 0 && navigator.maxTouchPoints > 0); // New ipads show up as macs in user agent, but they have a touch screen 96 | //#endregion 97 | //#region Playback Allowed State 98 | /** Indicates if audio should be allowed to play. */ 99 | var allowPlayback = true; // Assume page is visible and focused by default 100 | /** 101 | * Updates playback state. 102 | */ 103 | function updatePlaybackState() { 104 | // Check if should be active 105 | var shouldAllowPlayback = 106 | allowBackgroundPlayback || // always be active if noPause is indicated 107 | ((!pageVisibilityAPI || !document[pageVisibilityAPI.hidden]) && // can be active if no page vis api, or page not hidden 108 | (!isIOS || document.hasFocus())) // if ios, then document must also be focused because their page vis api is buggy 109 | ? true 110 | : false; 111 | // Change state 112 | if (shouldAllowPlayback !== allowPlayback) { 113 | allowPlayback = shouldAllowPlayback; 114 | // Update the channel state 115 | updateChannelState(false); 116 | // The playback allowed state has changed, update the context state to suspend / resume accordingly 117 | updateContextState(); 118 | } 119 | } 120 | /** 121 | * Handle visibility api events. 122 | */ 123 | function doc_visChange() { 124 | updatePlaybackState(); 125 | } 126 | if (pageVisibilityAPI) 127 | addEventListeners( 128 | document, 129 | [pageVisibilityAPI.visibilitychange], 130 | doc_visChange, 131 | true, 132 | true 133 | ); 134 | /** 135 | * Handles blur events (only used on iOS because it doesn't dispatch vis change events properly). 136 | */ 137 | function win_focusChange(evt) { 138 | if (evt && evt.target !== window) return; // ignore bubbles 139 | updatePlaybackState(); 140 | } 141 | if (isIOS) 142 | addEventListeners(window, ["focus", "blur"], win_focusChange, true, true); 143 | //#endregion 144 | //#region WebAudio Context State 145 | /** 146 | * Updates the context state. 147 | * NOTE: apple supports (and poorly at that) the proposed "interrupted" state spec, just ignore that for now. 148 | */ 149 | function updateContextState() { 150 | if (allowPlayback) { 151 | // Want to be running, so try resuming if necessary 152 | if (context.state !== "running" && context.state !== "closed") { 153 | // do nothing if the context was closed to avoid errors... can't check for the suspended state because of apple's crappy interrupted implementation 154 | // Can only resume after a media playback (input) event has occurred 155 | if (hasMediaPlaybackEventOccurred) { 156 | var p = context.resume(); 157 | if (p) p.then(noop, noop).catch(noop); 158 | } 159 | } 160 | } else { 161 | // Want to be suspended, so try suspending 162 | if (context.state === "running") { 163 | var p = context.suspend(); 164 | if (p) p.then(noop, noop).catch(noop); 165 | } 166 | } 167 | } 168 | /** 169 | * Handles context statechange events. 170 | * @param evt The event. 171 | */ 172 | function context_statechange(evt) { 173 | // Check if the event was already handled since we're listening for it both ways 174 | if (!evt || !evt.unmute_handled) { 175 | // Mark handled 176 | evt.unmute_handled = true; 177 | // The context may have auto changed to some undesired state, so immediately check again if we want to change it 178 | updateContextState(); 179 | } 180 | } 181 | addEventListeners(context, ["statechange"], context_statechange, true, true); // NOTE: IIRC some devices don't support the onstatechange event callback, so handle it both ways 182 | if (!context.onstatechange) context.onstatechange = context_statechange; // NOTE: IIRC older androids don't support the statechange event via addeventlistener, so handle it both ways 183 | //#endregion 184 | //#region HTML Audio Channel State 185 | /** The html audio element that forces web audio playback onto the media channel instead of the ringer channel. */ 186 | var channelTag = null; 187 | /** 188 | * A utility function for decompressing the base64 silence string. A poor-mans implementation of huffman decoding. 189 | * @param count The number of times the string is repeated in the string segment. 190 | * @param repeatStr The string to repeat. 191 | * @returns The 192 | */ 193 | function huffman(count, repeatStr) { 194 | var e = repeatStr; 195 | for (; count > 1; count--) e += repeatStr; 196 | return e; 197 | } 198 | /** 199 | * A very short bit of silence to be played with