详解如何用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实现编写网页版计算器

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

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

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

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

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

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

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

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

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

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

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

  • 详解如何用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/ah

  • 详解如何用JavaScript编写一个单元测试

    目录 为什么要进行单元测试? 范围界定和编写单元测试 保持单元测试简短而简单 考虑正面和负面的测试用例 分解长而复杂的函数 避免网络和数据库连接 如何编写单元测试 创建一个新项目 实现一个类 配置和添加我们的第一个单元测试 添加更多单元测试 修复错误 最后 测试代码是确保代码稳定的第一步.能做到这一点的最佳方法之一就是使用单元测试,确保应用程序中的每个较小的功能都按应有的方式运行——尤其是当应用程序接收到极端或无效输入,甚至可能有害的输入时. 为什么要进行单元测试? 进行单元测试有许多不同的方法

  • 详解如何用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而诞生的,它主要用于以下三个方面: 会话状态管理(如用户登录状态.购物车.游戏分数或其它需要记录的信息) 个性化

随机推荐