一系列的奮戰與血光當中,戰鼓已經響起,沈睡的力量都已經被召喚出了峽谷。合成器的旋鈕打磨拋光,內部的聲音元件也大致就定位,接下來就是把旋鈕的電線給接上,開始全身震動模式啦!
這篇主要分成兩個部分:
使用React當作 framework,我所有的旋鈕都是以 React Component 的架構來製作。我整個合成器的面板都放在Block
這個 React Component 當中。
// block.js
class Block extends Component {
render() {
const n = this.number;
const { on, position, data } = this.state;
return (
<div className={styles.container}>
<div className={styles.table}>
{data.map((row, r) => (
<div key={uuid4()} className={`${styles.row}`}>
{row.map((d, c) =>
<Knob
key={uuid4()}
r={r}
c={c}
onEnter={this.setPosition}
onMouseDown={this.handleMouseDown}
onWheel={this.handleWheel}
value={d}
active={r === position.r && c === position.c}
/>
)}
</div>
))}
</div>
</div>
);
}
}
而在Block
裡面,每個旋鈕都是叫做Knob
的 Component。這樣的版型因為整個都是方形的像是 grid 一樣的東西,我最後決定要用flexbox
去切割版型。
/* block.module.scss */
.table {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
justify-content: space-around;
}
.row {
height: 100vh;
display: flex;
flex-flow: row;
justify-content: space-around;
}
用 react 實作的時候,習慣盡量把底層的 component(Knob
和Button
)寫成 pure function,所有的 state 都只放在Block
裡面。像是所有的旋鈕的數字都是存在Block
的state
當中。
旋鈕最滑順的互動應該就是用 wheel 的方式了,不用點擊就可以直接調變參數。但是有時候onMouseEnter
等的 callback 不太穩定。所以也加入 Mouse Drag 的方式,讓使用者可以點擊後拖曳去改變旋鈕的參數。
我將整組鼓組打包在一個叫做Vsynth
的 class 裡面,並制定一個 update 參數的 API:
class Vsynth {
constuctor() {
// 設定所有的樂器與聲音
}
updateValue(r, c, vi) {
// vi: 0 ~ 360
// mapping vi 這個數字到樂器參數上
// 範例:
// 假如音量需要 0 ~ -40(dB),就做下面的轉換
// this.kick.synth.volume = vi * (-1 / 9);
}
}
我將Vsynth
的 instance 放在 block 的 state 裡面一起管理,透過handleWheel
和mousemove
等等的 event、callback 呼叫updateValue
去改變參數。
但是假如個數字都要這樣從 0 ~ 360 去算我們到底需要哪些,再做加減乘除就太煩了。我直接寫了一個function lerp
負責這樣的換算:
// lerp.js
export default function (low, high, from, to, v) {
const ratio = (v - low) / (high - low);
return from + (to - from) * ratio;
}
假設 v 介於low
到high
之間,會把v
這個數字線性轉換到from
到to
裡面。
// vsynth.js
...
updateValue(r, c, vi) {
const v = vi / 360; // 先將 vi 從 0 ~ 360 轉化到 0 ~ 1 上
// r 是 row 的 index
// c 是 column 的 index
switch (r) {
case 0:
switch (c) {
case 0:
this.kick.synth.envelope.decay = lerp(0, 1, 0.4, 6.0, v);
break;
case 1:
this.kick.synth.envelope.attack = lerp(0, 1, 0.001, 0.3, v);
break;
default:
break;
}
break;
default:
break;
}
...
我把最左上角(0, 0)的旋鈕 mapping 到大鼓(kick)的 envelope 的 decay 上面,第二個(0, 1)的旋鈕 mapping 到大鼓(kick)的 envelope 的 attack 上面。聽聽看!
接上了,終於接上了。音響已經開啟,派對蓄勢待發...
請繼續期待 web 的 audio / VISUAL。
請繼續思考 web 的 audio / VISUAL。
關於作者
Vibert Thio
致力於將對於技術的深度研究轉化為新型態藝術創作的能量,並思考技術的拓展/侷限與其對於藝術論述/呈現的影響。專長為數位藝術創作、音像程式設計、互動設計,喜愛即時運算的臨場感與不可預測。