ES6+Koa2+Web Audio API 可视化音频应用开发

目录

    • 导读
    • 项目成品预览
    • 一、整个开发思路
    • 二、后端配置
      • 1. 安装koa应用生成器
      • 2. 新增路由 `/musc`
    • 三、前端配置
      • 1. 目标页面 `views/index.ejs`, 编辑如下
      • 2. 配置前端交互脚本 `public/javascript/music.js`
      • 3. 第一阶段版本预览 `public/javascript/music-2.js`
    • 四. 项目源码地址

导读

这段时间遇到个项目需要涉及音频流的可视化开发,可H5 Web Audio API 都是好几年前在慕课网有接触过了,然后照着教程做了个小项目,然后一直都没再捡起来再看过,所以把以前的一个项目捡起来用ES6简单重构下,实现柱状和点状的可视化效果。

项目成品预览


1. 柱状可视化音频效果

ES6+Koa2+Web Audio API 可视化音频应用开发_第1张图片


2. 点状可视化音频效果
ES6+Koa2+Web Audio API 可视化音频应用开发_第2张图片


一、整个开发思路

前端使用ES6 面向对象和Fetch请求实现音频的可视化效果和资源获取,后端采用koa应用生成器生成koa2项目、手动配置路由和ejs模板渲染页面,以及文件模块fs,获取音频文件数据,通过配置的路由,将数据返回到前端页面。

二、后端配置

1. 安装koa应用生成器

npm install koa-generator -g

koa1.2生成一个test项目,切到test目录并下载依赖
koa test -> cd test -> npm install
koa2生成一个test项目,切到test目录并下载依赖
koa2 test -> cd test -> npm install

运行 :

npm run start

访问 http://localhost:3000 就可以看到项目效果

2. 新增路由 /musc

(1) 在app.j中新增如下:

const music = require('./routes/music')
app.use(music.routes(), music.allowedMethods())

(2)在routes目录下新增文件路由文件 routes/music.js,编辑如下:

const router = require('koa-router')()
const fs = require('fs');
const path = require('path');
const bodyparser = require('koa-bodyparser')
router.use(bodyparser())

router.prefix('/music')
const musicPath = path.resolve(path.dirname(__dirname),'./public/music/');
router.get('/', async function (ctx, next) {
  
  let files = await readFiles();
  //console.log(files);
  await ctx.render('index', {
    title: 'H5 Music Audio',
    music: files
  });
  
})

router.get('/file',async function(ctx, next) {

    // 根据name值 获取相关的音乐文件信息
    let filename = ctx.query.name;
    let filepath = musicPath + '\\' + filename;

    // 读取音乐文件信息
    let fileData = await new Promise(function(resolve,reject){
        fs.readFile(filepath,function(err, data){
            resolve(data);
        });
    });
    // console.log(fileData);
    ctx.body = fileData
});

router.get('/bar', function (ctx, next) {
  ctx.body = 'this is a music/bar response'
})


async function readFiles(){
    return fs.readdirSync(musicPath);
}

module.exports = router

三、前端配置

1. 目标页面 views/index.ejs, 编辑如下


<html>
  <head>
    <title><%= title %>title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  head>
  <body>

    <header>
        <h1><%= title %>h1>
      <ul class="type" id="type">
        <li data-type="dot">Dotli>
        <li data-type="column" class="selected">Columnli>
      ul>
      <p>
        Volume <input id="volume" type="range" min="0" max="100" value="30">
      p>
    header>

    <div class="left">
        <ul id="list">
            <% music.forEach(function(name,index){ %>
                <li title="<%= name %>" class="<%= index==0 ? 'selected': '' %>" data-index="<%= index %>"><%= name %>li>
            <% }) %>
        ul>
    div>

    <div class="right" id="box">div>
  <script type="text/javascript" src="/javascripts/visualizer.js">script> 
  <script type="text/javascript" src="/javascripts/music.js">script> 
  body>
html>

2. 配置前端交互脚本 public/javascript/music.js

(1) 初始化音频操作类 AuidoVisualization

class AuidoVisualization{

    constructor(){
        this.ac = null;
        this.gainNode = null;
        this.anayser = null;
        this.bufferSource = null;

        this.init()
    }   

    //初始化音频节点资源
    init(){
        this.ac = new AudioContext();
        this.gainNode = this.ac.createGain();
        this.anayser = this.ac.createAnalyser();
        this.bufferSource = this.ac.createBufferSource();
        this.anayser.fftSize = 128*2;
        this.bufferSource.connect(this.anayser);
        this.anayser.connect(this.gainNode);
        this.gainNode.connect(this.ac.destination);
    }

    // 获取音频资源
    request({url,method,callback}){
        fetch(url,{
            method: method,
            responseType: 'arraybuffer'
        }).then(res => {
            return res.arrayBuffer();
        }).then(data => {
            this.ac.decodeAudioData(data,(buffer) => {
                // 回调函数
                callback(buffer);
            },(err) => {
                console.log(err);
            });
        })
    }

    // 可视化音频数据
    visualize(callback){
        let arr = new Uint8Array(this.anayser.frequencyBinCount);
        let self = this;

        // 将分析到的音频数据存入 arr数组中
        function musicVisible(){
            self.anayser.getByteFrequencyData(arr);
            callback(arr);
            requestAnimationFrame(musicVisible);
        }
        musicVisible();
    }

    // 音频播放
    play(){
        this.bufferSource.start(0);
    }

    // 停止播放
    stop(){
        this.bufferSource.stop(0);
    }

    // 改变音量
    changeVolume(v){
        this.gainNode.gain.value = v;
    }
}

(2) 初始化点状图操作类 Dot

// 点状
class Dot{
    constructor({x,y,r,canvas,ctx,colors}){

        this.x = x;
        this.y = y;
        this.r = r;
        this.dy = Math.random()*1.2

        this.canvas = canvas;
        this.ctx = ctx;
        this.colors = colors;
    }

    draw(){

        this.ctx.beginPath();
        
        this.ctx.arc(this.x,this.y,this.r,0,Math.PI*2,true);
        this.fillRadial();
        //this.ctx.fillStyle = '#fff';
        //this.ctx.fill();
    }

    fillRadial(){

        let radial = this.ctx.createRadialGradient(this.x,this.y,0,this.x,this.y,this.r);
        radial.addColorStop(0, this.colors[0]);
        radial.addColorStop(0.5, this.colors[1]);
        radial.addColorStop(1, this.colors[2]);

        this.ctx.fillStyle = radial;
        this.ctx.fill();
    }

    update(maxRadius){
        this.y -= this.dy;

        if(this.y < - maxRadius){
            this.y = this.canvas.height;
        }
    }

}

(3) 初始化柱状图操作类 Column

// 柱状
class Column{

    constructor({w,h,x,y,canvas,ctx,colors}){
        this.w = w;
        this.h = h;
        this.x = x;
        this.y = y;
        
        this.canvas = canvas;
        this.ctx = ctx;
        this.colors = colors;
    }

    draw(){
        this.fillRects();
        this.ctx.beginPath();
        this.ctx.fillRect(this.x,this.y,this.w,this.h);
    }
    
    fillRects(){
        let line = this.ctx.createLinearGradient(0,0,0,this.canvas.height);
        line.addColorStop(0,this.colors[0]);
        line.addColorStop(0.5,this.colors[1]);
        line.addColorStop(1,this.colors[2]);
        this.ctx.fillStyle = line;
    }

    update(){

    }   
}

(4) 初始化canvas操作类 Canvas

class Canvas{

    constructor(){
        this.box = document.querySelector('#box');
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');
        this.music = new Music();
        this.av = av;
        this.canvasWidth = 0;
        this.canvasHeight = 0;
        this.size = 80;

        // 图形绘制
        this.type = 'column';
        this.dots = [];
        this.dotsPos = [];
        this.columns = [];
        this.caps = [];
        this.wh = {
            cw: 0,
            ch: 0
        };

        this.init();
    }

    init(){
        this.initCanvas();
    }

    // 初始化canvas盒子
    initCanvas(){
       
        this.box.appendChild(this.canvas);
        
        let t = setTimeout(() => {
            this.wh = {
                cw: this.canvas.width,
                ch: this.canvas.height
            };
            clearTimeout(t);
        },500)

        this.resizeCanvas();
        window.addEventListener('resize',() => {
            this.resizeCanvas();
            this.initialChart();
            //console.log(this.canvas.width,this.canvas.height,this.wh);
        })
        
        // 初始化图形
        this.initialChart();

        // 图形化音频数据
        this.av.visualize((arr) => {
            //console.log(arr);
            this.drawRect(arr);
        });

        // 点击切换柱状图和点状图
        let btns = document.querySelector('#type').children;
        let self = this;
        Array.prototype.slice.call(btns).forEach(v => {
            v.onclick = function(){
                for(var i=0;i<btns.length;i++){
                    btns[i].className = '';
                }
                this.className = 'selected';
                self.type = this.dataset.type;
            }
        })
    }


    initialChart(){

        this.columns = [];
        this.caps = [];

        // 初始化柱状
        for(let i=0;i<this.size;i++){
            //let color = [this.getRamdomColor(),this.getRamdomColor(),this.getRamdomColor()];
            let color = ['red','yellow','green'];
            this.columns.push(new Column({
                x: 0,
                y: 0,
                w: 0,
                h: 0,
                ctx: this.ctx,
                canvas: this.canvas,
                colors:color
            }));

            // 初始化柱状帽
            this.caps.push(new Cap({
                x: 0,
                y: 0,
                w: 0,
                h: 0,
                ctx: this.ctx,
                canvas: this.canvas,
                colors:color
            }));
        }
        
        //初始化点状图
        this.initDotPos();
    }

    initDotPos(){
        this.dotsPos = [];
        // 初始化dot图形位置
        for(let i=0;i<this.size;i++){
            this.dotsPos.push(new Dot({
                x: this.canvasWidth*Math.random(),
                y: this.canvasHeight*Math.random(),
                colors: ['rgba(255,255,255,0.8)',this.getRamdomColor(1),'rgba(255,255,255,0)'],
                ctx: this.ctx,
                canvas: this.canvas
            }))
        }    
    }

    resizeCanvas(){
        this.canvasWidth = this.box.clientWidth;
        this.canvasHeight = this.box.clientHeight;
        this.canvas.width = this.canvasWidth;
        this.canvas.height = this.canvasHeight;
    }


    // 绘制柱状图
    drawRect(arr){
        this.ctx.clearRect(0,0,this.canvasWidth,this.canvasHeight);

        let w = this.canvasWidth / this.size;
        let columnWidth = w*0.6;
        let radius = 60;

        for(let i=0;i<this.size;i++){
            if(this.type == 'column'){

                /*this.columns = [];
                this.caps = [];*/
                let h = this.canvasHeight * (arr[i]/256);
                let ch = columnWidth*0.5;

                // 柱状图 属性变化
                this.columns[i].x = w*i;
                this.columns[i].y = this.canvasHeight-h;
                this.columns[i].w = columnWidth;
                this.columns[i].h = h;
                this.columns[i].draw();

                // 柱状帽
                this.caps[i].x = w*i;
                this.caps[i].y = this.canvasHeight - ( ch + this.caps[i].cap ); 
                this.caps[i].w = columnWidth;
                this.caps[i].h = ch;
                this.caps[i].update(h);
                this.caps[i].draw();

            }else{
                
                if((arr[i]/256)*radius == 0) return;
                //计算比例
                let scale = (this.canvasHeight >= this.canvasWidth ? this.wh.cw/this.canvasWidth : this.wh.ch/this.canvasHeight )
                if(scale > 1) scale = 1;
                // 点状
                this.dotsPos[i].r = (arr[i]/256)*radius*scale;
                this.dotsPos[i].draw();
                this.dotsPos[i].update(radius);
            }
        }
    }

    // 绘制点状图
    drawDot(){
        this.dots.forEach(v => {
            v.draw();
        })
    }

    // 获取随机颜色
    getRamdomColor(alphas){
        let alpha = alphas || 1;
        return `rgba(${255*Math.random()},${255*Math.random()},${255*Math.random()},${alpha})`;
    }

    // 获取两个数之间的随机数
    getRamdomNumber(n,m){
        return n + Math.floor((m-n)*Math.random());
    }    
}

3. 第一阶段版本预览 public/javascript/music-2.js

(1)第一阶段柱状音频预览

ES6+Koa2+Web Audio API 可视化音频应用开发_第3张图片

(2)第一阶段点状音频预览

ES6+Koa2+Web Audio API 可视化音频应用开发_第4张图片

四. 项目源码地址

https://github.com/RiversCoder/canvas/tree/master/9_canvas_music_visualization

另附上Web audio API 原理图如下

ES6+Koa2+Web Audio API 可视化音频应用开发_第5张图片

你可能感兴趣的:(node,koa,server,javascript)