教你用原生JS造“钢琴”

灵感来源:https://www.zhangxinxu.com/wordpress/2017/06/html5-web-audio-api-js-ux-voice/
向大神张鑫旭致敬!

吾乃好乐者,一日,偶见旭之佳作,欣喜至极,即践之,故有此文。

华丽版 “钢琴” 本尊

在开始之前,我们先来感受一下优美的钢琴旋律 → https://www.zhanhu56.com/h5/music_box/
(请在PC端浏览哦~)

Ok,接下来,我将为大家详细讲解“钢琴”的开发过程~


一、乐音的诞生

1. 基本乐理

音乐有三大基本要素:旋律、节奏与和声,而旋律又是由音阶组成,音阶是什么?音阶就是我们所熟知的1(Do)、2(Re)、3(Mi)、4(Fa)、5(So)、6(La)、7(Si),每个音都有它们各自的音高(音调高低不同),而音调不同则表示物体振动的频率不同。所以,这个频率是我们创造音乐的关键因素。

2. Audio API

可以说,“钢琴”的核心就在于这个强大的Audio API了。

3. 产生音调

// 创建音频上下文  
var audioCtx = new AudioContext();
// 创建音调控制对象  
var oscillator = audioCtx.createOscillator();
// 创建音量控制对象  
var gainNode = audioCtx.createGain();
// 音调音量关联  
oscillator.connect(gainNode);
// 音量和设备关联  
gainNode.connect(audioCtx.destination);
// 音调类型指定为正弦波  
oscillator.type = 'sine';
// 设置音调频率  
oscillator.frequency.value = 196.00;
// 先把当前音量设为0  
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
// 0.01秒时间内音量从刚刚的0变成1,线性变化 
gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 0.01);
// 声音走起 
oscillator.start(audioCtx.currentTime);
// 1秒时间内音量从刚刚的1变成0.001,指数变化 
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1);
// 1秒后停止声音 
oscillator.stop(audioCtx.currentTime + 1);

以上代码写了这么多,无非就发出了一个频率为196Hz、类型为正弦波、时值为1秒的音调,既然如此,为了使其更加灵活,为了能发出更多不同音调的声音,我们可以将其封装成一个方法,甚至是类!

4. 封装音调类

封装类的好处就是能将某一模块中用于实现该模块不同功能的方法放在一起,当需要用到它们时,我们能够灵活地长期复用。

现在,我们要封装的这个类还比较简单,只有一个功能:产生音调

class MusicBox {
  constructor(options){
    // 默认值
    let defaults = {
      type: 'sine',  // 音色类型  sine|square|triangle|sawtooth
      duration: 2  // 键音延长时间
    };

    this.opts = Object.assign(defaults, options);

    // 创建新的音频上下文接口
    this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  }

  createSound(freq) {
    // 创建一个OscillatorNode, 它表示一个周期性波形(振荡),基本上来说创造了一个音调
    let oscillator = this.audioCtx.createOscillator();
    // 创建一个GainNode,它可以控制音频的总音量
    let gainNode = this.audioCtx.createGain();
    // 把音量,音调和终节点进行关联
    oscillator.connect(gainNode);
    // this.audioCtx.destination返回AudioDestinationNode对象,表示当前audio context中所有节点的最终节点,一般表示音频渲染设备
    gainNode.connect(this.audioCtx.destination);
    // 指定音调的类型  sine|square|triangle|sawtooth
    oscillator.type = this.opts.type;
    // 设置当前播放声音的频率,也就是最终播放声音的调调
    oscillator.frequency.value = freq;
    // 当前时间设置音量为0
    gainNode.gain.setValueAtTime(0, this.audioCtx.currentTime);
    // 0.01秒后音量为1
    gainNode.gain.linearRampToValueAtTime(1, this.audioCtx.currentTime + 0.01);
    // 音调从当前时间开始播放
    oscillator.start(this.audioCtx.currentTime);
    // this.opts.duration秒内声音慢慢降低,是个不错的停止声音的方法
    gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + this.opts.duration);
    // this.opts.duration秒后完全停止声音
    oscillator.stop(this.audioCtx.currentTime + this.opts.duration);
  }
}

然后我们就可以通过这个类创建的对象来调用createSound()方法了。

let music = new MusicBox({
  type: 'square',  // 音色类型  sine|square|triangle|sawtooth
  duration: 5  // 键音延长时间
});

music.createSound(262);   // 发出一个频率为262Hz的声音

打开浏览器运行代码,你就能听到一个时长为5秒渐渐减弱的声音。

5. 让音符“动”起来吧!

现在我们的这个MusicBox类功能还很单一,只能用来发出某一频率的声音,而我们真正想要做的,是想要让它产生悦耳动听的音乐,所以接下来我们要做的就是将乐谱中的音符转换为音频播放出来。

① 创建音符数组与音阶频率数组

// 音阶频率
this.arrFrequency = [262, 294, 330, 349, 392, 440, 494, 523, 587, 659, 698, 784, 880, 988, 1047, 1175, 1319, 1397, 1568, 1760, 1967];
// 音符
this.arrNotes = ['·1', '·2', '·3', '·4', '·5', '·6', '·7', '1', '2', '3', '4', '5', '6', '7', '1·', '2·', '3·', '4·', '5·', '6·', '7·'];

音符数组中,小圆点在数字前代表低音,在数字后代表高音,比如 ·1 代表低音Do,比如 代表高音Do。有了这两个数组,就可以将音符与频率对应起来,这样也就能把音符转换成声音了。

② 把音符转成乐音

继续给MusicBox类添加播放乐音的方法。

createMusic(note){
    let index = this.arrNotes.indexOf(note);
    if(index !== -1){
      this.createSound(this.arrFrequency[index]);
    }
}

然后调用该方法,我们就能发出悦耳的乐音了。

music.createMusic('1');  // 发出中音 Do

二、“钢琴”自动演奏

1. 绘制“钢琴”


/* 核心CSS */
.music-box ul{ display: flex; display: -webkit-flex; justify-content: center; -webkit-justify-content: center; width: 96%; margin: 0 auto 12%;}
.music-box li{ flex: 1; -webkit-flex: 1; position: relative; margin: 0 4px; color: #fff; font-size: 16px;}
.music-box li span{ display: block; height: 200px; box-shadow: 0 0 10px #000; border-radius: 4px; background-color: #fff; cursor: pointer;}
.music-box li span.cur{ background-color: #999;}
.music-box li i{ position: absolute; bottom: -30px; width: 100%; text-align: center; font-style: normal;}
.music-box li i.low::before,
.music-box li i.high::before{ content: '·'; position: absolute; left: 0; width: 100%;}
.music-box li i.low::before{ bottom: -10px;}
.music-box li i.high::before{ top: -10px;}
/* 核心JS */
draw(){    // 绘制钢琴
    this.musicBtn = null;

    let musicBtns = document.querySelector(this.selector),
        li = '',
        noteClass = '';

    //  绘制钢琴键
    for(let i = 0; i < this.arrFrequency.length; i++){
      noteClass = this.arrNotes[i][0] === '·' ? 'low' : (this.arrNotes[i][1] === '·' ? 'high' : '');
      li += '
  • '+ this.arrNotes[i].replace(/\·/,'') +'
  • ' } musicBtns.innerHTML = '
      '+ li +'
    '; // 给钢琴按键绑定方法 let oLi = musicBtns.querySelectorAll('li'); for(let i = 0; i < this.arrFrequency.length; i++){ oLi[i].addEventListener('mousedown',(e)=>{ this.pressBtn(e.target,i); }) } this.musicBtn = musicBtns.querySelectorAll('li span'); // 鼠标起来时样式消失 document.onmouseup = () => { for(let i = 0; i < this.arrFrequency.length; i++){ this.musicBtn[i].className = ''; } }; } pressBtn(obj,i) { // 按下钢琴键 obj.className = 'cur'; this.createSound(this.arrFrequency[i]); setTimeout(() => { this.musicBtn[i].className = ''; },200); }

    然后调用draw()方法就可以绘制钢琴。

    constructor(selector, options){
        let defaults = {
          type: 'sine',  // 音色类型  sine|square|triangle|sawtooth
          duration: 2  // 键音延长时间
        };
    
        this.selector = selector;
        this.opts = Object.assign(defaults, options);
    
        // 创建新的音频上下文接口
        this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    
        // 音阶频率
        this.arrFrequency = [262, 294, 330, 349, 392, 440, 494, 523, 587, 659, 698, 784, 880, 988, 1047, 1175, 1319, 1397, 1568, 1760, 1967];
        // 音符
        this.arrNotes = ['·1', '·2', '·3', '·4', '·5', '·6', '·7', '1', '2', '3', '4', '5', '6', '7', '1·', '2·', '3·', '4·', '5·', '6·', '7·'];
    
        // 绘制钢琴
        this.draw();
    }
    
    let music = new MusicBox('.music-box', {
      type: 'square',  // 音色类型  sine|square|triangle|sawtooth
      duration: 5  // 键音延长时间
    });
    

    2. 自动演奏

    // 播放乐谱
    playMusic(musicText, speed = 2) {
        let i = 0, musicArr = musicText.split(' ');
    
        let timer = setInterval(() => {
    
          try{
            let n = this.arrNotes.indexOf(musicArr[i]);  // 钢琴键位置
    
            if(musicArr[i] !== '-' && musicArr[i] !== '0'){
              this.pressBtn(this.musicBtn[n],n);
            }
            i++;
    
            if(i >= musicArr.length){
              this.opts.loop ? i = 0 : clearInterval(timer);
            }
          }
          catch (e) {
            alert('请输入正确的乐谱!');
            clearInterval(timer);
          }
    
        }, 1000 / speed);
    
        return timer;
    }
    
    constructor(selector, options){
        let defaults = {
          loop: false, // 循环播放
          musicText: '',  // 乐谱
          autoplay: false, // 自动弹奏速度
          type: 'sine',  // 音色类型  sine|square|triangle|sawtooth
          duration: 2  // 键音延长时间
        };
    
        this.selector = selector;
        this.opts = Object.assign(defaults, options);
    
        // 创建新的音频上下文接口
        this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    
        // 音阶频率
        this.arrFrequency = [262, 294, 330, 349, 392, 440, 494, 523, 587, 659, 698, 784, 880, 988, 1047, 1175, 1319, 1397, 1568, 1760, 1967];
        // 音符
        this.arrNotes = ['·1', '·2', '·3', '·4', '·5', '·6', '·7', '1', '2', '3', '4', '5', '6', '7', '1·', '2·', '3·', '4·', '5·', '6·', '7·'];
    
        // 绘制钢琴
        this.draw();
    
        // 播放乐谱
        this.opts.autoplay && this.playMusic(this.opts.musicText, this.opts.autoplay);
    }
    

    然后我们就可以输入自己想听曲子的乐谱,“钢琴”就会自动给你演奏,尽情享受吧!

    let music = new MusicBox('.music-box', {
      loop: true, // 循环播放
      musicText: '1 1 5 5 6 6 5 - 4 4 3 3 2 2 1 - 5 5 4 4 3 3 2 - 5 5 4 4 3 3 2 - 1 1 5 5 6 6 5 - 4 4 3 3 2 2 1 - -',  // 儿歌《小星星》乐谱
      autoplay: 2, // 自动弹奏速度
      type: 'square',  // 音色类型  sine|square|triangle|sawtooth
      duration: 5  // 键音延长时间
    });
    

    musicText为乐谱,其中-为延音符,每个音符之间用空格隔开,这里我导入的是儿歌《小星星》

    三、“钢琴”插件源码

    class MusicBox {
    
      constructor(selector, options){
    
        let defaults = {
    
          loop: false, // 循环播放
          musicText: '',  // 乐谱
          autoplay: false, // 自动弹奏速度
          type: 'sine',  // 音色类型  sine|square|triangle|sawtooth
          duration: 2  // 键音延长时间
    
        };
    
        this.selector = selector;
        this.opts = Object.assign(defaults, options);
    
        // 创建新的音频上下文接口
        this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    
        // 音阶频率
        this.arrFrequency = [262, 294, 330, 349, 392, 440, 494, 523, 587, 659, 698, 784, 880, 988, 1047, 1175, 1319, 1397, 1568, 1760, 1967];
        // 音符
        this.arrNotes = ['·1', '·2', '·3', '·4', '·5', '·6', '·7', '1', '2', '3', '4', '5', '6', '7', '1·', '2·', '3·', '4·', '5·', '6·', '7·'];
    
        // 绘制钢琴
        this.draw();
    
        // 播放乐谱
        this.opts.autoplay && this.playMusic(this.opts.musicText, this.opts.autoplay);
    
      }
    
      // 创建乐音
      createMusic(note){
    
        let index = this.arrNotes.indexOf(note);
    
        if(index !== -1){
          this.createSound(this.arrFrequency[index]);
        }
    
      }
    
      // 创建声音
      createSound(freq) {
    
        // 创建一个OscillatorNode, 它表示一个周期性波形(振荡),基本上来说创造了一个音调
        let oscillator = this.audioCtx.createOscillator();
        // 创建一个GainNode,它可以控制音频的总音量
        let gainNode = this.audioCtx.createGain();
        // 把音量,音调和终节点进行关联
        oscillator.connect(gainNode);
        // this.audioCtx.destination返回AudioDestinationNode对象,表示当前audio context中所有节点的最终节点,一般表示音频渲染设备
        gainNode.connect(this.audioCtx.destination);
        // 指定音调的类型  sine|square|triangle|sawtooth
        oscillator.type = this.opts.type;
        // 设置当前播放声音的频率,也就是最终播放声音的调调
        oscillator.frequency.value = freq;
        // 当前时间设置音量为0
        gainNode.gain.setValueAtTime(0, this.audioCtx.currentTime);
        // 0.01秒后音量为1
        gainNode.gain.linearRampToValueAtTime(1, this.audioCtx.currentTime + 0.01);
        // 音调从当前时间开始播放
        oscillator.start(this.audioCtx.currentTime);
        // this.opts.duration秒内声音慢慢降低,是个不错的停止声音的方法
        gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + this.opts.duration);
        // this.opts.duration秒后完全停止声音
        oscillator.stop(this.audioCtx.currentTime + this.opts.duration);
    
      }
    
      // 绘制钢琴
      draw(){
    
        this.musicBtn = null;
    
        let musicBtns = document.querySelector(this.selector),
            li = '',
            noteClass = '';
    
        for(let i = 0; i < this.arrFrequency.length; i++){
          noteClass = this.arrNotes[i][0] === '·' ? 'low' : (this.arrNotes[i][1] === '·' ? 'high' : '');
          li += '
  • '+ this.arrNotes[i].replace(/\·/,'') +'
  • ' } musicBtns.innerHTML = '
      '+ li +'
    '; let oLi = musicBtns.querySelectorAll('li'); for(let i = 0; i < this.arrFrequency.length; i++){ oLi[i].addEventListener('mousedown',(e)=>{ this.pressBtn(e.target,i); }) } this.musicBtn = musicBtns.querySelectorAll('li span'); // 鼠标起来时样式消失 document.onmouseup = () => { for(let i = 0; i < this.arrFrequency.length; i++){ this.musicBtn[i].className = ''; } }; } // 按下钢琴键 pressBtn(obj,i) { obj.className = 'cur'; this.createSound(this.arrFrequency[i]); setTimeout(() => { this.musicBtn[i].className = ''; },200); } // 播放乐谱 playMusic(musicText, speed = 2) { let i = 0, musicArr = musicText.split(' '); let timer = setInterval(() => { try{ let n = this.arrNotes.indexOf(musicArr[i]); // 钢琴键位置 if(musicArr[i] !== '-' && musicArr[i] !== '0'){ this.pressBtn(this.musicBtn[n],n); } i++; if(i >= musicArr.length){ this.opts.loop ? i = 0 : clearInterval(timer); } } catch (e) { alert('请输入正确的乐谱!'); clearInterval(timer); } }, 1000 / speed); return timer; } } window.MusicBox = MusicBox; export default MusicBox;

    至此,我们其实已经使用ES6语法完成了一个音乐盒插件的开发,由于ES6语法目前浏览器支持还不是很完善,所以可以使用Webpack进行打包处理。有关Webpack的基础入门教程,可以关注我相关系列文集 → Webpack轻松入门。


    结束语

    星星之火,可以燎原。一个可能很小的创意,都有可能产生一个伟大的产品。这台“钢琴”很小,仍有很多功能等着大家去完善,还有很多创意点等着大家去发现!

    你可能感兴趣的:(教你用原生JS造“钢琴”)