前置文章《 简易网页钢琴(一)》 移步 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.getItem
给resources
重新赋值 - 这里的
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(时间差越小,播放对应的曲目的时间越准确
),如果当前计时time
和stamps[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
网友评论