美文网首页
简易网页钢琴(二)

简易网页钢琴(二)

作者: kevin5979 | 来源:发表于2020-09-08 12:24 被阅读0次

    前置文章《 简易网页钢琴(一)》 移步 https://www.jianshu.com/p/7b472b2a9a95

    接下来讲解7-11步

    第七步:处理用户交互信息
    第八步:列表渲染
    第九步:接入声音类,将key转化成对应的琴音
    第十步:保存的曲目播放
    第十一步:优化代码,解决bug

    第七步:处理用户交互信息
    /**
     * 控制钢琴动画效果
     * @param {*} key 按钮对应的key
     */
    showEffect: function (key) {
      const index = keys.indexOf(key)
      if (index !== -1) {
        keysList[index].classList.add("active")
        setTimeout(() => {
          keysList[index].classList.remove('active')
        }, 300)
      }
       // 判断是否属于录制状态,保存key值和距离时间差
      if (isStart) {
        const stamp = new Date().getTime() - startstamp
        tempSong.push(key)
        tempStamp.push(stamp)
     }
    }
    
    • 用户每次按键或点击,都会通过showEffect的判断,如果是录制状态,就将信息添加到当前曲目的数组中,然后在end.onclick 中存储整个曲目数组

    第八步:列表渲染
    • 模板字符串的使用
      import { formatDate } from "./utils.js"  // 导入工具函数
    
      let data = JSON.parse(localStorage.getItem('resources'))
      if (data) {
        resources = data
      }
    
    /**
     * 歌曲列表渲染
     * @param {*} data 数据
     */
    function renderList (data) {
      if (data) {
        // 新保存的曲目
        let li = document.createElement("li")
        li.innerHTML = `
      <div>
        <span class="name">${data.name}</span>
        <span class="time">${formatDate(data.time)}</span>
      </div>
      <i class="iconfont icon-bofang" data-index="${songIndex++}"></i>
    `
        musicList.prepend(li)
      } else {
        // 渲染原来保存的曲目
        let tempStr = ""
        resources.names.map((item, index) => {
          tempStr += `
      <li>
        <div>
          <span class="name">${item}</span>
          <span class="time">${formatDate(resources.songTime[index])}</span>
        </div>
        <i class="iconfont icon-bofang" data-index="${songIndex++}"></i>
      </li>
    `
        })
        musicList.innerHTML = tempStr
      }
      musicList = document.querySelector('.music-list')
    }
    
    • 考虑到用户进入该网页需要恢复上次的网页状态,使用localStorage.getItemresources重新赋值
    • 这里的 renderList 对参数data的判断,使得这个方法兼容新加曲目和加载已保存曲目的功能
    在utils.js文件中
    /**
     * 时间戳格式化
     * @param {*} stamp 时间戳
     */
    export const formatDate = function (stamp) {
      const second =  parseInt(stamp / 1000 % 60)
      const minute = parseInt(stamp / 1000 / 60)
      return `${minute}:${second}`
    }
    
    • 简单的时间转换

    第九步:接入声音类,将key转化成对应的琴音
    utils.js文件中
    export class MusicBox {
      constructor(options) {
        let defaults = {
          type: 'sine', 
          duration: 2
        };
    
        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 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
      }
    
      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);
      }
    
      createMusic(note) {
        let index = this.arrNotes.indexOf(note);
        if (index !== -1) {
          this.createSound(this.arrFrequency[index]);
        }
      }
    }
    
    index.js文件中
    import { formatDate, MusicBox } from "./utils.js"
    
    const music = new MusicBox({
      type: 'triangle',
      duration: 3
    })
    
    function showEffect (key) {
      const index = keys.indexOf(key)
      if (index !== -1) {
        keysList[index].classList.add("active")
        // 根据key产生声音
        music.createSound(262);
        music.createMusic(index);
        setTimeout(() => {
          keysList[index].classList.remove('active')
        }, 300)
      }
      // ......
    }
    

    第十步:保存的曲目播放
    musicList.onclick = function (e) {
      if (e.target.nodeName === 'I') { // 判断是否是播放按钮
        // 拿到播放曲目所需要的数据
        const index = e.target.dataset.index
        const stamps = resources.stamps[index]
        const songs = resources.songs[index]
        const len = stamps.length
        let currIndex = 0
        let time = 0
        // 每次播放前,先清除上次的播放,避免曲目重复播放
        clearInterval(timer)
        timer = setInterval(() => {
          time += 10
          if (Math.abs(stamps[currIndex] - time) < 20) {
            const keyIndex = keys.indexOf(songs[currIndex])
            onViews.showEffect(songs[currIndex])
            music.createSound(262);
            music.createMusic(keyIndex);
            currIndex += 1
            if (currIndex >= len) {
              // 播放完毕,清除计时器
              clearInterval(timer)
              alert("播放完毕");
            }
          }
       }, 10)
      } else {
        return
     }
    }
    
    • 播放原理:由于我们前面存储了一个曲目中每个键的值songs和每个按键时间与开始录制的时间差stamps ,设置定时器的时间差为10ms(时间差越小,播放对应的曲目的时间越准确),如果当前计时timestamps[currIndex]小于20,则可以播放currIndex下标对应的key,然后currIndex ++,直到 currIndex >= len,结束该曲播放,清除计时器

    第十一步:优化代码,解决bug
    • 到这里为止,大概就基本完成任务了,但是代码好似太乱了,没有章法和和关联性,这时我们整理一下代码
    import { formatDate, MusicBox } from "./utils.js"
    
    window.onload = function () {
    
      const music = new MusicBox({
        type: 'triangle', 
        duration: 3 
      });
    
      const keys = "qwertyuasdfghjzxcvbnm"
      const keysUl = document.querySelector(".key-list")
      const keysList = keysUl.querySelectorAll("li")
      const btnList = document.querySelector(".btn-list")
      const record = btnList.querySelector(".record")
      const end = btnList.querySelector(".end")
      let musicList = document.querySelector('.music-list')
    
      let resources = {
        songs: [],
        stamps: [],
        startStamps: [],
        endStamps: [],
        names: [],
        songTime: []
      }
      let data = JSON.parse(localStorage.getItem('resources'))
      if (data) {
        resources = data
      }
    
      let tempSong = []
      let tempStamp = []
      let startstamp = null
      let endstamp = null
      let isStart = false
      let isEnd = true
      let songIndex = 0
      let timer = null
    
      /**
       * 事件方法
       */
      const onEvents = function () {
        window.onkeydown: function
        keysUl.onclick: function
        musicList.onclick: function
        record.onclick: function
        end.onclick: function
      }
    
      /**
       * 视图方法
       */
      const onViews = {
        showEffect: function,
        renderList: function,
      }
    
      /**
       * 初始化
       */
      const init = function () {
        onEvents()
        onViews.renderList()
      }
      init()
    }
    
    • 当然,也可以按照你自己的想法来对代码进行改造,或许会有更好的方法
    结束了。。

    有问题欢迎讨论,溜了。。


    END

    相关文章

      网友评论

          本文标题:简易网页钢琴(二)

          本文链接:https://www.haomeiwen.com/subject/mjfdektx.html