zoukankan      html  css  js  c++  java
  • 2018-11-14 MIDI音乐格式笔记学习

    2018-11-14 MIDI音乐格式笔记学习

    reference:
    https://www.midifan.com/modulearticle-detailview-901.htm

    关于MIDI文件格式

    目前已知的:

    1. .mid文件属于二进制文件,可以通过电脑或者手机上的音乐播放器直接播放。
    2. 若需要阅读(观看)midi格式文件,可以使用binary viewer等二进制读取器打开。

    通过研究,格式如下:

    文件格式

    主要包含两部分:首部块(Header chunk)音轨块(track chunk)
    其格式分别为:

    MThd <数据长度> <Header数据> //首部块
    MTrk <数据长度> <Track模块> //音轨块

    例如读取到的:
    MIDI源码之一
    其中:

    Header Chunk

    /* 
    4D 54 68 64 
    00 00 00 06 
    00 01                 ff ff
    00 06                 nn nn 指定轨道数:实际音轨数加上全局的音轨
    00 60                 dd dd指定基本时间:一般为120 (00 78),即一个四分音符的tick数,tick是MIDI中的最小时间单位
    */
    
    char Midild[4];  //MIDI文件头标志,一般为MThd 
    long length;     //文件首部数据长度(除它本身和文件头标志专用的字节以外),通常他设置为6,即下面三个部分的字节长度
    int format;        //表示MIDI文件存放的格式,包括:                                
    // 0)当format为0时只有一个Track Chunk,
    // 1)当format为1时有一个或多个Track Chunk
    // 2)当format为2时只有一个或多个各自独立的Track Chunk
    int TrackNum; 
    int division;   //指定了计数的方法
    

    其中对于division的详细说明:
    division讲解

    在上面我们注意到以下问题:上面的数据结构默认了:long类型是4Byte,int为2Byte,char为1Byte。

    音轨块 track chunk

    音轨块用来播放音乐文件的数据信息,每一个track chunk对应着一组MIDI码,它由**头部信息和若干个音轨事件(mtrk event)**构成。
    头部为:

    /* 头部信息
    4D 54 72 6B      ASCII:Mtrk
    00 00 00 0C                 表示后面有12位的数据
    */
    /*
    00 FF 58 04 04 02 18 08 00 FE 2E 00 
    
    
    */
    
    char TrackChunkId[4];      //Track Chunk标志MTrk
    long TrackChunkMsgLength;  //该Track Chunk信息长度

    Mtrk event是由事件计数值(dela-time)和event(MIDI码信息)组合而成的,即
    < Mtrk > = < dela-time > < event >
    emmmmm

    使用WriteLengthToBuf()函数

    目的是为了完成时间值二进制的转换

    源代码(这个C代码不是我写的,但是值得学习)

      #include <stdlib.h>
      #include <stdio.h>
      #include <io.h>
      #include <string.h>
      #define C1 60 //C调1的键名值
      #define FOURPAINUM 64 //1/4音符计数
      #define MIDICLOCK 24 //每1/64音符的MIDICLOCK数
      #define JumpNullChar(x) / //跳过空字符
       { /
       while(*x==' ' /
       ||*x=='/t' /
       ||*x=='/n' /
       ||*x=='|') /
       x++; /
       };
      enum ERRORCODE{ //处理错误信息
       ChangeOK, //转换成功
       TextFileNotOpen, //文本文件不能打开
       MidiFileCanNotCreate, //指定的MIDI文件不能建立
       TextFileToBig, //文本文件太大
       MallocError, //内存分配错误
       InvalideChar, //在文本文件中出现了非法字符
       NotFoundTrack, //没有找到指定的磁道信息
       NotMIDITextFile, //文本文件不是MIDI文本文件
       };
      void SWAP(char *x,char *y) //两数据交换
      { char i;
       i=*x;
       *x=*y;
       *y=i;
      }
      union LENGHT
      { long length;
      char b[4];
      } ;
      struct MH { //MIDI文件头
      char MidiId[4]; //MIDI文件标志MThd
      long length; //头部信息长度
      int format; //存放的格式
      int ntracks; //磁道数目
      int PerPaiNum; //每节计算器值
      };
      struct TH //音轨头
      { char TrackId[4]; //磁道标志MTrk
       long length; //信息长度
      } ;
      class MIDI
      {
      public:
      char ErrorMsg[100]; //错误信息
      private:
      unsigned char *TextFileBuf,
       *TextFileOldBuf;
      unsigned char *MidiFileBuf,
       *MidiFileOldBuf;
      char OneVal; //某调时,1的健值
      char PaiNum; //第一小节节拍总数
      char OnePaiToneNum; //用几分音符作为一基本拍
      public:
      //将符全MIDI书定格式的文本文件生成MIDI文件
      int ChangeTextToMidi(char *TextFileName,
       char *MidiFileName);
      char *GetErrorMsg() //获取错误信息
       { return(ErrorMsg);}
      private:
      char GetCurPaiSpeed(int n); //取当前拍的按下强度
      void WriteSoundSize(char ntrack,unsigned int );
      void SetOnePaiToneNum(int n)
       { OnePaiToneNum=n; };
      void SetOneVal(char *m) ; //取m大调或小调时,1的实际键值
      char GetToneNum(char c, //取记名对应的键值
       char flag) ;
      void WriteMHToFile(long length, //建立MIDI文件头
       int format,
       int ntracks,
       int PerPaiNum,
       FILE *fp);
      void WriteTHToFile(long lenght,
       FILE *fp); //建立MIDI磁道头
      void WriteTrackMsgToFile(FILE *fp);
      //将磁道音乐信息定入文件中
      void WriteSpeed(int speed);
      void SetPaiNum(int n)
       { PaiNum=n;}
      long NewLong(long n); //新的long值
      int NewInt(int n) //新的int值
       { return(n<<8|n>>8);}
      //将n改为可变长度,内入buf处
      void WriteLenghtToBuf(unsigned long n,
       char *buf);
      void ChangePrommgram(char channel, //设置音色
       char promgram);
      void NoteOn (char n, //演奏乐音
       char speed,
       unsigned long delaytime);
      void WriteNoteOn(char,char,char ,unsigned long) ;
      void WriteTextMsg(char *msg); //定一串文本信息
      void WriteTimeSignature(char n, //设置时间信息
       char d);
      void WriteTrackEndMsg(); //设置磁道结束信息
      };
    
      /**************************************************
      /* 作用:将符合MIDI文本文件的text文件转换成MIDI */
      /* 文件. */
      /* 入口参数:TextFileName 文本文件名 */
      /* MidiFileName MIDI文件名 */
      /* 出口参数:见 ERRORCODE 说明 */
      /*************************************************/
      int MIDI::ChangeTextToMidi(char *TextFileName,
      char *MidiFileName)
      { int tracks,ntrack,delaytime;
      int speed,IsFirst,nn,dd;
      unsigned char buf[80],*msgbuf,c;
      FILE *TextFp,*MidiFp;
      long FileSize;
      char SpeedVal;
      TextFp=fopen(TextFileName,"r");
      if (TextFp==NULL)
      {sprintf(ErrorMsg,
      "文本文件[%s]不能打开。/n",TextFileName);
       return(TextFileNotOpen);
       }
      fseek(TextFp,0,SEEK_END); /*测试文件大小*/
      FileSize=ftell(TextFp);
      TextFileBuf=(char *)malloc(FileSize);/*为文件分配内存*/
      if (TextFileBuf==NULL)
      { sprintf(ErrorMsg,
      "文本文件[%s]太大,没有足够的内存处理。/n",
      TextFileName);
       fclose(TextFp);
       return(TextFileToBig);
      }
      memset(TextFileBuf,0,FileSize);
      MidiFileBuf=(char *) malloc(FileSize*4);
      if ( MidiFileBuf==NULL)
      { sprintf(ErrorMsg,"不能为MIDI文件分配内存。/n");
      fclose(TextFp);
      free(TextFileBuf);
      return(MallocError);
      }
      MidiFp=fopen(MidiFileName,"wb");
      if (MidiFp==NULL)
       { sprintf(ErrorMsg,
      "Midi文件[%s]不能建立。/n",MidiFileName);
       fclose(TextFp);
       free(MidiFileBuf);
       free(TextFileBuf);
       return(MidiFileCanNotCreate);
       }
      MidiFileOldBuf=MidiFileBuf;
      TextFileOldBuf=TextFileBuf;
      fseek(TextFp,0,SEEK_SET);
      fread(TextFileBuf,FileSize,1,TextFp);
      fclose(TextFp);
      JumpNullChar(TextFileBuf);
      c=strnicmp(TextFileBuf,"[MIDI]",6);
      if (c)
      {sprintf(ErrorMsg,
      "文本文件[%s]不是MIDI文本文件。/n",MidiFileName);
      fcloseall();
      free(TextFileOldBuf);
      free(MidiFileOldBuf);
      return(NotMIDITextFile);
      }
      TextFileBuf+=6;
      JumpNullChar(TextFileBuf);
      sscanf(TextFileBuf,"%c,%d/%d,%d,%d", //取调号等信息
      c,nn,dd,speed,tracks);
      buf[0]=c;buf[1]=0; SetOneVal(buf); //设置该调1的键值
      if (nn<1 || nn> 7) nn=4;
      if (dd<2 || dd>16) dd=4;
      while(*TextFileBuf!='/n') TextfileBuf++;
      JumpNullChar(TextFileBuf);
      if (speed<60 || speed >200) speed=120;
      JumpNullChar(TextFileBuf);
      if (tracks<1 || tracks>16) tracks=1;
      JumpNullChar(TextFileBuf);
      ntrack=1;
      WriteMHToFile(6,1,tracks,speed,MidiFp);
      WriteTimeSignature(nn,dd); //设置时间记录格式
      SetPaiNum(nn);
      WriteSpeed(speed); //设置演奏速度
      while(ntrack<=tracks *TextFileBuf!=0)
      {sprintf(buf,"[%d]",ntrack);
      TextFileBuf=strstr(TextFileBuf,buf);//查找该磁道起始位置
      if (TextFileBuf==NULL) //没有找到
      { sprintf(ErrorMsg,
      "在文件[%s]中,第%d磁道音乐信息没找到。/n.",
       TextFileName,ntrack);
       free(MidiFileOldBuf);
       free(TextFileOldBuf);
       fcloseall();
       return(NotFoundTrack);
      }
      if (ntrack!=1) MidiFileBuf=MidiFileOldBuf;
      SpeedVal=0;
      TextFileBuf+=strlen(buf);
      IsFirst=1;
      while(*TextFileBuf!=0 *TextFileBuf!='[')
      { JumpNullChar(TextFileBuf);
      c=*(TextFileBuf++);
      if ( (c>='0' c<='7')
      || (c>='a' c<='g')
      || (c>='A' c<='G')
      )
      {JumpNullChar(TextFileBuf);
       if (*TextFileBuf=='b' || *TextFileBuf=='#')
       { c=GetToneNum(c,*TextFileBuf);/*取出实际的音符*/
       TextFileBuf++;
       JumpNullChar(TextFileBuf);
       }
       else c=GetToneNum(c,' ');
      switch(*(TextFileBuf++))
       { case '-': //延长一拍
       delaytime=2*FOURPAINUM;
       JumpNullChar(TextFileBuf);
       while(*TextFileBuf=='-')
       { TextFileBuf++;
       delaytime+=FOURPAINUM;
       JumpNullChar(TextFileBuf);
       }
       break;
       case '_': //8分音符
       delaytime=FOURPAINUM/2;
       JumpNullChar(TextFileBuf);
       if(*TextFileBuf=='.')
       {TextFileBuf++;
       delaytime=delaytime*3/2;
       }
       break;
       case '=': //16分音符
       delaytime=FOURPAINUM/4;
       JumpNullChar(TextFileBuf);
       if(*TextFileBuf=='.')
       {delaytime=delaytime*3/2;
       TextFileBuf++;}
       break;
       case '.': //附点音符
       delaytime=FOURPAINUM*3/2;
       break;
       case ':': //32分音符
       delaytime=FOURPAINUM/16;
       JumpNullChar(TextFileBuf);
       if(*TextFileBuf=='.')
       {delaytime=delaytime*3/2;
       TextFileBuf++;}
       break;
       case ';': //64分音符
       delaytime=FOURPAINUM/32;
       if(*TextFileBuf=='.')
       { delaytime=delaytime*3/2;
       TextFileBuf++;}
       break;
       default:
       delaytime=FOURPAINUM;
       TextFileBuf--;
       break;
       }
    
    if (IsFirst)
       {WriteNoteOn(ntrack,c,
      GetCurPaiSpeed(SpeedVal/(FOURPAINUM*4/dd)+1),
      delaytime);
       IsFirst=0;}
       else
       NoteOn(c,
      GetCurPaiSpeed(SpeedVal/(FOURPAINUM*4/dd)+1),
      delaytime);
       SpeedVal=(SpeedVal+delaytime) //下一音符所处的节拍
      %(PaiNum*FOURPAINUM*4/dd);
      }
      else
      {switch(c)
      { case 'S':
      case 's':
      case 'p':
      case 'P': /*设置音色*/
       sscanf(TextFileBuf,"%d",IsFirst);
       while(*TextFileBuf>='0' *TextFileBuf<='9')
      TextFileBuf++;
      if (c=='P'||c=='p') //若为P,表示改变音色
       ChangePrommgram(ntrack,(char)IsFirst);
       else //否则,表示设置音量大小
       WriteSoundSize(ntrack,(unsigned int)IsFirst);
       IsFirst=1;
       break;
      case '{': /*写歌词*/
       msgbuf=buf;
       while(*TextFileBuf!='}'
       *TextFileBuf!='/n'
       *TextFileBuf!=0
       *TextFileBuf!='[')
       *(msgbuf++)=*(TextFileBuf++);
       *msgbuf=0;
       IsFirst=1;
       WriteTextMsg(buf);
       if (*TextFileBuf=='}') TextFileBuf++;
       break;
      case '//': //降八度
       OneVal-=12;
       break;
      case '/': //升八度
       OneVal+=12;
       break;
      case '[':
      case 0:
       TextFileBuf--;
       break;
      default:
       sprintf(ErrorMsg,"文本文件[%s]出现非法字符(%c)。",
       TextFileName,c);
       free(MidiFileOldBuf);
       free(TextFileOldBuf);
       fcloseall();
       return(InvalideChar);
       }
      }
      }
      WriteTrackEndMsg(); //设置磁道结束信息
      WriteTrackMsgToFile(MidiFp); //将磁道音乐信息定入文件中
      ntrack++;
      }
      free(MidiFileOldBuf);
      free(TextFileOldBuf);
      fclose(MidiFp);
      sprintf(ErrorMsg,"MIDI文件[%s]转换成功。",MidiFileName);
      return(ChangeOK);
      }
     /*****************************************************/
      /*作用:将长整型数据变成可变长度,存入buf处 */
      /*入口参数:n 数据 buf 结果保存入 */
      /****************************************************/
      void MIDI::WriteLenghtToBuf(unsigned long n,char *buf)
      { unsigned char b[4]={0};
      int i;
      b[3]=(unsigned char)(n0x7f);
      i=2;
      while(n>>7)
      { n>>=7;
       b[i--]=(char)( (n0x7f)|0x80);
       }
      for (i=0;i<4;i++)
      if (b[i]) *(buf++)=b[i];
      *buf=0;
      }
      long MIDI::NewLong(long n) //将长整型数据改成高位在前
      { union LENGHT l={0};
      char i;
      l.length=n;
      SWAP(l.b[3],l.b[0]);
      SWAP(l.b[2],l.b[1]);
      return(l.length);
      }
      //开始演奏音乐
      void MIDI::WriteNoteOn(char channel, //通道号
      char note, //音符值
      char speed, //按键速度
      unsigned long delaytime) //延时数
      { unsigned char buf[5];
      int i;
      channel--;
      *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=0x90|channel0x7f;//Write Channel
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=speed;
      WriteLenghtToBuf(delaytime*MIDICLOCK,buf);
      i=0;
      while(buf[i]>=0x80) //Write Delay Time
       *(MidiFileBuf++)=buf[i++];
      *(MidiFileBuf++)=buf[i];
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=0;
      }
      void MIDI::NoteOn(char note,
      char speed,
      unsigned long delaytime) //发音
      { unsigned char buf[5];
      int i;
      *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=speed;
      WriteLenghtToBuf(delaytime*MIDICLOCK,buf);
      i=0;
      while(buf[i]>0x80)
       *(MidiFileBuf++)=buf[i++];
      *(MidiFileBuf++)=buf[i];
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=0;
      }
      void MIDI::ChangePrommgram(char channel,char n) //改变音色
      { *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=0xc0|(channel-1)0x7f;
      *(MidiFileBuf++)=n;
      }
      void MIDI::WriteTextMsg(char *msg) //向内存写入一文本信息
      { char bufmsg[100]={0xff,5,0,0,0};
      int len;
      *(MidiFileBuf++)=0;
      bufmsg[2]=(char)strlen(msg);
      strcpy(bufmsg[3],msg);
      strcpy(MidiFileBuf,bufmsg);
      MidiFileBuf+=strlen(bufmsg)+3;
      }
      void MIDI::WriteTrackEndMsg() //磁道结束信息
      { *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=0xff;
      *(MidiFileBuf++)=0x2f;
      *(MidiFileBuf++)=0;
      }
      char MIDI::GetToneNum(char n,char flag)
      /*入口参数: n 音高
       flag 升降记号
      返回值: 该乐音的实际标号值*/
      { static char val[7]={9 ,11,0,2,4,5,7};
      static char one[7]={0,2,4,5,7,9,11};
      int i;
      i=OneVal;
      if (n<='7' n>='1') i=i+one[n-'1'];
      else
       if (n>='a' n<='g')
      i=i+val[n-'a']-12; //低音,降12个半音
       else
       if (n>='A' n<='G') //高音,升12个半音
       i=i+val[n-'A']+12;
       else //否则,识为休止符
       i=0;
      if (flag=='b') i--;
      else if (flag=='#') i++;
      return(i);
      }
    
       if (IsFirst)
       {WriteNoteOn(ntrack,c,
      GetCurPaiSpeed(SpeedVal/(FOURPAINUM*4/dd)+1),
      delaytime);
       IsFirst=0;}
       else
       NoteOn(c,
      GetCurPaiSpeed(SpeedVal/(FOURPAINUM*4/dd)+1),
      delaytime);
       SpeedVal=(SpeedVal+delaytime) //下一音符所处的节拍
      %(PaiNum*FOURPAINUM*4/dd);
      }
      else
      {switch(c)
      { case 'S':
      case 's':
      case 'p':
      case 'P': /*设置音色*/
       sscanf(TextFileBuf,"%d",IsFirst);
       while(*TextFileBuf>='0' *TextFileBuf<='9')
      TextFileBuf++;
      if (c=='P'||c=='p') //若为P,表示改变音色
       ChangePrommgram(ntrack,(char)IsFirst);
       else //否则,表示设置音量大小
       WriteSoundSize(ntrack,(unsigned int)IsFirst);
       IsFirst=1;
       break;
      case '{': /*写歌词*/
       msgbuf=buf;
       while(*TextFileBuf!='}'
       *TextFileBuf!='/n'
       *TextFileBuf!=0
       *TextFileBuf!='[')
       *(msgbuf++)=*(TextFileBuf++);
       *msgbuf=0;
       IsFirst=1;
       WriteTextMsg(buf);
       if (*TextFileBuf=='}') TextFileBuf++;
       break;
      case '//': //降八度
       OneVal-=12;
       break;
      case '/': //升八度
       OneVal+=12;
       break;
      case '[':
      case 0:
       TextFileBuf--;
       break;
      default:
       sprintf(ErrorMsg,"文本文件[%s]出现非法字符(%c)。",
       TextFileName,c);
       free(MidiFileOldBuf);
       free(TextFileOldBuf);
       fcloseall();
       return(InvalideChar);
       }
      }
      }
      WriteTrackEndMsg(); //设置磁道结束信息
      WriteTrackMsgToFile(MidiFp); //将磁道音乐信息定入文件中
      ntrack++;
      }
      free(MidiFileOldBuf);
      free(TextFileOldBuf);
      fclose(MidiFp);
      sprintf(ErrorMsg,"MIDI文件[%s]转换成功。",MidiFileName);
      return(ChangeOK);
      }
     /*****************************************************/
      /*作用:将长整型数据变成可变长度,存入buf处 */
      /*入口参数:n 数据 buf 结果保存入 */
      /****************************************************/
      void MIDI::WriteLenghtToBuf(unsigned long n,char *buf)
      { unsigned char b[4]={0};
      int i;
      b[3]=(unsigned char)(n0x7f);
      i=2;
      while(n>>7)
      { n>>=7;
       b[i--]=(char)( (n0x7f)|0x80);
       }
      for (i=0;i<4;i++)
      if (b[i]) *(buf++)=b[i];
      *buf=0;
      }
      long MIDI::NewLong(long n) //将长整型数据改成高位在前
      { union LENGHT l={0};
      char i;
      l.length=n;
      SWAP(l.b[3],l.b[0]);
      SWAP(l.b[2],l.b[1]);
      return(l.length);
      }
      //开始演奏音乐
      void MIDI::WriteNoteOn(char channel, //通道号
      char note, //音符值
      char speed, //按键速度
      unsigned long delaytime) //延时数
      { unsigned char buf[5];
      int i;
      channel--;
      *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=0x90|channel0x7f;//Write Channel
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=speed;
      WriteLenghtToBuf(delaytime*MIDICLOCK,buf);
      i=0;
      while(buf[i]>=0x80) //Write Delay Time
       *(MidiFileBuf++)=buf[i++];
      *(MidiFileBuf++)=buf[i];
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=0;
      }
      void MIDI::NoteOn(char note,
      char speed,
      unsigned long delaytime) //发音
      { unsigned char buf[5];
      int i;
      *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=speed;
      WriteLenghtToBuf(delaytime*MIDICLOCK,buf);
      i=0;
      while(buf[i]>0x80)
       *(MidiFileBuf++)=buf[i++];
      *(MidiFileBuf++)=buf[i];
      *(MidiFileBuf++)=note;
      *(MidiFileBuf++)=0;
      }
      void MIDI::ChangePrommgram(char channel,char n) //改变音色
      { *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=0xc0|(channel-1)0x7f;
      *(MidiFileBuf++)=n;
      }
      void MIDI::WriteTextMsg(char *msg) //向内存写入一文本信息
      { char bufmsg[100]={0xff,5,0,0,0};
      int len;
      *(MidiFileBuf++)=0;
      bufmsg[2]=(char)strlen(msg);
      strcpy(bufmsg[3],msg);
      strcpy(MidiFileBuf,bufmsg);
      MidiFileBuf+=strlen(bufmsg)+3;
      }
      void MIDI::WriteTrackEndMsg() //磁道结束信息
      { *(MidiFileBuf++)=0;
      *(MidiFileBuf++)=0xff;
      *(MidiFileBuf++)=0x2f;
      *(MidiFileBuf++)=0;
      }
      char MIDI::GetToneNum(char n,char flag)
      /*入口参数: n 音高
       flag 升降记号
      返回值: 该乐音的实际标号值*/
      { static char val[7]={9 ,11,0,2,4,5,7};
      static char one[7]={0,2,4,5,7,9,11};
      int i;
      i=OneVal;
      if (n<='7' n>='1') i=i+one[n-'1'];
      else
       if (n>='a' n<='g')
      i=i+val[n-'a']-12; //低音,降12个半音
       else
       if (n>='A' n<='G') //高音,升12个半音
       i=i+val[n-'A']+12;
       else //否则,识为休止符
       i=0;
      if (flag=='b') i--;
      else if (flag=='#') i++;
      return(i);
      }
  • 相关阅读:
    java反射
    sql语句
    menu
    亮度
    自定义View
    将多层级xml解析为Map
    Theme.AppCompat无全屏主题解决办法
    Android EditText 限制输入为ip类型
    请不要乱用Kotlin ? 空检查
    配置 jvisualvm 监控Java虚拟机
  • 原文地址:https://www.cnblogs.com/liangzid/p/10910026.html
Copyright © 2011-2022 走看看