web页面能发出声音的方法有两种,一种是
autio
、video
这些标签,另一种就是音频上下文AudioContext
。接下来咱们看一下如何使用AudioContext
,写简易钢琴和曲子。而后赶在七夕以前,给心爱的人做一首曲子吧javascript
Mdn上面有具体介绍,咱们这里只用下面几个css
// 建立音频上下文
const audioCtx = new AudioContext();
// 建立音调控制对象
const oscillator = audioCtx.createOscillator();
// 建立音量控制对象
const gainNode = audioCtx.createGain();
// 音调音量关联
oscillator.connect(gainNode);
// 音量和设备关联
gainNode.connect(audioCtx.destination);
// 音调类型指定为正弦波。sin好听一些
oscillator.type = "sine";
// 设置音调频率(做曲的关键)
oscillator.frequency.value = 400;
// 先把当前音量设为0
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
// 0.01秒时间内音量从0到1线性变化,忽然变化的话很生硬
gainNode.gain.linearRampToValueAtTime(
1,
audioCtx.currentTime + 0.01
);
// 声音开始
oscillator.start(audioCtx.currentTime);
复制代码
不,这还没完,直接copy这段代码放你js文件里面是没用的前端
这里还须要一步:地址栏中输入chrome://flags/#autoplay-policy,把autoplay-policy改为图中所示java
ok,如今代码能够发出声音了,可是不会停下来,咱们须要把音频停下来:程序员
oscillator.stop(audioCtx.currentTime + 1);
复制代码
如今咱们知道怎么发出声音了,接下来是如何发出想要的声音,便是如何知道哆来咪发唆这些音所对应的频率是多少。web
搜索关键词:简谱频率、曲谱频率。很快,就能够找到映射表格chrome
咱们要作的就是把oscillator.frequency.value = 400;
这里的数字改为频率便可发出对应的声音。不妨先试一下数组
上面那个表抄了一份,具体以下,表明着低中高的哆来咪发唆啦希数据结构
[[261.63, 293.67, 329.63, 349.23, 391.99, 440, 493.88], [523.25, 587.33, 659.26, 698.46, 783.99, 880, 987.77], [1046.5, 1174.66, 1318.51, 1396.92, 1567.98, 1760, 1975.52]]
复制代码
只要把它和点击事件联系起来,就能够作到一个小钢琴了。先使用js渲染出每个按键app
// 简谱映射
const VOICE_MAP = {
0: [261.63, 293.67, 329.63, 349.23, 391.99, 440, 493.88],
1: [523.25, 587.33, 659.26, 698.46, 783.99, 880, 987.77],
2: [1046.5, 1174.66, 1318.51, 1396.92, 1567.98, 1760, 1975.52]
};
function renderBtns(level) {
let i = 0;
let res = "";
while (i < 7) {
res += `<span class="btn level${level}" data-index=${i}>${i + 1}</span>`; // 用data-属性辅助
i++;
}
const container = document.createElement("section");
container.className = `container${level}`;
// ------------------------
// 等下这里会加一些事件绑定
// ------------------------
container.innerHTML += res;
document.body.appendChild(container);
}
// 渲染节点
renderBtns(0);
renderBtns(1);
renderBtns(2);
复制代码
.btn {
cursor: pointer;
display: inline-block;
width: 100px;
height: 30px;
line-height: 30px;
user-select: none;
text-align: center;
border: 1px #a12d21 solid;
margin: 2px;
}
.level0::after {
content: ".";
position: relative;
top: 4px;
left: -7px;
}
.level2::before {
content: ".";
position: relative;
top: -16px;
left: 7px;
}
复制代码
最终效果以下
绑定事件
renderBtns方法加上事件绑定,移动端只需手动换成touch系列事件。
// 音频开始
function handleStart({ target }, level) {
const {
dataset: { index }
} = target;
if (index !== undefined) {
console.log(index, "start");
playAudio.call(target, index, level); // 后面加上playAudio的实现
}
}
// 中止音频
function handleStop({ target }) {
const {
dataset: { index }
} = target;
if (index !== undefined) {
console.log(index, "stop");
stopAudio.call(target); // 后面加上stopAudio的实现
}
}
function renderBtns(level) {
let i = 0;
let res = "";
while (i < 7) {
res += `<span class="btn level${level}" data-index=${i}>${i + 1}</span>`;
i++;
}
const container = document.createElement("section");
container.className = `container${level}`;
// 传入e和level,level指的是低中高音
const particalStart = e => handleStart(e, level);
container.addEventListener("mousedown", e => {
particalStart(e);
container.addEventListener("mouseout", handleStop);
});
container.addEventListener("mouseup", handleStop);
container.innerHTML += res;
document.body.appendChild(container);
}
复制代码
为何使用call?后面stopAudio
、playAudio
用了this,这样子就能够作到dom节点与事件和音频一对一绑定了
// 音频上下文
const audioCtx = new AudioContext();
function playAudio(index, level) {
// 若是以前正在播,那就清掉以前的音频
this.gainNode &&
this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
this.oscillator && this.oscillator.stop(audioCtx.currentTime + 1);
// 建立音调控制对象
this.oscillator = audioCtx.createOscillator();
// 建立音量控制对象
this.gainNode = audioCtx.createGain();
// 音调音量关联
this.oscillator.connect(this.gainNode);
// 音量和设备关联
this.gainNode.connect(audioCtx.destination);
// 音调类型指定为正弦波。sin好听一些
this.oscillator.type = "sine";
// 设置音调频率
this.oscillator.frequency.value = VOICE_MAP[level][index]; // 读取相应的简谱频率
// 先把当前音量设为0
this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
// 0.01秒时间内音量从刚刚的0变成1,线性变化
this.gainNode.gain.linearRampToValueAtTime(
1,
audioCtx.currentTime + 0.01
);
// 声音开始
this.oscillator.start(audioCtx.currentTime);
}
function stopAudio() {
this.gainNode &&
this.gainNode.gain.exponentialRampToValueAtTime(
0.001,
audioCtx.currentTime + 0.8
);
// 0.8秒内中止声音
this.oscillator && this.oscillator.stop(audioCtx.currentTime + 0.8);
this.oscillator = this.gainNode = null;
}
复制代码
const VOICE_MAP = {
0: [261.63, 293.67, 329.63, 349.23, 391.99, 440, 493.88],
1: [523.25, 587.33, 659.26, 698.46, 783.99, 880, 987.77],
2: [1046.5, 1174.66, 1318.51, 1396.92, 1567.98, 1760, 1975.52]
};
function handleStart({ target }, level) {
const {
dataset: { index }
} = target;
if (index !== undefined) {
console.log(index, "start");
playAudio.call(target, index, level);
}
}
function handleStop({ target }) {
const {
dataset: { index }
} = target;
if (index !== undefined) {
console.log(index, "stop");
stopAudio.call(target);
}
}
function renderBtns(level) {
let i = 0;
let res = "";
while (i < 7) {
res += `<span class="btn level${level}" data-index=${i}>${i + 1}</span>`;
i++;
}
const container = document.createElement("section");
container.className = `container${level}`;
const particalStart = e => handleStart(e, level);
container.addEventListener("mousedown", e => {
particalStart(e);
container.addEventListener("mouseout", handleStop);
});
container.addEventListener("mouseup", handleStop);
container.innerHTML += res;
document.body.appendChild(container);
}
renderBtns(0);
renderBtns(1);
renderBtns(2);
// 音频上下文
const audioCtx = new AudioContext();
function playAudio(index, level) {
// 若是以前正在播,那就清掉以前的音频
this.gainNode &&
this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
this.oscillator && this.oscillator.stop(audioCtx.currentTime + 1);
// 建立音调控制对象
this.oscillator = audioCtx.createOscillator();
// 建立音量控制对象
this.gainNode = audioCtx.createGain();
// 音调音量关联
this.oscillator.connect(this.gainNode);
// 音量和设备关联
this.gainNode.connect(audioCtx.destination);
// 音调类型指定为正弦波。sin好听一些
this.oscillator.type = "sine";
// 设置音调频率
this.oscillator.frequency.value = VOICE_MAP[level][index];
// 先把当前音量设为0
this.gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
// 0.01秒时间内音量从刚刚的0变成1,线性变化
this.gainNode.gain.linearRampToValueAtTime(
1,
audioCtx.currentTime + 0.01
);
// 声音开始
this.oscillator.start(audioCtx.currentTime);
}
function stopAudio() {
// 0.8秒后中止声音
this.gainNode &&
this.gainNode.gain.exponentialRampToValueAtTime(
0.001,
audioCtx.currentTime + 0.8
);
this.oscillator && this.oscillator.stop(audioCtx.currentTime + 0.8);
this.oscillator = this.gainNode = null;
}
复制代码
如今,就是一个小钢琴了,能够随便弹奏本身的歌曲。那么,问题来了,想弹一首歌,该怎么按键?搜索:xxx简谱,对着弹便可
固然,对于程序员确定想办法搞自动的。咱们已经知道怎么输出想要的音频了,接下来就是如何将真正的歌曲以js的数据结构保存,并使用AudioContext API
输出的事情了。个人实现是直接复用上面的事件绑定代码,使用脚本触发原生事件。固然还有不少其余方法实现。
// 先来一个sleep,确定须要使用延迟的
function sleep(delay = 80) {
return new Promise(r =>
setTimeout(() => {
r();
}, delay)
);
}
/** * @params arr 歌谱数组 * @example * { level: 0, index: 0 } 低音的哆 * { stop: true } 下一个循环什么都没有 * { delay: true } 下一个循环什么都不作 */
async function diyPlay(arr) {
let cursor = 0;
const a = [...arr];
const containers = document.querySelectorAll("section");
let ele;
// 一个个遍历歌曲数组
while (arr.length) {
// 先延迟一下,就能够避免上一个音戛然而止了
await sleep(300);
const current = a.shift();
// 留一个delay接口,便是延长一下上一个音
if (current && current.delay) {
continue;
}
// 下一个按键,停下以前的音
if (ele) {
// 手动用js触发原生事件中止音频
const evPre = document.createEvent("MouseEvents");
evPre.initMouseEvent("mouseout", true, true, window);
ele.dispatchEvent(evPre);
}
if (!arr.length || !current) {
return;
}
//
if (current.stop) {
continue;
}
await sleep(50); // 加一点延迟使得多个连续相同的音天然一些
const ev = document.createEvent("MouseEvents");
ele = containers[current.level].children[current.index - 1];
// 手动用js触发原生事件开始音频
if (ele) {
ev.initMouseEvent("mousedown", true, true, window);
ele.dispatchEvent(ev);
}
}
}
复制代码
如何将简谱转化为可用数据结构
好比下面简谱:
[
{ level: 1, index: 3 },
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ level: 1, index: 5 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 1, index: 7 },
{ delay: true }, // 7后面延迟一下
{ level: 1, index: 7 },
{ level: 1, index: 6 },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ delay: true }, // 6和6是连的,delay一下
{ stop: true }, // 一句唱完了,停一下
]
复制代码
按照规律,咱们随便搜一首歌抄一下就能够用代码输出了
因而,先上一首抖音歌曲吧:
// 《地铁等待》
diyPlay([{"level":2,"index":3},{"level":2,"index":4},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":3},{"level":2,"index":2},{"level":2,"index":2},{"level":2,"index":5},{"delay":true},{"stop":true},{"stop":true},{"level":2,"index":1},{"level":2,"index":1},{"level":1,"index":7},{"level":2,"index":3},{"level":2,"index":3},{"level":2,"index":1},{"level":1,"index":7},{"delay":true},{"level":2,"index":3},{"delay":true},{"stop":true},{"stop":true},{"level":1,"index":6},{"level":2,"index":1},{"level":2,"index":1},{"level":1,"index":6},{"level":1,"index":5},{"level":1,"index":5},{"level":2,"index":1},{"delay":true},{"stop":true},{"stop":true},{"level":2,"index":4},{"level":2,"index":3},{"level":2,"index":1},{"level":2,"index":2},{"level":2,"index":3},{"delay":true},{"level":2,"index":2},{"stop":true},{"stop":true},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":3},{"level":2,"index":2},{"delay":true},{"level":2,"index":5},{"level":2,"index":5},{"level":2,"index":2},{"stop":true},{"stop":true},{"level":2,"index":1},{"level":2,"index":3},{"level":2,"index":3},{"level":2,"index":1},{"level":1,"index":7},{"delay":true},{"level":2,"index":3},{"stop":true},{"stop":true},{"level":1,"index":6},{"level":2,"index":1},{"level":2,"index":1},{"level":2,"index":6},{"level":1,"index":5},{"delay":true},{"level":1,"index":6},{"level":2,"index":1},{"level":2,"index":2},{"level":2,"index":3},{"stop":true},{"stop":true},{"level":2,"index":4},{"level":2,"index":3},{"level":2,"index":1},{"level":2,"index":2},{"delay":true},{"delay":true},{"stop":true}]);
复制代码
// 抄得匆匆忙忙,后面有一些不许确的
diyPlay([
{ level: 1, index: 3 }, // 我听见雨落在青青草地
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ level: 1, index: 5 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 7 },
{ level: 1, index: 6 },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ delay: true },
{ stop: true },
{ level: 1, index: 6 }, // 我听见远方下课钟声响起
{ level: 1, index: 6 },
{ level: 1, index: 7 },
{ level: 1, index: 7 },
{ level: 2, index: 3 },
{ level: 2, index: 3 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 5 },
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ delay: true },
{ level: 1, index: 3 },// 但是我没有听见你的声音
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ level: 1, index: 5 },
{ level: 2, index: 1 },
{ delay: true },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 7 },
{ level: 1, index: 6 },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ delay: true },
{ level: 1, index: 6 }, // 认真呼唤我姓名
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 6 },
{ level: 1, index: 7 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ delay: true },
{ stop: true },
{ stop: true },
{ level: 1, index: 3 },// 爱上你的时候不懂感情
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ level: 1, index: 5 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 7 },
{ level: 1, index: 6 },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ delay: true },
{ stop: true },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ level: 1, index: 7 },
{ level: 1, index: 7 },
{ level: 2, index: 3 },
{ level: 2, index: 3 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 5 },
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ delay: true },
{ stop: true },
{ level: 1, index: 3 },
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ level: 1, index: 5 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 7 },
{ level: 1, index: 6 },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ delay: true },
{ stop: true },
{ level: 1, index: 6 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 6 },
{ level: 1, index: 7 },
{ level: 2, index: 3 },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ delay: true },
{ delay: true },
{ stop: true },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ delay: true },
{ level: 1, index: 7 },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ delay: true },
{ level: 2, index: 2 },
{ stop: true },
{ stop: true },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 6 },
{ stop: true },
{ stop: true },
{ level: 1, index: 5 },
{ level: 1, index: 5 },
{ delay: true },
{ level: 1, index: 3 },
{ level: 1, index: 5 },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ delay: true },
{ delay: true },
{ stop: true },
{ stop: true },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 1, index: 5 },
{ level: 1, index: 5 },
{ level: 1, index: 1 },
{ level: 1, index: 3 },
{ level: 1, index: 2 },
{ level: 1, index: 6 },
{ delay: true },
{ stop: true },
{ stop: true },
{ level: 1, index: 6 },
{ delay: true },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ level: 1, index: 6 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 1, index: 6 },
{ level: 2, index: 1 },
{ level: 1, index: 6 },
{ delay: true },
{ stop: true },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ level: 2, index: 2 },
{ delay: true },
{ delay: true },
{ stop: true },
{ level: 1, index: 5 },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ level: 2, index: 2 },
{ delay: true },
{ level: 2, index: 3 },
{ level: 1, index: 5 },
{ level: 2, index: 2 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 1, index: 5 },
{ level: 2, index: 2 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ level: 2, index: 2 },
{ level: 2, index: 3 },
{ level: 2, index: 4 },
// { delay: true },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ level: 1, index: 7 },
// { stop: true },
{ level: 2, index: 1 },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ level: 2, index: 1 },
{ delay: true },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ level: 1, index: 7 },
{ level: 1, index: 7 },
{ level: 1, index: 7 },
{ level: 2, index: 3 },
{ level: 2, index: 5 },
{ level: 2, index: 3 },
{ level: 2, index: 1 },
{ level: 1, index: 7 },
{ level: 1, index: 6 },
{ level: 2, index: 4 },
{ level: 2, index: 4 },
{ delay: true },
{ level: 2, index: 5 },
{ level: 2, index: 4 },
{ level: 2, index: 3 },
{ level: 1, index: 5 },
{ level: 2, index: 3 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 2, index: 4 },
{ level: 2, index: 3 },
{ level: 2, index: 1 },
{ level: 1, index: 4 },
{ delay: true },
{ level: 2, index: 2 },
{ level: 2, index: 2 },
{ delay: true },
{ level: 2, index: 2 },
{ delay: true },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 2, index: 2 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ delay: true },
{ level: 2, index: 3 },
{ level: 1, index: 5 },
{ level: 2, index: 2 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 1, index: 5 },
{ level: 2, index: 2 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ delay: true },
{ level: 2, index: 3 },
{ level: 2, index: 4 },
{ delay: true },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 2, index: 1 },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ level: 2, index: 1 },
{ delay: true },
{ level: 1, index: 3 },
{ level: 1, index: 6 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 7 },
{ level: 1, index: 7 },
{ level: 2, index: 3 },
{ level: 2, index: 5 },
{ delay: true },
{ level: 2, index: 3 },
{ level: 2, index: 1 },
{ level: 1, index: 7 },
{ delay: true },
{ level: 1, index: 6 },
{ level: 2, index: 4 },
{ level: 2, index: 4 },
{ delay: true },
{ stop: true },
{ level: 2, index: 5 },
{ level: 2, index: 4 },
{ level: 2, index: 3 },
{ level: 2, index: 5 },
{ level: 2, index: 3 },
{ delay: true },
{ level: 2, index: 3 },
{ delay: true },
{ stop: true },
{ level: 2, index: 4 },
{ level: 2, index: 3 },
{ level: 2, index: 1 },
{ level: 2, index: 4 },
{ level: 2, index: 2 },
{ level: 2, index: 2 },
{ delay: true },
{ delay: true },
{ level: 2, index: 3 },
{ level: 2, index: 1 },
{ level: 2, index: 1 },
{ level: 2, index: 3 },
{ level: 2, index: 2 },
{ delay: true },
{ level: 2, index: 1 },
{ delay: true },
]);
复制代码
关注公众号《不同的前端》,以不同的视角学习前端,快速成长,一块儿把玩最新的技术、探索各类黑科技