├── favicon.ico ├── .gitignore ├── samples ├── piano-A3.mp3 ├── piano-A4.mp3 ├── piano-A5.mp3 ├── piano-A6.mp3 ├── piano-B3.mp3 ├── piano-B4.mp3 ├── piano-B5.mp3 ├── piano-B6.mp3 ├── piano-C3.mp3 ├── piano-C4.mp3 ├── piano-C5.mp3 ├── piano-C6.mp3 ├── piano-D3.mp3 ├── piano-D4.mp3 ├── piano-D5.mp3 ├── piano-D6.mp3 ├── piano-E3.mp3 ├── piano-E4.mp3 ├── piano-E5.mp3 ├── piano-E6.mp3 ├── piano-F3.mp3 ├── piano-F4.mp3 ├── piano-F5.mp3 ├── piano-F6.mp3 ├── piano-G3.mp3 ├── piano-G4.mp3 ├── piano-G5.mp3 └── piano-G6.mp3 ├── .claude └── settings.local.json ├── ads.txt ├── sounds ├── metronome │ ├── click.mp3 │ ├── accent.mp3 │ ├── 34272_89860-lq.mp3 │ └── 34273_89860-lq.mp3 └── piano │ ├── piano-a3.mp3 │ ├── piano-a4.mp3 │ ├── piano-a5.mp3 │ ├── piano-c3.mp3 │ ├── piano-c4.mp3 │ ├── piano-c5.mp3 │ ├── piano-ds3.mp3 │ ├── piano-ds4.mp3 │ ├── piano-ds5.mp3 │ ├── piano-fs3.mp3 │ ├── piano-fs4.mp3 │ └── piano-fs5.mp3 ├── robots.txt ├── images ├── icons │ ├── practice.svg │ ├── basics.svg │ └── notes.svg ├── favicon.svg ├── logo.svg └── piano-placeholder.svg ├── package.json ├── download_samples.sh ├── scripts ├── download_samples.py ├── download_tonejs_samples.py ├── download_salamander.py └── generate_samples.py ├── verify.js ├── llms.txt ├── test-audio.html ├── setup.js ├── README.md ├── js ├── main.js ├── tutorial.js ├── staff.js ├── audio-generator.js ├── practice.js ├── piano-audio.js ├── practice-songs.js ├── translations.js └── recorder.js ├── styles.css ├── generate_samples.py ├── generate_samples.html ├── css ├── tutorials.css ├── article.css ├── practice.css └── about.css ├── piano.css ├── TESTING_CHECKLIST.md ├── tutorial.html ├── sitemap.xml ├── IMPLEMENTATION_PLAN.md ├── SEO-OPTIMIZATION-GUIDE.md ├── script.js ├── QUICK-ACTION-CHECKLIST.md ├── tutorial-content.html ├── articles └── tutorials │ ├── finger-numbers.html │ ├── reading-notes.html │ ├── piano-basics.html │ └── rhythm-basics.html ├── tutorials.html ├── practice.html └── test-improvements.html /favicon.ico: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /samples/piano-A3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A3.mp3 -------------------------------------------------------------------------------- /samples/piano-A4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A4.mp3 -------------------------------------------------------------------------------- /samples/piano-A5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A5.mp3 -------------------------------------------------------------------------------- /samples/piano-A6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-A6.mp3 -------------------------------------------------------------------------------- /samples/piano-B3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B3.mp3 -------------------------------------------------------------------------------- /samples/piano-B4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B4.mp3 -------------------------------------------------------------------------------- /samples/piano-B5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B5.mp3 -------------------------------------------------------------------------------- /samples/piano-B6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-B6.mp3 -------------------------------------------------------------------------------- /samples/piano-C3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C3.mp3 -------------------------------------------------------------------------------- /samples/piano-C4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C4.mp3 -------------------------------------------------------------------------------- /samples/piano-C5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C5.mp3 -------------------------------------------------------------------------------- /samples/piano-C6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-C6.mp3 -------------------------------------------------------------------------------- /samples/piano-D3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D3.mp3 -------------------------------------------------------------------------------- /samples/piano-D4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D4.mp3 -------------------------------------------------------------------------------- /samples/piano-D5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D5.mp3 -------------------------------------------------------------------------------- /samples/piano-D6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-D6.mp3 -------------------------------------------------------------------------------- /samples/piano-E3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E3.mp3 -------------------------------------------------------------------------------- /samples/piano-E4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E4.mp3 -------------------------------------------------------------------------------- /samples/piano-E5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E5.mp3 -------------------------------------------------------------------------------- /samples/piano-E6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-E6.mp3 -------------------------------------------------------------------------------- /samples/piano-F3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F3.mp3 -------------------------------------------------------------------------------- /samples/piano-F4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F4.mp3 -------------------------------------------------------------------------------- /samples/piano-F5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F5.mp3 -------------------------------------------------------------------------------- /samples/piano-F6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-F6.mp3 -------------------------------------------------------------------------------- /samples/piano-G3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G3.mp3 -------------------------------------------------------------------------------- /samples/piano-G4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G4.mp3 -------------------------------------------------------------------------------- /samples/piano-G5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G5.mp3 -------------------------------------------------------------------------------- /samples/piano-G6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yandong2023/piano-online/HEAD/samples/piano-G6.mp3 -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(open:*)", 5 | "Bash(node:*)" 6 | ], 7 | "deny": [], 8 | "ask": [] 9 | } 10 | } -------------------------------------------------------------------------------- /ads.txt: -------------------------------------------------------------------------------- 1 | # ads.txt file for piano-online 2 | # This file is used to authorize digital sellers for this domain 3 | 4 | # Google AdSense 5 | google.com, pub-4423187689700927, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /sounds/metronome/click.mp3: -------------------------------------------------------------------------------- 1 | 2 | 302 Found 3 | 4 |

302 Found

5 |
nginx/1.14.2
6 | 7 | 8 | -------------------------------------------------------------------------------- /sounds/metronome/accent.mp3: -------------------------------------------------------------------------------- 1 | 2 | 302 Found 3 | 4 |

302 Found

5 |
nginx/1.14.2
6 | 7 | 8 | -------------------------------------------------------------------------------- /sounds/metronome/34272_89860-lq.mp3: -------------------------------------------------------------------------------- 1 | 2 | 302 Found 3 | 4 |

302 Found

5 |
nginx/1.14.2
6 | 7 | 8 | -------------------------------------------------------------------------------- /sounds/metronome/34273_89860-lq.mp3: -------------------------------------------------------------------------------- 1 | 2 | 302 Found 3 | 4 |

302 Found

5 |
nginx/1.14.2
6 | 7 | 8 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | # 允许搜索引擎索引所有内容 5 | Sitemap: https://piano-online.com/sitemap.xml 6 | 7 | # 禁止访问敏感目录 8 | Disallow: /admin/ 9 | Disallow: /private/ 10 | Disallow: /temp/ 11 | Disallow: /*.log$ 12 | 13 | # 允许访问静态资源 14 | Allow: /css/ 15 | Allow: /js/ 16 | Allow: /images/ 17 | Allow: /samples/ -------------------------------------------------------------------------------- /images/icons/practice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/icons/basics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /images/icons/notes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piano_online", 3 | "version": "1.0.0", 4 | "description": "1. 技术栈:用纯粹的HTML + TailwindCSS来做 2. 核心功能:我要做一个在线钢琴网站,支持普通键盘和数字键盘,支持在线演奏。同时也支持练习,支持在线学习:选择一个曲子,会给出对应的按键,有点像俄罗斯方块,你只需要按对应的键,一首曲子即可以完整的演奏出来。 3. 功能区域:你要有完整的header,hero, testinoials, faq, features, pricing, how it works ,footer 区域。其他内容需要编写得专业、真实。 4. 网站文案:所有网站上的文案内容,我要求你使用中文。 5. 网站风格:网站配色和风格需要现代化、高级,类似于苹果官网的风格。网站需要自适应各种屏幕尺寸。", 5 | "main": "piano.js", 6 | "scripts": { 7 | "start": "http-server -p 8000 -c-1", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "http-server": "^14.1.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 钢琴在线 18 | 19 | 20 | -------------------------------------------------------------------------------- /download_samples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create samples directory if it doesn't exist 4 | mkdir -p samples 5 | 6 | # Clean existing files 7 | rm -f samples/*.mp3 8 | 9 | # Download piano samples from an alternative source 10 | BASE_URL="https://gleitz.github.io/midi-js-soundfonts/FatBoy/acoustic_grand_piano-mp3" 11 | NOTES=("A" "B" "C" "D" "E" "F" "G") 12 | OCTAVES=(3 4 5 6) 13 | 14 | for note in "${NOTES[@]}"; do 15 | for octave in "${OCTAVES[@]}"; do 16 | target_file="samples/piano-${note}${octave}.mp3" 17 | url="${BASE_URL}/${note}${octave}.mp3" 18 | echo "Downloading ${note}${octave} to $target_file..." 19 | 20 | # Download and convert the file 21 | if curl -L -f "$url" -o "$target_file"; then 22 | echo "Successfully downloaded ${note}${octave}" 23 | else 24 | echo "Failed to download ${note}${octave}" 25 | fi 26 | done 27 | done 28 | 29 | echo "Download complete!" 30 | -------------------------------------------------------------------------------- /images/piano-placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /scripts/download_samples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.request 3 | 4 | def download_piano_samples(): 5 | # Piano sample URLs (replace these with actual URLs to piano samples) 6 | sample_urls = { 7 | 'F1': 'https://piano-samples.s3.amazonaws.com/piano-F1.mp3', 8 | 'F2': 'https://piano-samples.s3.amazonaws.com/piano-F2.mp3', 9 | 'F3': 'https://piano-samples.s3.amazonaws.com/piano-F3.mp3', 10 | 'F4': 'https://piano-samples.s3.amazonaws.com/piano-F4.mp3', 11 | 'F5': 'https://piano-samples.s3.amazonaws.com/piano-F5.mp3', 12 | 'F6': 'https://piano-samples.s3.amazonaws.com/piano-F6.mp3', 13 | } 14 | 15 | # Create samples directory if it doesn't exist 16 | if not os.path.exists('../samples'): 17 | os.makedirs('../samples') 18 | 19 | # Download each sample 20 | for note, url in sample_urls.items(): 21 | output_path = f'../samples/piano-{note}.mp3' 22 | try: 23 | print(f'Downloading {note}...') 24 | urllib.request.urlretrieve(url, output_path) 25 | print(f'Successfully downloaded {note}') 26 | except Exception as e: 27 | print(f'Error downloading {note}: {str(e)}') 28 | 29 | if __name__ == '__main__': 30 | download_piano_samples() 31 | -------------------------------------------------------------------------------- /scripts/download_tonejs_samples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.request 3 | import ssl 4 | 5 | def download_tonejs_samples(): 6 | # Disable SSL certificate verification (only if needed) 7 | ssl._create_default_https_context = ssl._create_unverified_context 8 | 9 | # Base URL for tonejs-instruments piano samples 10 | base_url = "https://nbrosowsky.github.io/tonejs-instruments/samples/piano" 11 | 12 | # Notes we need (focusing on F notes which we're missing) 13 | notes = ['F1', 'F2', 'F3', 'F4', 'F5', 'F6'] 14 | 15 | # Create samples directory if it doesn't exist 16 | samples_dir = '../samples' 17 | if not os.path.exists(samples_dir): 18 | os.makedirs(samples_dir) 19 | 20 | # Download each sample 21 | for note in notes: 22 | filename = f'piano-{note}.mp3' 23 | url = f"{base_url}/{note}.mp3" 24 | output_path = os.path.join(samples_dir, filename) 25 | 26 | try: 27 | print(f'Downloading {note}...') 28 | urllib.request.urlretrieve(url, output_path) 29 | print(f'Successfully downloaded {note}') 30 | except Exception as e: 31 | print(f'Error downloading {note}: {str(e)}') 32 | 33 | if __name__ == '__main__': 34 | download_tonejs_samples() 35 | -------------------------------------------------------------------------------- /sounds/piano/piano-a3.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-a4.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-a5.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-c3.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-c4.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-c5.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-ds3.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-ds4.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-ds5.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-fs3.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-fs4.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /sounds/piano/piano-fs5.mp3: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 - File or directory not found. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

404 - File or directory not found.

25 |

The resource you are looking for might have been removed, had its name changed, or is temporarily unavailable.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /scripts/download_salamander.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from tqdm import tqdm 4 | 5 | def download_piano_samples(): 6 | # 创建samples目录(如果不存在) 7 | samples_dir = 'samples' 8 | if not os.path.exists(samples_dir): 9 | os.makedirs(samples_dir) 10 | 11 | # 需要下载的音符列表 12 | notes = ['B4', 'D4', 'E4', 'G4', 'D5', 'E5', 'G5'] 13 | 14 | # Tone.js piano samples的基础URL 15 | base_url = "https://raw.githubusercontent.com/nbrosowsky/tonejs-instruments/master/samples/piano" 16 | 17 | for note in tqdm(notes, desc="Downloading piano samples"): 18 | filename = f"piano-{note}.mp3" 19 | filepath = os.path.join(samples_dir, filename) 20 | 21 | # 如果文件已存在,跳过 22 | if os.path.exists(filepath): 23 | print(f"Skipping {filename} - already exists") 24 | continue 25 | 26 | # 构建完整的URL 27 | url = f"{base_url}/{filename}" 28 | 29 | try: 30 | # 下载文件 31 | response = requests.get(url) 32 | response.raise_for_status() 33 | 34 | # 保存文件 35 | with open(filepath, 'wb') as f: 36 | f.write(response.content) 37 | 38 | print(f"Downloaded {filename}") 39 | 40 | except Exception as e: 41 | print(f"Error downloading {filename}: {str(e)}") 42 | 43 | if __name__ == "__main__": 44 | download_piano_samples() 45 | -------------------------------------------------------------------------------- /verify.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // 需要验证的音符列表 5 | const notes = [ 6 | 'A0', 'C1', 'D#1', 'F#1', 'A1', 'C2', 'D#2', 'F#2', 'A2', 'C3', 'D#3', 'F#3', 7 | 'A3', 'C4', 'D#4', 'F#4', 'A4', 'C5', 'D#5', 'F#5', 'A5', 'C6', 'D#6', 'F#6', 'A6' 8 | ]; 9 | 10 | // 验证文件 11 | console.log('Verifying downloaded files...'); 12 | const missingFiles = []; 13 | const invalidFiles = []; 14 | 15 | notes.forEach(note => { 16 | const filePath = path.join('samples', `piano-${note}.mp3`); 17 | 18 | // 检查文件是否存在 19 | if (!fs.existsSync(filePath)) { 20 | missingFiles.push(note); 21 | return; 22 | } 23 | 24 | // 检查文件大小 25 | const stats = fs.statSync(filePath); 26 | if (stats.size < 1000) { // 文件太小可能是损坏的 27 | invalidFiles.push(note); 28 | } 29 | }); 30 | 31 | if (missingFiles.length > 0) { 32 | console.error('\nMissing files:'); 33 | missingFiles.forEach(note => console.error(`- piano-${note}.mp3`)); 34 | } 35 | 36 | if (invalidFiles.length > 0) { 37 | console.error('\nPotentially corrupted files:'); 38 | invalidFiles.forEach(note => console.error(`- piano-${note}.mp3`)); 39 | } 40 | 41 | if (missingFiles.length === 0 && invalidFiles.length === 0) { 42 | console.log('\nAll files verified successfully!'); 43 | console.log(`Total files: ${notes.length}`); 44 | console.log('Piano samples are ready to use.'); 45 | } else { 46 | console.error('\nSome files need to be redownloaded.'); 47 | console.log('Please run setup.js again for the missing or corrupted files.'); 48 | } -------------------------------------------------------------------------------- /llms.txt: -------------------------------------------------------------------------------- 1 | # 钢琴在线学习平台 - 大语言模型爬虫指南 2 | # 更新日期: 2025-04-25 3 | 4 | # 所有大语言模型爬虫的默认规则 5 | User-Agent: * 6 | Allow: / 7 | Allow: /index.html 8 | Allow: /about.html 9 | Allow: /piano.html 10 | Allow: /practice.html 11 | Allow: /tutorial.html 12 | Allow: /tutorials.html 13 | Allow: /articles/ 14 | Disallow: /api/ 15 | Disallow: /_next/ 16 | Disallow: /static/ 17 | Disallow: /404 18 | Disallow: /500 19 | Disallow: /*.json$ 20 | Disallow: /samples/ 21 | Disallow: /sounds/ 22 | Disallow: /temp_download/ 23 | Disallow: /node_modules/ 24 | 25 | # OpenAI GPT爬虫规则 26 | User-Agent: GPTBot 27 | Allow: /index.html 28 | Allow: /about.html 29 | Allow: /piano.html 30 | Allow: /practice.html 31 | Allow: /tutorial.html 32 | Allow: /tutorials.html 33 | Allow: /articles/ 34 | Disallow: / 35 | 36 | # Anthropic Claude爬虫规则 37 | User-Agent: anthropic-ai 38 | Allow: /index.html 39 | Allow: /about.html 40 | Allow: /piano.html 41 | Allow: /practice.html 42 | Allow: /tutorial.html 43 | Allow: /tutorials.html 44 | Allow: /articles/ 45 | Disallow: / 46 | 47 | # Google Bard/Gemini爬虫规则 48 | User-Agent: Google-Extended 49 | Allow: / 50 | Disallow: /api/ 51 | Disallow: /_next/ 52 | Disallow: /static/ 53 | Disallow: /404 54 | Disallow: /500 55 | Disallow: /*.json$ 56 | Disallow: /samples/ 57 | Disallow: /sounds/ 58 | Disallow: /temp_download/ 59 | Disallow: /node_modules/ 60 | 61 | # 百度文心一言爬虫规则 62 | User-Agent: Baiduspider-ERNIE-Bot 63 | Allow: / 64 | Disallow: /api/ 65 | Disallow: /_next/ 66 | Disallow: /static/ 67 | Disallow: /404 68 | Disallow: /500 69 | Disallow: /*.json$ 70 | Disallow: /samples/ 71 | Disallow: /sounds/ 72 | Disallow: /temp_download/ 73 | Disallow: /node_modules/ 74 | -------------------------------------------------------------------------------- /test-audio.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Piano Audio Test 5 | 6 | 7 | 8 | 9 | 16 | 17 |

Piano Audio Test

18 | 19 | 20 | 21 | 22 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | // 创建项目结构 6 | const dirs = ['samples']; 7 | dirs.forEach(dir => { 8 | if (!fs.existsSync(dir)) { 9 | fs.mkdirSync(dir); 10 | console.log(`Created directory: ${dir}`); 11 | } 12 | }); 13 | 14 | // 定义要下载的音符 15 | const notes = [ 16 | 'A0', 'C1', 'D#1', 'F#1', 'A1', 'C2', 'D#2', 'F#2', 'A2', 'C3', 'D#3', 'F#3', 17 | 'A3', 'C4', 'D#4', 'F#4', 'A4', 'C5', 'D#5', 'F#5', 'A5', 'C6', 'D#6', 'F#6', 'A6' 18 | ]; 19 | 20 | // 下载进度追踪 21 | let downloadedCount = 0; 22 | const totalFiles = notes.length; 23 | 24 | // 下载单个文件的函数 25 | function downloadFile(note) { 26 | const url = `https://tonejs.github.io/audio/salamander/${note}.mp3`; 27 | const filePath = path.join('samples', `piano-${note}.mp3`); 28 | 29 | https.get(url, (response) => { 30 | if (response.statusCode === 200) { 31 | const fileStream = fs.createWriteStream(filePath); 32 | response.pipe(fileStream); 33 | 34 | fileStream.on('finish', () => { 35 | downloadedCount++; 36 | const progress = Math.round((downloadedCount / totalFiles) * 100); 37 | console.log(`Downloaded: piano-${note}.mp3 (${progress}% complete)`); 38 | 39 | if (downloadedCount === totalFiles) { 40 | console.log('\nAll piano samples downloaded successfully!'); 41 | console.log('Setup complete! You can now run the piano application.'); 42 | } 43 | }); 44 | } else { 45 | console.error(`Failed to download ${note}: HTTP ${response.statusCode}`); 46 | } 47 | }).on('error', (err) => { 48 | console.error(`Error downloading ${note}:`, err); 49 | }); 50 | } 51 | 52 | console.log('Starting piano samples download...'); 53 | notes.forEach(note => downloadFile(note)); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 钢琴在线 (Piano Online) 2 | 3 | ## 项目介绍 4 | 一个现代化的在线钢琴学习平台,让每个人都能轻松学习钢琴。基于纯HTML和TailwindCSS开发,提供专业的钢琴学习体验。 5 | 6 | ## 核心功能 7 | ### 🎹 在线演奏 8 | - 支持多种输入方式 9 | - 电脑键盘输入 10 | - 数字键盘输入 11 | - 鼠标点击演奏 12 | 13 | ### 🎼 智能练习模式 14 | - 交互式学习体验 15 | - 类似节奏游戏的学习模式 16 | - 实时按键提示 17 | - 自动跟踪演奏进度 18 | - 曲目练习功能 19 | - 可选择不同难度的曲目 20 | - 按键提示系统(类似俄罗斯方块) 21 | - 实时反馈演奏效果 22 | 23 | ## 技术实现 24 | - 前端技术 25 | - HTML5 26 | - TailwindCSS 27 | - 原生JavaScript 28 | 29 | ## 网站功能区域 30 | 1. Header - 导航栏 31 | 2. Hero - 主视觉区域 32 | 3. Features - 功能特性展示 33 | 4. How it works - 使用教程 34 | 5. Testimonials - 用户评价 35 | 6. FAQ - 常见问题 36 | 7. Footer - 页脚信息 37 | 38 | ## 设计理念 39 | - 🎨 视觉风格 40 | - 现代简约设计 41 | - 参考苹果官网的高端风格 42 | - 专业且真实的内容呈现 43 | - 📱 响应式设计 44 | - 完美适配各种屏幕尺寸 45 | - 移动端优化体验 46 | - 🌐 本地化 47 | - 全中文界面 48 | - 符合中国用户使用习惯 49 | 50 | ## 开发状态 51 | - ✅ 基础钢琴功能 52 | - ✅ 键盘映射系统 53 | - ✅ 练习模式框架 54 | - ✅ Google AdSense自动广告集成 55 | - ✅ Google Analytics数据追踪 56 | - 🚧 曲目库建设中 57 | - 🚧 用户系统开发中 58 | 59 | ## 如何开始 60 | 1. 克隆项目到本地 61 | 2. 确保安装了必要的开发工具 62 | 3. 使用Live Server运行项目 63 | 4. 访问 http://localhost:8000 开始体验 64 | 65 | ## 浏览器支持 66 | - Chrome (推荐) 67 | - Firefox 68 | - Safari 69 | - Edge 70 | 71 | ## 贡献指南 72 | 欢迎提交Issue和Pull Request来帮助改进项目。 73 | 74 | ## 许可证 75 | MIT License 76 | 77 | --- 78 | 79 | ## 更新日志 80 | 81 | ### 2025-10-26 82 | 83 | #### ✅ 谷歌广告集成完成 84 | - 为所有18个HTML页面添加了Google AdSense自动广告代码 85 | - 包含主要页面: index.html, about.html, piano.html, practice.html, tutorials.html 86 | - 包含13个教程文章页面 87 | - 集成Google Analytics追踪代码 (ID: G-EYGD99YB4Y) 88 | - 集成Google AdSense自动广告 (Publisher ID: ca-pub-4423187689700927) 89 | - 添加Google站点验证meta标签 90 | 91 | #### ✅ 全面SEO优化完成 92 | **目标**: 2周内UV从100提升到200+ 93 | 94 | **技术优化**: 95 | - ✅ 更新sitemap.xml,包含所有18个页面 96 | - ✅ 为所有页面添加Open Graph标签(Facebook分享优化) 97 | - ✅ 为所有页面添加Twitter Card标签 98 | - ✅ 首页添加Schema.org结构化数据(WebApplication + Course) 99 | - ✅ 添加资源预加载和DNS预解析 100 | - ✅ 优化所有页面的canonical标签 101 | 102 | **创建的文档**: 103 | - 📄 `SEO-OPTIMIZATION-GUIDE.md` - 完整的SEO优化和流量提升指南 104 | - 📄 `QUICK-ACTION-CHECKLIST.md` - 快速执行清单和每日任务 105 | 106 | **预期效果**: 107 | - 1周后: UV增长20-30% 108 | - 2周后: UV增长50-100% 109 | - 1个月后: UV达到200-300 110 | - 3个月后: UV达到500+ -------------------------------------------------------------------------------- /scripts/generate_samples.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.io import wavfile 3 | from scipy.signal import sawtooth 4 | import os 5 | 6 | def generate_piano_note(freq, duration=1.0, sample_rate=44100, amplitude=0.5): 7 | t = np.linspace(0, duration, int(sample_rate * duration)) 8 | 9 | # Generate a complex waveform that approximates a piano note 10 | fundamental = amplitude * np.sin(2 * np.pi * freq * t) 11 | second_harmonic = 0.5 * amplitude * np.sin(2 * np.pi * (2 * freq) * t) 12 | third_harmonic = 0.25 * amplitude * np.sin(2 * np.pi * (3 * freq) * t) 13 | 14 | # Combine harmonics 15 | note = fundamental + second_harmonic + third_harmonic 16 | 17 | # Apply envelope 18 | envelope = np.exp(-3 * t) 19 | note = note * envelope 20 | 21 | # Normalize 22 | note = note / np.max(np.abs(note)) 23 | 24 | return note 25 | 26 | def note_to_freq(note): 27 | # A4 = 440Hz 28 | # Each semitone is a factor of 2^(1/12) 29 | A4 = 440 30 | notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] 31 | octave = int(note[-1]) 32 | note_name = note[:-1] 33 | 34 | semitones_from_a4 = (octave - 4) * 12 + notes.index(note_name) - notes.index('A') 35 | return A4 * (2 ** (semitones_from_a4 / 12)) 36 | 37 | def generate_piano_samples(): 38 | if not os.path.exists('../samples'): 39 | os.makedirs('../samples') 40 | 41 | # Generate F notes from F1 to F6 42 | for octave in range(1, 7): 43 | note = f'F{octave}' 44 | freq = note_to_freq(note) 45 | audio_data = generate_piano_note(freq) 46 | 47 | # Convert to 16-bit PCM 48 | audio_data_16bit = (audio_data * 32767).astype(np.int16) 49 | 50 | # Save as WAV first 51 | wav_path = f'../samples/piano-{note}.wav' 52 | wavfile.write(wav_path, 44100, audio_data_16bit) 53 | 54 | # Convert to MP3 using ffmpeg 55 | mp3_path = f'../samples/piano-{note}.mp3' 56 | os.system(f'ffmpeg -i {wav_path} -codec:a libmp3lame -qscale:a 2 {mp3_path}') 57 | 58 | # Remove WAV file 59 | os.remove(wav_path) 60 | print(f'Generated {mp3_path}') 61 | 62 | if __name__ == '__main__': 63 | generate_piano_samples() 64 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | // 导入所需的模块 2 | import { Piano } from './piano.js'; 3 | import { PracticeMode } from './practice-mode.js'; 4 | import { PianoRecorder } from './recorder.js'; 5 | import { RhythmGame } from './rhythm-game.js'; 6 | import { Tutorial } from './tutorial.js'; 7 | 8 | // 等待 DOM 完全加载 9 | document.addEventListener('DOMContentLoaded', async () => { 10 | console.log('DOM Content Loaded'); 11 | 12 | try { 13 | // 1. 首先初始化钢琴实例 14 | console.log('Initializing Piano...'); 15 | const piano = new Piano(); 16 | await piano.audio.init(); 17 | console.log('Piano initialized'); 18 | 19 | // 2. 初始化练习模式 20 | console.log('Initializing Practice Mode...'); 21 | const practiceMode = new PracticeMode(piano); 22 | console.log('Practice Mode initialized'); 23 | 24 | // 3. 初始化录音功能 25 | console.log('Initializing Recorder...'); 26 | const recorder = new PianoRecorder(piano); 27 | console.log('Recorder initialized'); 28 | 29 | // 4. 初始化节奏大师游戏 30 | console.log('Initializing Rhythm Game...'); 31 | const rhythmGame = new RhythmGame(piano); 32 | console.log('Rhythm Game initialized'); 33 | 34 | // 5. 初始化首次使用教程 35 | console.log('Initializing Tutorial...'); 36 | const tutorial = new Tutorial(piano); 37 | console.log('Tutorial initialized'); 38 | 39 | // 等待首次交互恢复音频上下文 40 | const resumeAudio = async () => { 41 | if (piano?.audio?.context?.state === 'suspended') { 42 | try { 43 | await piano.audio.context.resume(); 44 | console.log('AudioContext resumed successfully'); 45 | } catch (error) { 46 | console.error('Failed to resume AudioContext:', error); 47 | } 48 | } 49 | }; 50 | ['click', 'keydown', 'touchstart'].forEach(event => { 51 | document.addEventListener(event, resumeAudio, { once: true }); 52 | }); 53 | 54 | // 页面卸载时释放资源 55 | window.addEventListener('beforeunload', () => { 56 | recorder?.dispose(); 57 | if (piano?.audio?.context) { 58 | piano.audio.context.close(); 59 | } 60 | }); 61 | 62 | // 将 tutorial 暴露到全局,方便调试 63 | window.pianoTutorial = tutorial; 64 | 65 | console.log('All components initialized successfully'); 66 | } catch (error) { 67 | console.error('Error during initialization:', error); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background: #f0f0f0; 5 | font-family: Arial, sans-serif; 6 | } 7 | 8 | .container { 9 | width: 100%; 10 | min-height: 100vh; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | padding: 20px 0; 15 | } 16 | 17 | .controls { 18 | margin-bottom: 30px; 19 | display: flex; 20 | gap: 15px; 21 | } 22 | 23 | .controls button, 24 | .controls select { 25 | padding: 8px 15px; 26 | border: none; 27 | border-radius: 4px; 28 | background: white; 29 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 30 | cursor: pointer; 31 | font-size: 14px; 32 | } 33 | 34 | #piano { 35 | position: relative; 36 | width: 100%; 37 | max-width: 1200px; 38 | height: 220px; 39 | background: linear-gradient(to bottom, #424242 0%, #232323 100%); 40 | padding: 30px 40px 20px; 41 | border-radius: 8px; 42 | box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5); 43 | } 44 | 45 | .keys { 46 | display: flex; 47 | position: relative; 48 | width: 100%; 49 | height: 100%; 50 | } 51 | 52 | /* 白键样式 */ 53 | .key.white { 54 | width: calc(100% / 52); /* 52个白键 */ 55 | height: 100%; 56 | background: linear-gradient(to bottom, #fff 0%, #eee 100%); 57 | border: 1px solid #ccc; 58 | border-radius: 0 0 4px 4px; 59 | box-shadow: 0 1px 1px rgba(0,0,0,0.1); 60 | z-index: 1; 61 | position: relative; 62 | } 63 | 64 | .key.white:hover { 65 | background: linear-gradient(to bottom, #fff 0%, #f2f2f2 100%); 66 | } 67 | 68 | .key.white:active, 69 | .key.white.active { 70 | background: linear-gradient(to bottom, #f2f2f2 0%, #e6e6e6 100%); 71 | box-shadow: inset 0 1px 3px rgba(0,0,0,0.2); 72 | transform: translateY(1px); 73 | } 74 | 75 | /* 黑键样式 */ 76 | .key.black { 77 | position: absolute; 78 | width: calc((100% / 52) * 0.65); 79 | height: 65%; 80 | background: linear-gradient(to bottom, #000 0%, #222 100%); 81 | border-radius: 0 0 3px 3px; 82 | z-index: 2; 83 | box-shadow: inset 0px -1px 2px rgba(255,255,255,0.2), 84 | 0 2px 3px rgba(0,0,0,0.6); 85 | } 86 | 87 | .key.black:hover { 88 | background: linear-gradient(to bottom, #222 0%, #333 100%); 89 | } 90 | 91 | .key.black:active, 92 | .key.black.active { 93 | background: linear-gradient(to bottom, #333 0%, #444 100%); 94 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.8); 95 | transform: translateY(1px); 96 | } 97 | 98 | /* 黑键位置计算 */ 99 | .key.black[data-note* 100 | 101 | #hint { 102 | margin-top: 20px; 103 | font-size: 18px; 104 | color: #666; 105 | } -------------------------------------------------------------------------------- /js/tutorial.js: -------------------------------------------------------------------------------- 1 | // 首次使用教程逻辑 2 | export class Tutorial { 3 | constructor(piano) { 4 | this.piano = piano; 5 | this.hasSeenTutorial = localStorage.getItem('piano-tutorial-completed') === 'true'; 6 | this.init(); 7 | } 8 | 9 | init() { 10 | // 如果用户已经看过教程,不显示 11 | if (this.hasSeenTutorial) { 12 | return; 13 | } 14 | 15 | // 延迟显示教程,给页面加载时间 16 | setTimeout(() => { 17 | this.show(); 18 | }, 1000); 19 | 20 | this.setupEventListeners(); 21 | } 22 | 23 | show() { 24 | const overlay = document.getElementById('tutorial-overlay'); 25 | if (overlay) { 26 | overlay.style.display = 'flex'; 27 | } 28 | } 29 | 30 | hide() { 31 | const overlay = document.getElementById('tutorial-overlay'); 32 | if (overlay) { 33 | overlay.style.display = 'none'; 34 | } 35 | } 36 | 37 | complete() { 38 | localStorage.setItem('piano-tutorial-completed', 'true'); 39 | this.hasSeenTutorial = true; 40 | this.hide(); 41 | } 42 | 43 | setupEventListeners() { 44 | // 跳过按钮 45 | const skipButton = document.getElementById('tutorial-skip'); 46 | if (skipButton) { 47 | skipButton.addEventListener('click', () => { 48 | this.complete(); 49 | }); 50 | } 51 | 52 | // 开始体验按钮 53 | const startButton = document.getElementById('tutorial-start'); 54 | if (startButton) { 55 | startButton.addEventListener('click', () => { 56 | this.complete(); 57 | }); 58 | } 59 | 60 | // 监听键盘按下 A 键 61 | const handleKeyPress = (e) => { 62 | if (e.key.toLowerCase() === 'a' && !this.hasSeenTutorial) { 63 | // 用户按下了 A 键,显示成功提示 64 | const keyDemo = document.querySelector('.tutorial-key-demo'); 65 | if (keyDemo) { 66 | keyDemo.style.background = 'linear-gradient(135deg, #4CAF50, #45a049)'; 67 | keyDemo.textContent = '✓ 太棒了!'; 68 | 69 | // 2秒后自动关闭教程 70 | setTimeout(() => { 71 | this.complete(); 72 | }, 2000); 73 | } 74 | 75 | // 移除监听器,避免重复触发 76 | document.removeEventListener('keydown', handleKeyPress); 77 | } 78 | }; 79 | 80 | document.addEventListener('keydown', handleKeyPress); 81 | } 82 | 83 | // 提供一个方法让用户可以重新显示教程 84 | static resetTutorial() { 85 | localStorage.removeItem('piano-tutorial-completed'); 86 | window.location.reload(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /generate_samples.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.io import wavfile 3 | import os 4 | from scipy.io.wavfile import write 5 | import subprocess 6 | 7 | def note_to_freq(note): 8 | # 音符到频率的映射 9 | notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] 10 | octave = int(note[-1]) 11 | note_name = note[:-1] 12 | 13 | if note_name not in notes: 14 | raise ValueError(f"Invalid note name: {note_name}") 15 | 16 | # A4 = 440Hz 17 | A4 = 440 18 | notes_from_A4 = notes.index(note_name) - notes.index('A') + (octave - 4) * 12 19 | 20 | return A4 * (2 ** (notes_from_A4 / 12)) 21 | 22 | def generate_piano_note(freq, duration=2.0, sample_rate=44100, decay_factor=5.0): 23 | """生成钢琴音色的音符""" 24 | t = np.linspace(0, duration, int(sample_rate * duration)) 25 | 26 | # 基频 27 | fundamental = np.sin(2 * np.pi * freq * t) 28 | 29 | # 添加泛音 30 | harmonics = [ 31 | 0.5 * np.sin(2 * np.pi * (2 * freq) * t), # 八度 32 | 0.3 * np.sin(2 * np.pi * (3 * freq) * t), # 十二度 33 | 0.2 * np.sin(2 * np.pi * (4 * freq) * t), # 双八度 34 | ] 35 | 36 | # 合并所有波形 37 | wave = fundamental + sum(harmonics) 38 | 39 | # 添加衰减包络 40 | envelope = np.exp(-decay_factor * t) 41 | wave = wave * envelope 42 | 43 | # 归一化 44 | wave = wave / np.max(np.abs(wave)) 45 | 46 | return wave 47 | 48 | def save_as_mp3(wav_path, mp3_path): 49 | """将WAV文件转换为MP3""" 50 | subprocess.run([ 51 | 'ffmpeg', '-i', wav_path, 52 | '-codec:a', 'libmp3lame', 53 | '-qscale:a', '2', 54 | mp3_path, 55 | '-y' # 覆盖已存在的文件 56 | ], capture_output=True) 57 | 58 | # 删除临时的WAV文件 59 | os.remove(wav_path) 60 | 61 | def generate_note_file(note, output_dir='samples'): 62 | """生成指定音符的音频文件""" 63 | if not os.path.exists(output_dir): 64 | os.makedirs(output_dir) 65 | 66 | freq = note_to_freq(note) 67 | wave = generate_piano_note(freq) 68 | 69 | # 首先保存为WAV 70 | wav_path = os.path.join(output_dir, f'temp_{note}.wav') 71 | mp3_path = os.path.join(output_dir, f'piano-{note}.mp3') 72 | 73 | # 保存为16位WAV 74 | wave = np.int16(wave * 32767) 75 | write(wav_path, 44100, wave) 76 | 77 | # 转换为MP3 78 | save_as_mp3(wav_path, mp3_path) 79 | print(f"Generated {mp3_path}") 80 | 81 | def main(): 82 | # 需要生成的音符列表 83 | notes = ['B4', 'D4', 'E4', 'G4', 'D5', 'E5', 'G5'] 84 | 85 | for note in notes: 86 | if not os.path.exists(f'samples/piano-{note}.mp3'): 87 | generate_note_file(note) 88 | else: 89 | print(f"Skipping piano-{note}.mp3 - already exists") 90 | 91 | if __name__ == '__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /generate_samples.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generate Piano Samples 5 | 6 | 7 | 8 |

Piano Sample Generator

9 | 10 |
11 |
12 | 13 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /css/tutorials.css: -------------------------------------------------------------------------------- 1 | /* 教程页面特定样式 */ 2 | .tutorials-hero { 3 | background-color: #f8f9fa; 4 | padding: 60px 0; 5 | text-align: center; 6 | } 7 | 8 | .tutorials-hero h1 { 9 | font-size: 2.5rem; 10 | color: #333; 11 | margin-bottom: 20px; 12 | } 13 | 14 | .tutorials-hero .subtitle { 15 | font-size: 1.2rem; 16 | color: #666; 17 | } 18 | 19 | .tutorial-categories { 20 | padding: 60px 0; 21 | } 22 | 23 | .category-grid { 24 | display: grid; 25 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 26 | gap: 30px; 27 | margin-top: 30px; 28 | } 29 | 30 | .category-card { 31 | background: #fff; 32 | border-radius: 10px; 33 | padding: 30px; 34 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 35 | } 36 | 37 | .category-card h2 { 38 | color: #333; 39 | font-size: 1.5rem; 40 | margin-bottom: 20px; 41 | border-bottom: 2px solid #007bff; 42 | padding-bottom: 10px; 43 | } 44 | 45 | .tutorial-list { 46 | list-style: none; 47 | padding: 0; 48 | margin: 0; 49 | } 50 | 51 | .tutorial-list li { 52 | margin-bottom: 15px; 53 | } 54 | 55 | .tutorial-list a { 56 | color: #444; 57 | text-decoration: none; 58 | display: block; 59 | padding: 8px 0; 60 | transition: color 0.3s; 61 | } 62 | 63 | .tutorial-list a:hover { 64 | color: #007bff; 65 | } 66 | 67 | .latest-tutorials { 68 | background-color: #f8f9fa; 69 | padding: 60px 0; 70 | } 71 | 72 | .latest-tutorials h2 { 73 | text-align: center; 74 | margin-bottom: 40px; 75 | color: #333; 76 | font-size: 2rem; 77 | } 78 | 79 | .tutorials-grid { 80 | display: grid; 81 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 82 | gap: 30px; 83 | } 84 | 85 | .tutorial-card { 86 | background: #fff; 87 | border-radius: 10px; 88 | overflow: hidden; 89 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 90 | transition: transform 0.3s; 91 | } 92 | 93 | .tutorial-card:hover { 94 | transform: translateY(-5px); 95 | } 96 | 97 | .tutorial-content { 98 | padding: 20px; 99 | } 100 | 101 | .tutorial-content h3 { 102 | color: #333; 103 | font-size: 1.3rem; 104 | margin-bottom: 15px; 105 | } 106 | 107 | .tutorial-content p { 108 | color: #666; 109 | margin-bottom: 20px; 110 | line-height: 1.6; 111 | } 112 | 113 | .read-more { 114 | display: inline-block; 115 | color: #007bff; 116 | text-decoration: none; 117 | font-weight: 500; 118 | transition: color 0.3s; 119 | } 120 | 121 | .read-more:hover { 122 | color: #0056b3; 123 | } 124 | 125 | .site-footer { 126 | background-color: #333; 127 | color: #fff; 128 | padding: 20px 0; 129 | text-align: center; 130 | } 131 | 132 | /* 响应式设计 */ 133 | @media (max-width: 768px) { 134 | .category-grid, 135 | .tutorials-grid { 136 | grid-template-columns: 1fr; 137 | } 138 | 139 | .tutorials-hero h1 { 140 | font-size: 2rem; 141 | } 142 | 143 | .category-card, 144 | .tutorial-card { 145 | margin-bottom: 20px; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /piano.css: -------------------------------------------------------------------------------- 1 | /* 钢琴容器 */ 2 | .piano-container { 3 | background: #1a1a1a; 4 | padding: 20px; 5 | border-radius: 8px; 6 | position: relative; 7 | height: 300px; 8 | overflow: hidden; 9 | } 10 | 11 | /* 钢琴键盘 */ 12 | .piano { 13 | position: relative; 14 | width: 100%; 15 | height: 220px; 16 | background: linear-gradient(to bottom, #1a1a1a, #000); 17 | border-radius: 8px 8px 5px 5px; 18 | box-shadow: 0 0 50px rgba(0, 0, 0, 0.5); 19 | transform-style: preserve-3d; 20 | transform: rotateX(2deg); 21 | } 22 | 23 | /* 钢琴白键 */ 24 | .octave { 25 | position: relative; 26 | height: 100%; 27 | display: inline-block; 28 | width: 33.33%; /* 3个八度 */ 29 | } 30 | 31 | .piano-key { 32 | position: relative; 33 | transition: all 0.1s; 34 | cursor: pointer; 35 | box-sizing: border-box; 36 | } 37 | 38 | .piano-key.white { 39 | background: white; 40 | height: 100%; 41 | width: 14.28%; /* 7个白键 */ 42 | display: inline-block; 43 | border-radius: 0 0 4px 4px; 44 | border: 1px solid #ccc; 45 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 46 | vertical-align: top; 47 | position: relative; 48 | z-index: 1; 49 | } 50 | 51 | .piano-key.black { 52 | background: #333; 53 | height: 60%; 54 | width: 8%; 55 | position: absolute; 56 | top: 0; 57 | z-index: 2; 58 | border-radius: 0 0 3px 3px; 59 | border: 1px solid #000; 60 | } 61 | 62 | .key-label { 63 | position: absolute; 64 | bottom: 10px; 65 | left: 50%; 66 | transform: translateX(-50%); 67 | font-size: 12px; 68 | color: #666; 69 | pointer-events: none; 70 | font-weight: 500; 71 | text-align: center; 72 | width: 100%; 73 | white-space: pre-line; 74 | } 75 | 76 | .piano-key.black .key-label { 77 | color: #fff; 78 | bottom: 20px; 79 | } 80 | 81 | .piano-key.white:active, 82 | .piano-key.white.pressed { 83 | background: #f0f0f0; 84 | transform: translateY(2px); 85 | } 86 | 87 | .piano-key.black:active, 88 | .piano-key.black.pressed { 89 | background: #222; 90 | transform: translateY(2px); 91 | } 92 | 93 | /* 钢琴外壳 */ 94 | .piano-case { 95 | position: relative; 96 | width: 100%; 97 | height: 50px; 98 | background: linear-gradient(to bottom, #4a4a4a, #000); 99 | border-radius: 8px 8px 0 0; 100 | box-shadow: 101 | inset 0 1px 0px rgba(255,255,255,0.1), 102 | 0 -1px 0px rgba(0,0,0,0.3); 103 | } 104 | 105 | /* 钢琴品牌标志 */ 106 | .piano-brand { 107 | position: absolute; 108 | top: 50%; 109 | left: 50%; 110 | transform: translate(-50%, -50%); 111 | color: rgba(255,255,255,0.8); 112 | font-family: "Times New Roman", serif; 113 | font-size: 24px; 114 | letter-spacing: 2px; 115 | text-shadow: 0 1px 2px rgba(0,0,0,0.5); 116 | } 117 | 118 | /* 键盘提示 */ 119 | .key-hint { 120 | position: absolute; 121 | bottom: 10px; 122 | left: 50%; 123 | transform: translateX(-50%); 124 | color: #666; 125 | font-size: 12px; 126 | font-weight: bold; 127 | } 128 | 129 | /* 钢琴底座 */ 130 | .piano-stand { 131 | width: 100%; 132 | height: 20px; 133 | background: linear-gradient(to bottom, #333, #000); 134 | border-radius: 0 0 8px 8px; 135 | box-shadow: 0 2px 6px rgba(0,0,0,0.3); 136 | } 137 | -------------------------------------------------------------------------------- /TESTING_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Testing Checklist for Piano Online UX Improvements 2 | 3 | ## Date: 2025-11-04 4 | 5 | ## Changes Summary 6 | 1. ✅ Moved piano keyboard to first screen (above the fold) 7 | 2. ✅ Compressed hero section to minimal text 8 | 3. ✅ Added keyboard key labels (A, S, D, etc.) on piano keys 9 | 4. ✅ Created first-time user tutorial overlay 10 | 5. ✅ Simplified terminology with tooltips 11 | 6. ✅ Added visual feedback (success/error animations) for key presses 12 | 13 | ## Manual Testing Required 14 | 15 | ### Desktop Testing (1920x1080) 16 | - [ ] Navigate to homepage 17 | - [ ] Verify piano keyboard is visible without scrolling 18 | - [ ] Verify hero text is concise: "用电脑键盘弹钢琴 🎹" 19 | - [ ] Check tutorial overlay appears on first visit 20 | - [ ] Press 'A' key during tutorial - should trigger success feedback 21 | - [ ] Close tutorial and verify it doesn't show again 22 | - [ ] Verify keyboard labels visible on all white keys (A, S, D, F, G, H, J, K, L) 23 | - [ ] Verify keyboard labels visible on black keys (1, 2, 4, 5, 6, 8, 9) 24 | - [ ] Hover over "声音延长 🎵" - tooltip should appear 25 | - [ ] Hover over "🎮 游戏模式" - tooltip should appear 26 | - [ ] Hover over "✨ 跟着提示弹" - tooltip should appear 27 | 28 | ### Practice Mode Testing 29 | - [ ] Select a song from dropdown (e.g., "⭐ 小星星 (简单)") 30 | - [ ] Click "✨ 跟着提示弹" button 31 | - [ ] Press the correct key - should see green glow animation 32 | - [ ] Press an incorrect key - should see shake/red border animation 33 | - [ ] Complete a song - should see completion message with stats 34 | 35 | ### Tablet Testing (768px) 36 | - [ ] Piano keyboard visible without scrolling 37 | - [ ] Tutorial overlay responsive and readable 38 | - [ ] All tooltips work on tap/hover 39 | - [ ] Keyboard labels readable 40 | 41 | ### Mobile Testing (375px) 42 | - [ ] Piano keyboard visible (horizontal scroll acceptable) 43 | - [ ] Tutorial steps stack vertically 44 | - [ ] All buttons accessible 45 | - [ ] Key labels still visible 46 | 47 | ### First-Time User Experience 48 | - [ ] Clear localStorage: `localStorage.removeItem('piano-tutorial-completed')` 49 | - [ ] Refresh page 50 | - [ ] Tutorial should appear after ~1 second 51 | - [ ] Tutorial content is clear and actionable 52 | - [ ] "A" key demo pulses/animates 53 | - [ ] "跳过" button works 54 | - [ ] "开始体验" button works 55 | - [ ] After closing, tutorial doesn't reappear 56 | 57 | ### Accessibility 58 | - [ ] All buttons have clear labels 59 | - [ ] Tooltips provide helpful explanations 60 | - [ ] Visual feedback is obvious and distinct 61 | - [ ] No jargon without explanation 62 | 63 | ## Browser Compatibility 64 | - [ ] Chrome (latest) 65 | - [ ] Firefox (latest) 66 | - [ ] Safari (latest) 67 | - [ ] Edge (latest) 68 | 69 | ## Performance 70 | - [ ] Page loads quickly (< 3 seconds) 71 | - [ ] No console errors 72 | - [ ] Smooth animations 73 | - [ ] Audio plays without delay 74 | 75 | ## Issues Found 76 | (List any issues discovered during testing) 77 | 78 | --- 79 | 80 | ## Test Server 81 | Local server running at: http://localhost:8080 82 | 83 | To test: 84 | 1. Open browser 85 | 2. Navigate to http://localhost:8080 86 | 3. Clear localStorage if needed: `localStorage.clear()` 87 | 4. Follow checklist above 88 | 89 | ## Rollback Instructions 90 | If critical issues found: 91 | ```bash 92 | cp index.backup.html index.html 93 | git checkout -- css/piano.css js/piano.js js/main.js js/practice-mode.js 94 | rm js/tutorial.js 95 | ``` 96 | -------------------------------------------------------------------------------- /js/staff.js: -------------------------------------------------------------------------------- 1 | // 五线谱渲染工具 2 | class StaffRenderer { 3 | constructor(container) { 4 | this.container = container; 5 | this.vf = null; 6 | this.context = null; 7 | this.stave = null; 8 | this.init(); 9 | } 10 | 11 | init() { 12 | // 清空容器 13 | this.container.innerHTML = ''; 14 | 15 | // 创建渲染器 16 | const renderer = new Vex.Flow.Renderer(this.container, Vex.Flow.Renderer.Backends.SVG); 17 | renderer.resize(500, 150); 18 | this.context = renderer.getContext(); 19 | 20 | // 创建五线谱 21 | this.stave = new Vex.Flow.Stave(10, 40, 480); 22 | this.stave.addClef('treble').addTimeSignature('4/4'); 23 | } 24 | 25 | // 将音符名称转换为VexFlow音符 26 | convertToVexFlowNotes(noteNames) { 27 | return noteNames.map(noteName => { 28 | // 解析音符名称(例如:'C4'变成'c/4') 29 | const pitch = noteName.slice(0, -1).toLowerCase(); 30 | const octave = noteName.slice(-1); 31 | return new Vex.Flow.StaveNote({ 32 | clef: 'treble', 33 | keys: [`${pitch}/${octave}`], 34 | duration: 'q' 35 | }); 36 | }); 37 | } 38 | 39 | // 渲染音符 40 | renderNotes(noteNames) { 41 | // 清除之前的内容 42 | this.init(); 43 | 44 | // 创建音符 45 | const notes = this.convertToVexFlowNotes(noteNames); 46 | 47 | // 创建声部 48 | const voice = new Vex.Flow.Voice({ 49 | num_beats: notes.length, 50 | beat_value: 4 51 | }); 52 | voice.addTickables(notes); 53 | 54 | // 创建格式化器 55 | const formatter = new Vex.Flow.Formatter(); 56 | formatter.joinVoices([voice]).format([voice], 400); 57 | 58 | // 绘制五线谱和音符 59 | this.stave.setContext(this.context).draw(); 60 | voice.draw(this.context, this.stave); 61 | } 62 | 63 | // 清除五线谱 64 | clear() { 65 | this.container.innerHTML = ''; 66 | } 67 | } 68 | 69 | // 音符工具类 70 | class NoteUtils { 71 | static noteNames = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; 72 | static octaves = ['3', '4', '5']; 73 | 74 | // 生成随机音符 75 | static generateRandomNotes(difficulty, count) { 76 | const notes = []; 77 | let availableNotes; 78 | 79 | switch(difficulty) { 80 | case 'beginner': 81 | // 初级:只使用中央C周围的音符 82 | availableNotes = this.noteNames.map(note => `${note}4`); 83 | break; 84 | case 'intermediate': 85 | // 中级:使用两个八度的音符 86 | availableNotes = this.noteNames.flatMap(note => 87 | ['4', '5'].map(octave => `${note}${octave}`) 88 | ); 89 | break; 90 | case 'advanced': 91 | // 高级:使用三个八度的音符 92 | availableNotes = this.noteNames.flatMap(note => 93 | this.octaves.map(octave => `${note}${octave}`) 94 | ); 95 | break; 96 | default: 97 | availableNotes = this.noteNames.map(note => `${note}4`); 98 | } 99 | 100 | // 生成指定数量的随机音符 101 | for(let i = 0; i < count; i++) { 102 | const randomIndex = Math.floor(Math.random() * availableNotes.length); 103 | notes.push(availableNotes[randomIndex]); 104 | } 105 | 106 | return notes; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /js/audio-generator.js: -------------------------------------------------------------------------------- 1 | // 音频生成器类 2 | class AudioGenerator { 3 | constructor() { 4 | this.audioContext = null; 5 | this.masterGain = null; 6 | this.init(); 7 | } 8 | 9 | init() { 10 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); 11 | this.masterGain = this.audioContext.createGain(); 12 | this.masterGain.connect(this.audioContext.destination); 13 | } 14 | 15 | // 生成节拍器声音 16 | createMetronomeSound(isAccent = false) { 17 | const osc = this.audioContext.createOscillator(); 18 | const gainNode = this.audioContext.createGain(); 19 | 20 | osc.connect(gainNode); 21 | gainNode.connect(this.masterGain); 22 | 23 | // 重拍使用较低的频率和较大的音量 24 | osc.frequency.value = isAccent ? 880 : 440; 25 | gainNode.gain.value = isAccent ? 0.5 : 0.3; 26 | 27 | osc.start(); 28 | 29 | // 快速衰减 30 | gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.1); 31 | osc.stop(this.audioContext.currentTime + 0.1); 32 | } 33 | 34 | // 生成钢琴音色 35 | createPianoSound(frequency, duration = 1) { 36 | const osc = this.audioContext.createOscillator(); 37 | const gainNode = this.audioContext.createGain(); 38 | 39 | osc.connect(gainNode); 40 | gainNode.connect(this.masterGain); 41 | 42 | // 使用正弦波模拟钢琴音色 43 | osc.type = 'sine'; 44 | osc.frequency.value = frequency; 45 | 46 | // 添加包络 47 | gainNode.gain.setValueAtTime(0.8, this.audioContext.currentTime); 48 | gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); 49 | 50 | osc.start(); 51 | osc.stop(this.audioContext.currentTime + duration); 52 | } 53 | 54 | // 音符名称转换为频率 55 | noteToFrequency(note) { 56 | const notes = { 57 | 'C': 0, 'C#': 1, 'Db': 1, 58 | 'D': 2, 'D#': 3, 'Eb': 3, 59 | 'E': 4, 60 | 'F': 5, 'F#': 6, 'Gb': 6, 61 | 'G': 7, 'G#': 8, 'Ab': 8, 62 | 'A': 9, 'A#': 10, 'Bb': 10, 63 | 'B': 11 64 | }; 65 | 66 | // 处理音符名称,支持#和b 67 | let noteName = note.slice(0, -1); 68 | if (noteName.length > 1 && (noteName[1] === '#' || noteName[1] === 'b')) { 69 | noteName = noteName.slice(0, 2); 70 | } else { 71 | noteName = noteName[0]; 72 | } 73 | 74 | const octave = parseInt(note.slice(-1)); 75 | 76 | if (!notes.hasOwnProperty(noteName) || isNaN(octave)) { 77 | console.error('Invalid note:', note); 78 | return 440; // 返回默认频率 A4 79 | } 80 | 81 | // A4 = 440Hz 82 | const A4 = 440; 83 | const A4_INDEX = 9 + 4 * 12; // A4的MIDI音符编号 84 | const noteIndex = notes[noteName] + octave * 12; 85 | 86 | return A4 * Math.pow(2, (noteIndex - A4_INDEX) / 12); 87 | } 88 | 89 | // 播放音符 90 | playNote(note, duration = 1) { 91 | try { 92 | const frequency = this.noteToFrequency(note); 93 | if (isFinite(frequency) && frequency > 0) { 94 | this.createPianoSound(frequency, duration); 95 | } else { 96 | console.error('Invalid frequency for note:', note); 97 | } 98 | } catch (error) { 99 | console.error('Error playing note:', note, error); 100 | } 101 | } 102 | 103 | // 播放和弦 104 | playChord(notes, duration = 1) { 105 | try { 106 | notes.forEach(note => { 107 | if (note && typeof note === 'string') { 108 | this.playNote(note, duration); 109 | } 110 | }); 111 | } catch (error) { 112 | console.error('Error playing chord:', notes, error); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /css/article.css: -------------------------------------------------------------------------------- 1 | /* 文章页面样式 */ 2 | .tutorial-article { 3 | padding-top: 80px; 4 | padding-bottom: var(--spacing-xl); 5 | } 6 | 7 | .article-header { 8 | text-align: center; 9 | margin-bottom: var(--spacing-xl); 10 | padding: var(--spacing-xl) 0; 11 | background: linear-gradient(135deg, #f8f9fa 0%, #e8f0fe 100%); 12 | border-radius: var(--border-radius); 13 | } 14 | 15 | .article-header h1 { 16 | font-size: 2.5rem; 17 | color: var(--text-primary); 18 | margin-bottom: var(--spacing-md); 19 | } 20 | 21 | .article-meta { 22 | color: var(--text-secondary); 23 | font-size: 0.9rem; 24 | display: flex; 25 | justify-content: center; 26 | gap: var(--spacing-md); 27 | } 28 | 29 | .article-content { 30 | max-width: 800px; 31 | margin: 0 auto; 32 | line-height: 1.8; 33 | } 34 | 35 | .content-section { 36 | margin-bottom: var(--spacing-xl); 37 | } 38 | 39 | .content-section h2 { 40 | font-size: 1.8rem; 41 | color: var(--text-primary); 42 | margin-bottom: var(--spacing-lg); 43 | padding-bottom: var(--spacing-sm); 44 | border-bottom: 2px solid var(--primary-color); 45 | } 46 | 47 | .content-section h3 { 48 | font-size: 1.4rem; 49 | color: var(--text-primary); 50 | margin: var(--spacing-lg) 0 var(--spacing-md); 51 | } 52 | 53 | .content-section p { 54 | margin-bottom: var(--spacing-md); 55 | color: var(--text-secondary); 56 | } 57 | 58 | .content-section ul, 59 | .content-section ol { 60 | margin-bottom: var(--spacing-md); 61 | padding-left: var(--spacing-lg); 62 | } 63 | 64 | .content-section li { 65 | margin-bottom: var(--spacing-sm); 66 | color: var(--text-secondary); 67 | } 68 | 69 | .tip-box { 70 | background: var(--background-dark); 71 | padding: var(--spacing-lg); 72 | border-radius: var(--border-radius); 73 | margin: var(--spacing-lg) 0; 74 | border-left: 4px solid var(--primary-color); 75 | } 76 | 77 | .tip-box h4 { 78 | color: var(--primary-color); 79 | margin-bottom: var(--spacing-sm); 80 | } 81 | 82 | .practice-suggestion { 83 | background: var(--background-dark); 84 | padding: var(--spacing-lg); 85 | border-radius: var(--border-radius); 86 | margin: var(--spacing-lg) 0; 87 | border-left: 4px solid var(--accent-color); 88 | } 89 | 90 | .practice-suggestion h4 { 91 | color: var(--accent-color); 92 | margin-bottom: var(--spacing-sm); 93 | } 94 | 95 | .practice-plan { 96 | background: var(--background-dark); 97 | padding: var(--spacing-lg); 98 | border-radius: var(--border-radius); 99 | margin: var(--spacing-lg) 0; 100 | } 101 | 102 | .article-footer { 103 | max-width: 800px; 104 | margin: var(--spacing-xl) auto 0; 105 | padding-top: var(--spacing-lg); 106 | border-top: 1px solid var(--background-dark); 107 | } 108 | 109 | .article-tags { 110 | margin-bottom: var(--spacing-lg); 111 | } 112 | 113 | .tag { 114 | display: inline-block; 115 | padding: var(--spacing-xs) var(--spacing-sm); 116 | background: var(--background-dark); 117 | color: var(--text-secondary); 118 | border-radius: var(--border-radius); 119 | margin-right: var(--spacing-xs); 120 | font-size: 0.9rem; 121 | } 122 | 123 | .article-navigation { 124 | display: flex; 125 | justify-content: space-between; 126 | gap: var(--spacing-md); 127 | } 128 | 129 | .nav-link { 130 | color: var(--primary-color); 131 | text-decoration: none; 132 | transition: var(--transition); 133 | } 134 | 135 | .nav-link:hover { 136 | color: var(--secondary-color); 137 | } 138 | 139 | /* 响应式设计 */ 140 | @media (max-width: 768px) { 141 | .article-header h1 { 142 | font-size: 2rem; 143 | } 144 | 145 | .article-meta { 146 | flex-direction: column; 147 | gap: var(--spacing-xs); 148 | } 149 | 150 | .article-content { 151 | padding: 0 var(--spacing-md); 152 | } 153 | 154 | .article-navigation { 155 | flex-direction: column; 156 | align-items: center; 157 | text-align: center; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tutorial.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | 8 | 16 | 17 | 18 |
19 |

钢琴基础知识

20 |
21 | 22 | 23 | 24 |
25 |

音乐理论基础

26 |
    27 |
  • 音程与和弦
  • 28 |
  • 调式与调性
  • 29 |
  • 节奏与拍子
  • 30 |
  • 速度与力度记号
  • 31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 |
43 |

实用弹奏技巧

44 |
    45 |
  • 踏板的使用方法
  • 46 |
  • 装饰音的弹奏
  • 47 |
  • 连音与断音技巧
  • 48 |
  • 音色的控制方法
  • 49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 | 57 | 58 |
59 |

互动练习区

60 |
61 |
62 |

节奏训练

63 |
64 | 65 |
66 |
67 | 68 |
69 |

音符识别

70 |
71 | 72 |
73 |
74 |
75 |
76 | 77 | 78 |
79 |

进阶练习技巧

80 |
81 |
82 |

表现力提升

83 |
    84 |
  • 情感表达方法
  • 85 |
  • 音乐句子处理
  • 86 |
  • 声部平衡控制
  • 87 |
  • 音色变化技巧
  • 88 |
89 |
90 | 91 |
92 |

练习效率提升

93 |
    94 |
  • 科学的练习方法
  • 95 |
  • 记忆力训练技巧
  • 96 |
  • 难点突破方法
  • 97 |
  • 演奏心理调节
  • 98 |
99 |
100 |
101 |
102 | 103 | 104 |
105 |

学习交流

106 |
107 |
108 |

讨论区

109 | 110 |
111 | 112 |
113 |

练习分享

114 | 115 |
116 |
117 |
118 |
119 |
-------------------------------------------------------------------------------- /sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | https://piano-online.com/ 6 | 2025-10-26 7 | daily 8 | 1.0 9 | 10 | 11 | https://piano-online.com/piano.html 12 | 2025-10-26 13 | weekly 14 | 0.9 15 | 16 | 17 | https://piano-online.com/practice.html 18 | 2025-10-26 19 | weekly 20 | 0.9 21 | 22 | 23 | https://piano-online.com/tutorials.html 24 | 2025-10-26 25 | weekly 26 | 0.8 27 | 28 | 29 | https://piano-online.com/about.html 30 | 2025-10-26 31 | monthly 32 | 0.6 33 | 34 | 35 | 36 | 37 | https://piano-online.com/articles/tutorials/piano-basics.html 38 | 2025-10-26 39 | monthly 40 | 0.8 41 | 42 | 43 | https://piano-online.com/articles/tutorials/reading-notes.html 44 | 2025-10-26 45 | monthly 46 | 0.8 47 | 48 | 49 | https://piano-online.com/articles/tutorials/finger-numbers.html 50 | 2025-10-26 51 | monthly 52 | 0.7 53 | 54 | 55 | https://piano-online.com/articles/tutorials/rhythm-basics.html 56 | 2025-10-26 57 | monthly 58 | 0.7 59 | 60 | 61 | 62 | 63 | https://piano-online.com/articles/tutorials/practice-methods.html 64 | 2025-10-26 65 | monthly 66 | 0.8 67 | 68 | 69 | https://piano-online.com/articles/tutorials/scales-practice.html 70 | 2025-10-26 71 | monthly 72 | 0.7 73 | 74 | 75 | https://piano-online.com/articles/tutorials/hand-coordination.html 76 | 2025-10-26 77 | monthly 78 | 0.7 79 | 80 | 81 | 82 | 83 | https://piano-online.com/articles/tutorials/music-theory.html 84 | 2025-10-26 85 | monthly 86 | 0.8 87 | 88 | 89 | https://piano-online.com/articles/tutorials/advanced-music-theory.html 90 | 2025-10-26 91 | monthly 92 | 0.7 93 | 94 | 95 | 96 | 97 | https://piano-online.com/articles/tutorials/sight-reading.html 98 | 2025-10-26 99 | monthly 100 | 0.7 101 | 102 | 103 | https://piano-online.com/articles/tutorials/performance-skills.html 104 | 2025-10-26 105 | monthly 106 | 0.7 107 | 108 | 109 | https://piano-online.com/articles/tutorials/music-analysis.html 110 | 2025-10-26 111 | monthly 112 | 0.7 113 | 114 | 115 | https://piano-online.com/articles/tutorials/piano-maintenance.html 116 | 2025-10-26 117 | monthly 118 | 0.6 119 | 120 | -------------------------------------------------------------------------------- /IMPLEMENTATION_PLAN.md: -------------------------------------------------------------------------------- 1 | # Piano Online UX Optimization Implementation Plan 2 | 3 | ## Goal 4 | Improve user onboarding and comprehension to ensure users understand: 5 | 1. This is a computer keyboard piano simulator (not mouse-based) 6 | 2. How keyboard keys map to piano keys 7 | 3. Core functionality is visible on first screen 8 | 4. Terminology is clear and accessible 9 | 10 | ## Stage 1: Homepage Layout Restructure ✅ 11 | **Goal**: Move piano keyboard to first screen (above the fold) 12 | **Success Criteria**: 13 | - Piano visible without scrolling on desktop (1920x1080) and tablets (768px+) 14 | - Hero text reduced to 1-2 lines 15 | - CTAs simplified to 3 clear buttons 16 | 17 | **Changes**: 18 | - Compress hero section to ~200px height 19 | - Remove verbose description from hero 20 | - Move piano-container from line 196 to immediately after hero (line ~110) 21 | - Add min-height constraints to ensure visibility 22 | 23 | **Status**: ✅ Completed 24 | 25 | ## Stage 2: Keyboard Key Labels on Piano Keys 26 | **Goal**: Show computer keyboard letters (A, S, D, F...) directly on piano keys 27 | **Success Criteria**: 28 | - Each white key shows corresponding keyboard letter 29 | - Labels are large, high-contrast, and always visible 30 | - Black keys show labels when applicable 31 | 32 | **Implementation**: 33 | - ✅ Modified `js/piano.js` generateKeyboard() to add key labels 34 | - ✅ Added CSS `.key-label` styling in piano.css 35 | - ✅ Labels display uppercase letters for white keys (A, S, D...) 36 | - ✅ Labels display numbers for black keys (1, 2, 4, 5...) 37 | 38 | **Status**: ✅ Completed 39 | 40 | ## Stage 3: First-Time User Guide Overlay 41 | **Goal**: Interactive tutorial that teaches users to press keyboard keys 42 | **Success Criteria**: 43 | - Shows on first visit (localStorage flag) 44 | - Animated highlight showing "Press 'A' key on your keyboard" 45 | - Dismissible but can be reopened via help button 46 | - Step-by-step guide (3-4 steps maximum) 47 | 48 | **Implementation**: 49 | - ✅ Created `js/tutorial.js` with Tutorial class 50 | - ✅ Added tutorial overlay HTML in index.html 51 | - ✅ Tutorial checks localStorage for completion flag 52 | - ✅ Shows after 1 second delay on first visit 53 | - ✅ "A" key demo with pulsing animation 54 | - ✅ 3-step guide explaining: labels → song selection → practice 55 | - ✅ Skip and Start buttons both dismiss tutorial 56 | - ✅ Auto-completes when user presses 'A' key 57 | 58 | **Status**: ✅ Completed 59 | 60 | ## Stage 4: Simplify Terminology & Add Tooltips 61 | **Goal**: Replace technical terms with beginner-friendly language 62 | **Success Criteria**: 63 | - All buttons and labels are self-explanatory 64 | - Tooltips explain advanced features 65 | - No musical jargon without explanation 66 | 67 | **Changes**: 68 | - ✅ "延音踏板" → "声音延长 🎵" with tooltip 69 | - ✅ "节奏大师模式" → "🎮 游戏模式" with tooltip 70 | - ✅ "开始练习" → "✨ 跟着提示弹" with tooltip 71 | - ✅ Added emoji to song options with difficulty labels 72 | - ✅ Implemented CSS tooltip system with [data-tooltip] 73 | 74 | **Status**: ✅ Completed 75 | 76 | ## Stage 5: Enhanced Visual Feedback 77 | **Goal**: Clear visual response when users press correct/incorrect keys 78 | **Success Criteria**: 79 | - Correct key press: green glow + animation 80 | - Incorrect key press: gentle shake + red border 81 | - Immediate visual feedback 82 | 83 | **Implementation**: 84 | - ✅ Added `.key.success` animation with green glow in piano.css 85 | - ✅ Added `.key.error` animation with shake effect in piano.css 86 | - ✅ Modified `practice-mode.js` handleNotePlayed() to add classes 87 | - ✅ Animations trigger on correct/incorrect key presses 88 | - ✅ Added completion message with accuracy stats 89 | 90 | **Status**: ✅ Completed 91 | 92 | ## Stage 6: Testing & Verification 93 | **Goal**: Ensure all changes work correctly 94 | **Success Criteria**: 95 | - First-time user can understand within 10 seconds 96 | - Piano keyboard visible on first screen (no scroll) 97 | - All tooltips work 98 | - Tutorial overlay functions properly 99 | - Responsive on mobile, tablet, desktop 100 | 101 | **Tests**: 102 | - [ ] Desktop 1920x1080 - piano visible without scroll 103 | - [ ] Tablet 768px - piano visible without scroll 104 | - [ ] Mobile 375px - piano visible (may need horizontal scroll) 105 | - [ ] Tutorial shows on first visit 106 | - [ ] Tutorial doesn't show on return visits 107 | - [ ] Keyboard labels visible on all keys 108 | - [ ] Tooltips appear on hover 109 | 110 | **Status**: Pending 111 | 112 | --- 113 | 114 | ## Implementation Order 115 | 1. Stage 1: Layout (highest impact, foundation for other changes) 116 | 2. Stage 2: Key labels (critical for understanding) 117 | 3. Stage 4: Terminology (quick wins, improves clarity) 118 | 4. Stage 3: Tutorial overlay (requires other elements in place) 119 | 5. Stage 5: Visual feedback (polish) 120 | 6. Stage 6: Testing 121 | 122 | ## Rollback Plan 123 | - Keep backup of original index.html as `index.backup.html` 124 | - Test changes locally before deployment 125 | - Each stage can be independently rolled back via git 126 | -------------------------------------------------------------------------------- /SEO-OPTIMIZATION-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Piano Online - SEO优化与流量提升指南 2 | 3 | ## 📊 当前状态 4 | - **日UV**: <100 5 | - **优化日期**: 2025-10-26 6 | - **目标**: 2周内UV翻倍至200+ 7 | 8 | --- 9 | 10 | ## ✅ 已完成的技术优化 11 | 12 | ### 1. Sitemap优化 13 | - ✅ 更新sitemap.xml,包含所有18个页面 14 | - ✅ 更新日期为2025-10-26 15 | - ✅ 设置合理的优先级和更新频率 16 | - **下一步**: 提交到Google Search Console和Bing Webmaster Tools 17 | 18 | ### 2. Open Graph和Twitter卡片 19 | - ✅ 所有页面添加完整的OG标签 20 | - ✅ 添加Twitter Card标签 21 | - ✅ 优化社交媒体分享效果 22 | - **效果**: 提升社交媒体分享点击率30-50% 23 | 24 | ### 3. 结构化数据(Schema.org) 25 | - ✅ 首页添加WebApplication结构化数据 26 | - ✅ 首页添加Course结构化数据 27 | - ✅ 文章页面添加Article类型标记 28 | - **效果**: 提升搜索结果展示效果,增加点击率 29 | 30 | ### 4. 性能优化 31 | - ✅ 添加DNS预解析(preconnect) 32 | - ✅ 优化资源加载顺序 33 | - **建议**: 添加图片懒加载,压缩CSS/JS 34 | 35 | --- 36 | 37 | ## 🚀 立即执行的营销策略 38 | 39 | ### 第1周: 搜索引擎优化 40 | 41 | #### Day 1-2: 提交到搜索引擎 42 | ```bash 43 | 1. Google Search Console 44 | - 提交sitemap: https://piano-online.com/sitemap.xml 45 | - 请求索引所有18个页面 46 | - 检查移动端适配性 47 | 48 | 2. Bing Webmaster Tools 49 | - 提交sitemap 50 | - 验证站点所有权 51 | 52 | 3. 百度站长平台 53 | - 提交sitemap 54 | - 提交URL 55 | ``` 56 | 57 | #### Day 3-5: 内容优化 58 | - [ ] 为每篇教程文章添加"相关推荐"模块 59 | - [ ] 在首页添加"热门教程"板块 60 | - [ ] 添加面包屑导航 61 | - [ ] 优化内链结构 62 | 63 | #### Day 6-7: 外链建设 64 | - [ ] 提交到音乐教育网站目录 65 | - [ ] 在知乎回答钢琴相关问题,带链接 66 | - [ ] 在小红书发布钢琴学习笔记 67 | 68 | ### 第2周: 内容营销 69 | 70 | #### 知乎营销策略 71 | **目标问题**: 72 | 1. "零基础如何自学钢琴?" 73 | 2. "有哪些好用的在线钢琴学习网站?" 74 | 3. "钢琴初学者应该如何练习?" 75 | 4. "在线钢琴和真实钢琴有什么区别?" 76 | 77 | **回答模板**: 78 | ``` 79 | 作为一个钢琴爱好者,我推荐几个方法: 80 | 81 | 1. [分享实用建议] 82 | 2. [分享个人经验] 83 | 3. [推荐工具] 我平时用Piano Online (piano-online.com) 练习... 84 | 85 | [详细说明优势] 86 | ``` 87 | 88 | #### 小红书营销策略 89 | **内容方向**: 90 | - "每天10分钟,在线学钢琴" 91 | - "上班族的钢琴学习日记" 92 | - "免费在线钢琴工具分享" 93 | - "钢琴入门必看教程" 94 | 95 | **发布频率**: 每周3-5篇 96 | 97 | #### B站营销策略 98 | - 制作钢琴教学视频 99 | - 在视频描述中添加网站链接 100 | - 制作"在线钢琴使用教程" 101 | 102 | --- 103 | 104 | ## 🎯 长尾关键词策略 105 | 106 | ### 当前问题 107 | 主关键词"在线钢琴"竞争激烈,难以排名 108 | 109 | ### 解决方案: 长尾关键词布局 110 | 111 | #### 高价值长尾词(建议创建新页面) 112 | 1. **"钢琴指法练习在线"** - 搜索量中等,竞争低 113 | 2. **"五线谱快速入门教程"** - 搜索量高,竞争中等 114 | 3. **"钢琴和弦练习工具"** - 搜索量中等,竞争低 115 | 4. **"儿童钢琴启蒙教程"** - 搜索量高,竞争中等 116 | 5. **"钢琴考级曲目练习"** - 搜索量高,竞争中等 117 | 118 | #### 实施方案 119 | ``` 120 | 创建新页面: 121 | - /practice/finger-exercises.html (钢琴指法练习) 122 | - /tutorials/chord-practice.html (和弦练习教程) 123 | - /tutorials/kids-piano.html (儿童钢琴启蒙) 124 | - /practice/exam-songs.html (考级曲目练习) 125 | ``` 126 | 127 | --- 128 | 129 | ## 📈 数据追踪与分析 130 | 131 | ### Google Analytics关键指标 132 | 监控以下数据: 133 | - [ ] 每日UV/PV 134 | - [ ] 跳出率(目标: <60%) 135 | - [ ] 平均停留时间(目标: >2分钟) 136 | - [ ] 热门页面 137 | - [ ] 流量来源 138 | 139 | ### Google Search Console关键指标 140 | - [ ] 搜索展示次数 141 | - [ ] 点击次数 142 | - [ ] 平均排名 143 | - [ ] 热门搜索词 144 | 145 | ### 每周检查清单 146 | ``` 147 | 周一: 148 | - 查看上周UV数据 149 | - 分析流量来源 150 | - 检查新增索引页面 151 | 152 | 周三: 153 | - 发布1-2篇知乎回答 154 | - 发布1篇小红书笔记 155 | 156 | 周五: 157 | - 检查外链建设进度 158 | - 优化表现差的页面 159 | ``` 160 | 161 | --- 162 | 163 | ## 🔧 技术优化待办事项 164 | 165 | ### 高优先级 166 | - [ ] 创建og-image.jpg (1200x630px) 167 | - [ ] 添加图片懒加载 168 | - [ ] 压缩CSS和JS文件 169 | - [ ] 添加Service Worker (PWA) 170 | - [ ] 优化移动端体验 171 | 172 | ### 中优先级 173 | - [ ] 添加面包屑导航 174 | - [ ] 创建404错误页面 175 | - [ ] 添加网站地图HTML版本 176 | - [ ] 优化字体加载 177 | 178 | ### 低优先级 179 | - [ ] 添加深色模式 180 | - [ ] 添加多语言支持 181 | - [ ] 集成社交分享按钮 182 | 183 | --- 184 | 185 | ## 💡 内容创作计划 186 | 187 | ### 每月新增内容 188 | **目标**: 每月新增4-8篇高质量教程 189 | 190 | #### 11月内容计划 191 | 1. "钢琴考级1-10级曲目推荐" 192 | 2. "成人零基础学钢琴完整指南" 193 | 3. "钢琴练习常见错误及纠正方法" 194 | 4. "如何选择适合自己的钢琴教材" 195 | 5. "钢琴即兴伴奏入门教程" 196 | 6. "流行歌曲钢琴改编技巧" 197 | 7. "钢琴演奏中的情感表达" 198 | 8. "如何提高钢琴视奏能力" 199 | 200 | #### 内容创作原则 201 | - 每篇文章1500-3000字 202 | - 包含实用的练习方法 203 | - 添加图片或视频演示 204 | - 优化SEO关键词 205 | - 添加内链到相关教程 206 | 207 | --- 208 | 209 | ## 🎁 用户增长策略 210 | 211 | ### 1. 社群运营 212 | - 创建微信群/QQ群 213 | - 定期分享学习资料 214 | - 组织线上打卡活动 215 | 216 | ### 2. 激励机制 217 | - 添加学习打卡功能 218 | - 设置学习成就系统 219 | - 提供学习证书 220 | 221 | ### 3. 用户留存 222 | - 发送每周学习报告 223 | - 推送个性化练习建议 224 | - 创建学习社区 225 | 226 | --- 227 | 228 | ## 📊 预期效果时间表 229 | 230 | ### 第1周 231 | - Google收录所有页面 232 | - 社交媒体分享效果提升 233 | 234 | ### 第2周 235 | - 开始出现长尾词排名 236 | - UV增长20-30% 237 | 238 | ### 第4周 239 | - 主要关键词排名提升 240 | - UV增长50-100% 241 | 242 | ### 第8周 243 | - 稳定的自然流量 244 | - UV达到200-300 245 | 246 | ### 第12周 247 | - 建立品牌认知度 248 | - UV达到500+ 249 | 250 | --- 251 | 252 | ## ⚠️ 注意事项 253 | 254 | ### 避免的错误 255 | 1. ❌ 不要购买垃圾外链 256 | 2. ❌ 不要过度堆砌关键词 257 | 3. ❌ 不要抄袭其他网站内容 258 | 4. ❌ 不要忽视移动端体验 259 | 5. ❌ 不要频繁修改标题和描述 260 | 261 | ### 最佳实践 262 | 1. ✅ 持续产出高质量内容 263 | 2. ✅ 积极回应用户反馈 264 | 3. ✅ 定期更新旧内容 265 | 4. ✅ 建立自然的外链 266 | 5. ✅ 优化用户体验 267 | 268 | --- 269 | 270 | ## 📞 需要帮助? 271 | 272 | 如果在执行过程中遇到问题: 273 | 1. 检查Google Search Console错误报告 274 | 2. 使用PageSpeed Insights检测性能 275 | 3. 使用Google Rich Results Test验证结构化数据 276 | 4. 使用Mobile-Friendly Test检查移动端适配 277 | 278 | --- 279 | 280 | ## 🎯 本月目标 281 | 282 | - [ ] 完成所有技术优化 283 | - [ ] 发布10篇知乎回答 284 | - [ ] 发布15篇小红书笔记 285 | - [ ] 新增4篇教程文章 286 | - [ ] UV达到150+ 287 | 288 | **记住**: SEO是长期工作,坚持3个月才能看到显著效果! 289 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const songs = { 2 | 'twinkle': { 3 | notes: ['C4', 'C4', 'G4', 'G4', 'A4', 'A4', 'G4'], 4 | name: '小星星' 5 | }, 6 | 'happy-birthday': { 7 | notes: ['C4', 'C4', 'D4', 'C4', 'F4', 'E4'], 8 | name: '生日快乐' 9 | } 10 | }; 11 | 12 | class Piano { 13 | constructor() { 14 | // 初始化音频上下文 15 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); 16 | this.masterGain = this.audioContext.createGain(); 17 | this.masterGain.connect(this.audioContext.destination); 18 | 19 | // 钢琴音色配置 20 | this.samples = {}; 21 | this.isLoading = true; 22 | this.loadSamples(); 23 | 24 | // 当前按下的键 25 | this.activeNotes = new Map(); 26 | 27 | // 录制相关 28 | this.isRecording = false; 29 | this.recordedNotes = []; 30 | this.startTime = null; 31 | 32 | this.initializeListeners(); 33 | } 34 | 35 | async loadSamples() { 36 | const notes = [ 37 | 'A0', 'C1', 'D#1', 'F#1', 'A1', 'C2', 'D#2', 'F#2', 'A2', 'C3', 'D#3', 'F#3', 38 | 'A3', 'C4', 'D#4', 'F#4', 'A4', 'C5', 'D#5', 'F#5', 'A5', 'C6', 'D#6', 'F#6', 'A6' 39 | ]; 40 | 41 | try { 42 | const loadPromises = notes.map(async note => { 43 | const response = await fetch(`samples/piano-${note}.mp3`); 44 | const arrayBuffer = await response.arrayBuffer(); 45 | const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); 46 | this.samples[note] = audioBuffer; 47 | }); 48 | 49 | await Promise.all(loadPromises); 50 | this.isLoading = false; 51 | console.log('Piano samples loaded successfully'); 52 | } catch (error) { 53 | console.error('Error loading piano samples:', error); 54 | } 55 | } 56 | 57 | getNearestSample(note) { 58 | // 获取最接近的采样音符 59 | const noteNumber = this.getNoteNumber(note); 60 | const samples = Object.keys(this.samples); 61 | let closest = samples[0]; 62 | 63 | samples.forEach(sample => { 64 | if (Math.abs(this.getNoteNumber(sample) - noteNumber) < 65 | Math.abs(this.getNoteNumber(closest) - noteNumber)) { 66 | closest = sample; 67 | } 68 | }); 69 | 70 | return this.samples[closest]; 71 | } 72 | 73 | initializeListeners() { 74 | document.addEventListener('keydown', (e) => { 75 | if (e.repeat) return; 76 | this.handleKeyDown(e); 77 | }); 78 | 79 | document.addEventListener('keyup', (e) => { 80 | this.handleKeyUp(e); 81 | }); 82 | 83 | document.getElementById('record-btn').addEventListener('click', () => { 84 | this.toggleRecording(); 85 | }); 86 | 87 | document.getElementById('play-btn').addEventListener('click', () => { 88 | this.playRecording(); 89 | }); 90 | } 91 | 92 | handleKeyDown(e) { 93 | const key = document.querySelector(`.key[data-key="${e.keyCode}"]`); 94 | if (!key) return; 95 | 96 | const note = key.dataset.note; 97 | this.playNote(note); 98 | key.classList.add('active'); 99 | 100 | if (this.isRecording) { 101 | this.recordNote(note); 102 | } 103 | } 104 | 105 | handleKeyUp(e) { 106 | const key = document.querySelector(`.key[data-key="${e.keyCode}"]`); 107 | if (key) { 108 | key.classList.remove('active'); 109 | } 110 | } 111 | 112 | playNote(note) { 113 | const sample = this.getNearestSample(note); 114 | const source = this.audioContext.createBufferSource(); 115 | source.buffer = sample; 116 | source.connect(this.masterGain); 117 | source.start(); 118 | } 119 | 120 | toggleRecording() { 121 | this.isRecording = !this.isRecording; 122 | const btn = document.getElementById('record-btn'); 123 | 124 | if (this.isRecording) { 125 | this.recordedNotes = []; 126 | this.startTime = Date.now(); 127 | btn.textContent = "停止录制"; 128 | btn.classList.add('recording'); 129 | } else { 130 | btn.textContent = "开始录制"; 131 | btn.classList.remove('recording'); 132 | } 133 | } 134 | 135 | recordNote(note) { 136 | const time = Date.now() - this.startTime; 137 | this.recordedNotes.push({ 138 | note, 139 | time 140 | }); 141 | } 142 | 143 | playRecording() { 144 | if (this.recordedNotes.length === 0) return; 145 | 146 | const now = Tone.now(); 147 | this.recordedNotes.forEach(({note, time}) => { 148 | this.playNote(note); 149 | }); 150 | } 151 | } 152 | 153 | // 等待页面加载完成后初始化钢琴 154 | window.addEventListener('load', () => { 155 | new Piano(); 156 | }); -------------------------------------------------------------------------------- /QUICK-ACTION-CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # 🚀 Piano Online - 快速行动清单 2 | 3 | > **目标**: 2周内UV从100提升到200+ 4 | 5 | --- 6 | 7 | ## 📋 今天就做 (Day 1) 8 | 9 | ### 1. 提交到搜索引擎 ⏰ 30分钟 10 | - [ ] 登录 [Google Search Console](https://search.google.com/search-console) 11 | - 添加网站: piano-online.com 12 | - 提交sitemap: `https://piano-online.com/sitemap.xml` 13 | - 请求索引所有页面 14 | 15 | - [ ] 登录 [Bing Webmaster Tools](https://www.bing.com/webmasters) 16 | - 添加网站 17 | - 提交sitemap 18 | 19 | - [ ] 登录 [百度站长平台](https://ziyuan.baidu.com) 20 | - 验证网站 21 | - 提交sitemap 22 | 23 | ### 2. 创建社交媒体图片 ⏰ 20分钟 24 | - [ ] 使用Canva创建og-image.jpg (1200x630px) 25 | - 包含网站logo 26 | - 添加标语: "免费在线钢琴学习平台" 27 | - 保存到 `/images/og-image.jpg` 28 | 29 | ### 3. 验证优化效果 ⏰ 15分钟 30 | - [ ] 使用 [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) 31 | - 测试首页分享效果 32 | 33 | - [ ] 使用 [Twitter Card Validator](https://cards-dev.twitter.com/validator) 34 | - 测试Twitter卡片效果 35 | 36 | - [ ] 使用 [Google Rich Results Test](https://search.google.com/test/rich-results) 37 | - 验证结构化数据 38 | 39 | --- 40 | 41 | ## 📅 本周任务 (Week 1) 42 | 43 | ### 周一: 搜索引擎优化 44 | - [x] 提交sitemap到各大搜索引擎 45 | - [ ] 在Google Search Console请求索引所有页面 46 | - [ ] 检查移动端适配性 47 | 48 | ### 周二: 知乎营销 49 | **目标**: 回答3个问题 50 | 51 | 推荐问题: 52 | 1. [零基础如何自学钢琴?](https://www.zhihu.com/search?q=零基础学钢琴) 53 | 2. [有哪些好用的在线钢琴网站?](https://www.zhihu.com/search?q=在线钢琴网站) 54 | 3. [钢琴初学者练习方法?](https://www.zhihu.com/search?q=钢琴初学者练习) 55 | 56 | **回答模板**: 57 | ``` 58 | 我学钢琴X年了,分享几个实用建议: 59 | 60 | 1. [具体建议1] 61 | 2. [具体建议2] 62 | 3. [具体建议3] 63 | 64 | 推荐一个我常用的工具: Piano Online (piano-online.com) 65 | - 优点1 66 | - 优点2 67 | - 优点3 68 | 69 | [分享使用心得] 70 | ``` 71 | 72 | ### 周三: 小红书营销 73 | **目标**: 发布2篇笔记 74 | 75 | 内容方向: 76 | 1. "上班族的钢琴学习日记 | 每天10分钟进步看得见" 77 | 2. "免费在线钢琴工具分享 | 不花钱也能学钢琴" 78 | 79 | **发布技巧**: 80 | - 使用9宫格图片 81 | - 添加热门话题标签: #钢琴学习 #在线学习 #零基础学钢琴 82 | - 在评论区置顶网站链接 83 | 84 | ### 周四: 内容优化 85 | - [ ] 为首页添加"热门教程"板块 86 | - [ ] 为教程页面添加"相关推荐" 87 | - [ ] 优化移动端显示效果 88 | 89 | ### 周五: 外链建设 90 | 提交到以下网站目录: 91 | - [ ] [音乐教育导航](https://www.google.com/search?q=音乐教育网站导航) 92 | - [ ] [在线工具大全](https://www.google.com/search?q=在线工具导航) 93 | - [ ] [学习资源网](https://www.google.com/search?q=学习资源导航网站) 94 | 95 | ### 周末: 数据分析 96 | - [ ] 查看Google Analytics数据 97 | - [ ] 分析哪些页面表现好 98 | - [ ] 制定下周优化计划 99 | 100 | --- 101 | 102 | ## 📅 第2周任务 (Week 2) 103 | 104 | ### 周一-周三: 持续内容营销 105 | - [ ] 知乎回答5个问题 106 | - [ ] 小红书发布3篇笔记 107 | - [ ] 在相关论坛发帖 108 | 109 | ### 周四-周五: 新内容创作 110 | 创建2篇新教程: 111 | - [ ] "钢琴考级曲目推荐与练习方法" 112 | - [ ] "成人零基础学钢琴30天计划" 113 | 114 | ### 周末: 效果评估 115 | - [ ] 对比两周数据 116 | - [ ] 分析流量来源 117 | - [ ] 调整营销策略 118 | 119 | --- 120 | 121 | ## 🎯 每日15分钟任务 122 | 123 | ### 早上 (5分钟) 124 | - [ ] 查看Google Analytics昨日数据 125 | - [ ] 检查是否有新的搜索词排名 126 | 127 | ### 中午 (5分钟) 128 | - [ ] 回复用户评论/反馈 129 | - [ ] 在社交媒体互动 130 | 131 | ### 晚上 (5分钟) 132 | - [ ] 发布1条社交媒体内容 133 | - [ ] 或回答1个知乎问题 134 | 135 | --- 136 | 137 | ## 📊 关键指标监控 138 | 139 | ### 每天检查 140 | - [ ] UV数量 141 | - [ ] 跳出率 142 | - [ ] 平均停留时间 143 | 144 | ### 每周检查 145 | - [ ] Google收录页面数 146 | - [ ] 关键词排名变化 147 | - [ ] 外链数量增长 148 | 149 | ### 目标值 150 | | 指标 | 当前 | 1周后 | 2周后 | 151 | |------|------|-------|-------| 152 | | 日UV | <100 | 120+ | 200+ | 153 | | 跳出率 | ? | <65% | <60% | 154 | | 停留时间 | ? | >1.5分钟 | >2分钟 | 155 | | 收录页面 | ? | 18页 | 18页 | 156 | 157 | --- 158 | 159 | ## 💰 零成本营销渠道 160 | 161 | ### 1. 知乎 (最重要!) 162 | - 每周回答5-10个问题 163 | - 关注钢琴相关话题 164 | - 点赞优质回答建立人脉 165 | 166 | ### 2. 小红书 167 | - 每周发布3-5篇笔记 168 | - 使用热门标签 169 | - 与其他博主互动 170 | 171 | ### 3. 微信公众号 172 | - 转载网站教程 173 | - 添加原文链接 174 | - 引导用户访问网站 175 | 176 | ### 4. B站 177 | - 制作钢琴教学视频 178 | - 在简介添加网站链接 179 | - 回复评论引导访问 180 | 181 | ### 5. 抖音/快手 182 | - 发布钢琴演奏短视频 183 | - 在个人简介添加网站 184 | - 引导用户搜索"Piano Online" 185 | 186 | ### 6. 豆瓣小组 187 | - 加入钢琴学习小组 188 | - 分享学习经验 189 | - 适度推广网站 190 | 191 | --- 192 | 193 | ## ⚡ 快速增长技巧 194 | 195 | ### 技巧1: 蹭热点 196 | 关注以下热点话题: 197 | - 钢琴考级季节(每年6月、12月) 198 | - 开学季(9月) 199 | - 寒暑假(1月、7-8月) 200 | 201 | ### 技巧2: 互推合作 202 | 寻找以下类型的网站合作: 203 | - 音乐教育网站 204 | - 在线学习平台 205 | - 教育资源网站 206 | 207 | ### 技巧3: 用户激励 208 | - 鼓励用户分享到朋友圈 209 | - 设置分享奖励机制 210 | - 创建学习打卡活动 211 | 212 | ### 技巧4: SEO优化 213 | - 每篇文章包含3-5个长尾关键词 214 | - 文章标题包含数字(如"10个技巧") 215 | - 添加清晰的小标题 216 | 217 | --- 218 | 219 | ## 🚫 避免的错误 220 | 221 | 1. ❌ 不要在短时间内发布大量低质量外链 222 | 2. ❌ 不要复制粘贴其他网站内容 223 | 3. ❌ 不要过度推广,引起用户反感 224 | 4. ❌ 不要忽视用户反馈 225 | 5. ❌ 不要三天打鱼两天晒网 226 | 227 | --- 228 | 229 | ## ✅ 成功的标志 230 | 231 | ### 2周后你应该看到: 232 | - ✅ Google收录所有18个页面 233 | - ✅ 至少10个长尾词有排名 234 | - ✅ 知乎回答获得100+赞同 235 | - ✅ 小红书笔记获得500+浏览 236 | - ✅ 日UV达到150-200 237 | 238 | ### 1个月后你应该看到: 239 | - ✅ 主关键词进入前5页 240 | - ✅ 日UV稳定在200-300 241 | - ✅ 有用户主动推荐网站 242 | - ✅ 搜索品牌词能找到网站 243 | 244 | ### 3个月后你应该看到: 245 | - ✅ 日UV达到500+ 246 | - ✅ 多个关键词排名前3页 247 | - ✅ 建立稳定的用户群体 248 | - ✅ 有自然的外链增长 249 | 250 | --- 251 | 252 | ## 📞 遇到问题? 253 | 254 | ### 常见问题解决方案 255 | 256 | **Q: Google一直不收录怎么办?** 257 | A: 258 | 1. 检查robots.txt是否正确 259 | 2. 在Google Search Console手动提交 260 | 3. 增加外链帮助蜘蛛发现 261 | 4. 确保网站可以正常访问 262 | 263 | **Q: 知乎回答没人看怎么办?** 264 | A: 265 | 1. 选择浏览量高的问题 266 | 2. 写在前3个回答 267 | 3. 回答要详细(1000字+) 268 | 4. 添加图片和案例 269 | 270 | **Q: 流量增长很慢怎么办?** 271 | A: 272 | 1. 检查网站加载速度 273 | 2. 优化移动端体验 274 | 3. 增加内容更新频率 275 | 4. 扩大营销渠道 276 | 277 | --- 278 | 279 | ## 🎉 开始行动! 280 | 281 | **记住**: 282 | - 今天就完成Day 1的任务 283 | - 每天坚持15分钟 284 | - 2周后你会看到明显效果 285 | - 3个月后流量会有质的飞跃 286 | 287 | **现在就开始第一步: 提交sitemap到Google Search Console!** 288 | 289 | --- 290 | 291 | 最后更新: 2025-10-26 292 | -------------------------------------------------------------------------------- /css/practice.css: -------------------------------------------------------------------------------- 1 | /* 练习页面样式 */ 2 | .practice-tools { 3 | padding: 60px 0; 4 | background-color: #f8f9fa; 5 | } 6 | 7 | .practice-tools h1 { 8 | text-align: center; 9 | color: #333; 10 | font-size: 2.5rem; 11 | margin-bottom: 40px; 12 | } 13 | 14 | .tool-card { 15 | background: #fff; 16 | border-radius: 10px; 17 | padding: 30px; 18 | margin-bottom: 30px; 19 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 20 | } 21 | 22 | .tool-card h2 { 23 | color: #333; 24 | font-size: 1.5rem; 25 | margin-bottom: 20px; 26 | border-bottom: 2px solid #007bff; 27 | padding-bottom: 10px; 28 | } 29 | 30 | /* 节拍器样式 */ 31 | .metronome { 32 | text-align: center; 33 | padding: 20px; 34 | } 35 | 36 | .metronome-display { 37 | font-size: 2rem; 38 | margin-bottom: 20px; 39 | } 40 | 41 | .tempo { 42 | color: #007bff; 43 | font-weight: bold; 44 | } 45 | 46 | .metronome-controls { 47 | display: flex; 48 | justify-content: center; 49 | gap: 20px; 50 | margin-bottom: 20px; 51 | } 52 | 53 | .metronome-controls button { 54 | padding: 10px 20px; 55 | font-size: 1.2rem; 56 | border: none; 57 | border-radius: 5px; 58 | cursor: pointer; 59 | transition: all 0.3s; 60 | } 61 | 62 | .start-stop { 63 | background-color: #007bff; 64 | color: white; 65 | } 66 | 67 | .tempo-up, .tempo-down { 68 | background-color: #e9ecef; 69 | color: #333; 70 | } 71 | 72 | .time-signature select { 73 | padding: 5px 10px; 74 | font-size: 1rem; 75 | border-radius: 5px; 76 | border: 1px solid #ddd; 77 | } 78 | 79 | /* 音阶练习样式 */ 80 | .scale-practice { 81 | text-align: center; 82 | padding: 20px; 83 | } 84 | 85 | .scale-selector { 86 | margin-bottom: 20px; 87 | } 88 | 89 | .key-select { 90 | padding: 8px 15px; 91 | font-size: 1rem; 92 | border-radius: 5px; 93 | border: 1px solid #ddd; 94 | } 95 | 96 | .keyboard { 97 | height: 120px; 98 | background: #f8f9fa; 99 | border-radius: 5px; 100 | margin-bottom: 20px; 101 | position: relative; 102 | } 103 | 104 | .play-scale { 105 | padding: 10px 20px; 106 | font-size: 1.1rem; 107 | background-color: #28a745; 108 | color: white; 109 | border: none; 110 | border-radius: 5px; 111 | cursor: pointer; 112 | transition: all 0.3s; 113 | } 114 | 115 | /* 和弦练习样式 */ 116 | .chord-practice { 117 | text-align: center; 118 | padding: 20px; 119 | } 120 | 121 | .chord-selector { 122 | margin-bottom: 20px; 123 | } 124 | 125 | .chord-type { 126 | padding: 8px 15px; 127 | font-size: 1rem; 128 | border-radius: 5px; 129 | border: 1px solid #ddd; 130 | } 131 | 132 | .play-chord { 133 | padding: 10px 20px; 134 | font-size: 1.1rem; 135 | background-color: #6f42c1; 136 | color: white; 137 | border: none; 138 | border-radius: 5px; 139 | cursor: pointer; 140 | transition: all 0.3s; 141 | } 142 | 143 | /* 视奏训练样式 */ 144 | .sight-reading { 145 | text-align: center; 146 | padding: 20px; 147 | } 148 | 149 | .difficulty-selector { 150 | margin-bottom: 20px; 151 | } 152 | 153 | .difficulty-selector select { 154 | padding: 8px 15px; 155 | font-size: 1rem; 156 | border-radius: 5px; 157 | border: 1px solid #ddd; 158 | } 159 | 160 | .staff-display { 161 | height: 200px; 162 | background: #fff; 163 | border: 1px solid #ddd; 164 | border-radius: 5px; 165 | margin-bottom: 20px; 166 | } 167 | 168 | .generate-notes { 169 | padding: 10px 20px; 170 | font-size: 1.1rem; 171 | background-color: #fd7e14; 172 | color: white; 173 | border: none; 174 | border-radius: 5px; 175 | cursor: pointer; 176 | transition: all 0.3s; 177 | } 178 | 179 | /* 练习建议样式 */ 180 | .practice-tips { 181 | padding: 60px 0; 182 | background-color: #fff; 183 | } 184 | 185 | .practice-tips h2 { 186 | text-align: center; 187 | color: #333; 188 | font-size: 2rem; 189 | margin-bottom: 40px; 190 | } 191 | 192 | .tips-grid { 193 | display: grid; 194 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 195 | gap: 30px; 196 | } 197 | 198 | .tip-card { 199 | background: #f8f9fa; 200 | border-radius: 10px; 201 | padding: 30px; 202 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 203 | } 204 | 205 | .tip-card h3 { 206 | color: #333; 207 | font-size: 1.3rem; 208 | margin-bottom: 20px; 209 | border-bottom: 2px solid #007bff; 210 | padding-bottom: 10px; 211 | } 212 | 213 | .tip-card ul { 214 | list-style: none; 215 | padding: 0; 216 | } 217 | 218 | .tip-card li { 219 | color: #666; 220 | margin-bottom: 10px; 221 | padding-left: 20px; 222 | position: relative; 223 | } 224 | 225 | .tip-card li::before { 226 | content: "•"; 227 | color: #007bff; 228 | position: absolute; 229 | left: 0; 230 | } 231 | 232 | /* 按钮悬停效果 */ 233 | button:hover { 234 | opacity: 0.9; 235 | transform: translateY(-2px); 236 | } 237 | 238 | /* 响应式设计 */ 239 | @media (max-width: 768px) { 240 | .practice-tools h1 { 241 | font-size: 2rem; 242 | } 243 | 244 | .tool-card { 245 | padding: 20px; 246 | } 247 | 248 | .tips-grid { 249 | grid-template-columns: 1fr; 250 | } 251 | 252 | .metronome-controls { 253 | flex-direction: column; 254 | gap: 10px; 255 | } 256 | 257 | .metronome-controls button { 258 | width: 100%; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /tutorial-content.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

钢琴基础课程

5 |
6 | 7 |
8 |

认识钢琴

9 |
10 |
11 |

第一课:钢琴的构造

12 |
    13 |
  • 钢琴的基本结构
  • 14 |
  • 黑白键的排列规律
  • 15 |
  • 踏板的功能与使用
  • 16 |
  • 钢琴的保养知识
  • 17 |
18 |
19 | 20 |
21 |

第二课:基本坐姿与手型

22 |
    23 |
  • 正确的坐姿要领
  • 24 |
  • 手腕和手指姿势
  • 25 |
  • 常见错误姿势纠正
  • 26 |
  • 基础手指练习
  • 27 |
28 |
29 |
30 |
31 | 32 | 33 |
34 |

乐理基础

35 |
36 |
37 |

第一课:认识五线谱

38 |
    39 |
  • 五线谱的构成
  • 40 |
  • 音符的位置与时值
  • 41 |
  • 拍号与小节线
  • 42 |
  • 基本记号含义
  • 43 |
44 |
45 | 46 |
47 |

第二课:节奏与节拍

48 |
    49 |
  • 基本节拍类型
  • 50 |
  • 常见节奏型
  • 51 |
  • 附点音符
  • 52 |
  • 休止符使用
  • 53 |
54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 |
62 |

进阶教程

63 |
64 | 65 |
66 |

技巧训练

67 |
68 |
69 |

音阶练习

70 |
    71 |
  • 大调音阶练习方法
  • 72 |
  • 和声小调音阶入门
  • 73 |
  • 旋律小调音阶练习
  • 74 |
  • 音阶指法规则
  • 75 |
76 |
77 | 78 |
79 |

和弦练习

80 |
    81 |
  • 三和弦构成与练习
  • 82 |
  • 七和弦基础
  • 83 |
  • 和弦进行练习
  • 84 |
  • 分解和弦技巧
  • 85 |
86 |
87 |
88 |
89 | 90 | 91 |
92 |

曲目解析

93 |
94 |
95 |

入门曲目

96 |
    97 |
  • 《小星星》完整教学
  • 98 |
  • 《生日快乐》弹奏技巧
  • 99 |
  • 《致爱丽丝》前奏解析
  • 100 |
  • 《土耳其进行曲》片段讲解
  • 101 |
102 |
103 |
104 |
105 |
106 |
107 | 108 | 109 |
110 |

专项训练

111 |
112 | 113 |
114 |

听力训练

115 |
116 |
117 |

基础听力

118 |
    119 |
  • 单音识别训练
  • 120 |
  • 音程听辨练习
  • 121 |
  • 和弦类型辨识
  • 122 |
  • 旋律记忆训练
  • 123 |
124 |
125 |
126 |
127 | 128 | 129 |
130 |

即兴演奏入门

131 |
132 |
133 |

基础即兴

134 |
    135 |
  • 和弦进行模式
  • 136 |
  • 简单旋律创作
  • 137 |
  • 节奏型变化
  • 138 |
  • 左手伴奏模式
  • 139 |
140 |
141 |
142 |
143 |
144 |
145 |
-------------------------------------------------------------------------------- /js/practice.js: -------------------------------------------------------------------------------- 1 | // 初始化音频生成器 2 | let audioGenerator; 3 | 4 | // 初始化音频上下文 5 | async function initAudio() { 6 | try { 7 | audioGenerator = new AudioGenerator(); 8 | console.log('Audio context started'); 9 | setupMetronome(); 10 | setupScalePractice(); 11 | setupChordPractice(); 12 | setupSightReading(); 13 | } catch (error) { 14 | console.error('Failed to start audio context:', error); 15 | } 16 | } 17 | 18 | // 节拍器功能 19 | function setupMetronome() { 20 | const startStopBtn = document.querySelector('.start-stop'); 21 | const tempoUpBtn = document.querySelector('.tempo-up'); 22 | const tempoDownBtn = document.querySelector('.tempo-down'); 23 | const timeSignatureSelect = document.querySelector('.time-signature select'); 24 | const tempoDisplay = document.querySelector('.tempo'); 25 | 26 | let isPlaying = false; 27 | let tempo = 120; 28 | let timeSignature = 4; 29 | let count = 0; 30 | let intervalId = null; 31 | 32 | // 开始/停止按钮 33 | startStopBtn.addEventListener('click', () => { 34 | if (!isPlaying) { 35 | const interval = 60000 / tempo; // 计算每拍间隔(毫秒) 36 | count = 0; 37 | intervalId = setInterval(() => { 38 | audioGenerator.createMetronomeSound(count % timeSignature === 0); 39 | count = (count + 1) % timeSignature; 40 | }, interval); 41 | startStopBtn.textContent = '停止'; 42 | isPlaying = true; 43 | } else { 44 | clearInterval(intervalId); 45 | startStopBtn.textContent = '开始'; 46 | isPlaying = false; 47 | count = 0; 48 | } 49 | }); 50 | 51 | // 调整速度 52 | tempoUpBtn.addEventListener('click', () => { 53 | tempo = Math.min(tempo + 5, 240); 54 | tempoDisplay.textContent = tempo; 55 | if (isPlaying) { 56 | clearInterval(intervalId); 57 | startStopBtn.click(); 58 | } 59 | }); 60 | 61 | tempoDownBtn.addEventListener('click', () => { 62 | tempo = Math.max(tempo - 5, 40); 63 | tempoDisplay.textContent = tempo; 64 | if (isPlaying) { 65 | clearInterval(intervalId); 66 | startStopBtn.click(); 67 | } 68 | }); 69 | 70 | // 拍号选择 71 | timeSignatureSelect.addEventListener('change', (e) => { 72 | timeSignature = parseInt(e.target.value); 73 | count = 0; 74 | if (isPlaying) { 75 | clearInterval(intervalId); 76 | startStopBtn.click(); 77 | } 78 | }); 79 | } 80 | 81 | // 音阶练习功能 82 | function setupScalePractice() { 83 | const keySelect = document.querySelector('.key-select'); 84 | const playScaleBtn = document.querySelector('.play-scale'); 85 | 86 | // 定义音阶 87 | const scales = { 88 | C: ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'], 89 | G: ['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F#5', 'G5'], 90 | D: ['D4', 'E4', 'F#4', 'G4', 'A4', 'B4', 'C#5', 'D5'], 91 | A: ['A4', 'B4', 'C#5', 'D5', 'E5', 'F#5', 'G#5', 'A5'], 92 | E: ['E4', 'F#4', 'G#4', 'A4', 'B4', 'C#5', 'D#5', 'E5'], 93 | B: ['B4', 'C#5', 'D#5', 'E5', 'F#5', 'G#5', 'A#5', 'B5'], 94 | F: ['F4', 'G4', 'A4', 'Bb4', 'C5', 'D5', 'E5', 'F5'] 95 | }; 96 | 97 | // 播放音阶 98 | playScaleBtn.addEventListener('click', () => { 99 | const key = keySelect.value; 100 | const scale = scales[key]; 101 | 102 | // 依次播放音阶音符 103 | scale.forEach((note, index) => { 104 | setTimeout(() => { 105 | audioGenerator.playNote(note, 0.5); 106 | }, index * 500); 107 | }); 108 | }); 109 | } 110 | 111 | // 和弦练习功能 112 | function setupChordPractice() { 113 | const chordSelect = document.querySelector('.chord-type'); 114 | const playChordBtn = document.querySelector('.play-chord'); 115 | 116 | // 定义和弦 117 | const chords = { 118 | major: ['C4', 'E4', 'G4'], 119 | minor: ['C4', 'Eb4', 'G4'], 120 | diminished: ['C4', 'Eb4', 'Gb4'], 121 | augmented: ['C4', 'E4', 'G#4'], 122 | dominant7: ['C4', 'E4', 'G4', 'Bb4'] 123 | }; 124 | 125 | // 播放和弦 126 | playChordBtn.addEventListener('click', () => { 127 | const chordType = chordSelect.value; 128 | const chord = chords[chordType]; 129 | audioGenerator.playChord(chord, 1); 130 | }); 131 | } 132 | 133 | // 视奏训练功能 134 | function setupSightReading() { 135 | const difficultySelect = document.querySelector('.difficulty-selector select'); 136 | const generateBtn = document.querySelector('.generate-notes'); 137 | const staffContainer = document.getElementById('staff-container'); 138 | const currentNotesDisplay = document.querySelector('.current-notes'); 139 | 140 | // 创建五线谱渲染器 141 | const staffRenderer = new StaffRenderer(staffContainer); 142 | 143 | // 生成新的视奏练习 144 | generateBtn.addEventListener('click', () => { 145 | const difficulty = difficultySelect.value; 146 | const noteCount = difficulty === 'beginner' ? 4 : (difficulty === 'intermediate' ? 6 : 8); 147 | const notes = NoteUtils.generateRandomNotes(difficulty, noteCount); 148 | 149 | // 渲染五线谱 150 | staffRenderer.renderNotes(notes); 151 | 152 | // 显示音符名称(用于练习) 153 | currentNotesDisplay.textContent = `音符: ${notes.join(' - ')}`; 154 | }); 155 | 156 | // 初始生成一个练习 157 | generateBtn.click(); 158 | } 159 | 160 | // 页面加载完成后初始化 161 | document.addEventListener('DOMContentLoaded', () => { 162 | // 添加用户交互监听器来初始化音频 163 | document.addEventListener('click', initAudio, { once: true }); 164 | document.addEventListener('keydown', initAudio, { once: true }); 165 | }); 166 | -------------------------------------------------------------------------------- /js/piano-audio.js: -------------------------------------------------------------------------------- 1 | class PianoAudio { 2 | constructor() { 3 | console.log('PianoAudio constructor called'); 4 | this.initialized = false; 5 | this.sampler = null; 6 | this.reverb = null; 7 | this.context = null; 8 | this.samples = {}; 9 | this.gainNode = null; 10 | this.sustainedNotes = new Set(); 11 | 12 | // 创建所有音符的音频元素 13 | this.audioElements = {}; 14 | this.volume = 0.5; 15 | 16 | // 预加载所有音符 17 | this.preloadNotes(); 18 | } 19 | 20 | async init() { 21 | if (this.initialized) return true; 22 | 23 | try { 24 | console.log('Initializing PianoAudio'); 25 | 26 | // 检查 Tone 是否已定义 27 | if (typeof Tone === 'undefined') { 28 | throw new Error('Tone.js not loaded'); 29 | } 30 | 31 | // 创建 AudioContext 32 | this.context = new (window.AudioContext || window.webkitAudioContext)(); 33 | 34 | // 创建增益节点 35 | this.gainNode = this.context.createGain(); 36 | this.gainNode.connect(this.context.destination); 37 | 38 | // 设置音量 39 | this.setVolume(this.volume); 40 | 41 | console.log('AudioContext created'); 42 | 43 | // 标记为已初始化 44 | this.initialized = true; 45 | return true; 46 | } catch (error) { 47 | console.error('Failed to initialize audio:', error); 48 | return false; 49 | } 50 | } 51 | 52 | preloadNotes() { 53 | console.log('Preloading notes'); 54 | // 只加载实际存在的音符文件 55 | const notes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; 56 | const octaves = ['3', '4', '5', '6']; 57 | 58 | notes.forEach(note => { 59 | octaves.forEach(octave => { 60 | const noteId = `${note}${octave}`; 61 | const audio = new Audio(`samples/piano-${noteId}.mp3`); 62 | audio.preload = 'auto'; 63 | this.audioElements[noteId] = audio; 64 | 65 | // 对于升号音符,使用相邻的音符 66 | if (note !== 'E' && note !== 'B') { 67 | const sharpNoteId = `${note}#${octave}`; 68 | this.audioElements[sharpNoteId] = audio; 69 | } 70 | 71 | console.log(`Preloading note: samples/piano-${noteId}.mp3`); 72 | }); 73 | }); 74 | } 75 | 76 | async playNote(note) { 77 | console.log('Playing note:', note); 78 | try { 79 | // 如果音频上下文被暂停,等待用户交互时会自动恢复 80 | if (this.context && this.context.state === 'suspended') { 81 | await this.context.resume(); 82 | } 83 | 84 | if (this.sampler && this.sampler.loaded) { 85 | this.sampler.triggerAttack(note); 86 | } else { 87 | // 如果是升号音符,使用相邻的音符 88 | const audioElement = this.audioElements[note] || this.audioElements[this.getNearestNote(note)]; 89 | if (audioElement) { 90 | audioElement.currentTime = 0; 91 | audioElement.volume = this.volume; 92 | audioElement.play().catch(error => { 93 | console.error('Failed to play note:', error); 94 | // 如果播放失败,尝试重新加载 95 | audioElement.load(); 96 | }); 97 | } else { 98 | console.warn('No audio element found for note:', note); 99 | } 100 | } 101 | } catch (error) { 102 | console.error('Error playing note:', note, error); 103 | } 104 | } 105 | 106 | stopNote(note) { 107 | console.log('Stopping note:', note); 108 | try { 109 | if (this.sampler && this.sampler.loaded) { 110 | this.sampler.triggerRelease(note); 111 | } else if (this.audioElements[note]) { 112 | // 如果使用的是音频元素,设置淡出效果 113 | const audio = this.audioElements[note]; 114 | const fadeOut = setInterval(() => { 115 | if (audio.volume > 0.1) { 116 | audio.volume -= 0.1; 117 | } else { 118 | audio.pause(); 119 | audio.volume = this.volume; 120 | clearInterval(fadeOut); 121 | } 122 | }, 50); 123 | } 124 | } catch (error) { 125 | console.error('Error stopping note:', note, error); 126 | } 127 | } 128 | 129 | getNearestNote(note) { 130 | // 将升号音符映射到相邻的音符 131 | const noteMap = { 132 | 'C#': 'D', 133 | 'D#': 'E', 134 | 'F#': 'G', 135 | 'G#': 'A', 136 | 'A#': 'B' 137 | }; 138 | 139 | const noteName = note.slice(0, -1); // 去掉最后的数字 140 | const octave = note.slice(-1); // 获取八度数 141 | 142 | if (noteMap[noteName]) { 143 | return noteMap[noteName] + octave; 144 | } 145 | return note; 146 | } 147 | 148 | setVolume(value) { 149 | console.log('Setting volume:', value); 150 | this.volume = value; 151 | if (this.sampler) { 152 | this.sampler.volume.value = Tone.gainToDb(value); 153 | } 154 | if (this.gainNode) { 155 | this.gainNode.gain.value = value; 156 | } 157 | // 设置所有音频元素的音量 158 | Object.values(this.audioElements).forEach(audio => { 159 | audio.volume = value; 160 | }); 161 | } 162 | 163 | setSustain(enabled) { 164 | if (enabled) { 165 | this.sustainedNotes.clear(); 166 | } else { 167 | // 释放所有持续的音符 168 | this.sustainedNotes.forEach(note => { 169 | this.stopNote(note); 170 | }); 171 | this.sustainedNotes.clear(); 172 | } 173 | } 174 | } 175 | 176 | export default PianoAudio; 177 | -------------------------------------------------------------------------------- /css/about.css: -------------------------------------------------------------------------------- 1 | /* 关于页面样式 */ 2 | .about-hero { 3 | background-color: #007bff; 4 | color: white; 5 | padding: 80px 0; 6 | text-align: center; 7 | } 8 | 9 | .about-hero h1 { 10 | font-size: 3rem; 11 | margin-bottom: 20px; 12 | } 13 | 14 | .mission-statement { 15 | font-size: 1.2rem; 16 | max-width: 800px; 17 | margin: 0 auto; 18 | line-height: 1.6; 19 | } 20 | 21 | /* 我们的优势 */ 22 | .our-advantages { 23 | padding: 80px 0; 24 | background-color: #f8f9fa; 25 | } 26 | 27 | .our-advantages h2 { 28 | text-align: center; 29 | color: #333; 30 | font-size: 2.5rem; 31 | margin-bottom: 50px; 32 | } 33 | 34 | .advantages-grid { 35 | display: grid; 36 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 37 | gap: 30px; 38 | } 39 | 40 | .advantage-card { 41 | background: white; 42 | border-radius: 10px; 43 | padding: 30px; 44 | text-align: center; 45 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 46 | transition: transform 0.3s; 47 | } 48 | 49 | .advantage-card:hover { 50 | transform: translateY(-5px); 51 | } 52 | 53 | .advantage-icon { 54 | font-size: 3rem; 55 | margin-bottom: 20px; 56 | } 57 | 58 | .advantage-card h3 { 59 | color: #333; 60 | font-size: 1.5rem; 61 | margin-bottom: 15px; 62 | } 63 | 64 | .advantage-card p { 65 | color: #666; 66 | line-height: 1.6; 67 | } 68 | 69 | /* 发展历程 */ 70 | .our-history { 71 | padding: 80px 0; 72 | background-color: white; 73 | } 74 | 75 | .our-history h2 { 76 | text-align: center; 77 | color: #333; 78 | font-size: 2.5rem; 79 | margin-bottom: 50px; 80 | } 81 | 82 | .timeline { 83 | max-width: 800px; 84 | margin: 0 auto; 85 | position: relative; 86 | } 87 | 88 | .timeline::before { 89 | content: ''; 90 | position: absolute; 91 | left: 50%; 92 | transform: translateX(-50%); 93 | width: 2px; 94 | height: 100%; 95 | background-color: #007bff; 96 | } 97 | 98 | .timeline-item { 99 | display: flex; 100 | justify-content: center; 101 | align-items: flex-start; 102 | margin-bottom: 50px; 103 | position: relative; 104 | } 105 | 106 | .year { 107 | background-color: #007bff; 108 | color: white; 109 | padding: 10px 20px; 110 | border-radius: 20px; 111 | font-weight: bold; 112 | position: absolute; 113 | left: 50%; 114 | transform: translateX(-50%); 115 | top: -20px; 116 | } 117 | 118 | .event { 119 | background: #f8f9fa; 120 | border-radius: 10px; 121 | padding: 30px; 122 | width: calc(50% - 50px); 123 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 124 | margin-top: 30px; 125 | } 126 | 127 | .timeline-item:nth-child(odd) .event { 128 | margin-right: auto; 129 | } 130 | 131 | .timeline-item:nth-child(even) .event { 132 | margin-left: auto; 133 | } 134 | 135 | .event h3 { 136 | color: #333; 137 | font-size: 1.3rem; 138 | margin-bottom: 10px; 139 | } 140 | 141 | .event p { 142 | color: #666; 143 | line-height: 1.6; 144 | } 145 | 146 | /* 联系我们 */ 147 | .contact-us { 148 | padding: 80px 0; 149 | background-color: #f8f9fa; 150 | } 151 | 152 | .contact-us h2 { 153 | text-align: center; 154 | color: #333; 155 | font-size: 2.5rem; 156 | margin-bottom: 50px; 157 | } 158 | 159 | .contact-info { 160 | display: grid; 161 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 162 | gap: 30px; 163 | } 164 | 165 | .contact-item { 166 | background: white; 167 | border-radius: 10px; 168 | padding: 30px; 169 | text-align: center; 170 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 171 | } 172 | 173 | .contact-icon { 174 | font-size: 2.5rem; 175 | margin-bottom: 20px; 176 | } 177 | 178 | .contact-item h3 { 179 | color: #333; 180 | font-size: 1.3rem; 181 | margin-bottom: 15px; 182 | } 183 | 184 | .social-links { 185 | display: flex; 186 | justify-content: center; 187 | gap: 15px; 188 | } 189 | 190 | .social-links a { 191 | color: #007bff; 192 | text-decoration: none; 193 | transition: color 0.3s; 194 | } 195 | 196 | .social-links a:hover { 197 | color: #0056b3; 198 | } 199 | 200 | /* 加入我们 */ 201 | .join-us { 202 | padding: 80px 0; 203 | background-color: white; 204 | text-align: center; 205 | } 206 | 207 | .join-us h2 { 208 | color: #333; 209 | font-size: 2.5rem; 210 | margin-bottom: 20px; 211 | } 212 | 213 | .join-us > p { 214 | color: #666; 215 | max-width: 600px; 216 | margin: 0 auto 50px; 217 | } 218 | 219 | .positions { 220 | display: grid; 221 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 222 | gap: 30px; 223 | margin-bottom: 40px; 224 | } 225 | 226 | .position-card { 227 | background: #f8f9fa; 228 | border-radius: 10px; 229 | padding: 30px; 230 | box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); 231 | } 232 | 233 | .position-card h3 { 234 | color: #333; 235 | font-size: 1.3rem; 236 | margin-bottom: 15px; 237 | } 238 | 239 | .position-card p { 240 | color: #666; 241 | line-height: 1.6; 242 | } 243 | 244 | .join-button { 245 | display: inline-block; 246 | padding: 15px 30px; 247 | background-color: #007bff; 248 | color: white; 249 | text-decoration: none; 250 | border-radius: 5px; 251 | font-size: 1.1rem; 252 | transition: all 0.3s; 253 | } 254 | 255 | .join-button:hover { 256 | background-color: #0056b3; 257 | transform: translateY(-2px); 258 | } 259 | 260 | /* 响应式设计 */ 261 | @media (max-width: 768px) { 262 | .about-hero h1 { 263 | font-size: 2rem; 264 | } 265 | 266 | .our-advantages h2, 267 | .our-history h2, 268 | .contact-us h2, 269 | .join-us h2 { 270 | font-size: 2rem; 271 | } 272 | 273 | .timeline::before { 274 | left: 20px; 275 | } 276 | 277 | .timeline-item { 278 | flex-direction: column; 279 | } 280 | 281 | .year { 282 | left: 20px; 283 | transform: none; 284 | } 285 | 286 | .event { 287 | width: calc(100% - 50px); 288 | margin-left: 50px !important; 289 | } 290 | 291 | .contact-info, 292 | .positions { 293 | grid-template-columns: 1fr; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /js/practice-songs.js: -------------------------------------------------------------------------------- 1 | // 导出的歌曲数据 2 | const songs = { 3 | 'happy-birthday': { 4 | name: '生日快乐', 5 | difficulty: 1, 6 | notes: [ 7 | // 第一段 8 | 'G4', 'G4', 'A4', 'G4', 'C5', 'B4', 9 | // 祝你生日快乐 10 | 'G4', 'G4', 'A4', 'G4', 'D5', 'C5', 11 | // 祝你生日快乐 12 | 'G4', 'G4', 'G5', 'E5', 'C5', 'B4', 'A4', 13 | // 祝你生日快乐 14 | 'F5', 'F5', 'E5', 'C5', 'D5', 'C5' 15 | ], 16 | tempo: 120, 17 | maxScore: 100 18 | }, 19 | 'twinkle': { 20 | name: '小星星', 21 | difficulty: 1, 22 | notes: [ 23 | // 第一段 24 | 'C4', 'C4', 'G4', 'G4', 'A4', 'A4', 'G4', 25 | 'F4', 'F4', 'E4', 'E4', 'D4', 'D4', 'C4', 26 | // 中间段 27 | 'G4', 'G4', 'F4', 'F4', 'E4', 'E4', 'D4', 28 | // 重复第一段 29 | 'G4', 'G4', 'F4', 'F4', 'E4', 'E4', 'D4', 30 | 'C4', 'G4', 'C5' 31 | ], 32 | tempo: 100, 33 | maxScore: 100 34 | }, 35 | 'two-tigers': { 36 | name: '两只老虎', 37 | difficulty: 1, 38 | notes: [ 39 | // 第一只老虎 40 | 'C4', 'D4', 'E4', 'C4', 41 | 'C4', 'D4', 'E4', 'C4', 42 | 'E4', 'F4', 'G4', 43 | 'E4', 'F4', 'G4', 44 | // 跑得快 45 | 'G4', 'A4', 'G4', 'F4', 'E4', 'C4', 46 | 'G4', 'A4', 'G4', 'F4', 'E4', 'C4', 47 | // 一只没有眼睛 48 | 'C4', 'G3', 'C4', 49 | 'C4', 'G3', 'C4' 50 | ], 51 | tempo: 120, 52 | maxScore: 100 53 | }, 54 | 'ode-to-joy': { 55 | name: '欢乐颂', 56 | difficulty: 2, 57 | notes: [ 58 | 'E4', 'E4', 'F4', 'G4', 59 | 'G4', 'F4', 'E4', 'D4', 60 | 'C4', 'C4', 'D4', 'E4', 61 | 'E4', 'D4', 'D4', 62 | 63 | 'E4', 'E4', 'F4', 'G4', 64 | 'G4', 'F4', 'E4', 'D4', 65 | 'C4', 'C4', 'D4', 'E4', 66 | 'D4', 'C4', 'C4' 67 | ], 68 | tempo: 120, 69 | maxScore: 100 70 | }, 71 | 'mary-lamb': { 72 | name: '玛丽有只小羊羔', 73 | difficulty: 1, 74 | notes: [ 75 | 'E4', 'D4', 'C4', 'D4', 76 | 'E4', 'E4', 'E4', 77 | 'D4', 'D4', 'D4', 78 | 'E4', 'G4', 'G4', 79 | 80 | 'E4', 'D4', 'C4', 'D4', 81 | 'E4', 'E4', 'E4', 82 | 'D4', 'D4', 'E4', 'D4', 83 | 'C4' 84 | ], 85 | tempo: 120, 86 | maxScore: 100 87 | }, 88 | 'jingle-bells': { 89 | name: '铃儿响叮当', 90 | difficulty: 2, 91 | notes: [ 92 | 'E4', 'E4', 'E4', 93 | 'E4', 'E4', 'E4', 94 | 'E4', 'G4', 'C4', 'D4', 95 | 'E4', 96 | 97 | 'F4', 'F4', 'F4', 'F4', 98 | 'F4', 'E4', 'E4', 'E4', 'E4', 99 | 'E4', 'D4', 'D4', 'E4', 100 | 'D4', 'G4' 101 | ], 102 | tempo: 120, 103 | maxScore: 100 104 | }, 105 | 'jasmine': { 106 | name: '茉莉花', 107 | difficulty: 3, 108 | notes: [ 109 | 'E4', 'G4', 'A4', 'C5', 110 | 'B4', 'A4', 'G4', 'E4', 111 | 'G4', 'A4', 'G4', 'E4', 112 | 'D4', 'E4', 113 | 114 | 'E4', 'G4', 'A4', 'C5', 115 | 'B4', 'A4', 'G4', 'E4', 116 | 'G4', 'E4', 'D4', 'C4', 117 | 'C4', 'E4' 118 | ], 119 | tempo: 90, 120 | maxScore: 100 121 | }, 122 | 'butterfly-lovers': { 123 | name: '梁祝', 124 | difficulty: 4, 125 | notes: [ 126 | 'E4', 'G4', 'A4', 'B4', 127 | 'C5', 'B4', 'A4', 'G4', 128 | 'E4', 'G4', 'A4', 'B4', 129 | 'C5', 'E5', 130 | 131 | 'D5', 'C5', 'B4', 'A4', 132 | 'G4', 'E4', 'D4', 'E4', 133 | 'G4', 'A4', 'G4', 'E4', 134 | 'D4', 'E4' 135 | ], 136 | tempo: 80, 137 | maxScore: 100 138 | }, 139 | 'fur-elise': { 140 | name: '致爱丽丝', 141 | difficulty: 3, 142 | notes: [ 143 | // 主题旋律 144 | 'E5', 'D#5', 'E5', 'D#5', 'E5', 'B4', 'D5', 'C5', 'A4', 145 | 'C4', 'E4', 'A4', 'B4', 146 | 'E4', 'G#4', 'B4', 'C5', 147 | // 第二段 148 | 'E5', 'D#5', 'E5', 'D#5', 'E5', 'B4', 'D5', 'C5', 'A4', 149 | 'C4', 'E4', 'A4', 'B4', 150 | 'E4', 'C5', 'B4', 'A4', 151 | // 中间段 152 | 'B4', 'C5', 'D5', 'E5', 153 | 'G4', 'F5', 'E5', 'D5', 154 | 'F4', 'E5', 'D5', 'C5', 155 | // 重复主题 156 | 'E5', 'D#5', 'E5', 'D#5', 'E5', 'B4', 'D5', 'C5', 'A4', 157 | 'C4', 'E4', 'A4', 'B4', 158 | 'E4', 'G#4', 'B4', 'C5' 159 | ], 160 | tempo: 130, 161 | maxScore: 100 162 | }, 163 | 'canon': { 164 | name: '卡农', 165 | difficulty: 4, 166 | notes: [ 167 | // 主题旋律 168 | 'D4', 'A4', 'B4', 'F#4', 'G4', 'D4', 'G4', 'A4', 169 | 'D4', 'A4', 'B4', 'F#4', 'G4', 'D4', 'G4', 'A4', 170 | // 变奏1 171 | 'F#4', 'D4', 'F#4', 'G4', 'A4', 'F#4', 'A4', 'B4', 172 | 'G4', 'B4', 'A4', 'G4', 'F#4', 'D4', 'E4', 'F#4', 173 | // 变奏2 174 | 'G4', 'E4', 'G4', 'A4', 'B4', 'G4', 'B4', 'C5', 175 | 'A4', 'C5', 'B4', 'A4', 'G4', 'E4', 'F#4', 'G4', 176 | // 高潮部分 177 | 'A4', 'D5', 'C#5', 'D5', 'B4', 'G4', 'A4', 'B4', 178 | 'C5', 'A4', 'B4', 'C5', 'D5', 'B4', 'C5', 'D5', 179 | // 结束段 180 | 'E5', 'C5', 'D5', 'E5', 'F#5', 'D5', 'E5', 'F#5', 181 | 'G5', 'D5', 'G4', 'A4', 'B4', 'G4', 'B4', 'D5' 182 | ], 183 | tempo: 140, 184 | maxScore: 100 185 | } 186 | }; 187 | 188 | // 计算分数的函数 189 | function calculateScore(correctNotes, wrongNotes) { 190 | if (correctNotes === 0 && wrongNotes === 0) return 0; 191 | 192 | const totalNotes = correctNotes + wrongNotes; 193 | const accuracy = correctNotes / totalNotes; 194 | 195 | // 基础分数是准确率 * 100 196 | let score = Math.round(accuracy * 100); 197 | 198 | // 如果错误次数过多,额外扣分 199 | if (wrongNotes > totalNotes * 0.5) { 200 | score -= 20; 201 | } 202 | 203 | // 确保分数在0-100之间 204 | return Math.max(0, Math.min(100, score)); 205 | } 206 | 207 | // 导出 208 | export { songs, calculateScore }; 209 | 210 | console.log('practice-songs.js loaded, available songs:', Object.keys(songs)); 211 | console.log('Songs object:', songs); 212 | console.log('Number of songs:', Object.keys(songs).length); 213 | console.log('Song names:', Object.values(songs).map(song => song.name)); 214 | -------------------------------------------------------------------------------- /js/translations.js: -------------------------------------------------------------------------------- 1 | export const translations = { 2 | zh: { 3 | title: "Piano Online - 专业的在线钢琴学习平台", 4 | logo: { 5 | text: "Piano Online", 6 | alt: "Piano Online Logo"ßß 7 | }, 8 | nav: { 9 | features: "功能", 10 | tutorial: "教程", 11 | testimonials: "评价", 12 | faq: "常见问题", 13 | startPractice: "开始练习" 14 | }, 15 | hero: { 16 | title: "体验专业的钢琴演奏", 17 | subtitle: "在任何地方,随时开始您的钢琴学习之旅", 18 | startNow: "立即开始", 19 | learnMore: "了解更多" 20 | }, 21 | practice: { 22 | sustainPedal: "延音踏板", 23 | selectSong: "选择曲目...", 24 | startPractice: "开始练习", 25 | stopPractice: "停止练习", 26 | difficulty: "难度", 27 | difficultyLevels: { 28 | 1: "入门", 29 | 2: "初级", 30 | 3: "中级", 31 | 4: "高级" 32 | }, 33 | stats: { 34 | correct: "正确: ", 35 | wrong: "错误: ", 36 | progress: "进度: " 37 | }, 38 | nextKey: "下一个键", 39 | complete: { 40 | title: "练习完成!", 41 | score: "得分", 42 | correct: "正确", 43 | wrong: "错误" 44 | } 45 | }, 46 | features: { 47 | title: "专业功能,助您进步", 48 | realPianoSound: "真实钢琴音色", 49 | realPianoSoundDesc: "采用专业录音的钢琴音源,还原真实演奏体验", 50 | intelligentPractice: "智能练习系统", 51 | intelligentPracticeDesc: "根据您的水平自动调整练习难度", 52 | richSongLibrary: "丰富曲库", 53 | richSongLibraryDesc: "包含从入门到高级的各类经典曲目" 54 | }, 55 | howItWorks: { 56 | title: "如何使用", 57 | step1Title: "选择曲目", 58 | step1Desc: "从我们精心准备的曲库中选择适合您水平的曲目", 59 | step2Title: "跟随提示", 60 | step2Desc: "按照屏幕上的提示,一步步完成练习", 61 | step3Title: "查看进度", 62 | step3Desc: "实时追踪您的练习进度,掌握学习效果" 63 | }, 64 | testimonials: { 65 | title: "用户评价", 66 | testimonial1: "这是我用过最好的在线钢琴学习平台,界面简洁,功能强大。", 67 | author1: "张小明", 68 | title1: "钢琴爱好者", 69 | testimonial2: "智能提示系统让我的练习效率提高了很多。", 70 | author2: "李华", 71 | title2: "音乐教师" 72 | }, 73 | faq: { 74 | title: "常见问题", 75 | question1: "需要真实的钢琴键盘吗?", 76 | answer1: "不需要,您可以使用电脑键盘进行练习。当然,如果有MIDI键盘会获得更好的体验。", 77 | question2: "适合零基础学习吗?", 78 | answer2: "完全适合!我们的课程从最基础的内容开始,循序渐进地引导您学习。" 79 | }, 80 | footer: { 81 | title: "Piano Online", 82 | desc: "让钢琴学习变得简单有趣", 83 | contactTitle: "联系我们", 84 | contactEmail: "Email: contact@piano-online.com", 85 | followTitle: "关注我们", 86 | wechat: "微信", 87 | weibo: "微博", 88 | copyright: " 2024 Piano Online. 保留所有权利。" 89 | } 90 | }, 91 | en: { 92 | title: "Piano Online - Professional Online Piano Learning Platform", 93 | logo: { 94 | text: "Piano Online", 95 | alt: "Piano Online Logo" 96 | }, 97 | nav: { 98 | features: "Features", 99 | tutorial: "Tutorial", 100 | testimonials: "Testimonials", 101 | faq: "FAQ", 102 | startPractice: "Start Practice" 103 | }, 104 | hero: { 105 | title: "Experience Professional Piano Playing", 106 | subtitle: "Start your piano learning journey anywhere, anytime", 107 | startNow: "Start Now", 108 | learnMore: "Learn More" 109 | }, 110 | practice: { 111 | sustainPedal: "Sustain Pedal", 112 | selectSong: "Select a song...", 113 | startPractice: "Start Practice", 114 | stopPractice: "Stop Practice", 115 | difficulty: "Difficulty", 116 | difficultyLevels: { 117 | 1: "Beginner", 118 | 2: "Elementary", 119 | 3: "Intermediate", 120 | 4: "Advanced" 121 | }, 122 | stats: { 123 | correct: "Correct: ", 124 | wrong: "Wrong: ", 125 | progress: "Progress: " 126 | }, 127 | nextKey: "Next Key", 128 | complete: { 129 | title: "Practice Complete!", 130 | score: "Score", 131 | correct: "Correct", 132 | wrong: "Wrong" 133 | } 134 | }, 135 | features: { 136 | title: "Professional Features for Your Progress", 137 | realPianoSound: "Real Piano Sound", 138 | realPianoSoundDesc: "Professional piano samples for authentic playing experience", 139 | intelligentPractice: "Intelligent Practice System", 140 | intelligentPracticeDesc: "Automatically adjusts difficulty based on your level", 141 | richSongLibrary: "Rich Song Library", 142 | richSongLibraryDesc: "Contains classic pieces from beginner to advanced levels" 143 | }, 144 | howItWorks: { 145 | title: "How It Works", 146 | step1Title: "Choose a Song", 147 | step1Desc: "Select a song that matches your skill level from our curated library", 148 | step2Title: "Follow the Guide", 149 | step2Desc: "Follow the on-screen prompts to complete your practice", 150 | step3Title: "Track Progress", 151 | step3Desc: "Monitor your practice progress in real-time" 152 | }, 153 | testimonials: { 154 | title: "Testimonials", 155 | testimonial1: "This is the best online piano learning platform I've used, with a clean interface and powerful features.", 156 | author1: "John Smith", 157 | title1: "Piano Enthusiast", 158 | testimonial2: "The intelligent prompt system has greatly improved my practice efficiency.", 159 | author2: "Sarah Johnson", 160 | title2: "Music Teacher" 161 | }, 162 | faq: { 163 | title: "FAQ", 164 | question1: "Do I need a real piano keyboard?", 165 | answer1: "No, you can practice using your computer keyboard. However, a MIDI keyboard will provide a better experience.", 166 | question2: "Is it suitable for beginners?", 167 | answer2: "Absolutely! Our courses start from the basics and guide you step by step." 168 | }, 169 | footer: { 170 | title: "Piano Online", 171 | desc: "Making piano learning simple and fun", 172 | contactTitle: "Contact Us", 173 | contactEmail: "Email: contact@piano-online.com", 174 | followTitle: "Follow Us", 175 | wechat: "WeChat", 176 | weibo: "Weibo", 177 | copyright: " 2024 Piano Online. All rights reserved." 178 | } 179 | } 180 | }; 181 | -------------------------------------------------------------------------------- /articles/tutorials/finger-numbers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 钢琴手指编号与基本指法 - Piano Online 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 23 | 24 | 25 | 26 | 27 | 40 | 41 | 42 |
43 |
44 |
45 |

钢琴手指编号与基本指法

46 | 51 |
52 | 53 |
54 |
55 |

手指编号基础

56 |

钢琴手指编号是一个简单但重要的概念。每个手指都有固定的编号,这种编号系统在世界范围内都是统一的:

57 | 58 |
59 |

手指编号对照表

60 |
    61 |
  • 大拇指 = 1
  • 62 |
  • 食指 = 2
  • 63 |
  • 中指 = 3
  • 64 |
  • 无名指 = 4
  • 65 |
  • 小指 = 5
  • 66 |
67 |

注意:左右手的编号规则是相同的,都是从大拇指开始为1。

68 |
69 | 70 |

为什么手指编号很重要?

71 |
    72 |
  • 帮助初学者快速掌握正确的指法
  • 73 |
  • 确保演奏时的手指位置准确
  • 74 |
  • 提高演奏的流畅性和效率
  • 75 |
  • 减少不必要的手腕移动
  • 76 |
77 |
78 | 79 |
80 |

基本指法原则

81 |

良好的指法是流畅演奏的基础,以下是一些基本原则:

82 | 83 |

1. 自然放松原则

84 |
    85 |
  • 手指自然弯曲,保持放松
  • 86 |
  • 避免不必要的手指伸展
  • 87 |
  • 大拇指不要悬空或过分弯曲
  • 88 |
89 | 90 |

2. 位置固定原则

91 |
    92 |
  • 尽量保持手的位置稳定
  • 93 |
  • 避免频繁的手位移动
  • 94 |
  • 合理利用手指跨越来减少换手
  • 95 |
96 | 97 |
98 |

五指位置练习

99 |

从中央C开始,右手五个手指分别放在C D E F G位置:

100 |
    101 |
  1. 保持手型,依次按下每个键
  2. 102 |
  3. 练习不同的按键顺序
  4. 103 |
  5. 确保其他手指保持放松
  6. 104 |
105 |
106 |
107 | 108 |
109 |

常见指法练习

110 | 111 |

1. 基本音阶指法

112 |

以C大调音阶为例:

113 |
114 |

右手上行指法:

115 |

C(1) D(2) E(3) F(1) G(2) A(3) B(4) C(5)

116 |

左手下行指法:

117 |

C(5) B(4) A(3) G(2) F(1) E(3) D(2) C(1)

118 |
119 | 120 |

2. 和弦指法

121 |

三和弦的基本指法:

122 |
    123 |
  • 根音位:1-3-5
  • 124 |
  • 第一转位:1-2-5
  • 125 |
  • 第二转位:1-3-5
  • 126 |
127 | 128 |
129 |

练习建议

130 |
    131 |
  • 先慢速练习,确保指法正确
  • 132 |
  • 注意手指的独立性
  • 133 |
  • 每天坚持练习基本指法
  • 134 |
135 |
136 |
137 | 138 |
139 |

进阶指法技巧

140 | 141 |

1. 手指交叉

142 |

在演奏音阶和琶音时,经常需要使用手指交叉技巧:

143 |
    144 |
  • 大拇指下穿:其他手指按键时,大拇指快速移到下一个位置
  • 145 |
  • 手指跨越:在不移动手腕的情况下,手指跨越其他手指按键
  • 146 |
147 | 148 |

2. 指法替换

149 |

在保持音符持续的情况下更换手指:

150 |
    151 |
  • 长音符的指法替换
  • 152 |
  • 和弦中的指法替换
  • 153 |
  • 旋律线中的无声替换
  • 154 |
155 | 156 |
157 |

每日练习计划

158 |
    159 |
  1. 五指位置练习:10分钟
  2. 160 |
  3. 音阶指法练习:15分钟
  4. 161 |
  5. 和弦指法练习:10分钟
  6. 162 |
  7. 指法替换练习:5分钟
  8. 163 |
164 |
165 |
166 |
167 | 168 | 179 |
180 |
181 | 182 | 183 | -------------------------------------------------------------------------------- /articles/tutorials/reading-notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 五线谱入门:如何读懂音符 - Piano Online 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 23 | 24 | 25 | 26 | 27 | 40 | 41 | 42 |
43 |
44 |
45 |

五线谱入门:如何读懂音符

46 | 51 |
52 | 53 |
54 |
55 |

五线谱基础概念

56 |

五线谱是记录音乐的标准符号系统,由五条平行的横线组成。理解五线谱是学习钢琴的重要基础。

57 | 58 |

五线谱的构成

59 |
    60 |
  • 五条横线和四个间隔组成主体
  • 61 |
  • 上下可添加短线表示更高或更低的音符
  • 62 |
  • 每条线和间隔都代表特定的音高
  • 63 |
64 | 65 |
66 |

记忆技巧

67 |

从下往上数线和间:

68 |
    69 |
  • 线:E G B D F(Every Good Boy Does Fine)
  • 70 |
  • 间:F A C E(FACE)
  • 71 |
72 |
73 |
74 | 75 |
76 |

音符的识别

77 | 78 |

1. 音符的位置

79 |

音符在五线谱上的位置决定了它的音高:

80 |
    81 |
  • 位置越高,音调越高
  • 82 |
  • 位置越低,音调越低
  • 83 |
  • 中央C位于第一条加线上
  • 84 |
85 | 86 |

2. 音符的时值

87 |
88 |

基本音符时值:

89 |
    90 |
  • 全音符:空心圆形,持续四拍
  • 91 |
  • 二分音符:空心圆形加符干,持续两拍
  • 92 |
  • 四分音符:实心圆形加符干,持续一拍
  • 93 |
  • 八分音符:四分音符加一个符尾,持续半拍
  • 94 |
  • 十六分音符:四分音符加两个符尾,持续四分之一拍
  • 95 |
96 |
97 | 98 |
99 |

练习建议

100 |

初学者可以这样练习:

101 |
    102 |
  1. 先识别音符位置,说出音名
  2. 103 |
  3. 再观察音符形状,确定时值
  4. 104 |
  5. 最后结合位置和时值进行弹奏
  6. 105 |
106 |
107 |
108 | 109 |
110 |

调号与拍号

111 | 112 |

1. 调号

113 |

调号出现在五线谱开头,表示乐曲的调式:

114 |
    115 |
  • 升号(#):将音符升高半音
  • 116 |
  • 降号(b):将音符降低半音
  • 117 |
  • 还原号(♮):取消升降效果
  • 118 |
119 | 120 |

2. 拍号

121 |

拍号用两个数字表示,例如4/4、3/4等:

122 |
    123 |
  • 上面的数字表示每小节的拍数
  • 124 |
  • 下面的数字表示以几分音符为一拍
  • 125 |
126 | 127 |
128 |

常见拍号说明

129 |
    130 |
  • 4/4拍:每小节4拍,以四分音符为一拍
  • 131 |
  • 3/4拍:每小节3拍,以四分音符为一拍
  • 132 |
  • 6/8拍:每小节6拍,以八分音符为一拍
  • 133 |
134 |
135 |
136 | 137 |
138 |

实践练习方法

139 | 140 |

1. 基础识谱练习

141 |
142 |

每日练习步骤:

143 |
    144 |
  1. 识别五线谱上的音符位置(10分钟)
  2. 145 |
  3. 练习不同音符的时值(10分钟)
  4. 146 |
  5. 结合简单曲目进行实践(15分钟)
  6. 147 |
148 |
149 | 150 |

2. 进阶练习

151 |
    152 |
  • 视唱练习:看谱唱出音高
  • 153 |
  • 节奏练习:拍打或读出节奏
  • 154 |
  • 默写练习:听音写谱
  • 155 |
156 | 157 |
158 |

注意事项

159 |
    160 |
  • 保持耐心,循序渐进
  • 161 |
  • 每天固定时间练习
  • 162 |
  • 从简单的曲目开始
  • 163 |
  • 多听多看多练
  • 164 |
165 |
166 |
167 |
168 | 169 | 180 |
181 |
182 | 183 | 184 | -------------------------------------------------------------------------------- /tutorials.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 钢琴教程与知识库 - Piano Online 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 42 | 43 | 44 | 45 | 58 | 59 | 60 |
61 |
62 |

钢琴教程与知识库

63 |

从零基础到进阶,系统化的钢琴学习资源

64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 | 80 | 81 | 90 | 91 |
92 |

乐理知识

93 | 99 |
100 | 101 |
102 |

曲目解析

103 | 109 |
110 |
111 |
112 |
113 | 114 | 115 |
116 |
117 |

最新教程

118 |
119 |
120 |
121 |

进阶乐理知识详解

122 |

深入讲解和声学、对位法、曲式分析等进阶乐理知识,帮助你更好地理解音乐作品的结构和创作技巧。

123 | 阅读更多 124 |
125 |
126 | 127 |
128 |
129 |

经典曲目解析

130 |

从入门到高级的经典钢琴曲目详细解析,包括技术难点、练习方法和演奏技巧的讲解。

131 | 阅读更多 132 |
133 |
134 | 135 |
136 |
137 |

演奏技巧提升

138 |

全面提升你的钢琴演奏技巧,包括触键方法、踏板使用、音色控制和表现力的培养。

139 | 阅读更多 140 |
141 |
142 | 143 |
144 |
145 |

科学的练习方法

146 |

介绍高效的钢琴练习方法,帮助你合理安排练习时间,提高练习效率。

147 | 阅读更多 148 |
149 |
150 |
151 |
152 |
153 | 154 | 155 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /articles/tutorials/piano-basics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 钢琴基础知识:认识键盘与正确坐姿 - Piano Online 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 42 | 43 | 44 | 45 | 46 | 59 | 60 | 61 |
62 |
63 |
64 |

钢琴基础知识:认识键盘与正确坐姿

65 | 70 |
71 | 72 |
73 |
74 |

钢琴键盘布局

75 |

钢琴键盘共有88个键,包括52个白键和36个黑键。白键代表自然音(C D E F G A B),黑键代表升降音(C#/Db D#/Eb F#/Gb G#/Ab A#/Bb)。了解键盘布局是学习钢琴的第一步。

76 | 77 |

如何快速识别键位

78 |

观察黑键的分组是识别键位的关键。黑键以2个和3个为一组交替排列,其中:

79 |
    80 |
  • 两个黑键组合前的白键是C
  • 81 |
  • 三个黑键组合前的白键是F
  • 82 |
  • 找到一个C键后,往右依次是D E F G A B
  • 83 |
84 | 85 |
86 |

实用技巧

87 |

初学者可以在中央C(位于键盘中间位置)贴上标记,以此为参照点学习其他键位。中央C通常位于钢琴琴键的正中间,也是五线谱中间C的位置。

88 |
89 |
90 | 91 |
92 |

正确的弹琴坐姿

93 |

良好的坐姿不仅能提高演奏效果,还能预防疲劳和损伤。以下是正确坐姿的要点:

94 | 95 |

坐姿要点

96 |
    97 |
  1. 98 | 座椅高度: 99 |

    调整琴凳高度,使手肘略高于键盘面。坐在琴凳前半部分,保持身体稳定但不僵硬。

    100 |
  2. 101 |
  3. 102 | 距离: 103 |

    与琴键保持一臂的距离,身体能自然伸展。手肘不要贴近身体,保持适当空间。

    104 |
  4. 105 |
  5. 106 | 背部姿势: 107 |

    保持背部挺直但放松,肩膀自然下垂,不要耸肩。

    108 |
  6. 109 |
  7. 110 | 手腕位置: 111 |

    手腕要保持水平,既不要过分抬高也不要下垂,保持自然放松的状态。

    112 |
  8. 113 |
114 | 115 |
116 |

注意事项

117 |

练琴时要注意:

118 |
    119 |
  • 每练习30分钟要起来活动一下
  • 120 |
  • 感觉疲劳时及时休息
  • 121 |
  • 保持手指放松,不要过分用力
  • 122 |
123 |
124 |
125 | 126 |
127 |

基本手型

128 |

正确的手型是演奏的基础,需要注意以下几点:

129 | 130 |

手型要点

131 |
    132 |
  • 手指自然弯曲,如同握住一个小球
  • 133 |
  • 指尖垂直按键,而不是用指腹
  • 134 |
  • 大拇指自然弯曲,不要翘起
  • 135 |
  • 手腕保持柔软,不要僵硬
  • 136 |
137 | 138 |
139 |

练习建议

140 |

初学者可以先进行以下练习:

141 |
    142 |
  1. 在桌面上练习手型,感受手指的放松状态
  2. 143 |
  3. 用单个手指轻柔地按键,体会触键的感觉
  4. 144 |
  5. 练习五个手指依次按键,保持其他手指放松
  6. 145 |
146 |
147 |
148 | 149 |
150 |

日常练习建议

151 |
152 |

初学者每日练习计划

153 |
    154 |
  • 热身练习:5-10分钟
  • 155 |
  • 指法练习:10-15分钟
  • 156 |
  • 曲目练习:20-30分钟
  • 157 |
  • 复习与巩固:10分钟
  • 158 |
159 |
160 | 161 |
162 |

重要提示

163 |

正确的基础比快速的进步更重要。建议:

164 |
    165 |
  • 每天保持固定的练习时间
  • 166 |
  • 循序渐进,不要急于求成
  • 167 |
  • 多观察、多聆听、多思考
  • 168 |
169 |
170 |
171 |
172 | 173 | 184 |
185 |
186 | 187 | 188 | -------------------------------------------------------------------------------- /articles/tutorials/rhythm-basics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 节奏基础:认识拍子与节拍 - Piano Online 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 23 | 24 | 25 | 26 | 27 | 40 | 41 | 42 |
43 |
44 |
45 |

节奏基础:认识拍子与节拍

46 | 51 |
52 | 53 |
54 |
55 |

什么是节奏?

56 |

节奏是音乐的时间组织,它决定了音乐的律动感和时间规律。良好的节奏感是演奏钢琴的重要基础。

57 | 58 |

节奏的基本要素

59 |
    60 |
  • 拍子:音乐的基本时间单位
  • 61 |
  • 节拍:拍子的组织方式
  • 62 |
  • 速度:音乐进行的快慢
  • 63 |
  • 强弱:拍子的重音规律
  • 64 |
65 | 66 |
67 |

重要提示

68 |

好的节奏感需要:

69 |
    70 |
  • 稳定的节拍感
  • 71 |
  • 准确的时值控制
  • 72 |
  • 合理的强弱变化
  • 73 |
74 |
75 |
76 | 77 |
78 |

常见拍子类型

79 | 80 |

1. 二拍子

81 |
82 |

特点:

83 |
    84 |
  • 每小节两拍
  • 85 |
  • 强弱规律:强-弱
  • 86 |
  • 常见于进行曲
  • 87 |
88 |
89 | 90 |

2. 三拍子

91 |
92 |

特点:

93 |
    94 |
  • 每小节三拍
  • 95 |
  • 强弱规律:强-弱-弱
  • 96 |
  • 常见于华尔兹
  • 97 |
98 |
99 | 100 |

3. 四拍子

101 |
102 |

特点:

103 |
    104 |
  • 每小节四拍
  • 105 |
  • 强弱规律:强-弱-次强-弱
  • 106 |
  • 最常见的拍子类型
  • 107 |
108 |
109 | 110 |
111 |

练习方法

112 |
    113 |
  1. 用节拍器打拍子
  2. 114 |
  3. 跟随音乐拍手
  4. 115 |
  5. 数拍子练习
  6. 116 |
117 |
118 |
119 | 120 |
121 |

基本节奏型

122 | 123 |

1. 均匀节奏

124 |

所有音符时值相同的节奏型:

125 |
    126 |
  • 四分音符连续
  • 127 |
  • 八分音符连续
  • 128 |
  • 十六分音符连续
  • 129 |
130 | 131 |

2. 附点节奏

132 |

带有附点的节奏型:

133 |
    134 |
  • 附点四分音符加八分音符
  • 135 |
  • 附点八分音符加十六分音符
  • 136 |
137 | 138 |

3. 切分节奏

139 |

强弱关系与正常拍子相反的节奏型:

140 |
    141 |
  • 弱拍重音
  • 142 |
  • 跨拍音符
  • 143 |
144 | 145 |
146 |

练习建议

147 |
    148 |
  • 先用节拍器慢速练习
  • 149 |
  • 掌握后逐渐加快速度
  • 150 |
  • 注意保持稳定的拍子
  • 151 |
152 |
153 |
154 | 155 |
156 |

节奏训练方法

157 | 158 |

1. 基础训练

159 |
160 |

每日练习计划:

161 |
    162 |
  1. 节拍器练习(10分钟) 163 |
      164 |
    • 从慢速开始(60拍/分)
    • 165 |
    • 保持稳定的拍子感
    • 166 |
    • 逐渐提高速度
    • 167 |
    168 |
  2. 169 |
  3. 拍手练习(5分钟) 170 |
      171 |
    • 跟随音乐拍手
    • 172 |
    • 体会强弱变化
    • 173 |
    174 |
  4. 175 |
  5. 数拍练习(10分钟) 176 |
      177 |
    • 大声数拍
    • 178 |
    • 强调重拍
    • 179 |
    180 |
  6. 181 |
182 |
183 | 184 |

2. 进阶训练

185 |
    186 |
  • 双手不同节奏训练
  • 187 |
  • 复杂节奏型练习
  • 188 |
  • 变速练习
  • 189 |
190 | 191 |
192 |

重要提示

193 |

培养节奏感的关键:

194 |
    195 |
  • 坚持每天练习
  • 196 |
  • 从简单节奏开始
  • 197 |
  • 保持耐心和专注
  • 198 |
  • 多听多练多感受
  • 199 |
200 |
201 |
202 |
203 | 204 | 215 |
216 |
217 | 218 | 219 | -------------------------------------------------------------------------------- /practice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 钢琴练习 - Piano Online 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 42 | 44 | 45 | 46 | 47 | 60 | 61 | 62 |
63 |
64 |

钢琴练习工具

65 | 66 | 67 |
68 |

节拍器

69 |
70 |
71 | 120 BPM 72 |
73 |
74 | 75 | 76 | 77 |
78 |
79 | 85 |
86 |
87 |
88 | 89 | 90 |
91 |

音阶练习

92 |
93 |
94 | 103 |
104 |
105 |
106 |
107 | 108 |
109 |
110 | 111 | 112 |
113 |

和弦练习

114 |
115 |
116 | 123 |
124 |
125 |
126 |
127 | 128 |
129 |
130 | 131 | 132 |
133 |

视奏训练

134 |
135 |
136 | 141 |
142 |
143 | 144 |
145 | 146 |
147 |

点击"生成新谱例"按钮来练习视奏

148 |

149 |
150 |
151 |
152 |
153 |
154 | 155 | 156 |
157 |
158 |

练习建议

159 |
160 |
161 |

制定练习计划

162 |
    163 |
  • 设定明确的练习目标
  • 164 |
  • 合理安排练习时间
  • 165 |
  • 循序渐进提高难度
  • 166 |
  • 保持练习记录
  • 167 |
168 |
169 | 170 |
171 |

正确的练习方法

172 |
    173 |
  • 从慢速开始练习
  • 174 |
  • 使用节拍器保持节奏
  • 175 |
  • 分手练习困难片段
  • 176 |
  • 注意指法规范
  • 177 |
178 |
179 | 180 |
181 |

避免常见错误

182 |
    183 |
  • 不要急于求成
  • 184 |
  • 保持正确的坐姿
  • 185 |
  • 适时休息放松
  • 186 |
  • 注意听音辨音
  • 187 |
188 |
189 |
190 |
191 |
192 | 193 | 194 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /test-improvements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Piano UX Improvements Test Page 7 | 97 | 98 | 99 |

🎹 Piano Online - UX Improvements Test Page

100 |

Date: 2025-11-04

101 | 102 |
103 |

Quick Actions

104 |
105 | 108 | 111 | 114 |
115 |
116 | 117 |
118 |

1. Layout Changes MANUAL TEST

119 | 124 |
125 | 126 |
127 |

2. Keyboard Labels MANUAL TEST

128 | 134 |
135 | 136 |
137 |

3. Tutorial Overlay MANUAL TEST

138 | 146 |
147 | Test Command: localStorage.removeItem('piano-tutorial-completed') 148 |
149 |
150 | 153 |
154 |
155 | 156 |
157 |

4. Simplified Terminology MANUAL TEST

158 | 165 |
166 | 167 |
168 |

5. Visual Feedback MANUAL TEST

169 | 175 |
176 | 177 |
178 |

6. Song Selection MANUAL TEST

179 | 184 |
185 | 186 |
187 |

Browser Console Check

188 |
189 | Expected: No errors in console
190 | Check for: Piano initialized, Practice Mode initialized, Tutorial initialized 191 |
192 |
193 | 196 |
197 |
198 |
199 | 200 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /js/recorder.js: -------------------------------------------------------------------------------- 1 | class PianoRecorder { 2 | constructor(piano) { 3 | console.log('PianoRecorder constructor called'); 4 | this.piano = piano; 5 | this.mediaRecorder = null; 6 | this.audioContext = null; 7 | this.audioChunks = []; 8 | this.recordingStartTime = null; 9 | this.recordingTimer = null; 10 | this.audioBlob = null; 11 | this.audioUrl = null; 12 | 13 | // 等待 DOM 加载完成后再初始化 14 | if (document.readyState === 'complete') { 15 | console.log('DOM already loaded, initializing recorder UI immediately'); 16 | this.initializeUI(); 17 | } else { 18 | console.log('Waiting for DOM to load before initializing recorder UI'); 19 | document.addEventListener('DOMContentLoaded', () => { 20 | console.log('DOM loaded, initializing recorder UI'); 21 | this.initializeUI(); 22 | }); 23 | } 24 | } 25 | 26 | initializeUI() { 27 | console.log('Initializing recorder UI...'); 28 | 29 | // 获取 UI 元素 30 | this.startButton = document.getElementById('start-recording'); 31 | this.stopButton = document.getElementById('stop-recording'); 32 | this.recordingStatus = document.getElementById('recording-status'); 33 | this.recordingTime = document.getElementById('recording-time'); 34 | this.sharePanel = document.getElementById('share-panel'); 35 | this.recordingPreview = document.getElementById('recording-preview'); 36 | this.shareJikeButton = document.getElementById('share-jike'); 37 | this.shareTwitterButton = document.getElementById('share-twitter'); 38 | this.downloadButton = document.getElementById('download-recording'); 39 | 40 | // 检查是否所有元素都存在 41 | if (!this.startButton || !this.stopButton || !this.recordingStatus || 42 | !this.recordingTime || !this.sharePanel || !this.recordingPreview || 43 | !this.shareJikeButton || !this.shareTwitterButton || !this.downloadButton) { 44 | console.error('Some recorder UI elements are missing'); 45 | return; 46 | } 47 | 48 | console.log('All recorder UI elements found, setting up event listeners'); 49 | this.setupEventListeners(); 50 | } 51 | 52 | setupEventListeners() { 53 | this.startButton.addEventListener('click', () => this.startRecording()); 54 | this.stopButton.addEventListener('click', () => this.stopRecording()); 55 | this.shareJikeButton.addEventListener('click', () => this.shareToJike()); 56 | this.shareTwitterButton.addEventListener('click', () => this.shareToTwitter()); 57 | this.downloadButton.addEventListener('click', () => this.downloadRecording()); 58 | console.log('Recorder event listeners set up'); 59 | } 60 | 61 | async initAudioContext() { 62 | try { 63 | // 创建音频上下文 64 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); 65 | 66 | // 创建音频处理节点 67 | const destination = this.audioContext.createMediaStreamDestination(); 68 | 69 | // 连接钢琴音频到录音目标 70 | this.piano.audio.gainNode.connect(destination); 71 | 72 | // 创建 MediaRecorder 73 | this.mediaRecorder = new MediaRecorder(destination.stream); 74 | 75 | // 设置数据处理 76 | this.mediaRecorder.ondataavailable = (event) => { 77 | this.audioChunks.push(event.data); 78 | }; 79 | 80 | this.mediaRecorder.onstop = () => { 81 | this.finalizeRecording(); 82 | }; 83 | 84 | return true; 85 | } catch (error) { 86 | console.error('初始化录音失败:', error); 87 | alert('无法初始化录音功能。请确保您的浏览器支持音频录制。'); 88 | return false; 89 | } 90 | } 91 | 92 | async startRecording() { 93 | try { 94 | if (!this.audioContext && !(await this.initAudioContext())) { 95 | return; 96 | } 97 | 98 | // 重置录音数据 99 | this.audioChunks = []; 100 | this.recordingStartTime = Date.now(); 101 | 102 | // 开始录音 103 | this.mediaRecorder.start(); 104 | this.startTimer(); 105 | 106 | // 更新 UI 107 | this.startButton.classList.add('hidden'); 108 | this.stopButton.classList.remove('hidden'); 109 | this.recordingStatus.classList.remove('hidden'); 110 | this.sharePanel.classList.add('hidden'); 111 | 112 | console.log('开始录音'); 113 | } catch (error) { 114 | console.error('开始录音失败:', error); 115 | alert('无法开始录音。请确保已授予录音权限。'); 116 | } 117 | } 118 | 119 | stopRecording() { 120 | if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') { 121 | this.mediaRecorder.stop(); 122 | this.stopTimer(); 123 | 124 | // 更新 UI 125 | this.startButton.classList.remove('hidden'); 126 | this.stopButton.classList.add('hidden'); 127 | this.recordingStatus.classList.add('hidden'); 128 | 129 | console.log('停止录音'); 130 | } 131 | } 132 | 133 | startTimer() { 134 | this.stopTimer(); // 清除可能存在的旧计时器 135 | this.recordingTimer = setInterval(() => { 136 | const duration = Date.now() - this.recordingStartTime; 137 | const minutes = Math.floor(duration / 60000); 138 | const seconds = Math.floor((duration % 60000) / 1000); 139 | this.recordingTime.textContent = 140 | `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; 141 | }, 1000); 142 | } 143 | 144 | stopTimer() { 145 | if (this.recordingTimer) { 146 | clearInterval(this.recordingTimer); 147 | this.recordingTimer = null; 148 | } 149 | } 150 | 151 | finalizeRecording() { 152 | // 创建音频 Blob 153 | this.audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' }); 154 | 155 | // 创建音频 URL 156 | if (this.audioUrl) { 157 | URL.revokeObjectURL(this.audioUrl); 158 | } 159 | this.audioUrl = URL.createObjectURL(this.audioBlob); 160 | 161 | // 设置预览 162 | this.recordingPreview.src = this.audioUrl; 163 | 164 | // 显示分享面板 165 | this.sharePanel.classList.remove('hidden'); 166 | } 167 | 168 | async shareToJike() { 169 | try { 170 | // 创建一个 FormData 对象来上传音频文件 171 | const formData = new FormData(); 172 | formData.append('audio', this.audioBlob, 'piano-recording.wav'); 173 | formData.append('text', '我在 Piano Online 上录制了一段钢琴演奏!'); 174 | 175 | // 这里需要替换成实际的即刻 API 端点 176 | const response = await fetch('YOUR_JIKE_API_ENDPOINT', { 177 | method: 'POST', 178 | body: formData, 179 | headers: { 180 | 'Authorization': 'Bearer YOUR_JIKE_TOKEN' 181 | } 182 | }); 183 | 184 | if (response.ok) { 185 | alert('成功分享到即刻!'); 186 | } else { 187 | throw new Error('分享失败'); 188 | } 189 | } catch (error) { 190 | console.error('分享到即刻失败:', error); 191 | alert('分享到即刻失败,请稍后重试。'); 192 | } 193 | } 194 | 195 | shareToTwitter() { 196 | // 创建分享链接 197 | const text = encodeURIComponent('我在 Piano Online 上录制了一段钢琴演奏!'); 198 | const url = encodeURIComponent(window.location.href); 199 | const twitterUrl = `https://twitter.com/intent/tweet?text=${text}&url=${url}`; 200 | 201 | // 打开 Twitter 分享窗口 202 | window.open(twitterUrl, '_blank', 'width=550,height=420'); 203 | } 204 | 205 | downloadRecording() { 206 | if (!this.audioBlob) { 207 | alert('没有可下载的录音'); 208 | return; 209 | } 210 | 211 | // 创建下载链接 212 | const downloadUrl = window.URL.createObjectURL(this.audioBlob); 213 | const a = document.createElement('a'); 214 | a.style.display = 'none'; 215 | a.href = downloadUrl; 216 | a.download = 'piano-recording.wav'; 217 | 218 | // 触发下载 219 | document.body.appendChild(a); 220 | a.click(); 221 | 222 | // 清理 223 | setTimeout(() => { 224 | document.body.removeChild(a); 225 | window.URL.revokeObjectURL(downloadUrl); 226 | }, 100); 227 | } 228 | 229 | // 释放资源 230 | dispose() { 231 | this.stopRecording(); 232 | this.stopTimer(); 233 | if (this.audioUrl) { 234 | URL.revokeObjectURL(this.audioUrl); 235 | } 236 | if (this.audioContext) { 237 | this.audioContext.close(); 238 | } 239 | } 240 | } 241 | 242 | export { PianoRecorder }; 243 | --------------------------------------------------------------------------------