Experience address: http://wscats.gitee.io/piano/build
Project address: https://github.com/Wscats/piano or https://gitee.com/wscats/piano
Play a dandelion with 8 keys on the keyboard. The promise to give to 996’s self or the moon to represent my heart to her of Tanabata, it’s very simple~
This project was implemented using only a few simple front-end technologies, dedicated to every coder who loves music🎹
If you like or are helpful to you, give me a like and support it😊
Technical points and directory structure
The project did not use the mainstream frameworks (React, Vue and Angular) and popular technologies on the market, but used the Omi framework ( JSX+WebComponents
), and the Omil
single-file component SFCs
loader. The component communication is based on the Proxy
feature and combined VScode's plug-in Eno-Snippets
based on AST
and regular real-time compilation
.eno or .omi suffix components to reduce part of
Webpack
, of course, other technologies familiar to students will not be mentioned here.
src
- assets
element
app-piano
- songs piano notation catalog
- app-piano.eno single file component
- The compiled JS file of app-piano.js component
- Mapping of notes.js keyboard keys and notes
- index.js component root container, configure the communication method of
Proxy
public
- samples/piano piano single note material
app-piano.eno | Single file components you need to write in development |
---|---|
app-piano.js | After Eno-Snippets modify or save the file Hello.eno after the plug-in conversion js file |
As shown in the figure below, the code on the left is the single file component with the .eno suffix we wrote, and the right is the .js suffix file generated by Eno Snippets.
Development and installation
Develop, build and run.
# 获取远程仓库代码
git clone https://github.com/Wscats/piano
# 进入目录
cd piano
# 安装依赖
npm install
# 启动项目
npm start
# 在浏览器访问 http://localhost:3000
Use the npm package manager to install.
npm install omi-piano
Run or release your own performance version.
# 进入目录
cd omi-piano
# 安装依赖
npm install
# 启动项目
npm start
# 发布自已的演奏版本
npm run build
Simple music theory knowledge
First, let’s learn the basics of music, and collect the most basic piano monophonic material . Each note corresponds to a .mp3
file, which is recorded with an object, similar to the following. For example, A
here refers to CDEFGAB
The sound name A
also La
. This is the most basic music theory. Does it remind you of music lessons when you were a child? The staff on the drawing board.
export default {
A2: "./samples/piano/a54.mp3",
A3: "./samples/piano/a69.mp3",
A4: "./samples/piano/a80.mp3",
A5: "./samples/piano/a74.mp3",
A6: "./samples/piano/a66.mp3",
'A#3': "./samples/piano/b69.mp3",
'A#4': "./samples/piano/b80.mp3",
'A#5': "./samples/piano/b74.mp3",
'A#6': "./samples/piano/b66.mp3",
// other...
}
Of course, here we use numbers to substitute equivalently to reduce the difficulty for beginners. 1
equivalent to C
midrange is Do
. Since many songs will use the more dense middle part of the piano, we default to the midrange corresponding. number key:
1 === C4 === Do
number key | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
musical alphabet | C4 | D4 | E4 | F4 | G4 | A4 | B4 |
Note | Do | Re | Mi | Fa | Sol | La | Si |
Here is a special picture to facilitate our understanding:
Of course, the actual situation as well as tones and semitones distinction, such as A
chromatic is A#
, as well as midrange, treble and treble times, we are here with A4
expressed Alto, A5
represents treble, A6
represent times treble, so you can continue to order form more clearly, when we want to play alto A4
, just press the number keys on the keyboard 6
, if you want to play the treble A5
, just use the key combination Option+6
, we just need to learn by analogy, we can know each note corresponding Keyboard keys.
Contrabass | C2 | D2 | E2 | F2 | G2 | A2 | B2 |
---|---|---|---|---|---|---|---|
Shift+(1-7) | Shift+1 | Shift+2 | Shift+3 | Shift+4 | Shift+5 | Shift+6 | Shift+7 |
bass | C3 | D3 | E3 | F3 | G3 | A3 | B3 |
Ctrl+(1-7) | Ctrl+1 | Ctrl+2 | Ctrl+3 | Ctrl+4 | Ctrl+5 | Ctrl+6 | Ctrl+7 |
Midrange | C4 | D4 | E4 | F4 | G4 | A4 | B4 |
Number keys 1-7 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Treble | C5 | D5 | E5 | F5 | G5 | A5 | B5 |
Option key+(1-7) | Option+1 | Option+2 | Option+3 | Option+4 | Option+5 | Option+6 | Option+7 |
Double treble | C6 | D6 | E6 | F6 | G6 | A6 | B6 |
Command key + (1-7) | Command+1 | Command+2 | Command+3 | Command+4 | Command+5 | Command+6 | Command+7 |
Note | Do | Re | Mi | Fa | Sol | La | Si |
The above is the whole tone table, here is the semitone table:
Double bass | C#2 | D#2 | F#2 | G#2 | A#2 |
---|---|---|---|---|---|
Shift+ | Shift+q | Shift+w | Shift+e | Shift+r | Shift+t |
Low semitone | C#3 | D#3 | F#3 | G#3 | A#3 |
Ctrl+ | Ctrl+q | Ctrl+w | Ctrl+e | Ctrl+r | Ctrl+t |
Middle semitone | C#4 | D#4 | F#4 | G#4 | A#4 |
Letter key | q | w | e | r | t |
High semitone | C#5 | D#5 | F#5 | G#5 | A#5 |
Option+ | Option+q | Option+w | Option+e | Option+r | Option+t |
Double high semitone | C#6 | D#6 | F#6 | G#6 | A#6 |
Command+ | Command+q | Command+w | Command+e | Command+r | Command+t |
So now we only need to use 5 alphabet keys (q, w, e, r, t) + 4
function keys (Shift, Control, Option and Command) on the keyboard + 7
numeric keys (1,2 ,3,4,5,6,7) A total of 16 keys, playing the piano 60 monotones (35 whole tones + 25 semitones), the actual situation may not need to use so many for a simple piano song, use a few A simple chord is enough.
Build the piano interface
There are preparatory above, the following is transformed into our programming knowledge, and we need to use HTML to draw our piano interface, we can refer to codepen and codesandbox material, here I used flex
layout with shadow and over Realize the black and white keys of the piano, which uses React's JSX syntax to traverse and render the black and white keys.
<div class="piano">
{this.data.pianoKeys.map((item)=>{return(
<div class="piano-key">
<div data-type="white" ref={e=>{ this[item.white.name] = e }} class="piano-key__white"
onClick={this.playNote.bind(this,item.white.name)} data-key={item.white.keyCode}
data-note={item.white.name}>
<span class="piano-note">{item.white.name}</span>
<audio preload="auto" src={this.data.notes[item.white.name]} hidden='true' data-note={item.white.name}
class='audioEle'></audio>
</div>
<div data-type="black" ref={e=>{ this[item.black.name] = e }} style={{
display: item.black.name ? 'block' : 'none'
}} class="piano-key__black" onClick={this.playNote.bind(this,item.black.name)} data-key={item.black.keyCode}
data-note={item.black.name}>
<span class="piano-note" style="color:#fff">{item.black.name}</span>
<audio preload="auto" src={this.data.notes[item.white.name]} hidden='true' data-note={item.white.name}
class='audioEle'></audio>
</div>
</div>
)})}
</div>
You can observe the source code of CSS, corresponding to the styles of black keys and white keys. You can also write one more style for the effect when the keyboard or mouse clicks on the keys. You can simply add a background color to it to achieve the overall implementation. Not too complicated, you can adjust the parameters of the style to create your own piano style.
.piano {
margin: 0 200px;
background: linear-gradient(-65deg, #000, #222, #000, #666, #222 75%);
border-top: .8rem solid #282828;
box-shadow: inset 0 -1px 1px hsla(0, 0%, 100%, .5), inset -0.4rem 0.4rem #282828;
display: flex;
height: 80vh;
height: 20vh;
justify-content: center;
overflow: hidden;
padding-bottom: 2%;
padding-left: 2.5%;
padding-right: 2.5%;
}
.piano-key {
color: blue;
flex: 1;
margin: 0 .1rem;
max-width: 8.8rem;
position: relative;
}
.piano-key__white {
display: flex;
flex-direction: column-reverse;
background: linear-gradient(-30deg, #f8f8f8, #fff);
box-shadow: inset 0 1px 0 #fff, inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px 0 0 #fff, 0 4px 3px rgba(0, 0, 0, .7), inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px -1px 15px rgba(0, 0, 0, .5), -3px 4px 6px rgba(0, 0, 0, .5);
height: 100%;
position: relative;
}
.piano-key__black {
display: flex;
flex-direction: column-reverse;
background: linear-gradient(-20deg, #222, #000, #222);
box-shadow: inset 0 -1px 2px hsla(0, 0%, 100%, .4), 0 2px 3px rgba(0, 0, 0, .4);
border-width: .2rem .4rem 1.2rem;
border-style: solid;
border-color: #666 #222 #111 #555;
height: 60%;
left: 100%;
position: absolute;
transform: translateX(-50%);
top: 0;
width: 70%;
z-index: 1;
}
Play piano sound
When we finish implementing the piano interface, we need to match the sound for each button. Here we use the HTML5 <audio>
, which can be loaded with piano notes. When we trigger a mouse click event or a keyboard click event, we let it play Before the piano is played, we use the attribute value preload="auto"
to preload it.
<audio preload="auto" src={this.data.notes[item.white.name]} hidden='true' data-note={item.white.name} class='audioEle'></audio>
To play, just use the ref
property to get the node of the piano tone, and then control the playback logic for its trigger method, audio.currentTime = 0
reset the playback progress and audio.play()
perform playback. When the playback is triggered, the delayer can be used to realize the button animation.
playNote(name) {
let audio = this[name].childNodes[1]
this[name].style.background = `linear-gradient(-20deg, #3330fb, #000, #222)`
let timer = setTimeout(() => {
this[name].getAttribute('data-type') === 'white' ? this[name].style.background = `linear-gradient(-30deg, #f8f8f8, #fff)` : this[name].style.background = `linear-gradient(-20deg, #222, #000, #222)`
clearTimeout(timer)
}, 1000)
audio.currentTime = 0;
audio.play();
}
After completing <audio>
, you need to make the keyboard event and its binding logic. Here you need to understand the keycode
of the keyboard. Each physical key of the keyboard will correspond to a key code. According to the key code, use JS
keyboard event monitor to determine whether the key is Hold on.
We use window.document.onkeydown
to monitor the global keyboard events of the page, and then determine whether the e.altKey
, e.ctrlKey
, e.metaKey
and e.shiftKey
are triggered, then judge whether the numeric keys are triggered, and finally judge whether the letter keys are triggered.
document.onkeydown = (event) => {
var e = event || window.event || arguments.callee.caller.arguments[0];
let playNote = (key) => {
if (e.shiftKey === true) {
this.playNote(`${key}2`)
} else if (e.altKey === true) {
this.playNote(`${key}5`)
} else if (e.ctrlKey === true) {
this.playNote(`${key}3`)
} else if (e.metaKey === true) {
this.playNote(`${key}6`)
e.returnValue = false;
} else {
this.playNote(`${key}4`)
}
}
if (e && 49 <= e.keyCode && e.keyCode <= 55) {
switch (e.keyCode) {
case 49:
playNote('C')
break;
case 50:
playNote('D')
break;
case 51:
playNote('E')
break;
case 52:
playNote('F')
break;
case 53:
playNote('G')
break;
case 54:
playNote('A')
break;
case 55:
playNote('B')
break;
}
}
if (e && (81 === e.keyCode || e.keyCode === 87 || e.keyCode === 69 || e.keyCode === 82 || e.keyCode === 84)) {
switch (e.keyCode) {
case 81:
playNote('C#')
break;
case 87:
playNote('D#')
break;
case 69:
playNote('F#')
break;
case 82:
playNote('G#')
break;
case 84:
playNote('A#')
break;
}
}
};
Autoplay
The following is the logic of how to play automatically. If you want to play a complex song, especially in the case of multiple chords, we can write a good score, and then hand it over to the program to automatically play it. The following is the piano notation "Dandelion Promise" by Jay Chou, We use an array to record the notes of each key, and then just use a timer or recursively to take out each note for the function to recognize, and then trigger the corresponding
<audio>
tag to play, here is an explanation of each item in the array, If there is a number in the string, it corresponds to the midrange, that is, if it is '3'
, then you only need to press 3
on the keyboard, if it is '+3'
it is the high pitch, which is the aforementioned combination key option + 3
, if it is +1..
, then It is to tell the programmer that we need to pause for two beats here. When we are actually playing, just pause here to control the melody.
const song = [
'3', '4',
'5', '5', '5', '6', '7', '+1..',
'+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..',
'+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6',
'6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2',
// 将愿望...
'+2..', '3', '4', '5',
// 折飞机寄成信...
'5', '5', '5', '6', '7', '+1..',
'+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..',
'+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6',
'6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2..',
// 一起长大的约定...
'3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..',
'+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..',
// 说好要一起旅行...
'3', '5', '+1.', '+3', '+3.', '+4', '+2..',
// 是你如今...
'+2', '+5', '7', '+1..',
// 唯一坚持的任性
'+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1',
// 在走廊...
'3', '4',
'5', '5', '5', '6', '7', '+1..',
'+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..',
'+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6',
'6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2',
// 一起长大的约定...
'3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..',
'+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..',
// 说好要一起旅行...
'3', '5', '+1.', '+3', '+3.', '+4', '+2..',
// 是你如今...
'+2', '+5', '7', '+1..',
// 唯一坚持的任性...
'+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1',
// 一起长大的约定...
'+6', '+5', '+3', '+2', '+1', '+3.', '+4', '+2..',
'+6', '+5', '7', '+1..',
// 与你聊不完的曾经...
'+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..',
// 而我已经分不清...
'3', '5', '+1', '+3', '+3.', '+2', '+2', '+2..', '+2', '+5', '7', '+2', '+1', '+1',
// 还是错过的爱情...
'+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1..'
]
export default [...song]
With the above array, we only need to write a recursive function to traverse the array, and then convert it to the note CDEFGAB
according to the number-like notation. At the beginning, I used a timer to implement the notation reading function. Later, I found that, It is more difficult to control the pause time between notes with a timer. On the contrary, it is easier to implement recursion. However, it is also difficult to achieve the pause playback function with recursion, because interrupting the recursive function from the outside is also more complicated, so if students want to implement the piano by themselves If you do, pay attention to this place a little bit. Promise
appearing in the following code with await, async
and timer is to accept a time variable to control the pause time between notes, and the outer layer if(offset < song.length && this.store.data.song.length > 0)
judgment condition on the left is that the judgment index value is less than the length of the numbered musical notation array, and the right is the outer layer. The incoming judgment value is used as the termination boundary condition of the recursive function.
playSong(song) {
this.setSong([...song])
let offset = 0
let time = 0
let playSong = async () => {
// 右边是从外部来中断递归
if (offset < song.length && this.store.data.song.length > 0) {
switch (typeof song[offset]) {
// 简谱2演奏方法 根据 ++12345--6. 简单旋律情况
case 'string':
let letters = song[offset].match(/[0-9]/g)
switch (letters.length) {
case 1:
time = this.handleString(song, offset)
break
default:
time = this.handleStrings(song, offset)
break
}
break
// 简谱1演奏方法 根据 CDEFGAB,复杂旋律情况,比如有和弦
case 'object':
console.log(song[offset]['note'])
time = song[offset]['time'];
this.playNote(song[offset]['note'])
break;
case 'number':
// 休止符
switch (song[offset]) {
case 0:
time = 1000
break
}
break
}
await new Promise((resolve) => {
let timer = setTimeout(() => {
clearInterval(timer)
resolve()
}, time)
})
offset++
// 自定义事件,跟下面底部的音符自动跳动结合
this.add()
playSong()
} else {
// 暂停播放
clearTimeout(this.timer)
this.store.data.song = []
this.store.data.count = 0
return
}
}
playSong()
}
Dandelion Promise
After reading the numbered musical notation of the array above, of course, some students will ask. There are more than 8 keys in the array above. If you look carefully, you will find that only the midrange and high (1-7)
are used here, which is the pure number keys 06101e889165e8 and Option
The key combination is not even used for semitones, so only 8 keys are actually used. Therefore, the numbered musical notation for programming recognition above can be transformed into the keyboard musical notation recognized by human beings. It only needs to be slightly adjusted to the following key combination.
'3', '4', '5', '5', '5', '6', '7', 'Option+1..',
'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..',
'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6',
'6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2',
// 将愿望...
'Option+2..', '3', '4', '5',
// 折飞机寄成信...
'5', '5', '5', '6', '7', 'Option+1..',
'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..',
'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6',
'6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2..',
// 一起长大的约定...
'3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+1..',
'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..',
// 说好要一起旅行...
'3', '5', 'Option+1.', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..',
// 是你如今...
'Option+2', 'Option+5', '7', 'Option+1..',
// 唯一坚持的任性
'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1',
// 在走廊...
'3', '4', '5', '5', '5', '6', '7', 'Option+1..',
'Option+1', 'Option+1', '7', 'Option+2', '6', '5', '5', '5', 'Option+2', 'Option+1', 'Option+1', 'Option+3', 'Option+3..',
'Option+1', 'Option+2', 'Option+3', 'Option+3', 'Option+4', 'Option+3', 'Option+2', 'Option+3', 'Option+1', 'Option+1', '6', '6',
'6', '7', 'Option+1', 'Option+2', 'Option+2', 'Option+1', '7', '6', 'Option+4', 'Option+2',
// 一起长大的约定...
'3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+1..',
'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..',
// 说好要一起旅行...
'3', '5', 'Option+1.', 'Option+3', 'Option+3.', 'Option+4', 'Option+2..',
// 是你如今...
'Option+2', 'Option+5', '7', 'Option+1..',
// 唯一坚持的任性...
'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1',
// 一起长大的约定...
'Option+6', 'Option+5', 'Option+3', 'Option+2', 'Option+1', 'Option+3.', 'Option+4', 'Option+2..',
'Option+6', 'Option+5', '7', 'Option+1..',
// 与你聊不完的曾经...
'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2', 'Option+3', 'Option+3..',
// 而我已经分不清...
'3', '5', 'Option+1', 'Option+3', 'Option+3.', 'Option+2', 'Option+2', 'Option+2..', 'Option+2', 'Option+5', '7', 'Option+2', 'Option+1', 'Option+1',
// 还是错过的爱情...
'Option+3', 'Option+4', 'Option+5', 'Option+1', 'Option+1', 'Option+2.', 'Option+1', 'Option+1..'
the moon represents my heart
We can also play another familiar piano piece "The Moon Represents My Heart".
'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', '1', 'Ctrl+6', '2',
'3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', '1',
'Ctrl+6', '2', '3', '2', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+5', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', '1', '1', '1', '2',
'3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3', '2', 'Ctrl+6',
'Ctrl+7', '1', '2', '1', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1', '2', '3',
'2', '1', 'Ctrl+6', '2', '3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1',
'2', '3', '2', '1', 'Ctrl+6', '2', '3', '2', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7', 'Ctrl+6', 'Ctrl+5', '3', '5', '3', '2', '1', '5', 'Ctrl+7', 'Ctrl+6', 'Ctrl+7',
'1', '1', '1', '2', '3', '2', 'Ctrl+5', '1', '3', '5', '1', 'Ctrl+7', '3', '5', '5', '6', '7', 'Option+1', '6', '5', '3', '2', '1', '1', '1', '3', '2', '1', '1', '1',
'2', '3', '2', 'Ctrl+6', 'Ctrl+7', '1', '2', '1'
thank
Thanks for the company of music and programming! are struggling in 996, welcome to share, and look forward to your contribution of code, PR, discuss the problem in 16101e889166dd issue 16101e889166df, or talk about your suggestions, as Leehom Wang sings in the song:
If the world is too dangerous, only music is the safest. Take me into my dream and let the lyrics come true 💞 —— "Our Song"
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。