该文档用于详细介绍“交互式二胡弹奏 | 白色极简版”网页的功能、结构、技术实现及使用方法,帮助开发者理解代码逻辑与用户快速上手操作。

 

一、核心概述

 

- 功能定位:一款基于HTML5、Web Audio API和Canvas的网页端虚拟乐器应用,通过可视化交互模拟二胡演奏,支持自定义参数调节、乐谱播放、录音回放等功能。

- 设计风格:白色极简风,以黑白两色为主色调,搭配简洁线条装饰,界面层次清晰,交互反馈直观。

- 适配场景:支持PC端与移动端,兼容现代主流浏览器(Chrome、Firefox等),无需安装额外插件即可运行。

 

二、页面结构

 

网页分为3个核心屏幕(区域)+ 2个模态框 + 页脚,采用“滚动差分效果”实现屏幕间平滑过渡。

 

模块 核心内容 功能作用 

首屏(screen1) 标题“交互式二胡弹奏”、功能简介、二胡图片、操作按钮 应用入口,引导用户“开始演奏”或“了解更多” 

演奏区域(screen2) 左侧虚拟二胡(Canvas绘制)、右侧控制面板 核心交互区,实现二胡弹奏、参数调节 

帮助与设置(screen3) 使用帮助(操作指南、演奏技巧)、应用设置(视觉效果、音频质量等) 辅助功能区,提供操作说明与个性化配置 

录音回放模态框 播放/暂停、进度条、保存/删除按钮 展示录音内容,支持回放、保存或删除录音 

乐谱显示模态框 乐谱标题、音符网格(含音名、八度、拍数) 展示预设乐谱,同步高亮当前演奏音符 

页脚 版权信息、技术栈说明、社交图标 版权声明与外部链接入口 

 

三、核心功能与交互

 

1. 基础演奏功能

 

- 琴弓拖拽:鼠标/手指点击Canvas中的琴弓并拖动,琴弓移动速度决定音量大小,纵向位置决定音高(上方为高音,下方为低音)。

- 按弦定位:点击Canvas中琴弦(中间两条竖线),生成“手指指示器”,模拟二胡按弦动作,改变当前音高。

- 键盘控制:开启“键盘控制”后,通过A、W、S、E等按键(对应C4、C#4、D4等音符)快速演奏,释放按键停止发声。

 

2. 参数调节功能

 

右侧控制面板支持3类核心参数调节,实时作用于演奏效果:

 

- 音量控制:

- 主音量:调节整体输出音量(0-100);

- 弓压:模拟二胡弓毛与琴弦的压力,数值越高音色越浑厚;

- 音色:通过低通滤波器调节音色明暗(数值越高,高频保留越多,音色越亮)。

- 乐谱选择:提供4首预设乐谱(小星星、茉莉花、梁祝、赛马),点击后弹出乐谱模态框并自动播放。

- 演奏技巧:4种二胡常用技巧,点击按钮切换,按钮高亮表示当前激活状态:

- 颤音(tremolo):快速交替按弦,产生颤动音效;

- 滑音(glissando):音高渐变,模拟手指在琴弦上滑动;

- 顿弓(staccato):快速加重弓压后衰减,产生短促重音;

- 拨弦(pizzicato):模拟手指直接拨弦,音效短促清脆。

 

3. 录音与回放

 

- 录音:点击演奏区右上角“录音按钮”(圆形图标),按钮变为“停止”图标,开始记录演奏的音符(音高、时间、时长);再次点击停止录音,自动弹出“录音回放模态框”。

- 回放:在录音模态框中点击“播放”按钮,同步还原录制的演奏效果,进度条实时更新播放位置。

- 保存/删除:支持保存录音(当前为提示性功能)或删除录音(清空已录制的音符数据)。

 

4. 个性化设置

 

在“帮助与设置”屏可配置3项核心参数,点击“保存设置”生效:

 

- 视觉效果:开启/关闭琴弦振动、音量波纹等动态效果;

- 音频质量:高/中/低三档,影响泛音数量与滤波器参数(高音质泛音更丰富);

- 键盘控制:开启/关闭键盘按键演奏功能。

 

四、技术实现

 

1. 核心技术栈

 

- 前端框架/工具:HTML5(语义化标签)、Tailwind CSS(样式开发)、Font Awesome(图标);

- 交互与音频:JavaScript(逻辑控制)、Web Audio API(合成二胡音色)、Canvas(绘制虚拟二胡与动态效果);

- 响应式适配:Tailwind CSS的flex/grid布局、viewport元标签,实现PC/移动端适配。

 

2. 关键技术逻辑

 

- 二胡音色合成(Web Audio API):

1. 主振荡器:使用“锯齿波(sawtooth)”作为基础音色,模拟二胡的明亮质感;

2. 泛音叠加:生成2-5倍频的泛音振荡器,增强音色层次感;

3. 滤波器:通过低通滤波器(lowpass)调节音色,“音色滑块”控制滤波器频率(1000-5000Hz);

4. 音量包络:根据演奏技巧(如顿弓、拨弦)调整ADSR包络,模拟真实演奏的音量变化。

- Canvas动态绘制:

1. 静态元素:二胡琴筒、琴杆、琴轴、琴弦等基础结构,初始化时绘制;

2. 动态元素:琴弓(跟随鼠标/手指移动)、手指指示器(按弦时显示)、琴弦振动(演奏时生成正弦波动效果)、音量波纹(根据音量大小生成同心圆)。

- 滚动差分效果:监听window.scroll事件,根据滚动距离计算“进度值”,通过transform: translateY()控制各屏幕的位置,实现“前覆盖后”“后覆盖前”的平滑过渡。

 

五、使用说明

 

1. 基础操作步骤

 

1. 打开网页,进入首屏后点击“开始演奏”,滚动至“演奏区域”;

2. 鼠标点击Canvas中的琴弓并拖动,体验基础演奏;

3. 点击琴弦调整按弦位置,改变音高;

4. 在右侧控制面板调节“音量”“音色”,或切换“演奏技巧”;

5. 选择预设乐谱(如“小星星”),观看乐谱模态框并聆听自动演奏;

6. 点击“录音按钮”录制个人演奏,录制完成后在模态框中回放。

 

2. 常见问题

 

- 无声音输出:检查浏览器是否支持Web Audio API(建议使用Chrome),或是否开启“静音模式”;

- 移动端交互异常:确保触摸操作时点击琴弓/琴弦区域,避免点击边缘空白处;

- 录音无内容:需在“开始演奏”后点击录音按钮,确保录制过程中有琴弓拖拽操作。

 

六、代码结构说明

 

1. HTML部分

 

- 头部(head):引入Tailwind CSS、Font Awesome,配置Tailwind自定义主题(颜色、字体、动画),定义工具类(@layer utilities)与自定义样式(滚动条、鼠标、模态框等);

- 主体(body):按“屏幕-模态框-页脚”顺序编写DOM结构,为交互元素添加唯一ID(如erhuCanvas、recordBtn),便于JS获取。

 

2. JavaScript部分

 

- 全局变量:存储音频上下文(audioContext)、合成器(erhuSynth)、演奏状态(isPlaying、currentNote)、参数配置(volume、bowPressure)等核心数据;

- 初始化函数(init):页面加载后执行,完成Canvas尺寸设置、音频上下文初始化、事件监听绑定、自定义鼠标初始化;

- 核心逻辑函数:

- 音频相关:createErhuSynth(创建合成器)、erhuSynth.play/stop(播放/停止音符);

- 交互相关:handleMouseDown/Move/Up(鼠标事件)、handleTouchStart/Move/End(触摸事件);

- 绘制相关:drawErhu(绘制二胡)、animate(动画循环,调用requestAnimationFrame);

- 功能相关:toggleRecording(切换录音)、playScore(播放乐谱)、saveSettings(保存设置)。

 

七、扩展建议

 

1. 新增乐谱:在 scores 对象中添加新乐谱数组(含note(音高)、duration(时长)),即可在“乐谱选择”中显示;

2. 自定义音色:扩展“音色滑块”功能,增加“高通滤波器”“失真效果”等选项,丰富音色种类;

3. 分享功能:对接第三方分享接口,支持将录音或演奏视频分享至社交平台;

4. 多乐器切换:基于现有架构,扩展小提琴、古筝等其他乐器的音色合成逻辑。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>交互式二胡弹奏 | 白色极简版</title>
  <!-- Tailwind CSS v3 -->
  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Font Awesome -->
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <!-- 统一的 Tailwind 配置 -->
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#000000',
            secondary: '#666666',
            accent: '#333333',
            light: '#ffffff',
            dark: '#111111'
          },
          fontFamily: {
            sans: ['Inter', 'system-ui', 'sans-serif'],
          },
          animation: {
            'vibrate': 'vibrate 0.3s ease-in-out infinite',
          },
          keyframes: {
            vibrate: {
              '0%, 100%': { transform: 'translateY(0)' },
              '50%': { transform: 'translateY(-1px)' },
            }
          }
        }
      }
    }
  </script>
  <style type="text/tailwindcss">
    @layer utilities {
      .text-shadow {
        text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
      }
      .line-element {
        @apply h-px bg-black my-4 transition-all duration-300;
      }
      .hover-scale {
        @apply transition-transform duration-300 hover:scale-105;
      }
      .diagonal-line {
        @apply absolute h-24 w-px bg-black origin-bottom-left transform rotate-45;
      }
      .horizontal-line {
        @apply absolute left-0 w-full h-px bg-black opacity-70;
      }
      .corner-line {
        @apply absolute;
      }
      .corner-line::before {
        content: '';
        @apply absolute w-16 h-px bg-black;
      }
      .corner-line::after {
        content: '';
        @apply absolute h-16 w-px bg-black;
      }
    }
  </style>
  <style>
    /* 自定义滚动条 */
    ::-webkit-scrollbar {
      width: 6px;
      height: 6px;
    }
    ::-webkit-scrollbar-track {
      background: #f1f1f1;
    }
    ::-webkit-scrollbar-thumb {
      background: #cccccc;
      border-radius: 3px;
    }
    ::-webkit-scrollbar-thumb:hover {
      background: #999999;
    }
    
    /* 自定义鼠标样式 */
    #custom-cursor {
      position: fixed;
      width: 24px;
      height: 24px;
      border-radius: 50%;
      border: 1px solid #000;
      pointer-events: none;
      mix-blend-mode: difference;
      z-index: 50;
      transform: translate(-50%, -50%);
      opacity: 0;
      transition: opacity 0.3s ease;
    }
    
    /* 滚动差分效果 */
    .screen {
      position: relative;
      min-height: 100vh;
      transition: transform 0.5s ease-out;
    }
    
    /* 线条装饰 */
    .decorative-line {
      position: absolute;
      background-color: #000;
      transition: all 0.3s ease;
    }
    
    /* 按钮悬停效果 */
    .minimal-btn {
      position: relative;
      overflow: hidden;
      transition: all 0.3s ease;
    }
    .minimal-btn::after {
      content: '';
      position: absolute;
      bottom: 0;
      left: 0;
      width: 0;
      height: 1px;
      background-color: #000;
      transition: width 0.3s ease;
    }
    .minimal-btn:hover::after {
      width: 100%;
    }
    
    /* 滑块样式 */
    input[type="range"] {
      -webkit-appearance: none;
      width: 100%;
      height: 1px;
      background: #000;
      outline: none;
    }
    input[type="range"]::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: #000;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    input[type="range"]::-webkit-slider-thumb:hover {
      transform: scale(1.2);
    }
    
    /* 模态框动画 */
    .modal {
      opacity: 0;
      visibility: hidden;
      transition: all 0.3s ease;
    }
    .modal.active {
      opacity: 1;
      visibility: visible;
    }
    .modal-content {
      transform: translateY(20px);
      transition: transform 0.3s ease;
    }
    .modal.active .modal-content {
      transform: translateY(0);
    }
  </style>
</head>
<body class="bg-white min-h-screen font-sans text-black overflow-x-hidden">
  <!-- 自定义鼠标 -->
  <div id="custom-cursor" class="fixed w-6 h-6 rounded-full border border-black pointer-events-none mix-blend-difference z-50 transform -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-300"></div>

  <!-- 线条装饰 -->
  <div class="decorative-line w-1/3 h-px top-20 left-0"></div>
  <div class="decorative-line w-px h-1/3 top-0 left-20"></div>
  <div class="decorative-line w-1/3 h-px bottom-20 right-0"></div>
  <div class="decorative-line w-px h-1/3 bottom-0 right-20"></div>

  <!-- 主容器 -->
  <div class="relative min-h-screen overflow-hidden">
    <!-- 首屏 -->
    <section id="screen1" class="screen relative min-h-screen flex items-center justify-center z-50">
      <div class="container mx-auto px-4 py-12">
        <div class="flex flex-col md:flex-row items-center justify-between">
          <div class="w-full md:w-1/2 mb-8 md:mb-0">
            <div class="relative">
              <h1 class="text-5xl font-light mb-6 relative inline-block">
                <span class="relative z-10">交互式二胡弹奏</span>
                <div class="absolute bottom-2 left-0 w-full h-1 bg-black"></div>
              </h1>
              <p class="text-lg text-gray-600 mb-8 max-w-md">
                通过现代网页技术,体验中国传统乐器二胡的魅力。简单直观的交互方式,让任何人都能轻松演奏。
              </p>
              <div class="flex space-x-4">
                <a href="#screen2" class="minimal-btn px-6 py-2 inline-block">
                  开始演奏
                  <i class="fa fa-arrow-right ml-2"></i>
                </a>
                <a href="#help" class="minimal-btn px-6 py-2 inline-block">
                  了解更多
                </a>
              </div>
              
              <!-- 装饰线条 -->
              <div class="absolute -right-10 top-10 w-20 h-px bg-black"></div>
              <div class="absolute -right-10 top-10 w-px h-20 bg-black"></div>
            </div>
          </div>
          
          <div class="w-full md:w-1/2 flex justify-center">
            <div class="relative w-full max-w-md">
              <img src="https://p11-doubao-search-sign.byteimg.com/tos-cn-i-be4g95zd3a/871555826284101651~tplv-be4g95zd3a-image.jpeg?rk3s=542c0f93&x-expires=1776927777&x-signature=0NERVdr0sTEl54o1Z90Dxxltr18%3D" 
                 alt="二胡" 
                 class="w-full h-auto object-contain hover-scale">
              
              <!-- 装饰线条 -->
              <div class="absolute -left-4 top-1/4 w-px h-32 bg-black"></div>
              <div class="absolute -right-4 bottom-1/4 w-px h-32 bg-black"></div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <!-- 第二屏:演奏区域 -->
    <section id="screen2" class="screen relative min-h-screen bg-white z-40 flex items-center">
      <div class="container mx-auto px-4 py-12">
        <div class="mb-8">
          <h2 class="text-3xl font-light mb-2">演奏区域</h2>
          <div class="w-24 h-px bg-black"></div>
        </div>
        
        <div class="flex flex-col lg:flex-row gap-8">
          <!-- 左侧:虚拟二胡区域 -->
          <div class="lg:w-2/3 bg-white border border-gray-200 p-4 relative overflow-hidden">
            <div class="absolute top-4 right-4 flex space-x-2">
              <button id="recordBtn" class="w-10 h-10 rounded-full bg-black text-white flex items-center justify-center hover:bg-gray-800 transition-all">
                <i class="fa fa-circle"></i>
              </button>
              <button id="playBtn" class="w-10 h-10 rounded-full bg-black text-white flex items-center justify-center hover:bg-gray-800 transition-all">
                <i class="fa fa-play"></i>
              </button>
            </div>
            
            <div class="relative">
              <canvas id="erhuCanvas" class="w-full border border-gray-200 bg-white" height="500"></canvas>
              
              <!-- 按弦位置指示器 -->
              <div id="fingerIndicator" class="absolute hidden w-6 h-6 bg-black rounded-full transform -translate-x-1/2 -translate-y-1/2 z-10">
                <div class="absolute -top-1 -left-1 w-8 h-8 border-2 border-gray-300 rounded-full animate-ping"></div>
              </div>
              
              <!-- 音量指示器 -->
              <div class="absolute bottom-4 left-4 bg-white border border-gray-200 p-2">
                <div class="flex items-center">
                  <i class="fa fa-volume-up mr-2"></i>
                  <div id="volumeBar" class="w-24 h-1 bg-gray-200">
                    <div id="volumeLevel" class="h-full bg-black w-0 transition-all"></div>
                  </div>
                </div>
              </div>
            </div>
            
            <!-- 当前音符显示 -->
            <div class="mt-4 text-center">
              <div id="currentNoteDisplay" class="text-4xl font-light text-black">C4</div>
              <div id="noteInfo" class="text-gray-600">点击琴弦并拖动琴弓演奏</div>
            </div>
          </div>

          <!-- 右侧:控制面板 -->
          <div class="lg:w-1/3 flex flex-col gap-6">
            <!-- 音量控制 -->
            <div class="bg-white border border-gray-200 p-6">
              <h3 class="text-xl font-light mb-4 flex items-center">
                <i class="fa fa-volume-up text-black mr-2"></i> 音量控制
              </h3>
              <div class="flex flex-col space-y-4">
                <div>
                  <label for="volumeSlider" class="block text-sm text-gray-600 mb-1">主音量</label>
                  <input type="range" id="volumeSlider" min="0" max="100" value="80" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
                </div>
                <div>
                  <label for="bowPressureSlider" class="block text-sm text-gray-600 mb-1">弓压</label>
                  <input type="range" id="bowPressureSlider" min="0" max="100" value="50" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
                </div>
                <div>
                  <label for="toneSlider" class="block text-sm text-gray-600 mb-1">音色</label>
                  <input type="range" id="toneSlider" min="0" max="100" value="70" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
                </div>
              </div>
            </div>

            <!-- 乐谱选择 -->
            <div class="bg-white border border-gray-200 p-6">
              <h3 class="text-xl font-light mb-4 flex items-center">
                <i class="fa fa-music text-black mr-2"></i> 乐谱选择
              </h3>
              <div class="space-y-2">
                <button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="小星星">
                  <span>小星星</span>
                  <i class="fa fa-play-circle text-black"></i>
                </button>
                <button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="茉莉花">
                  <span>茉莉花</span>
                  <i class="fa fa-play-circle text-black"></i>
                </button>
                <button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="梁祝">
                  <span>梁祝</span>
                  <i class="fa fa-play-circle text-black"></i>
                </button>
                <button class="score-btn w-full text-left px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex justify-between items-center" data-score="赛马">
                  <span>赛马</span>
                  <i class="fa fa-play-circle text-black"></i>
                </button>
              </div>
            </div>

            <!-- 演奏技巧 -->
            <div class="bg-white border border-gray-200 p-6">
              <h3 class="text-xl font-light mb-4 flex items-center">
                <i class="fa fa-sliders text-black mr-2"></i> 演奏技巧
              </h3>
              <div class="grid grid-cols-2 gap-2">
                <button id="tremoloBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
                  <i class="fa fa-refresh mr-1"></i> 颤音
                </button>
                <button id="glissandoBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
                  <i class="fa fa-arrows-h mr-1"></i> 滑音
                </button>
                <button id="staccatoBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
                  <i class="fa fa-stop mr-1"></i> 顿弓
                </button>
                <button id="pizzicatoBtn" class="px-3 py-2 border border-gray-200 hover:bg-gray-50 transition-all flex items-center justify-center">
                  <i class="fa fa-hand-paper-o mr-1"></i> 拨弦
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <!-- 第三屏:帮助与设置 -->
    <section id="screen3" class="screen relative min-h-screen bg-white z-30 flex items-center">
      <div class="container mx-auto px-4 py-12">
        <div class="mb-8">
          <h2 class="text-3xl font-light mb-2">帮助与设置</h2>
          <div class="w-24 h-px bg-black"></div>
        </div>
        
        <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
          <!-- 帮助部分 -->
          <div id="help" class="bg-white border border-gray-200 p-6">
            <h3 class="text-xl font-light mb-4">使用帮助</h3>
            <div class="space-y-4">
              <div>
                <h4 class="text-lg font-medium mb-2">基本操作</h4>
                <ul class="list-disc pl-5 space-y-1 text-gray-600">
                  <li>点击并拖动琴弓来演奏二胡</li>
                  <li>点击琴弦上的不同位置来改变音高</li>
                  <li>使用右侧控制面板调整音量、音色等参数</li>
                </ul>
              </div>
              <div>
                <h4 class="text-lg font-medium mb-2">演奏技巧</h4>
                <ul class="list-disc pl-5 space-y-1 text-gray-600">
                  <li><strong>颤音</strong>:快速交替按弦产生颤动效果</li>
                  <li><strong>滑音</strong>:手指在琴弦上滑动,音高逐渐变化</li>
                  <li><strong>顿弓</strong>:突然加重弓压并迅速减轻,产生重音效果</li>
                  <li><strong>拨弦</strong>:用手指直接拨动琴弦,产生短促的音</li>
                </ul>
              </div>
            </div>
          </div>
          
          <!-- 设置部分 -->
          <div class="bg-white border border-gray-200 p-6">
            <h3 class="text-xl font-light mb-4">应用设置</h3>
            <div class="space-y-4">
              <div>
                <label for="visualEffectToggle" class="flex items-center justify-between cursor-pointer">
                  <span class="text-gray-700">视觉效果</span>
                  <div class="relative">
                    <input type="checkbox" id="visualEffectToggle" class="sr-only" checked>
                    <div class="block bg-gray-200 w-14 h-8 rounded-full"></div>
                    <div class="dot absolute left-1 top-1 bg-black w-6 h-6 rounded-full transition-transform translate-x-6"></div>
                  </div>
                </label>
              </div>
              <div>
                <label for="audioQualitySelect" class="block text-sm text-gray-600 mb-1">音频质量</label>
                <select id="audioQualitySelect" class="w-full px-3 py-2 border border-gray-200 bg-white">
                  <option value="high">高</option>
                  <option value="medium" selected>中</option>
                  <option value="low">低</option>
                </select>
              </div>
              <div>
                <label for="keyboardControlToggle" class="flex items-center justify-between cursor-pointer">
                  <span class="text-gray-700">键盘控制</span>
                  <div class="relative">
                    <input type="checkbox" id="keyboardControlToggle" class="sr-only" checked>
                    <div class="block bg-gray-200 w-14 h-8 rounded-full"></div>
                    <div class="dot absolute left-1 top-1 bg-black w-6 h-6 rounded-full transition-transform translate-x-6"></div>
                  </div>
                </label>
              </div>
              <div class="pt-4">
                <button id="saveSettingsBtn" class="w-full px-4 py-2 bg-black text-white hover:bg-gray-800 transition-all">
                  保存设置
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>

  <!-- 录音回放模态框 -->
  <div id="recordingModal" class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
    <div class="modal-content bg-white p-6 max-w-md w-full">
      <div class="flex justify-between items-center mb-4">
        <h2 class="text-2xl font-light">录音回放</h2>
        <button id="closeRecordingBtn" class="text-gray-500 hover:text-black">
          <i class="fa fa-times text-xl"></i>
        </button>
      </div>
      <div class="space-y-4">
        <div class="flex justify-center">
          <button id="playRecordingBtn" class="w-16 h-16 rounded-full bg-black text-white flex items-center justify-center hover:bg-gray-800 transition-all">
            <i class="fa fa-play text-2xl"></i>
          </button>
        </div>
        <div>
          <label for="recordingProgress" class="block text-sm text-gray-600 mb-1">进度</label>
          <input type="range" id="recordingProgress" min="0" max="100" value="0" class="w-full h-1 bg-black rounded-lg appearance-none cursor-pointer">
        </div>
        <div class="flex justify-between text-sm text-gray-500">
          <span id="recordingCurrentTime">00:00</span>
          <span id="recordingTotalTime">00:00</span>
        </div>
        <div class="flex space-x-2 pt-4">
          <button id="saveRecordingBtn" class="flex-1 px-4 py-2 bg-black text-white hover:bg-gray-800 transition-all">
            <i class="fa fa-save mr-1"></i> 保存
          </button>
          <button id="deleteRecordingBtn" class="flex-1 px-4 py-2 border border-gray-200 hover:bg-gray-50 transition-all">
            <i class="fa fa-trash mr-1"></i> 删除
          </button>
        </div>
      </div>
    </div>
  </div>

  <!-- 乐谱显示模态框 -->
  <div id="scoreModal" class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
    <div class="modal-content bg-white p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
      <div class="flex justify-between items-center mb-4">
        <h2 id="scoreTitle" class="text-2xl font-light">乐谱</h2>
        <button id="closeScoreBtn" class="text-gray-500 hover:text-black">
          <i class="fa fa-times text-xl"></i>
        </button>
      </div>
      <div id="scoreContent" class="space-y-4">
        <!-- 乐谱内容将通过JavaScript动态插入 -->
      </div>
    </div>
  </div>

  <!-- 页脚 -->
  <footer class="bg-white border-t border-gray-200 py-8">
    <div class="container mx-auto px-4">
      <div class="flex flex-col md:flex-row justify-between items-center">
        <div class="mb-4 md:mb-0">
          <p class="text-gray-600">© 2025 交互式二胡弹奏 | 基于HTML5 Canvas和Web Audio API</p>
        </div>
        <div class="flex space-x-4">
          <a href="#" class="text-gray-600 hover:text-black transition-colors">
            <i class="fa fa-github text-xl"></i>
          </a>
          <a href="#" class="text-gray-600 hover:text-black transition-colors">
            <i class="fa fa-twitter text-xl"></i>
          </a>
          <a href="#" class="text-gray-600 hover:text-black transition-colors">
            <i class="fa fa-weixin text-xl"></i>
          </a>
        </div>
      </div>
    </div>
  </footer>

  <script>
    // 全局变量
    const canvas = document.getElementById('erhuCanvas');
    const ctx = canvas.getContext('2d');
    let audioContext;
    let erhuSynth;
    let isDragging = false;
    let bowPosition = { x: 350, y: 300 };
    let lastBowPosition = null;
    let bowSpeed = 0;
    let isPlaying = false;
    let currentNote = 60; // C4
    let fingerPosition = null;
    let volume = 0.8;
    let bowPressure = 0.5;
    let tone = 0.7;
    let currentTechnique = 'normal';
    let isRecording = false;
    let recordedNotes = [];
    let recordingStartTime = 0;
    let isPlayback = false;
    let playbackIndex = 0;
    let playbackInterval = null;
    let visualEffectsEnabled = true;
    let keyboardControlEnabled = true;
    let audioQuality = 'medium';
    let currentScore = null;
    let scorePlaybackInterval = null;
    
    // 音符名称映射
    const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
    
    // 预设乐谱
    const scores = {
      '小星星': [
        { note: 60, duration: 0.5 }, // C4
        { note: 60, duration: 0.5 }, // C4
        { note: 67, duration: 0.5 }, // G4
        { note: 67, duration: 0.5 }, // G4
        { note: 69, duration: 0.5 }, // A4
        { note: 69, duration: 0.5 }, // A4
        { note: 67, duration: 1 },   // G4
        { note: 65, duration: 0.5 }, // F4
        { note: 65, duration: 0.5 }, // F4
        { note: 64, duration: 0.5 }, // E4
        { note: 64, duration: 0.5 }, // E4
        { note: 62, duration: 0.5 }, // D4
        { note: 62, duration: 0.5 }, // D4
        { note: 60, duration: 1 },   // C4
      ],
      '茉莉花': [
        { note: 65, duration: 0.5 }, // F4
        { note: 69, duration: 0.5 }, // A4
        { note: 72, duration: 1 },   // C5
        { note: 69, duration: 0.5 }, // A4
        { note: 72, duration: 1.5 }, // C5
        { note: 71, duration: 0.5 }, // B4
        { note: 69, duration: 1 },   // A4
        { note: 65, duration: 0.5 }, // F4
        { note: 67, duration: 0.5 }, // G4
        { note: 69, duration: 1.5 }, // A4
        { note: 67, duration: 0.5 }, // G4
        { note: 65, duration: 1 },   // F4
        { note: 64, duration: 0.5 }, // E4
        { note: 65, duration: 2 },   // F4
      ],
      '梁祝': [
        { note: 65, duration: 1 },   // F4
        { note: 62, duration: 0.5 }, // D4
        { note: 64, duration: 0.5 }, // E4
        { note: 65, duration: 1 },   // F4
        { note: 69, duration: 0.5 }, // A4
        { note: 67, duration: 0.5 }, // G4
        { note: 65, duration: 1 },   // F4
        { note: 64, duration: 0.5 }, // E4
        { note: 62, duration: 0.5 }, // D4
        { note: 64, duration: 2 },   // E4
        { note: 62, duration: 1 },   // D4
        { note: 60, duration: 1 },   // C4
        { note: 62, duration: 2 },   // D4
      ],
      '赛马': [
        { note: 62, duration: 0.25 }, // D4
        { note: 64, duration: 0.25 }, // E4
        { note: 65, duration: 0.25 }, // F4
        { note: 67, duration: 0.25 }, // G4
        { note: 69, duration: 0.25 }, // A4
        { note: 71, duration: 0.25 }, // B4
        { note: 72, duration: 0.25 }, // C5
        { note: 71, duration: 0.25 }, // B4
        { note: 69, duration: 0.25 }, // A4
        { note: 67, duration: 0.25 }, // G4
        { note: 65, duration: 0.25 }, // F4
        { note: 64, duration: 0.25 }, // E4
        { note: 62, duration: 0.25 }, // D4
        { note: 60, duration: 0.25 }, // C4
      ]
    };
    
    // 初始化函数
    function init() {
      // 设置Canvas尺寸
      resizeCanvas();
      window.addEventListener('resize', resizeCanvas);
      
      // 初始化音频上下文
      try {
        window.AudioContext = window.AudioContext || window.webkitAudioContext;
        audioContext = new AudioContext();
        createErhuSynth();
      } catch (e) {
        console.error('Web Audio API is not supported in this browser', e);
        alert('您的浏览器不支持Web Audio API,请使用最新版Chrome或Firefox浏览器');
      }
      
      // 初始化事件监听
      initEventListeners();
      
      // 初始化自定义鼠标
      initCustomCursor();
      
      // 初始化滚动差分效果
      initScrollEffects();
      
      // 开始动画循环
      animate();
    }
    
    // 调整Canvas尺寸
    function resizeCanvas() {
      const container = canvas.parentElement;
      canvas.width = container.clientWidth;
      canvas.height = 500;
      
      // 重新绘制二胡
      drawErhu();
    }
    
    // 创建二胡合成器
    function createErhuSynth() {
      // 主振荡器
      const oscillator = audioContext.createOscillator();
      oscillator.type = 'sawtooth';
      
      // 共鸣滤波器
      const filter = audioContext.createBiquadFilter();
      filter.type = 'lowpass';
      filter.frequency.value = 3000;
      
      // 音量控制
      const gainNode = audioContext.createGain();
      gainNode.gain.value = 0;
      
      // 连接节点
      oscillator.connect(filter);
      filter.connect(gainNode);
      gainNode.connect(audioContext.destination);
      
      // 创建泛音
      const harmonics = [];
      for (let i = 2; i <= 5; i++) {
        const harmonicOsc = audioContext.createOscillator();
        const harmonicGain = audioContext.createGain();
        
        harmonicOsc.type = 'sawtooth';
        harmonicGain.gain.value = 0.1 / i;
        
        harmonicOsc.connect(harmonicGain);
        harmonicGain.connect(gainNode);
        
        harmonics.push({
          oscillator: harmonicOsc,
          gain: harmonicGain
        });
      }
      
      // 保存合成器对象
      erhuSynth = {
        oscillator,
        filter,
        gainNode,
        harmonics,
        playing: false,
        
        play(note, duration = 0.1, velocity = 1) {
          if (!this.playing) {
            this.playing = true;
            
            // 计算频率
            const frequency = 440 * Math.pow(2, (note - 69) / 12);
            
            // 设置主振荡器
            this.oscillator.frequency.value = frequency;
            
            // 设置泛音
            this.harmonics.forEach((harmonic, index) => {
              harmonic.oscillator.frequency.value = frequency * (index + 2);
              harmonic.oscillator.start();
            });
            
            // 开始主振荡器
            this.oscillator.start();
            
            // 更新显示
            updateNoteDisplay(note);
          }
          
          // 调整音量
          const now = audioContext.currentTime;
          const currentVolume = volume * velocity * bowPressure;
          
          // 根据当前技巧调整ADSR包络
          if (currentTechnique === 'staccato') {
            // 顿弓:快速攻击,快速衰减
            this.gainNode.gain.cancelScheduledValues(now);
            this.gainNode.gain.setValueAtTime(0, now);
            this.gainNode.gain.linearRampToValueAtTime(currentVolume, now + 0.01);
            this.gainNode.gain.linearRampToValueAtTime(0, now + duration);
          } else if (currentTechnique === 'pizzicato') {
            // 拨弦:快速攻击,快速衰减
            this.gainNode.gain.cancelScheduledValues(now);
            this.gainNode.gain.setValueAtTime(0, now);
            this.gainNode.gain.linearRampToValueAtTime(currentVolume * 0.8, now + 0.005);
            this.gainNode.gain.linearRampToValueAtTime(0, now + 0.3);
          } else if (currentTechnique === 'tremolo') {
            // 颤音:快速波动
            this.gainNode.gain.cancelScheduledValues(now);
            this.gainNode.gain.setValueAtTime(0, now);
            this.gainNode.gain.linearRampToValueAtTime(currentVolume, now + 0.02);
            
            // 创建颤音效果
            for (let i = 0; i < duration * 20; i++) {
              const time = now + 0.02 + i * 0.05;
              const value = currentVolume * (0.7 + Math.sin(i * Math.PI * 4) * 0.3);
              this.gainNode.gain.setValueAtTime(value, time);
            }
            
            this.gainNode.gain.linearRampToValueAtTime(0, now + duration);
          } else {
            // 普通演奏
            this.gainNode.gain.cancelScheduledValues(now);
            this.gainNode.gain.setValueAtTime(0, now);
            this.gainNode.gain.linearRampToValueAtTime(currentVolume, now + 0.05);
            this.gainNode.gain.linearRampToValueAtTime(currentVolume * 0.8, now + 0.1);
            this.gainNode.gain.linearRampToValueAtTime(currentVolume * 0.8, now + duration - 0.1);
            this.gainNode.gain.linearRampToValueAtTime(0, now + duration);
          }
          
          // 更新音量指示器
          updateVolumeIndicator(currentVolume);
          
          // 如果正在录音,记录音符
          if (isRecording) {
            recordedNotes.push({
              note,
              time: Date.now() - recordingStartTime,
              duration
            });
          }
        },
        
        stop() {
          if (this.playing) {
            const now = audioContext.currentTime;
            
            // 停止所有振荡器
            this.oscillator.stop(now + 0.1);
            this.harmonics.forEach(harmonic => {
              harmonic.oscillator.stop(now + 0.1);
            });
            
            // 重置状态
            this.playing = false;
            
            // 重新创建振荡器(因为一旦停止就不能再次启动)
            setTimeout(() => {
              this.oscillator = audioContext.createOscillator();
              this.oscillator.type = 'sawtooth';
              this.oscillator.connect(this.filter);
              
              this.harmonics.forEach((harmonic, index) => {
                harmonic.oscillator = audioContext.createOscillator();
                harmonic.oscillator.type = 'sawtooth';
                harmonic.oscillator.connect(harmonic.gain);
              });
            }, 100);
          }
        }
      };
    }
    
    // 初始化事件监听
    function initEventListeners() {
      // 琴弓拖拽交互
      canvas.addEventListener('mousedown', handleMouseDown);
      canvas.addEventListener('mousemove', handleMouseMove);
      canvas.addEventListener('mouseup', handleMouseUp);
      canvas.addEventListener('mouseleave', handleMouseUp);
      
      // 触摸设备支持
      canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
      canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
      canvas.addEventListener('touchend', handleTouchEnd);
      canvas.addEventListener('touchcancel', handleTouchEnd);
      
      // 控制面板滑块
      document.getElementById('volumeSlider').addEventListener('input', handleVolumeChange);
      document.getElementById('bowPressureSlider').addEventListener('input', handleBowPressureChange);
      document.getElementById('toneSlider').addEventListener('input', handleToneChange);
      
      // 演奏技巧按钮
      document.getElementById('tremoloBtn').addEventListener('click', () => setCurrentTechnique('tremolo'));
      document.getElementById('glissandoBtn').addEventListener('click', () => setCurrentTechnique('glissando'));
      document.getElementById('staccatoBtn').addEventListener('click', () => setCurrentTechnique('staccato'));
      document.getElementById('pizzicatoBtn').addEventListener('click', () => setCurrentTechnique('pizzicato'));
      
      // 录音按钮
      document.getElementById('recordBtn').addEventListener('click', toggleRecording);
      
      // 播放按钮
      document.getElementById('playBtn').addEventListener('click', togglePlayback);
      
      // 乐谱按钮
      document.querySelectorAll('.score-btn').forEach(btn => {
        btn.addEventListener('click', () => {
          const scoreName = btn.getAttribute('data-score');
          playScore(scoreName);
        });
      });
      
      // 设置按钮
      document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings);
      
      // 视觉效果开关
      document.getElementById('visualEffectToggle').addEventListener('change', toggleVisualEffect);
      
      // 键盘控制开关
      document.getElementById('keyboardControlToggle').addEventListener('change', toggleKeyboardControl);
      
      // 音频质量选择
      document.getElementById('audioQualitySelect').addEventListener('change', changeAudioQuality);
      
      // 录音回放按钮
      document.getElementById('playRecordingBtn').addEventListener('click', toggleRecordingPlayback);
      document.getElementById('saveRecordingBtn').addEventListener('click', saveRecording);
      document.getElementById('deleteRecordingBtn').addEventListener('click', deleteRecording);
      document.getElementById('closeRecordingBtn').addEventListener('click', () => {
        document.getElementById('recordingModal').classList.remove('active');
      });
      
      // 乐谱关闭按钮
      document.getElementById('closeScoreBtn').addEventListener('click', () => {
        document.getElementById('scoreModal').classList.remove('active');
        stopScorePlayback();
      });
      
      // 键盘控制
      document.addEventListener('keydown', handleKeyDown);
      document.addEventListener('keyup', handleKeyUp);
      
      // 模态框点击外部关闭
      document.querySelectorAll('.modal').forEach(modal => {
        modal.addEventListener('click', (e) => {
          if (e.target === modal) {
            modal.classList.remove('active');
          }
        });
      });
      
      // 平滑滚动
      document.querySelectorAll('a[href^="#"]').forEach(anchor => {
        anchor.addEventListener('click', function(e) {
          e.preventDefault();
          const targetId = this.getAttribute('href');
          const targetElement = document.querySelector(targetId);
          
          if (targetElement) {
            window.scrollTo({
              top: targetElement.offsetTop,
              behavior: 'smooth'
            });
          }
        });
      });
    }
    
    // 初始化自定义鼠标
    function initCustomCursor() {
      const cursor = document.getElementById('custom-cursor');
      let mouseX = 0;
      let mouseY = 0;
      let cursorX = 0;
      let cursorY = 0;
      
      // 初始显示光标
      document.addEventListener('mousemove', (e) => {
        cursor.style.opacity = '1';
        mouseX = e.clientX;
        mouseY = e.clientY;
      });
      
      // 平滑跟随效果
      function animateCursor() {
        const speed = 0.2; // 跟随速度
        cursorX += (mouseX - cursorX) * speed;
        cursorY += (mouseY - cursorY) * speed;
        
        cursor.style.transform = `translate(${cursorX}px, ${cursorY}px)`;
        requestAnimationFrame(animateCursor);
      }
      
      animateCursor();
      
      // 悬停效果
      const hoverElements = document.querySelectorAll('button, a, input, select');
      hoverElements.forEach(element => {
        element.addEventListener('mouseenter', () => {
          cursor.classList.add('scale-150');
        });
        
        element.addEventListener('mouseleave', () => {
          cursor.classList.remove('scale-150');
        });
      });
    }
    
    // 初始化滚动差分效果
    function initScrollEffects() {
      const screens = document.querySelectorAll('.screen');
      
      window.addEventListener('scroll', () => {
        const scrollY = window.scrollY;
        const windowHeight = window.innerHeight;
        
        // 前覆盖后模式 (第一屏和第二屏之间)
        if (scrollY < windowHeight) {
          // 确定第二屏的显示程度 (0到1之间)
          const progress = Math.min(1, scrollY / windowHeight);
          
          // 第二屏从下方出现
          const translateY = -windowHeight * (1 - progress);
          screens[1].style.transform = `translateY(${translateY}px)`;
        }
        
        // 后覆盖前模式 (第二屏和第三屏之间)
        if (scrollY >= windowHeight && scrollY < windowHeight * 2) {
          // 计算第三屏的过渡进度
          const progress = Math.min(1, (scrollY - windowHeight) / windowHeight);
          
          // 第二屏向上移动
          const translateY = windowHeight * 0.3 * progress;
          screens[1].style.transform = `translateY(${translateY}px)`;
          
          // 第三屏从下方出现
          const translateY2 = -windowHeight * (1 - progress);
          screens[2].style.transform = `translateY(${translateY2}px)`;
        }
        
        // 更新装饰线条位置
        updateDecorativeLines(scrollY, windowHeight);
      });
    }
    
    // 更新装饰线条位置
    function updateDecorativeLines(scrollY, windowHeight) {
      const lines = document.querySelectorAll('.decorative-line');
      const progress = scrollY / (document.body.offsetHeight - windowHeight);
      
      lines.forEach((line, index) => {
        if (index === 0) { // 顶部水平线
          line.style.width = `${33 + progress * 34}%`;
        } else if (index === 1) { // 左侧垂直线
          line.style.height = `${33 + progress * 34}%`;
        } else if (index === 2) { // 底部水平线
          line.style.width = `${33 + progress * 34}%`;
        } else if (index === 3) { // 右侧垂直线
          line.style.height = `${33 + progress * 34}%`;
        }
      });
    }
    
    // 鼠标按下事件处理
    function handleMouseDown(e) {
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      
      // 检查是否点击琴弓 - 扩大点击区域
      if (Math.abs(x - bowPosition.x) < 100 && Math.abs(y - bowPosition.y) < 30) {
        isDragging = true;
        lastBowPosition = { x, y };
      }
      
      // 检查是否点击琴弦 - 扩大点击区域
      if (x >= 340 && x <= 370) {
        setFingerPosition(y);
      }
    }
    
    // 鼠标移动事件处理
    function handleMouseMove(e) {
      if (!isDragging) return;
      
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      
      // 更新琴弓位置
      bowPosition = { x, y };
      
      // 计算弓速
      if (lastBowPosition) {
        const dx = x - lastBowPosition.x;
        const dy = y - lastBowPosition.y;
        bowSpeed = Math.sqrt(dx * dx + dy * dy);
      }
      
      lastBowPosition = { x, y };
      
      // 播放声音 - 只要拖动就持续发声
      const note = getNoteFromPosition(y);
      currentNote = note;
      
      if (erhuSynth) {
        if (!erhuSynth.playing) {
          erhuSynth.play(note, 1.0, Math.max(0.1, bowSpeed / 10));
        } else {
          // 如果已经在播放,更新音符和音量
          const frequency = 440 * Math.pow(2, (note - 69) / 12);
          erhuSynth.oscillator.frequency.value = frequency;
          
          // 更新泛音
          erhuSynth.harmonics.forEach((harmonic, index) => {
            harmonic.oscillator.frequency.value = frequency * (index + 2);
          });
          
          // 更新音量
          const now = audioContext.currentTime;
          const velocity = Math.max(0.1, bowSpeed / 10);
          const currentVolume = volume * velocity * bowPressure;
          
          erhuSynth.gainNode.gain.cancelScheduledValues(now);
          erhuSynth.gainNode.gain.setValueAtTime(currentVolume, now);
          
          // 更新显示
          updateNoteDisplay(note);
          updateVolumeIndicator(currentVolume);
        }
      }
    }
    
    // 鼠标释放事件处理
    function handleMouseUp() {
      isDragging = false;
      lastBowPosition = null;
      bowSpeed = 0;
      
      if (erhuSynth && erhuSynth.playing) {
        erhuSynth.stop();
      }
    }
    
    // 触摸开始事件处理
    function handleTouchStart(e) {
      e.preventDefault();
      
      if (e.touches.length === 1) {
        const touch = e.touches[0];
        const mouseEvent = new MouseEvent('mousedown', {
          clientX: touch.clientX,
          clientY: touch.clientY
        });
        
        handleMouseDown(mouseEvent);
      }
    }
    
    // 触摸移动事件处理
    function handleTouchMove(e) {
      e.preventDefault();
      
      if (e.touches.length === 1) {
        const touch = e.touches[0];
        const mouseEvent = new MouseEvent('mousemove', {
          clientX: touch.clientX,
          clientY: touch.clientY
        });
        
        handleMouseMove(mouseEvent);
      }
    }
    
    // 触摸结束事件处理
    function handleTouchEnd(e) {
      e.preventDefault();
      handleMouseUp();
    }
    
    // 音量变化事件处理
    function handleVolumeChange(e) {
      volume = parseInt(e.target.value) / 100;
    }
    
    // 弓压变化事件处理
    function handleBowPressureChange(e) {
      bowPressure = parseInt(e.target.value) / 100;
    }
    
    // 音色变化事件处理
    function handleToneChange(e) {
      tone = parseInt(e.target.value) / 100;
      
      // 调整滤波器频率
      if (erhuSynth) {
        const frequency = 1000 + tone * 4000;
        erhuSynth.filter.frequency.value = frequency;
      }
    }
    
    // 设置当前演奏技巧
    function setCurrentTechnique(technique) {
      currentTechnique = technique;
      
      // 更新按钮样式
      document.querySelectorAll('#tremoloBtn, #glissandoBtn, #staccatoBtn, #pizzicatoBtn').forEach(btn => {
        btn.classList.remove('bg-black', 'text-white');
        btn.classList.add('border', 'border-gray-200');
      });
      
      document.getElementById(`${technique}Btn`).classList.remove('border', 'border-gray-200');
      document.getElementById(`${technique}Btn`).classList.add('bg-black', 'text-white');
    }
    
    // 切换录音状态
    function toggleRecording() {
      const recordBtn = document.getElementById('recordBtn');
      
      if (!isRecording) {
        // 开始录音
        isRecording = true;
        recordedNotes = [];
        recordingStartTime = Date.now();
        
        recordBtn.innerHTML = '<i class="fa fa-stop"></i>';
      } else {
        // 停止录音
        isRecording = false;
        
        recordBtn.innerHTML = '<i class="fa fa-circle"></i>';
        
        // 显示录音回放模态框
        if (recordedNotes.length > 0) {
          document.getElementById('recordingModal').classList.add('active');
          document.getElementById('recordingTotalTime').textContent = formatTime(recordedNotes[recordedNotes.length - 1].time);
        }
      }
    }
    
    // 切换回放状态
    function togglePlayback() {
      const playBtn = document.getElementById('playBtn');
      
      if (!isPlayback) {
        // 开始回放
        if (recordedNotes.length === 0) return;
        
        isPlayback = true;
        playbackIndex = 0;
        
        playBtn.innerHTML = '<i class="fa fa-pause"></i>';
        
        // 开始回放
        playNextRecordedNote();
      } else {
        // 停止回放
        isPlayback = false;
        
        playBtn.innerHTML = '<i class="fa fa-play"></i>';
        
        // 停止当前播放的音符
        if (erhuSynth && erhuSynth.playing) {
          erhuSynth.stop();
        }
        
        // 清除定时器
        if (playbackInterval) {
          clearTimeout(playbackInterval);
          playbackInterval = null;
        }
      }
    }
    
    // 播放下一个录制的音符
    function playNextRecordedNote() {
      if (!isPlayback || playbackIndex >= recordedNotes.length) {
        // 回放结束
        isPlayback = false;
        document.getElementById('playBtn').innerHTML = '<i class="fa fa-play"></i>';
        return;
      }
      
      const noteData = recordedNotes[playbackIndex];
      
      // 更新进度条
      const progress = (noteData.time / recordedNotes[recordedNotes.length - 1].time) * 100;
      document.getElementById('recordingProgress').value = progress;
      document.getElementById('recordingCurrentTime').textContent = formatTime(noteData.time);
      
      // 播放音符
      if (erhuSynth) {
        erhuSynth.play(noteData.note, noteData.duration);
        
        // 设置手指位置
        setFingerPosition(getPositionFromNote(noteData.note));
      }
      
      // 准备播放下一个音符
      playbackIndex++;
      
      // 计算下一个音符的延迟
      let delay = noteData.duration * 500; // 默认延迟
      
      if (playbackIndex < recordedNotes.length) {
        const nextNoteTime = recordedNotes[playbackIndex].time;
        delay = nextNoteTime - noteData.time;
      }
      
      // 设置定时器
      playbackInterval = setTimeout(playNextRecordedNote, delay);
    }
    
    // 播放乐谱
    function playScore(scoreName) {
      if (!scores[scoreName]) return;
      
      // 停止当前的乐谱播放
      stopScorePlayback();
      
      // 设置当前乐谱
      currentScore = scores[scoreName];
      
      // 显示乐谱模态框
      document.getElementById('scoreTitle').textContent = scoreName;
      document.getElementById('scoreContent').innerHTML = generateScoreHTML(currentScore);
      document.getElementById('scoreModal').classList.add('active');
      
      // 开始播放
      let index = 0;
      
      function playNextNote() {
        if (index >= currentScore.length) {
          // 播放结束
          return;
        }
        
        const noteData = currentScore[index];
        
        // 高亮当前音符
        highlightCurrentNote(index);
        
        // 播放音符
        if (erhuSynth) {
          erhuSynth.play(noteData.note, noteData.duration);
          
          // 设置手指位置
          setFingerPosition(getPositionFromNote(noteData.note));
        }
        
        // 准备播放下一个音符
        index++;
        
        // 设置定时器
        scorePlaybackInterval = setTimeout(playNextNote, noteData.duration * 500);
      }
      
      // 开始播放
      playNextNote();
    }
    
    // 停止乐谱播放
    function stopScorePlayback() {
      if (scorePlaybackInterval) {
        clearTimeout(scorePlaybackInterval);
        scorePlaybackInterval = null;
      }
      
      // 停止当前播放的音符
      if (erhuSynth && erhuSynth.playing) {
        erhuSynth.stop();
      }
      
      // 隐藏手指位置
      hideFingerPosition();
    }
    
    // 生成乐谱HTML
    function generateScoreHTML(score) {
      let html = '<div class="grid grid-cols-4 gap-2">';
      
      score.forEach((noteData, index) => {
        const noteName = getNoteName(noteData.note);
        const octave = Math.floor(noteData.note / 12) - 1;
        
        html += `
          <div class="score-note p-2 border border-gray-200 text-center transition-all" data-index="${index}">
            <div class="text-xl font-light">${noteName}</div>
            <div class="text-sm text-gray-500">${octave}</div>
            <div class="text-xs mt-1">${noteData.duration}拍</div>
          </div>
        `;
      });
      
      html += '</div>';
      return html;
    }
    
    // 高亮当前音符
    function highlightCurrentNote(index) {
      document.querySelectorAll('.score-note').forEach((el, i) => {
        if (i === index) {
          el.classList.add('bg-black', 'text-white');
        } else {
          el.classList.remove('bg-black', 'text-white');
        }
      });
    }
    
    // 切换录音回放
    function toggleRecordingPlayback() {
      const playRecordingBtn = document.getElementById('playRecordingBtn');
      
      if (!isPlayback) {
        // 开始回放
        if (recordedNotes.length === 0) return;
        
        isPlayback = true;
        playbackIndex = 0;
        
        playRecordingBtn.innerHTML = '<i class="fa fa-pause text-2xl"></i>';
        
        // 开始回放
        playNextRecordedNote();
      } else {
        // 停止回放
        isPlayback = false;
        
        playRecordingBtn.innerHTML = '<i class="fa fa-play text-2xl"></i>';
        
        // 停止当前播放的音符
        if (erhuSynth && erhuSynth.playing) {
          erhuSynth.stop();
        }
        
        // 清除定时器
        if (playbackInterval) {
          clearTimeout(playbackInterval);
          playbackInterval = null;
        }
      }
    }
    
    // 保存录音
    function saveRecording() {
      alert('录音已保存!');
      document.getElementById('recordingModal').classList.remove('active');
    }
    
    // 删除录音
    function deleteRecording() {
      recordedNotes = [];
      document.getElementById('recordingModal').classList.remove('active');
    }
    
    // 保存设置
    function saveSettings() {
      // 保存设置
      visualEffectsEnabled = document.getElementById('visualEffectToggle').checked;
      keyboardControlEnabled = document.getElementById('keyboardControlToggle').checked;
      audioQuality = document.getElementById('audioQualitySelect').value;
      
      // 显示保存成功提示
      alert('设置已保存!');
    }
    
    // 切换视觉效果
    function toggleVisualEffect(e) {
      visualEffectsEnabled = e.target.checked;
      
      // 更新开关样式
      const dot = e.target.nextElementSibling.nextElementSibling;
      if (visualEffectsEnabled) {
        dot.classList.remove('translate-x-0');
        dot.classList.add('translate-x-6');
      } else {
        dot.classList.remove('translate-x-6');
        dot.classList.add('translate-x-0');
      }
    }
    
    // 切换键盘控制
    function toggleKeyboardControl(e) {
      keyboardControlEnabled = e.target.checked;
      
      // 更新开关样式
      const dot = e.target.nextElementSibling.nextElementSibling;
      if (keyboardControlEnabled) {
        dot.classList.remove('translate-x-0');
        dot.classList.add('translate-x-6');
      } else {
        dot.classList.remove('translate-x-6');
        dot.classList.add('translate-x-0');
      }
    }
    
    // 改变音频质量
    function changeAudioQuality(e) {
      audioQuality = e.target.value;
      
      // 根据音频质量调整合成器参数
      if (erhuSynth) {
        if (audioQuality === 'high') {
          // 高音质:增加泛音数量
          erhuSynth.filter.Q.value = 1.5;
        } else if (audioQuality === 'medium') {
          // 中等音质:默认设置
          erhuSynth.filter.Q.value = 1.0;
        } else {
          // 低音质:减少泛音数量
          erhuSynth.filter.Q.value = 0.5;
        }
      }
    }
    
    // 键盘按下事件处理
    function handleKeyDown(e) {
      if (!keyboardControlEnabled) return;
      
      // 映射键盘按键到音符
      const keyToNote = {
        'a': 60, // C4
        'w': 61, // C#4
        's': 62, // D4
        'e': 63, // D#4
        'd': 64, // E4
        'f': 65, // F4
        't': 66, // F#4
        'g': 67, // G4
        'y': 68, // G#4
        'h': 69, // A4
        'u': 70, // A#4
        'j': 71, // B4
        'k': 72, // C5
        'o': 73, // C#5
        'l': 74, // D5
        'p': 75, // D#5
        ';': 76  // E5
      };
      
      if (keyToNote[e.key]) {
        currentNote = keyToNote[e.key];
        
        // 播放音符
        if (erhuSynth && !erhuSynth.playing) {
          erhuSynth.play(currentNote, 0.5, 1);
        }
        
        // 设置手指位置
        setFingerPosition(getPositionFromNote(currentNote));
      }
    }
    
    // 键盘释放事件处理
    function handleKeyUp(e) {
      if (!keyboardControlEnabled) return;
      
      // 停止当前播放的音符
      if (erhuSynth && erhuSynth.playing) {
        erhuSynth.stop();
      }
      
      // 隐藏手指位置
      hideFingerPosition();
    }
    
    // 根据位置计算音符
    function getNoteFromPosition(y) {
      // 将y坐标映射到音符
      // 假设琴杆长度为300像素,对应2个八度
      const minNote = 60; // C4
      const maxNote = 84; // C6
      const noteRange = maxNote - minNote;
      
      // 反转y轴,因为上方是高音
      const normalizedY = 1 - (y - 100) / 300;
      const note = Math.round(minNote + normalizedY * noteRange);
      
      return note;
    }
    
    // 根据音符计算位置
    function getPositionFromNote(note) {
      // 将音符映射到y坐标
      const minNote = 60; // C4
      const maxNote = 84; // C6
      const noteRange = maxNote - minNote;
      
      const normalizedNote = (note - minNote) / noteRange;
      const y = 100 + (1 - normalizedNote) * 300;
      
      return y;
    }
    
    // 获取音符名称
    function getNoteName(note) {
      const octave = Math.floor(note / 12) - 1;
      const noteIndex = note % 12;
      return noteNames[noteIndex] + octave;
    }
    
    // 设置手指位置
    function setFingerPosition(y) {
      fingerPosition = y;
      
      // 显示手指指示器
      const fingerIndicator = document.getElementById('fingerIndicator');
      const stringX = canvas.width / 2;
      fingerIndicator.style.left = `${stringX + 5}px`;
      fingerIndicator.style.top = `${y}px`;
      fingerIndicator.classList.remove('hidden');
    }
    
    // 隐藏手指位置
    function hideFingerPosition() {
      fingerPosition = null;
      
      // 隐藏手指指示器
      const fingerIndicator = document.getElementById('fingerIndicator');
      fingerIndicator.classList.add('hidden');
    }
    
    // 更新音符显示
    function updateNoteDisplay(note) {
      const noteName = getNoteName(note);
      document.getElementById('currentNoteDisplay').textContent = noteName;
      document.getElementById('noteInfo').textContent = `频率: ${Math.round(440 * Math.pow(2, (note - 69) / 12))} Hz`;
    }
    
    // 更新音量指示器
    function updateVolumeIndicator(volume) {
      const volumeLevel = document.getElementById('volumeLevel');
      const percentage = Math.round(volume * 100);
      volumeLevel.style.width = `${percentage}%`;
    }
    
    // 格式化时间
    function formatTime(timeMs) {
      const seconds = Math.floor(timeMs / 1000);
      const minutes = Math.floor(seconds / 60);
      const remainingSeconds = seconds % 60;
      
      return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
    }
    
    // 绘制二胡
    function drawErhu() {
      // 清空画布
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      
      // 计算琴弦位置(居中)
      const stringX = canvas.width / 2;
      
      // 绘制琴筒
      ctx.fillStyle = '#000000';
      ctx.beginPath();
      ctx.rect(stringX - 50, 400, 100, 80);
      ctx.fill();
      
      // 绘制琴筒纹理
      ctx.fillStyle = '#333333';
      ctx.beginPath();
      ctx.arc(stringX, 440, 30, 0, Math.PI * 2);
      ctx.fill();
      
      // 绘制琴杆
      ctx.fillStyle = '#000000';
      ctx.beginPath();
      ctx.rect(stringX - 10, 100, 20, 300);
      ctx.fill();
      
      // 绘制琴头
      ctx.fillStyle = '#000000';
      ctx.beginPath();
      ctx.moveTo(stringX - 20, 100);
      ctx.lineTo(stringX + 20, 100);
      ctx.lineTo(stringX + 15, 80);
      ctx.lineTo(stringX - 15, 80);
      ctx.closePath();
      ctx.fill();
      
      // 绘制琴弦
      ctx.strokeStyle = '#000000';
      ctx.lineWidth = 1;
      
      // 第一根弦
      ctx.beginPath();
      ctx.moveTo(stringX - 5, 80);
      ctx.lineTo(stringX - 5, 400);
      ctx.stroke();
      
      // 第二根弦
      ctx.beginPath();
      ctx.moveTo(stringX + 5, 80);
      ctx.lineTo(stringX + 5, 400);
      ctx.stroke();
      
      // 绘制琴轴
      ctx.fillStyle = '#333333';
      ctx.beginPath();
      ctx.arc(stringX - 15, 120, 8, 0, Math.PI * 2);
      ctx.fill();
      
      ctx.beginPath();
      ctx.arc(stringX + 15, 120, 8, 0, Math.PI * 2);
      ctx.fill();
      
      ctx.beginPath();
      ctx.arc(stringX - 15, 160, 8, 0, Math.PI * 2);
      ctx.fill();
      
      ctx.beginPath();
      ctx.arc(stringX + 15, 160, 8, 0, Math.PI * 2);
      ctx.fill();
      
      // 绘制千斤
      ctx.fillStyle = '#333333';
      ctx.beginPath();
      ctx.rect(stringX - 15, 200, 30, 3);
      ctx.fill();
      
      // 绘制琴码
      ctx.fillStyle = '#333333';
      ctx.beginPath();
      ctx.rect(stringX - 8, 380, 16, 8);
      ctx.fill();
      
      // 绘制琴弓
      if (bowPosition) {
        const bowX = bowPosition.x;
        const bowY = bowPosition.y;
        
        // 绘制琴弓杆
        ctx.strokeStyle = '#000000';
        ctx.lineWidth = 4;
        ctx.beginPath();
        ctx.moveTo(bowX - 100, bowY);
        ctx.lineTo(bowX + 100, bowY);
        ctx.stroke();
        
        // 绘制马尾 - 连接到琴弦
        ctx.strokeStyle = '#666666';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(bowX - 50, bowY);
        ctx.lineTo(stringX - 5, bowY);
        ctx.stroke();
        
        ctx.beginPath();
        ctx.moveTo(stringX + 5, bowY);
        ctx.lineTo(bowX + 50, bowY);
        ctx.stroke();
        
        // 如果正在演奏,添加振动效果
        if (erhuSynth && erhuSynth.playing && visualEffectsEnabled) {
          // 绘制琴弦振动效果
          ctx.strokeStyle = '#000000';
          ctx.lineWidth = 1;
          
          // 第一根弦
          ctx.beginPath();
          ctx.moveTo(stringX - 5, 80);
          
          for (let y = 80; y <= 400; y += 10) {
            const vibration = Math.sin(y / 10 + Date.now() / 100) * 2;
            ctx.lineTo(stringX - 5 + vibration, y);
          }
          
          ctx.lineTo(stringX - 5, 400);
          ctx.stroke();
          
          // 第二根弦
          ctx.beginPath();
          ctx.moveTo(stringX + 5, 80);
          
          for (let y = 80; y <= 400; y += 10) {
            const vibration = Math.sin(y / 10 + Date.now() / 100 + Math.PI) * 2;
            ctx.lineTo(stringX + 5 + vibration, y);
          }
          
          ctx.lineTo(stringX + 5, 400);
          ctx.stroke();
          
          // 添加音量波纹效果
          const volume = erhuSynth.gainNode.gain.value;
          if (volume > 0.1) {
            const rippleRadius = volume * 50;
            const rippleOpacity = 1 - volume;
            
            ctx.strokeStyle = `rgba(0, 0, 0, ${rippleOpacity})`;
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.arc(stringX, bowY, rippleRadius, 0, Math.PI * 2);
            ctx.stroke();
            
            ctx.strokeStyle = `rgba(0, 0, 0, ${rippleOpacity * 0.7})`;
            ctx.beginPath();
            ctx.arc(stringX, bowY, rippleRadius * 1.5, 0, Math.PI * 2);
            ctx.stroke();
          }
        }
      }
      
      // 如果有手指位置,绘制手指
      if (fingerPosition !== null && visualEffectsEnabled) {
        // 绘制手指
        ctx.fillStyle = '#000000';
        ctx.beginPath();
        ctx.arc(stringX, fingerPosition, 10, 0, Math.PI * 2);
        ctx.fill();
        
        // 绘制按弦效果
        ctx.strokeStyle = '#000000';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(stringX, fingerPosition, 15, 0, Math.PI * 2);
        ctx.stroke();
      }
    }
    
    // 动画循环
    function animate() {
      // 绘制二胡
      drawErhu();
      
      // 继续动画循环
      requestAnimationFrame(animate);
    }
    
    // 页面加载完成后初始化
    window.addEventListener('DOMContentLoaded', init);
  </script>
</body>
</html>

 

 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐