├── .eslintrc ├── .github └── workflows │ └── render-video.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── out └── video.mp4 ├── package-lock.json ├── package.json ├── remotion.config.ts ├── src ├── Composition.tsx ├── ToneJS │ ├── EndCard.tsx │ ├── Note.tsx │ └── index.tsx ├── Video.tsx └── index.tsx └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@remotion" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/render-video.yml: -------------------------------------------------------------------------------- 1 | on: workflow_dispatch 2 | 3 | name: Render video 4 | jobs: 5 | render: 6 | name: Render video 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@main 10 | - uses: actions/setup-node@main 11 | - run: sudo apt update 12 | - run: sudo apt install ffmpeg 13 | - run: npm i 14 | - run: echo $WORKFLOW_INPUT > input-props.json 15 | env: 16 | WORKFLOW_INPUT: ${{ toJson(github.event.inputs) }} 17 | - run: npm run build -- --props="./input-props.json" 18 | - uses: actions/upload-artifact@v2 19 | with: 20 | name: video.mp4 21 | path: out/video.mp4 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": false, 4 | "jsxBracketSameLine": false, 5 | "useTabs": true, 6 | "overrides": [ 7 | { 8 | "files": ["*.yml"], 9 | "options": { 10 | "singleQuote": false 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": "never", 6 | "source.fixAll": "explicit" 7 | }, 8 | "typescript.enablePromptUseWorkspaceTsdk": true 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tone.JS X Remotion 2 | 3 | https://user-images.githubusercontent.com/1629785/150182821-67241528-8bb2-4c50-89b2-5acf0984735e.mp4 4 | 5 | ## Commands 6 | 7 | **Start Preview** 8 | 9 | ```console 10 | npm start 11 | ``` 12 | 13 | **Render video** 14 | 15 | ```console 16 | npm run build 17 | ``` 18 | 19 | **Upgrade Remotion** 20 | 21 | ```console 22 | npm run upgrade 23 | ``` 24 | 25 | ## Docs 26 | 27 | Get started with Remotion by reading the [fundamentals page](https://www.remotion.dev/docs/the-fundamentals). 28 | 29 | ## Help 30 | 31 | We provide help [on our Discord server](https://discord.gg/6VzzNDwUwV). 32 | 33 | ## Issues 34 | 35 | Found an issue with Remotion? [File an issue here](https://github.com/remotion-dev/remotion/issues/new). 36 | 37 | ## License 38 | 39 | Note that for some entities a company license is needed. Read [the terms here](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md). 40 | -------------------------------------------------------------------------------- /out/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/tone-js-example/b98ef5d8b575f112b30e74e5ab76bc8ecf8cb9f5/out/video.mp4 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remotion-template", 3 | "version": "1.0.0", 4 | "description": "My Remotion video", 5 | "scripts": { 6 | "start": "remotion studio src/index.tsx", 7 | "build": "remotion render src/index.tsx tone out/video.mp4", 8 | "upgrade": "remotion upgrade", 9 | "test": "eslint src --ext ts,tsx,js,jsx && tsc" 10 | }, 11 | "repository": {}, 12 | "license": "UNLICENSED", 13 | "dependencies": { 14 | "@remotion/bundler": "4.0.118", 15 | "@remotion/cli": "4.0.118", 16 | "@remotion/eslint-config": "4.0.118", 17 | "@remotion/media-utils": "4.0.118", 18 | "@remotion/renderer": "4.0.118", 19 | "eslint": "^8.44.0", 20 | "prettier": "^2.8.8", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "remotion": "4.0.118", 24 | "tone": "^14.7.77", 25 | "typescript": "^4.9.5" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.2.14", 29 | "@types/web": "^0.0.61" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /remotion.config.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '@remotion/cli/config'; 2 | 3 | Config.setVideoImageFormat('jpeg'); 4 | Config.setOverwriteOutput(true); 5 | -------------------------------------------------------------------------------- /src/Composition.tsx: -------------------------------------------------------------------------------- 1 | export const MyComposition = () => { 2 | return null; 3 | }; 4 | -------------------------------------------------------------------------------- /src/ToneJS/EndCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const EndCard: React.FC = () => { 4 | return ( 5 | 12 | 20 | 28 | 36 | 40 | 41 | 47 | 48 | 49 | 55 | 56 | 57 | 63 | 64 | 65 | 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/ToneJS/Note.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | 4 | export const Note: React.FC<{ 5 | note: string; 6 | }> = ({note}) => { 7 | return ( 8 | 14 | 33 |
45 | {``} 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/ToneJS/index.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useState} from 'react'; 2 | import { 3 | AbsoluteFill, 4 | Audio, 5 | continueRender, 6 | delayRender, 7 | interpolate, 8 | Sequence, 9 | spring, 10 | useCurrentFrame, 11 | useVideoConfig, 12 | } from 'remotion'; 13 | import * as Tone from 'tone'; 14 | import {audioBufferToDataUrl} from '@remotion/media-utils'; 15 | import {Note} from './Note'; 16 | import {EndCard} from './EndCard'; 17 | 18 | const notesSequence = [ 19 | 'E4', 20 | 'E4', 21 | 'E4', 22 | 'E4', 23 | 'E4', 24 | 'E4', 25 | 'E4', 26 | 'G4', 27 | 'C4', 28 | 'D4', 29 | 'E4', 30 | 'F4', 31 | 'F4', 32 | 'F4', 33 | 'F4', 34 | 'F4', 35 | 'E4', 36 | 'E4', 37 | 'E4', 38 | 'E4', 39 | 'E4', 40 | 'D4', 41 | 'D4', 42 | 'E4', 43 | 'D4', 44 | 'D4', 45 | 'E4', 46 | 'D4', 47 | 'G4', 48 | 'E4', 49 | 'E4', 50 | 'E4', 51 | 'E4', 52 | 'E4', 53 | 'E4', 54 | 'E4', 55 | 'G4', 56 | 'C4', 57 | 'D4', 58 | 'E4', 59 | 'F4', 60 | 'F4', 61 | 'F4', 62 | 'F4', 63 | 'F4', 64 | 'E4', 65 | 'E4', 66 | 'E4', 67 | 'E4', 68 | 'G4', 69 | 'G4', 70 | 'F4', 71 | 'D4', 72 | 'C4', 73 | ].map((note) => ({note, duration: '8n'})); 74 | 75 | export const ToneJS: React.FC = () => { 76 | const frame = useCurrentFrame(); 77 | const [handle] = useState(() => delayRender()); 78 | const [audioBuffer, setAudioBuffer] = useState(null); 79 | const {fps, durationInFrames} = useVideoConfig(); 80 | 81 | const lengthInSeconds = durationInFrames / fps; 82 | 83 | const renderAudio = useCallback(async () => { 84 | const toneBuffer = await Tone.Offline(() => { 85 | const synth = new Tone.Synth().toDestination(); 86 | const now = Tone.now(); 87 | let accumulatedTime = 0; 88 | notesSequence.forEach((sequenceItem) => { 89 | accumulatedTime += Tone.Time(sequenceItem.duration).toSeconds(); 90 | synth.triggerAttackRelease( 91 | sequenceItem.note, 92 | sequenceItem.duration, 93 | now + accumulatedTime 94 | ); 95 | }); 96 | }, lengthInSeconds); 97 | 98 | const buffer = toneBuffer.get() as AudioBuffer; 99 | setAudioBuffer(audioBufferToDataUrl(buffer)); 100 | 101 | continueRender(handle); 102 | }, [handle, lengthInSeconds]); 103 | 104 | useEffect(() => { 105 | renderAudio(); 106 | }, [renderAudio]); 107 | 108 | let accumulatedFrames = 0; 109 | const sequenceWithFrames = notesSequence.map((sequenceItem) => { 110 | accumulatedFrames += Tone.Time(sequenceItem.duration).toSeconds() * fps; 111 | return { 112 | note: sequenceItem.note, 113 | midiNote: Tone.Midi(sequenceItem.note).toMidi(), 114 | frame: accumulatedFrames, 115 | }; 116 | }); 117 | 118 | const transition = spring({ 119 | fps, 120 | frame: frame - 440, 121 | config: { 122 | damping: 200, 123 | }, 124 | }); 125 | 126 | const rotateY = interpolate(transition, [0, 0.5], [0, Math.PI / 2], { 127 | extrapolateRight: 'clamp', 128 | extrapolateLeft: 'clamp', 129 | }); 130 | const rotateY2 = interpolate(transition, [0.5, 1], [-Math.PI / 2, 0], { 131 | extrapolateLeft: 'clamp', 132 | extrapolateRight: 'clamp', 133 | }); 134 | 135 | return ( 136 | 141 | 142 | 149 | {sequenceWithFrames 150 | .filter((sequenceItem) => frame > sequenceItem.frame) 151 | .slice(-1) 152 | .map((sequenceItem, i) => { 153 | return ( 154 | 159 | 160 | 161 | ); 162 | })} 163 | 164 | 165 | {audioBuffer && 176 | ); 177 | }; 178 | 179 | export default ToneJS; 180 | -------------------------------------------------------------------------------- /src/Video.tsx: -------------------------------------------------------------------------------- 1 | import {Composition} from 'remotion'; 2 | import ToneJS from './ToneJS'; 3 | 4 | export const RemotionVideo: React.FC = () => { 5 | return ( 6 | <> 7 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import {registerRoot} from 'remotion'; 2 | import {RemotionVideo} from './Video'; 3 | 4 | registerRoot(RemotionVideo); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "jsx": "react-jsx", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "noEmit": true, 9 | "lib": ["es2015"], 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | } 14 | } 15 | --------------------------------------------------------------------------------