javascript - ฉันจะหลีกเลี่ยงไม่ให้เสียงหายไปเมื่อใช้ `Tone.PolySynth()` กับวิธี `Sequence` ได้อย่างไร?
บริบท
ฉันพยายามจะสร้างสเต็ปซีเควนเซอร์ที่คล้ายกับตัวอย่างอย่างเป็นทางการในเอกสารของ Tone.js อย่างไรก็ตาม แทนที่จะใช้การเล่นไฟล์ MP3 ด้วยวิธี Players ฉันต้องการนำ PolySynth มาใช้เพื่อสำรวจเสียงที่หลากหลายเหมือนตัวอย่างของ Jon Oliver
หลังจากดูตัวอย่างสาธิตไม่กี่แบบที่มีวิธีการต่างกัน—เช่น ตัวเรียงลำดับกลองนี้ (ใช้ MP3 กับ Players) ตัวเรียงลำดับสเต็ปนี้ (ใช้ Synth โมโนหลายตัว)—ฉันสามารถสร้างตัวเรียงลำดับพื้นฐานที่ใช้งานได้ ตัวสาธิตที่ทำงานได้ของฉันอยู่ใน CodeSandbox ที่นี่
ปัญหา
แม้ว่ามันจะทำงานตามที่ตั้งใจไว้—เช่น การเล่นโน้ตแต่ละตัวเมื่อสลับเปิด/ปิด การวนซ้ำลำดับตามความเร็วที่กำหนด การสลับโหมดเล่น/หยุดชั่วคราว ฯลฯ—เสียงโพลีซินธ์เริ่มเพี้ยนเล็กน้อยหลังจากวนซ้ำไปไม่กี่ครั้งและไม่นานก็เงียบสนิท
แม้ว่าจะเสียงจะหายไป การเรียก Sequence ดูเหมือนว่าจะยังคงดำเนินต่อไปและคุณสมบัติอื่น ๆ ทั้งหมดก็ทำงานตามที่ตั้งใจไว้ แต่สังเกตได้ว่ามีการกระตุกและหน่วงชัดเจน
ฉันสงสัยว่ามันอาจเป็นวิธีที่ฉันกำลังจัดการการเรียก Sequence และโดยไม่ได้ตั้งใจทำให้หน่วยความจำเกินกำลัง แม้ว่าฉันจะไม่เห็นข้อผิดพลาดสำคัญใด ๆ ในคอนโซล
สิ่งที่ฉันพยายาม
- การใช้
Transport.scheduleRepeatพร้อม callback: (คล้ายกับบทความ Medium นี้ซึ่งส่งผลให้เกิดพฤติกรรมหน่วงและปิดเสียงเหมือนกัน) - การใช้การเรียก
asyncเพื่อจัดการการสลับการเล่นSequence: (คล้ายกับตัวอย่าง CodeSandbox นี้ซึ่งส่งผลให้เกิดอาการหน่วงและเสียงเบาเหมือนกัน) - การใช้
Array.forEachหรือforloop เพื่อวนซ้ำลำดับด้วยตนเอง: ไม่สามารถรักษาเวลาซิงค์ได้โดยไม่มีการควบคุมเวลาของTone - การใช้
refเพื่อติดตามSequenceในขณะที่คอยลบ beat ก่อนหน้าอย่างต่อเนื่อง: (แก้ไขจากตัวอย่าง CodeSandbox นี้ซึ่งส่งผลให้เกิดพฤติกรรมหน่วงและเงียบเหมือนเดิม นี่คือตัวอย่างโค้ดที่ทำงานได้ในปัจจุบัน)
โค้ดตัวอย่าง
มันถูกสร้างขึ้นด้วย React และ TypeScript ส่วนหลักของแอปจะแสดงด้านล่าง คุณสามารถดูโค้ดที่ทำงานได้เต็มรูปแบบที่ CodeSandbox
ฉันเป็นผู้เริ่มต้นอย่างสมบูรณ์ใน Tone.js ถึงแม้ว่าจะมีความชำนาญใน React พอสมควร คำแนะนำและข้อมูลเชิงลึกใด ๆ จะได้รับการชื่นชมอย่างมาก
ฉันเข้าใจว่าการจัดการข้อมูล การจัดการ DOM และด้านอื่น ๆ ของแอปก็ยังค่อนข้างยุ่งเหยิงเช่นกัน และหวังว่ามันจะไม่ใช่สาเหตุที่ทำให้เกิดปัญหานี้
function App() {
type Loop = {
dispose: any;
};
const [isPlaying, setIsPlaying] = React.useState(false);
const [currentBeat, setCurrentBeat] = React.useState(0);
const [currentBPM, setCurrentBPM] = React.useState(120);
const [noteMatrix, setNoteMatrix] = React.useState(defaultNoteMatrix);
const [currentScale, setCurrentScale] = React.useState(SCALES.major);
const synth = new Tone.PolySynth().toDestination();
const loop = React.useRef<Loop | null>(null);
const noteMap = noteMatrix.map((beat) =>
beat
.map((note, i) => {
return { note: currentScale[i], on: note };
})
.filter((note) => note.on)
.map((item) => item.note)
);
const playSingleNote = (note = "") => synth.triggerAttackRelease(note, "16n");
const tick = (e: React.MouseEvent<HTMLButtonElement>): void => {
const currentBeat = parseFloat(e.currentTarget.dataset.beat!);
const selectedNote = parseFloat(e.currentTarget.dataset.index!);
const isOn = e.currentTarget.dataset.on === "1" ? 0 : 1;
const updatedMatrix = [...noteMatrix];
playSingleNote(currentScale[selectedNote]);
updatedMatrix[currentBeat][selectedNote] = isOn;
setNoteMatrix(updatedMatrix);
};
const reset = useCallback(() => {
setNoteMatrix([
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat],
[...emptyBeat]
]);
setIsPlaying(false);
Tone.Transport.stop();
}, []);
React.useEffect(() => {
Tone.Transport.loop = false;
Tone.Transport.on("stop", () => setCurrentBeat(0));
});
React.useEffect(() => {
Tone.Transport.bpm.value = currentBPM;
}, [currentBPM]);
React.useEffect(() => {
if (loop.current) {
loop.current.dispose();
}
loop.current = new Tone.Sequence(
(time, beat) => {
setCurrentBeat(beat);
synth.triggerAttackRelease(noteMap[beat], "16n", time);
},
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
"16n"
).start(0);
}, [noteMap, isPlaying]);
const togglePlay = React.useCallback(() => {
Tone.context.resume();
Tone.Transport.toggle();
setIsPlaying((isPlaying) => !isPlaying);
}, []);
return (
<>
<GridStyle>
<div className="beat">
{currentScale.map((note, i) => (
<div className="head" key={i}>
{note}
</div>
))}
</div>
{noteMatrix.map((beat, i) => (
<div className="beat" key={i} data-active={currentBeat === i ? 1 : 0}>
{currentScale.map((note, j) => (
<button
className="note"
key={j}
onClick={tick}
data-note={note}
data-on={beat[j]}
data-beat={i}
data-index={j}
/>
))}
</div>
))}
</GridStyle>
<button onClick={togglePlay}>{isPlaying ? "Stop" : "Play"}</button>
<button onClick={reset}>Clear</button>
Scale
<select
name="scale"
id="scale"
defaultValue="major"
onChange={(e) => setCurrentScale(SCALES[e.currentTarget.value])}
>
{["major", "minor", "suspended"].map((scale) => (
<option value={scale} key={scale}>
{scale}
</option>
))}
</select>
BPM
<select
name="bpm"
id="bpm"
onChange={(e) => setCurrentBPM(parseFloat(e.currentTarget.value))}
defaultValue={currentBPM}
>
{[80, 100, 120, 140, 160, 180, 200].map((bpm) => (
<option value={bpm} key={bpm}>
{bpm}
</option>
))}
</select>
</>
);
}
export default App;
คำตอบ #1
วัตถุ synth ถูกสร้างขึ้นทุกครั้งที่มีการเรนเดอร์ คุณสามารถย้ายมันออกจาก App (หรือ useRef) และทุกอย่างจะทำงานได้ปกติ
อีกอย่างที่ฉันสังเกตเห็นคือไม่มีรายการการพึ่งพาที่นี่:
React.useEffect(() => {
Tone.Transport.loop = false;
Tone.Transport.on("stop", () => setCurrentBeat(0));
}, []); // <--
ป.ล. โครงการนี้น่ารักจริงๆ!
