<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
平時練尤克里裡經常用到節拍器,突發奇想自己用js開發一個。
最後實現的效果如下:ahao430.github.io/metronome/。
程式碼見github倉庫:github.com/ahao430/met…。
節拍器主要是可以設定不同的速度和節奏打拍子。看各種節拍器,有簡單的,也有複雜的。
這裡拍速是指一分鐘有多少拍。
而節拍是以幾分音符為1拍,每小節幾拍。這個不影響拍速,觀察各種節拍器,這裡會展示幾個小點,每一拍一個點,其中第一拍第一下重聲,後面的輕聲。
節奏型決定每一拍響幾下,以及這幾下之前的節奏。比如這一拍響一下、響兩下、響三下、響四下;如果是一個swing就是前8後16分音符的時長;也可能這個節奏型的時長是兩拍,比如民謠掃弦的下----,下空下上。
這裡沒有UI,就簡單的寫下樣式,沒有做什麼圖。去找了個節拍器的圖示做favicon,找了幾個不同節奏型的圖片(截圖->裁剪o(╥﹏╥)o),最後音訊素材扒到一個強一個弱的敲擊聲。
準備開工。
這裡選了 vue3,沒啥特別的原因,就是平常經常用vue2和react,vue3沒怎麼用過,練練手。試試vue3+vite的開發體驗。直接用官方腳手架開搞。
設定rem,引入amfe-flexible和ostcss-px2rem-exclude。
ui元件引入nutui。
<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,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}, ]
播放元件這裡比較複雜。當點選播放按鈕時,要開始打節拍。這是先播放一次重聲,然後根據拍速、節拍和節奏型計算下一次聲音的間隔,後續都按照這個間隔播放輕聲,直到小節結束。
// 點選播放,重置節拍和節奏型計數,狀態置為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) } }
音訊的播放,用到了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() })
在播放的時候,按照節拍數量做了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) }
amfe-flexible會始終按照螢幕寬度計算rem。實際上我們只做了行動端樣式,大屏的時候最好居中固定寬度展示,所以自己寫一個rem.js,設定最大寬度,超過最大寬度時,只按照最大寬度計算rem,同時給body新增maxWidth屬性。
增加一個元件,支援下拉選擇聲音型別,暫時有人聲和敲擊聲。選擇人聲時,改為播報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() } } }
用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
目前最大的問題是IOS沒有聲音,這個目前沒啥好辦法,因為ios的許可權問題,只有手動點選才能播放,所以只播放了一下,就不再播放了,定時器後面的播放沒法觸發。
目測要解決這個問題,只有換平臺了,利用小程式或者app的native api去實現。
這個功能好實現,就是素材不好找。不過有些節拍器支援人聲,如果播放1234,,2234, 需要在播放時加些邏輯。人聲貌似用api可以實現。
以上就是詳解如何用js實現一個網頁版節拍器的詳細內容,更多關於js實現網頁版節拍器的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45