详解如何用js实现一个网页版节拍器

目录
  • 引言
  • 1. 需求分析
  • 2. 素材准备
  • 3. 开发实现
    • 3.1 框架选型
    • 3.2 模块设计
    • 3.3 数据结构设计
    • 3.4 播放逻辑
    • 3.5 音频控制
    • 3.6 动效
    • 3.7 大屏展示
    • 3.8 新增人声发音
  • 4. 部署
  • 5. 后续工作
    • 5.1 目前存在的问题
      • ios声音
    • 5.2 TODO
      • 切换不同音效

引言

平时练尤克里里经常用到节拍器,突发奇想自己用js开发一个。

最后实现的效果如下:ahao430.github.io/metronome/

代码见github仓库:github.com/ahao430/met…

1. 需求分析

节拍器主要是可以设定不同的速度和节奏打拍子。看各种节拍器,有简单的,也有复杂的。

  • 设定不同的速度,每分钟多少拍
  • 选择节拍,比如4/4拍、3/4拍、6/8拍等等。
  • 选择节拍的节奏型,一拍一个,一拍两个,一拍三个(三连音),一拍四个,swing,等等。这个很多简易节拍器就没有了。
  • 切换不同的音色,比如敲击声、鼓声、人声等等。

这里拍速是指一分钟有多少拍。

而节拍是以几分音符为1拍,每小节几拍。这个不影响拍速,观察各种节拍器,这里会展示几个小点,每一拍一个点,其中第一拍第一下重声,后面的轻声。

节奏型决定每一拍响几下,以及这几下之前的节奏。比如这一拍响一下、响两下、响三下、响四下;如果是一个swing就是前8后16分音符的时长;也可能这个节奏型的时长是两拍,比如民谣扫弦的下----,下空下上。

2. 素材准备

这里没有UI,就简单的写下样式,没有做什么图。去找了个节拍器的图标做favicon,找了几个不同节奏型的图片(截图->裁剪o(╥﹏╥)o),最后音频素材扒到一个强一个弱的敲击声。

准备开工。

3. 开发实现

3.1 框架选型

这里选了 vue3,没啥特别的原因,就是平常经常用vue2和react,vue3没怎么用过,练练手。试试vue3+vite的开发体验。直接用官方脚手架开搞。

配置rem,引入amfe-flexible和ostcss-px2rem-exclude。

ui组件引入nutui。

3.2 模块设计

<script setup lang="ts">
  import Speed from "./components/Speed.vue";
  import Rhythm from "./components/Rhythm.vue";
  import Beat from "./components/Beat.vue";
  import Play from "./components/Play.vue";
</script>
<template>
  <p class="title">节拍器</p>
  <main>
    <Speed></Speed>
    <div class="flex">
      <Beat></Beat>
      <Rhythm></Rhythm>
    </div>
    <Play></Play>
  </main>
</template>

将页面按照功能模块划分了几个组件,上面是调节拍速,中间是选择节拍和节奏型,最下面是播放。

由于播放组件要用到其他组件的设置,引入pinia状态管理,数据都存放到store。由于播放组件要获取其他组件的数据,就每个组件都建了一个store,数据和计算逻辑都放到里面了。

这里写组件时遇到vue3的第一个坑,数据解构失去响应性了,后面使用store的数据,直接用store.xxx。

3.3 数据结构设计

拍速、节拍、节奏型组件都很简单,下拉选择就行了。重点需要设计一下数据结构。

节拍我是用一个数组来存储,如[3,4],看数组第一项知道这一小节节拍的数量。

节奏型考虑到有的节奏型不止一拍,用了一个二维数组来表示,每一项是一拍,然后这一拍由1和0的数组来表示,如民谣扫弦的↓ ↓ ↓↑,读作下空空空下空下上,写成[[1,0,0,0],[1,0,1,1]]。

export const MIN_SPEED = 40
export const MAX_SPEED = 400
export const DEF_SPEED = 120
export const DEF_BEAT = [4,4]
export const BEAT_OPTIONS = [
  [1,4],
  [2,4],
  [3,4],
  [4,4],
  [3,8],
  [6,8],
  [7,8],
]
export const DEF_RHYTHM = 1
export const RHYTHM_OPTIONS = [
  { id: 1, name: '♪', value: [[1]], img: './img/1.jpg', rate: 30},
  { id: 2, name: '♪♪', value: [[1,1]], img: './img/2.jpg', rate: 15},
  { id: 3, name: '三连音', value: [[1, 1, 1]], img: './img/3.jpg', rate: 10},
  { id: 4, name: '♪♪♪♪', value: [[1,1,1,1]], img: './img/4.jpg', rate: 10},
  { id: 5, name: 'swing', value: [[1, 0, 1]], img: './img/5.jpg', rate: 10},
  { id: 6, name: '民谣扫弦', value: [[1, 0, 0,0], [1,0,1,1]], img: './img/6.png', rate: 10},
  { id: 7, name: '民谣扫弦2', value: [[1, 0, 1, 1], [0,1,1,1]], img: './img/7.png', rate: 10},
]

3.4 播放逻辑

播放组件这里比较复杂。当点击播放按钮时,要开始打节拍。这是先播放一次重声,然后根据拍速、节拍和节奏型计算下一次声音的间隔,后续都按照这个间隔播放轻声,直到小节结束。

// 点击播放,重置节拍和节奏型计数,状态置为true,执行播放小节函数
function play() {
  beatCount.value = 0
  rhythmCount.value = 0
  isPlaying.value = true
  playBeat()
}
// 播放整个小节,节拍计数重置为0,允许播放重声,播放节奏型
function playBeat () {
  if (!isPlaying.value) return false
  beat = useBeatStore().beat
  console.log('播放节拍:', beat)
  beatCount.value = 0
  heavy = true
  playRhythm()
}
// 播放整个节奏型(可能多拍), 节奏型音符计数重置
  function playRhythm () {
    if (!isPlaying.value) return false
    rhythm = useRhythmStore().rhythm.value
    rhythmRate = useRhythmStore().rhythm.rate
    console.log('播放节奏型:', rhythm)
    rhythmNotesLen = 0
    rhythmCount.value = 0
    rhythm.forEach(item => {
      rhythmNotesLen += item.length
    })
    playNote()
  }

播放期间,可能在不暂停播放的情况下,修改拍速、节拍和节奏型的值。因此在播放音符时,动态计算拍速,再根据节奏型的音符数量,去计算到下个音符的timeout时间。下个音符如果是1就播放,如果是0就不播放,然后继续定时器。注意一个节奏型或者一个小节完成去重置计数。这里就不看单拍完成情况了。

  // 播放单个音符位置,可能是空拍
  function playNote () {
    // 一个节奏型可能有多拍
    speed = useSpeedStore().speed
    // 调整播放倍速
      player.playbackRate = Math.max(1, Math.min(10, speed / rhythmRate))
      player2.playbackRate = player.playbackRate
    const rhythmItemIndex = beatCount.value % rhythm.length
    // 播放音频
    const rhythmItem = rhythm[rhythmItemIndex]
    const note = rhythmItem[rhythmCount.value]
    console.log('播放音频:',
      note ?
        (heavy ? '重' : '轻')
      : '空'
    )
    if (note) {
      // 播放
      if (heavy) {
        player.currentTime = 0;
        player.play()
        heavy = false
      } else {
        player2.currentTime = 0;
        player2.play()
      }
    }
    // 计算间隔时间
    const oneBeatTime = ONE_MINUTE / speed
    const rhythmNoteTime = oneBeatTime / rhythmItem.length
    // 定时器,播放下一个音符
    timer = setTimeout(() => {
      let newRhythmCount = rhythmCount.value + 1
      if (newRhythmCount >= rhythmItem.length) {
        if (newRhythmCount >= rhythmNotesLen) {
          // 新的节奏型
          newRhythmCount = 0
          rhythmCount.value = newRhythmCount
        } else {
          // 当前节奏型新的一拍
          rhythmCount.value = newRhythmCount
        }
        let newBeatCount = beatCount.value + 1
        if (newBeatCount >= beat[0]) {
          newBeatCount = 0
          // 新的节拍
          beatCount.value = newBeatCount
          playBeat()
        } else {
          beatCount.value = newBeatCount
          playRhythm()
        }
      } else {
        rhythmCount.value = newRhythmCount
        playNote()
      }
    }, rhythmNoteTime)
    // 呼吸样式
    if (note) {
      const styleTime = rhythmNoteTime * 0.8
      rhythmCircleStyle.value = `transform: scale(1.5); transition: all linear ${styleTime / 1000}s; opacity: 0.5;`
      timer2 = setTimeout(() => {
        rhythmCircleStyle.value = 'transform: scale(0); transition: none; opacity: 0;'
      }, styleTime)
    }
  }

3.5 音频控制

音频的播放,用到了Audio对象。

  const player = new Audio('./audio/beat1.mp3')
  const player2 = new Audio('./audio/beat2.mp3')
// player.play()
// player.pause()

我们找的音频播放速度和时长是固定的,但是当拍速调快,或者一拍的节奏型有多个音符,前一次播放还没结束,后一次播放就开始了,听起来无法区分。这时我们可以调整播放速度,根据前面的音符间的间隔时间来调整倍率,修改player的playbackRate值。

不过实际发现浏览器的倍数有上限和下限,超出范围会报错。而且计算的也不是特别的准,前面音符数量我们用[1]表示一拍一下,其实不是很准,应该是[1,0,0,0,...],但是几个0也得看节拍。干脆直接在那几个节奏型的选项加了个rate字段,凭感觉调节了。

// 调整播放倍速
player.playbackRate = Math.max(1, Math.min(10, speed / rhythmRate))
player2.playbackRate = player.playbackRate

在每次播放音符重新取值,是可以做到切换后在下一个音符修正的,但是如果前面速度选的过慢,到下一次播放要等很久。改为三个选项切换任意值时,停止播放,再启动。

watch([
  () => beatStore.beat,
  () => rhythmStore.rhythm,
  () => speedStore.speed
], () => {
  console.log('restart')
  restart()
})

3.6 动效

在播放的时候,按照节拍数量做了n个小圆点,第几拍就亮哪一个。

然后做了一个呼吸动效,每个音符播放时,都有一个圆环从播放按钮下方向外扩散开来。

    // 呼吸样式
    if (note) {
      const styleTime = rhythmNoteTime * 0.8
      rhythmCircleStyle.value = `transform: scale(1.5); transition: all linear ${styleTime / 1000}s; opacity: 0.5;`
      timer2 = setTimeout(() => {
        rhythmCircleStyle.value = 'transform: scale(0); transition: none; opacity: 0;'
      }, styleTime)
    }

3.7 大屏展示

amfe-flexible会始终按照屏幕宽度计算rem。实际上我们只做了移动端样式,大屏的时候最好居中固定宽度展示,所以自己写一个rem.js,设置最大宽度,超过最大宽度时,只按照最大宽度计算rem,同时给body添加maxWidth属性。

3.8 新增人声发音

增加一个组件,支持下拉选择声音类型,暂时有人声和敲击声。选择人声时,改为播报1234,,2234...。

import Speech from 'speak-tts'
const speech = new Speech()
speech.init({
  volume: 1,
  rate: 1,
  pitch: 1,
  lang: 'zh-CN',
})
  function playVoice () {
    const voice = useVoiceStore().voice
    console.log('voice: ', voice)
    if (voice === 'human') {
      const text = rhythmCount.value === 0 ? (beatCount.value + 1) : (rhythmCount.value + 1)
      speech.speak({
        text: '' + text,
        queue: false
      })
      if (heavy) {
        heavy = false
        speech.setPitch(0.5)
      }
    } else {
      if (heavy) {
        player.currentTime = 0;
        player.play()
        heavy = false
        speech.setPitch(0.5)
      } else {
        player2.currentTime = 0;
        player2.play()
      }
    }
  }

4. 部署

用github pages部署项目打包文件。这里找了一个别人提供的配置文件,实现push分支后利用github actions自动部署。

在项目根目录新建.github/workflows目录,然后新建一个任意名称,.yml后缀的文件,填入下面配置推送即可。其中branches指定了main,看实际情况可以改成master。推送后action会自动打包main分支代码,将dist目录放到gh-pages分支根目录,并将settings/pages自动设置为gh-pages分支根目录展示。

name: CI
on:
  push:
    branches:
    - main
jobs:
  job:
    name: Deployment
    runs-on: macos-latest
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      # setup node
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 16.16.0
      # setup pnpm
      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        id: pnpm-install
        with:
          version: 7
          run_install: false
      # cache
      - name: Get pnpm store directory
        id: pnpm-cache
        shell: bash
        run: |
          echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
      - name: Setup pnpm cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-
      # cache fail and install dependencies
      - name: Install dependencies
        if: steps.pnpm-cache.outputs.cache-hit != 'true'
        run: |
          pnpm install
      - name: Build
        run: pnpm run build
      - name: upload production artifacts
        uses: actions/upload-pages-artifact@v1
        with:
          path: dist
      # deploy
      - name: Deploy Page To Release
        id: deployment
        uses: actions/deploy-pages@v1

5. 后续工作

5.1 目前存在的问题

ios声音

目前最大的问题是IOS没有声音,这个目前没啥好办法,因为ios的权限问题,只有手动点击才能播放,所以只播放了一下,就不再播放了,定时器后面的播放没法触发。

目测要解决这个问题,只有换平台了,利用小程序或者app的native api去实现。

5.2 TODO

切换不同音效

这个功能好实现,就是素材不好找。不过有些节拍器支持人声,如果播放1234,,2234, 需要在播放时加些逻辑。人声貌似用api可以实现。

以上就是详解如何用js实现一个网页版节拍器的详细内容,更多关于js实现网页版节拍器的资料请关注我们其它相关文章!

(0)

相关推荐

  • JavaScript实现简单网页版计算器

    背景 由于我又被分进了一个新的项目组,该项目需要用js,因为我没接触过,所以领导准备给我一周时间学习,没错,实现一个简单的支持四则混合运算的计算器就是作业,所以有了这篇文章 故,这篇文章主要重点就不在html和css了,毕竟我也只是略懂皮毛,并未深究过 实现效果 最终展现的页面如下图,当鼠标点击按键时,按键会变色,可以进行四则混合运算 上面一行显示计算式,当按下"="时,显示计算结果 用到的技术 计算器的页面是使用html的table绘制的 按键的大小,颜色,鼠标悬浮变色是用css设置

  • 基于JS制作一个简单的网页版地图

    目录 前言 一.申请地图的AK密钥 二.主要代码分析 三.全部代码 四.结果展示 前言 以前做了一个安卓版的地图应用,现在突然想做一个简单的网页版地图.这个简单的网页版地图能根据城市名进行位置查询(有个城市列表的小控件,支持城市列表选择),还能根据经纬度进行位置查询.当你进行城市搜索时,或者经纬度查询城市时,该小控件也能自由地切换到目标城市. 一.申请地图的AK密钥 1.首先找到一个地图开放平台,这里以百度地图开放平台为例,步骤如下:进入百度地图开放平台,拉到最底下,进行登录注册,然后进入应用管

  • JavaScript实现前端网页版倒计时

    使用原生JavaScript简单实现倒计时,供大家参考,具体内容如下 效果 代码 // An highlighted block <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <!-- css样式 --> <style type="text/css"> * { margin: 0;

  • js实现网页版贪吃蛇游戏

    使用原生 js 实现贪吃蛇小游戏,首先这个 小游戏的目录结构如下: 有 贪吃蛇 , 食物 ,地图 ,还有 游戏 当我们在浏览器打开 index.html 的时候,会出现 移动的小蛇 ,随机生成的食物(这里只有一个,当前食物被吃掉,才会初始化下一个),用户通过键盘上的方向键控制小蛇移动的方向 当小蛇触碰到了墙,即画布边缘的时候,游戏结束! 接下来就是代码实现啦 ~ 食物模块 //食物的自调用函数 (function(){ //创建一个数组 来存放元素 var elements=[]; //食物就是

  • javascript实现编写网页版计算器

    本篇主要纪录的是利用javscript实现一个网页计算器的效果,供大家参考,具体内容如下 话不多说,代码如下: 首先是html的代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>利用js实现网页版计算器</title> <link rel="stylesheet" href

  • JavaScript实现网页版五子棋游戏

    本文实例为大家分享了JavaScript实现网页版五子棋游戏的具体代码,供大家参考,具体内容如下 学习js的第三天,跟着老师完成的五子棋小游戏,记录学习成果欢迎大佬们一起分享经验,批评指正. 本程序主要通过三部分实现: 1.棋盘绘制 2.鼠标交互 3.输赢判断 <!DOCTYPE html> <html> <head> <title> canvastest </title> </head> <body> <h1>

  • 详解如何用webpack打包一个网站应用项目

    本文介绍了如何用webpack打包一个网站应用,现在分享给大家,有需要的可以了解一下 随着前端技术的发展,越来越多新名词出现在我们眼前.angularjs.react.gulp.webpack.es6.babel--新技术出现,让我们了解了解用起来吧!今天我来介绍一下如何用webpack打包一个网页应用. 一般我们写页面,大概都是这样的结构: index.html css style.css js index.js ........... 这样我们的html里直接引用css和js,完成一个网页应

  • 详解用Node.js写一个简单的命令行工具

    本文介绍了用Node.js写一个简单的命令行工具,分享给大家,具体如下: 操作系统需要为Linux 1. 目标 在命令行输入自己写的命令,完成目标任务 命令行要求全局有效 命令行要求可以删除 命令行作用,生成一个文件,显示当前的日期 2. 代码部分 新建一个文件,命名为sherryFile 文件sherryFile的内容 介绍: 生成一个文件,文件内容为当前日期和创建者 #! /usr/bin/env node console.log('command start'); const fs = r

  • 详解如何用VUE写一个多用模态框组件模版

    对于新手们来说,如何写一个可以多用的组件,还是有点难度的,组件如何重用,如何传值这些在实际使用中,是多少会存在一些障碍的,所以今天特意写一个最常用的模态框组件提供给大家,希望能帮助到您! 懒癌患者直接复制粘贴即可 Modal.vue组件 <template> <!-- 过渡动画 --> <transition name="modal-fade"> <!-- 关闭模态框事件 和 控制模态框是否显示 --> <div class=&qu

  • 详解如何用python实现一个简单下载器的服务端和客户端

    话不多说,先看代码: 客户端: import socket def main(): #creat: download_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #link: serv_ip=input("please input server IP") serv_port=int(input(("please input server port"))) serv_addr=(serv_ip,ser

  • 一文详解如何用原型链的方式实现JS继承

    目录 原型链是什么 通过构造函数创建实例对象 用原型链的方式实现继承 方法1:Object.create 方法2:直接修改 [[prototype]] 方法3:使用父类的实例 总结 今天讲一道经典的原型链面试题. 原型链是什么 JavaScript 中,每当创建一个对象,都会给这个对象提供一个内置对象 [[Prototype]] .这个对象就是原型对象,[[Prototype]] 的层层嵌套就形成了原型链. 当我们访问一个对象的属性时,如果自身没有,就会通过原型链向上追溯,找到第一个存在该属性原

  • 详解如何用alpine镜像做一个最小的镜像并运行c++程序

    需求 工作中我们如果要制作镜像,一般都是直接pull官方镜像,比如我们要运行一个c++程序我们可能直接pull一个gcc,或者ubuntu镜像就可以了,但是存在一个问题,我们只是要运行一个c++程序却要运行一个ubuntu系统,这是非常消耗资源的,所以就去网上搜了搜发现早期的docker都是使用alpine镜像来做基础镜像,所以就用alpile镜像来制作镜像 dockerfile FROM alpine:3.7 MAINTAINER Rethink #更新Alpine的软件源为国内(清华大学)的

  • 详解阿里Node.js技术文档之process模块学习指南

    模块概览 process是node的全局模块,作用比较直观.可以通过它来获得node进程相关的信息,比如运行node程序时的命令行参数.或者设置进程相关信息,比如设置环境变量. 环境变量:process.env 使用频率很高,node服务运行时,时常会判断当前服务运行的环境,如下所示 if(process.env.NODE_ENV === 'production'){ console.log('生产环境'); }else{ console.log('非生产环境'); } 运行命令 NODE_EN

  • 详解如何用Python登录豆瓣并爬取影评

    目录 一.需求背景 二.功能描述 三.技术方案 四.登录豆瓣 1.分析豆瓣登录接口 2.代码实现登录豆瓣 3.保存会话状态 4.这个Session对象是我们常说的session吗? 五.爬取影评 1.分析豆瓣影评接口 2.爬取一条影评数据 3.影评内容提取 4.批量爬取 六.分析影评 1.使用结巴分词 七.总结 上一篇我们讲过Cookie相关的知识,了解到Cookie是为了交互式web而诞生的,它主要用于以下三个方面: 会话状态管理(如用户登录状态.购物车.游戏分数或其它需要记录的信息) 个性化

  • 详解在Vue.js编写更好的v-for循环的6种技巧

    在VueJS中,v-for循环是每个项目都会使用的东西,它允许您在模板代码中编写for循环. 在最基本的用法中,它们的用法如下. <ul> <li v-for='product in products'> {{ product.name }} </li> </ul> 但是,在本文中,我将介绍六种方法来使你的 v-for 代码更加精确,可预测和强大. 让我们开始吧. 1.始终在v-for循环中使用key 首先,我们将讨论大多数Vue开发人员已经知道的常见最佳做

  • 详解如何用SpringBoot 2.3.0.M1创建Docker映像

    1.发布 SpringBoot2.3.0.M1刚刚发布,它带来了一些有趣的新特性,可以帮助您将SpringBoot应用程序打包到Docker映像中.在这篇博客文章中,我们将查看创建Docker映像的典型方式,并展示如何通过使用这些新特性来改进这些镜像 2.说明 SpringBoot 2.3.0.M1 暂时不支持Windows, 很鸡肋 暂时在Mac 和Linux 上运行良好 3.常见的Docker 运行方式 一般情况下,通过docker 运行springboot 是这样的 FROM openjd

随机推荐

其他