简易钢琴琴键布局与触摸演奏,鸿蒙 Canvas 发音实践


实例:简易钢琴(Piano)|技术:琴键几何布局、黑白键命中判定、按下高亮、AVPlayer 发音
一、为什么选简易钢琴
1.1 从"视觉"到"视听结合"
前 9 个 Canvas 应用都是纯视觉(图表/粒子/游戏/导图)。简易钢琴引入声音维度:
- 琴键布局——钢琴键盘是"白键 + 黑键"的复杂几何(黑键不连续、跨白键交界);
- 精确命中——黑白键交错,触摸要判定"按到了白键还是黑键";
- 按下反馈——视觉(高亮)+ 听觉(发音)双重反馈。
钢琴是"几何布局 + 精确命中 + 多通道反馈"的综合练习——琴键排列是有规律的几何,命中判定比矩形网格(贪吃蛇)复杂。
1.2 一个八度的键盘
真实钢琴 88 键。本应用实现一个八度(C3~B3):
| 白键 | C3 | D3 | E3 | F3 | G3 | A3 | B3 |
|---|---|---|---|---|---|---|---|
| 黑键 | C#3 | D#3 | — | F#3 | G#3 | A#3 | — |
黑键位置:C-D 间、D-E 间、F-G 间、G-A 间、A-B 间(E-F 间、B-C 间没有黑键——钢琴的天然半音规律)。
二、琴键几何布局 ★核心算法
2.1 数据结构
/** 白键(C 大调音阶) */
const WHITE_KEYS: string[] = ['C3', 'D3', 'E3', 'F3', 'G3', 'A3', 'B3'];
/** 黑键位置:白键之间的间隔序号 */
const BLACK_POS: number[] = [1, 3, 5, 6, 7];
/** 黑键音符名 */
const BLACK_NAMES: string[] = ['C#3', 'D#3', 'F#3', 'G#3', 'A#3'];
黑键位置编码:BLACK_POS = [1,3,5,6,7] 表示黑键位于第 1、3、5、6、7 个"白键间隔"。间隔编号规则:间隔 i 在白键 i-1 与白键 i 之间(间隔 1 = C-D 间,间隔 3 = D-E 间,间隔 5 = F-G 间,间隔 6 = G-A 间,间隔 7 = A-B 间)。用间隔序号而非像素,让黑键位置与白键布局解耦——调整白键宽度,黑键自动跟随。
2.2 坐标计算
private whiteX(idx: number): number {
return this.startX + idx * this.whiteW; // 白键 idx 左边缘
}
private blackX(pos: number): number {
return this.startX + pos * this.whiteW - this.blackW / 2; // 黑键中心在交界处
}
白键:startX + idx × whiteW——等宽线性排布(idx 0~6)。
黑键:startX + pos × whiteW - blackW/2——黑键中心对准白键间隔的中点(交界处),左右各露半宽。黑键跨越两个白键的交界,覆盖在交界线上。
三、触摸命中判定 ★核心交互
private hitKey(x: number, y: number): void {
// ① 先查黑键(黑键在上半区)
for (let i = 0; i < BLACK_POS.length; i++) {
const bx = this.blackX(BLACK_POS[i]);
if (x >= bx && x <= bx + this.blackW && y >= 0 && y <= this.blackH) {
this.pressedKey = BLACK_NAMES[i];
this.noteText = `♩ ${BLACK_NAMES[i]}`;
this.playNote(BLACK_NAMES[i]);
this.draw();
return;
}
}
// ② 再查白键(下半区全高)
for (let i = 0; i < WHITE_KEYS.length; i++) {
const wx = this.whiteX(i);
if (x >= wx && x <= wx + this.whiteW && y >= 0 && y <= this.whiteH) {
this.pressedKey = WHITE_KEYS[i];
this.noteText = `♩ ${WHITE_KEYS[i]}`;
this.playNote(WHITE_KEYS[i]);
this.draw();
return;
}
}
}
黑白键的命中顺序(先黑后白):
- 黑键优先:黑键只有白键高度的约 60%(blackH=126 vs whiteH=200),且覆盖在白键上方。若先查白键,触摸黑键区域会误命中其下的白键(黑键矩形在白键矩形内)。先查黑键保证黑键区域优先命中黑键。
- 判定条件:
x ∈ [bx, bx+blackW] && y ∈ [0, blackH]——黑键只在上半区有效(y < blackH);白键全高(y < whiteH)。Y 分区:上半区可能是黑键,下半区一定是白键。
滑动连弹:TouchType.Move 也调用 hitKey——手指滑过多个琴键时逐个触发("滑音"效果)。
四、按下高亮
private draw(): void {
// ① 白键
for (let i = 0; i < WHITE_KEYS.length; i++) {
const x = this.whiteX(i);
const pressed = this.pressedKey === WHITE_KEYS[i];
this.ctx.fillStyle = pressed ? '#BFDBFE' : '#FFFFFF'; // 按下浅蓝
this.ctx.fillRect(x, 0, this.whiteW, this.whiteH);
this.ctx.strokeStyle = '#CBD5E1';
this.ctx.strokeRect(x, 0, this.whiteW, this.whiteH);
// 白键标签
this.ctx.fillStyle = '#64748B';
this.ctx.font = '12px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText(WHITE_KEYS[i], x + this.whiteW / 2, this.whiteH - 10);
this.ctx.textAlign = 'start';
}
// ② 黑键(后画,覆盖在白键上)
for (let i = 0; i < BLACK_POS.length; i++) {
const x = this.blackX(BLACK_POS[i]);
const pressed = this.pressedKey === BLACK_NAMES[i];
this.ctx.fillStyle = pressed ? '#1E3A8A' : '#0F172A'; // 按下深蓝
this.ctx.fillRect(x, 0, this.blackW, this.blackH);
this.ctx.strokeStyle = '#334155';
this.ctx.strokeRect(x, 0, this.blackW, this.blackH);
}
}
绘制顺序:先白键后黑键——黑键"压"在白键交界上,视觉正确(黑键高于白键)。
按下高亮:pressedKey === 音符名 时白键浅蓝(#BFDBFE)、黑键深蓝(#1E3A8A)——按下态颜色可视化。抬起后(Up 事件 pressedKey=‘’)重绘恢复。
五、发音(AVPlayer 接入说明)
private playNote(name: string): void {
if (this.muted) {
return;
}
// 尝试播放音阶资源(assets/entry/resources/rawfile/notes/C3.wav 等)
hilog.info(0x0000, TAG, 'play note: %{public}s', name);
}
发音方案(注释说明接入方式):
- 准备音阶资源:把 C3~B3 的 12 个音符 wav 文件放入
resources/rawfile/notes/; - 创建播放器:
audio.createAudioPlayer()(@kit.AudioKit); - 播放:
player.src = 'rawfile:///notes/C3.wav'; player.play()。
教学版 playNote 只打日志(无资源不报错),视觉高亮是主演示,声音是可选增强。真机接入时按上述三步替换即可。
静音开关:🔊/🔇 切换 muted——无资源环境下的演示开关。
六、技术要点对照表
| 技术点 | 实现方式 | 生产价值 |
|---|---|---|
| 黑键位置编码 | 间隔序号数组 | 布局解耦 |
| 黑白键几何 | 白等宽 + 黑跨交界 | 真实键盘 |
| 命中顺序 | 先黑后白 | 防误命中 |
| Y 分区 | 黑键半高判定 | 分区精确 |
| 按下高亮 | pressedKey 比较 | 视觉反馈 |
| 滑动连弹 | Move 也 hitKey | 滑音效果 |
| 声音降级 | 无资源仅日志 | 环境自适应 |
| 静音开关 | muted 切换 | 演示控制 |
七、文章小结
本篇完成了简易钢琴的数据与几何层:白键等宽排布 + 黑键跨交界的琴键几何、间隔序号编码的黑键位置、先黑后白 + Y 分区的命中判定、按下高亮绘制、AVPlayer 发音接入说明。这套"几何编码 + 分区命中"的键盘实现是乐器类应用的基础——黑键位置用间隔序号而非像素,是"布局参数化"的典范。
下一篇《页面 UI 与弹奏交互》将搭建:钢琴画布、音符提示、静音开关、音阶说明。
八、深度扩展
1. 多八度扩展
一个八度扩展到 88 键:WHITE_KEYS 生成 52 个白键名(C1~C7),BLACK_POS 模式每八度重复。八度 = 位置模式 + 音名循环:
// 每八度 7 白键 5 黑键,位置模式固定 [1,3,5,6,7]
// 白键名:C D E F G A B + 八度号
2. 和弦
多指触摸(touches 数组)同时按下多个键 → 和弦。实现:pressedKeys 数组(非单值)+ 多播放器并发。多点触控是乐器的刚需。
3. 录音与回放
录制弹奏序列(音符 + 时间戳)→ 回放(定时依次触发)。弹奏轨迹是教学/分享功能。
4. 音高合成(无资源)
不依赖 wav 资源,用 WebAudio API(@kit.ArkWeb?HarmonyOS 需查)或原生音频合成正弦波。程序化合成免资源,但 HarmonyOS 封装有限,教学用资源方案。
5. FAQ
Q1:为什么黑键要先于白键命中?
A:黑键矩形完全位于白键矩形内(黑键盖在白键交界上)。若先查白键,黑键区域会被其下的白键"截胡"。先查覆盖层是命中检测的通用原则(36 的叶子优先同理)。
Q2:黑键为什么只在上半区有效(y < blackH)?
A:黑键比白键短(126 vs 200),下半区没有黑键。y < blackH 限定黑键的有效高度,防止"白键下半区却命中黑键"的误判。
Q3:没有音频资源能跑吗?
A:能——playNote 降级为日志,视觉高亮完整可用。声音是增强不是必需,教学环境友好。
Q4:滑动弹奏的 Move 判定会不会太灵敏?
A:手指滑过时每帧 hitKey,琴键逐个点亮——"滑音"正是想要的效果。若要去抖可加"同键不重复触发"判断(pressedKey 相同则跳过)。
Q5:为什么只做一个八度?
A:教学聚焦"琴键几何 + 命中 + 反馈"核心,88 键是重复模式的扩展(见深度扩展 1)。先做最小可玩,再横向扩展。
九、下篇预告
下一篇《页面 UI 与弹奏交互》将完成:钢琴画布(96% 宽 × 210 高)、音符提示、静音开关、音阶说明——并演示 C 大调音阶的弹奏体验。
更多推荐




所有评论(0)