1import cx from '@src/cx.mjs'; 2import { useState } from 'react'; 3import { Textbox } from '@src/repl/components/panel/SettingsTab'; 4import { getAudioContext } from '@strudel/webaudio'; 5 6function Checkbox({ label, value, onChange, disabled = false }) { 7 return ( 8 <label className={cx(disabled && 'opacity-50')}> 9 <input disabled={disabled} type="checkbox" checked={value} onChange={onChange} /> 10 {' ' + label} 11 </label> 12 ); 13} 14 15function FormItem({ label, children, disabled }) { 16 return ( 17 <div className="grid gap-2 w-full"> 18 <label className={cx(disabled && 'opacity-50')}>{label}</label> 19 {children} 20 </div> 21 ); 22} 23 24export default function ExportTab(Props) { 25 const { handleExport } = Props; 26 27 const [downloadName, setDownloadName] = useState(''); 28 const [startCycle, setStartCycle] = useState(0); 29 const [endCycle, setEndCycle] = useState(1); 30 const [sampleRate, setSampleRate] = useState(48000); 31 const [multiChannelOrbits, setMultiChannelOrbits] = useState(true); 32 const [maxPolyphony, setMaxPolyphony] = useState(1024); 33 const [exporting, setExporting] = useState(false); 34 const [progress, setProgress] = useState(0); 35 const [length, setLength] = useState(1); 36 37 const refreshProgress = () => { 38 const audioContext = getAudioContext(); 39 if (audioContext instanceof OfflineAudioContext) { 40 setProgress(audioContext.currentTime); 41 setLength(audioContext.length / sampleRate); 42 setTimeout(refreshProgress, 100); 43 } 44 }; 45 46 return ( 47 <> 48 <div className="text-foreground w-full space-y-4 p-4"> 49 <FormItem label="File name" disabled={exporting}> 50 <Textbox 51 onBlur={(e) => { 52 setDownloadName(e.target.value); 53 }} 54 onChange={(v) => { 55 setDownloadName(v); 56 }} 57 disabled={exporting} 58 placeholder="Leave empty to use current date" 59 className={cx('placeholder-muted', exporting && 'opacity-50 border-opacity-50')} 60 value={downloadName ?? ''} 61 /> 62 </FormItem> 63 <div className="flex flex-row gap-4 w-full"> 64 <FormItem label="Start cycle" disabled={exporting}> 65 <Textbox 66 min={0} 67 max={Infinity} 68 onBlur={(e) => { 69 let v = parseInt(e.target.value); 70 v = isNaN(v) ? 0 : Math.max(0, v); 71 setStartCycle(v); 72 }} 73 onChange={(v) => { 74 v = parseInt(v); 75 setStartCycle(v); 76 }} 77 type="number" 78 placeholder="" 79 disabled={exporting} 80 className={cx(exporting && 'opacity-50 border-opacity-50', 'w-full')} 81 value={startCycle ?? ''} 82 /> 83 </FormItem> 84 <FormItem label="End cycle" disabled={exporting}> 85 <Textbox 86 min={1} 87 max={Infinity} 88 onBlur={(e) => { 89 let v = parseInt(e.target.value); 90 v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; 91 setEndCycle(v); 92 }} 93 onChange={(v) => { 94 v = parseInt(v); 95 setEndCycle(v); 96 }} 97 type="number" 98 placeholder="" 99 disabled={exporting} 100 className={cx(exporting && 'opacity-50 border-opacity-50', 'w-full')} 101 value={endCycle ?? ''} 102 /> 103 </FormItem> 104 </div> 105 <div className="flex flex-row gap-4"> 106 <FormItem label="Sample rate" disabled={exporting}> 107 <Textbox 108 min={1} 109 max={Infinity} 110 onBlur={(e) => { 111 let v = parseInt(e.target.value); 112 v = isNaN(v) ? 1 : Math.max(1, v); 113 setSampleRate(v); 114 }} 115 onChange={(v) => { 116 v = parseInt(v); 117 setSampleRate(v); 118 }} 119 type="number" 120 placeholder="" 121 disabled={exporting} 122 className={cx(exporting && 'opacity-50 border-opacity-50')} 123 value={sampleRate ?? ''} 124 /> 125 </FormItem> 126 <FormItem label="Maximum polyphony" disabled={exporting}> 127 <Textbox 128 min={1} 129 max={Infinity} 130 onBlur={(e) => { 131 let v = parseInt(e.target.value); 132 v = isNaN(v) ? Math.max(1, parseInt(v)) : v; 133 setMaxPolyphony(v); 134 }} 135 onChange={(v) => { 136 v = Math.max(1, parseInt(v)); 137 setMaxPolyphony(v); 138 }} 139 type="number" 140 placeholder="" 141 disabled={exporting} 142 className={cx(exporting && 'opacity-50 border-opacity-50')} 143 value={maxPolyphony ?? ''} 144 /> 145 </FormItem> 146 </div> 147 <div> 148 <Checkbox 149 label="Multi Channel Orbits" 150 onChange={(cbEvent) => { 151 const val = cbEvent.target.checked; 152 setMultiChannelOrbits(val); 153 }} 154 disabled={exporting} 155 value={multiChannelOrbits} 156 /> 157 </div> 158 <button 159 className={cx('bg-background p-2 w-full rounded-md hover:opacity-75 relative', exporting && 'opacity-50')} 160 disabled={exporting} 161 onClick={async () => { 162 setExporting(true); 163 setTimeout(refreshProgress, 2000); 164 const modal = document.getElementById('exportProgressModal'); 165 modal.showModal(); 166 await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName) 167 .then(() => { 168 const modal = document.getElementById('exportProgressModal'); 169 modal.close(); 170 }) 171 .finally(() => { 172 setExporting(false); 173 setProgress(0); 174 setLength(1); 175 }); 176 }} 177 > 178 <div 179 className="absolute top-0 left-0 right-0 bottom-0 backdrop-invert" 180 style={{ 181 width: `${(exporting ? 1 : 0) + (progress / length) * 99}%`, 182 }} 183 /> 184 <span className="text-foreground">{exporting ? 'Exporting...' : 'Export to WAV'}</span> 185 </button> 186 </div> 187 <dialog 188 closedby={exporting ? 'none' : 'closerequest'} 189 id="exportProgressModal" 190 className="text-md bg-background text-foreground rounded-lg backdrop:bg-background backdrop:opacity-25" 191 /> 192 </> 193 ); 194}