jevstrudel.git / website / src / repl / components / panel / ExportTab.jsx
ExportTab.jsx194 lines · 6.5 KB · raw
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}