zoukankan      html  css  js  c++  java
  • FFMPEG + SDL音频播放分析

    目录 [hide]

    抽象流程:

    设置SDL的音频参数 —-> 打开声音设备,播放静音 —-> ffmpeg读取音频流中数据放入队列 —-> SDL调用用户设置的函数来获取音频数据 —-> 播放音频

    SDL内部维护了一个buffer来存放解码后的数据,这个buffer中的数据来源是我们注册的回调函数(audio_callback),audio_callback调用audio_decode_frame来做具体的音频解码工作,需要引起注意的是:从流中读取出的一个音频包(avpacket)可能含有多个音频桢(avframe),所以需要多次调用avcodec_decode_audio4来完成整个包的解码,解码出来的数据存放在我们自己的缓冲中(audio_buf2)。SDL每一次回调都会引起数据从audio_buf2拷贝到SDL内部缓冲区,当audio_buf2中的数据大于SDL的缓冲区大小时,需要分多次拷贝。

    关键实现:

    main()函数

    1 int main(int argc, char **argv){
    2     SDL_Event event; //SDL事件变量
    3     VideoState    *is; // 纪录视频及解码器等信息的大结构体
    4     is = (VideoState*) av_mallocz(sizeof(VideoState));
    5     if(argc < 2){
    6         fprintf(stderr, "Usage: play <file> ");
    7         exit(1);
    8     }
    9     av_register_all(); //注册所有ffmpeg的解码器
    10     /* 初始化SDL,这里只实用了AUDIO,如果有视频,好需要SDL_INIT_VIDEO等等 */
    11     if(SDL_Init(SDL_INIT_AUDIO)){
    12         fprintf(stderr, "Count not initialize SDL - %s ", SDL_GetError());
    13         exit(1);
    14     }
    15     is_strlcpy(is->filename, argv[1], sizeof(is->filename));
    16     /* 创建一个SDL线程来做视频解码工作,主线程进入SDL事件循环 */
    17     is->parse_tid = SDL_CreateThread(decode_thread, is);
    18     if(!is->parse_tid){
    19         SDL_WaitEvent(&event);
    20         switch(event.type){
    21             case FF_QUIT_EVENT:
    22             case SDL_QUIT:
    23                  is->quit = 1;
    24                 SDL_Quit();
    25                 exit(0);
    26                 break;
    27             default:
    28                  break;
    29         }
    30     }
    31     return 0;
    32 }

    decode_thread()读取文件信息和音频包

    1 static int decode_thread(void *arg){
    2     VideoState *is = (VideoState*)arg;
    3     AVFormatContext *ic = NULL;
    4     AVPacket pkt1, *packet = &pkt1;
    5     int ret, i, audio_index = -1;
    6  
    7     is->audioStream = -1;
    8     global_video_state = is;
    9     /*  使用ffmpeg打开视频,解码器等 常规工作 */
    10     if(avFormat_open_input(&ic, is->filename, NULL,  NULL) != 0)  {
    11         fprintf(stderr, "open file error: %s ", is->filename);
    12         return -1;
    13     }
    14     is->ic = ic;
    15     if(avformat_find_stream_info(ic, NULL) < 0){
    16         fprintf(stderr, "find stream info error ");
    17         return -1;
    18     }
    19     av_dump_format(ic, 0, is->filename, 0);
    20     for(i  = 0; i < ic->nb_streams; i++){
    21          if(ic->streams[i])->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index == -1){
    22             audio_index = i;
    23             break;
    24         }
    25     }
    26     if(audio_index >= 0) {
    27         /* 所有设置SDL音频流信息的步骤都在这个函数里完成 */
    28         stream_component_open(is, audio_index);
    29     }
    30     if(is->audioStream < 0){
    31         fprintf(stderr, "could not open codecs for file: %s ", is->filename);
    32         goto fail;
    33     }
    34     /* 读包的主循环, av_read_frame不停的从文件中读取数据包(这里只取音频包)*/
    35     for(;;){
    36         if(is->quit) break;
    37         /* 这里audioq.size是指队列中的所有数据包带的音频数据的总量,并不是包的数量 */
    38         if(is->audioq.size > MAX_AUDIO_SIZE){
    39             SDL_Delay(10); // 毫秒
    40             continue;
    41         }
    42          ret = av_read_frame(is->ic, packet);
    43          if(ret < 0){
    44                 if(ret == AVERROR_EOF || url_feof(is->ic->pb))    break;
    45                 if(is->ic->pb && is->ic->pb->error)    break;
    46                 contiue;                 
    47           
    48           if(packet->stream_index == is->audioStream){
    49                     packet_queue_put(&is->audioq, packet);
    50            } else{
    51                      av_free_packet(packet);
    52             }
    53     }
    54      while(!is->quit)    SDL_Delay(100);
    55 fail: {
    56                SDL_Event event;
    57                event.type = FF_QUIT_EVENT;
    58                event.user.data1 = is;
    59                SDL_PushEvent(&event);
    60         }
    61         return 0;
    62 }

    stream_component_open():设置音频参数和打开设备

    1 int stream_component_open(videoState *is, int stream_index){
    2     AVFormatContext *ic = is->ic;
    3     AVCodecContext *codecCtx;
    4     AVCodec *codec;
    5     /* 在用SDL_OpenAudio()打开音频设备的时候需要这两个参数*/
    6     /* wanted_spec是我们期望设置的属性,spec是系统最终接受的参数 */
    7     /* 我们需要检查系统接受的参数是否正确 */
    8     SDL_AudioSpec wanted_spec, spec;
    9     int64_t wanted_channel_layout = 0; // 声道布局(SDL中的具体定义见“FFMPEG结构体”部分)
    10     int wanted_nb_channels; // 声道数
    11     /*  SDL支持的声道数为 1, 2, 4, 6 */
    12     /*  后面我们会使用这个数组来纠正不支持的声道数目 */
    13     const int next_nb_channels[] = { 0, 0, 1, 6,  2, 6, 4, 6 };
    14  
    15     if(stream_index < 0 || stream_index >= ic->nb_streams)    return -1;
    16     codecCtx = ic->streams[stream_index]->codec;
    17     wanted_nb_channels = codecCtx->channels;
    18     if(!wanted_channel_layout || wanted_nb_channels != av_get_channel_layout_nb_channels(wanted_channel_layout)) {
    19         wanted_channel_layout = av_get_default_channel_lauout(wanted_channel_nb_channels);
    20         wanted_channel_layout &= ~AV_CH_LAYOUT_STEREO_DOWNMIX;
    21     }
    22     wanted_spec.channels = av_get_channels_layout_nb_channels(wanted_channel_layout);
    23     wanted_spec.freq = codecCtx->sample_rate;
    24     if(wanted_spec.freq <= 0 || wanted_spec.channels <=0){
    25            fprintf(stderr, "Invaild sample rate or channel count! ");
    26             return -1;
    27     }
    28     wanted_spec.format = AUDIO_S16SYS; // 具体含义请查看“SDL宏定义”部分
    29     wanted_spec.silence = 0; // 0指示静音
    30     wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; // 自定义SDL缓冲区大小
    31     wanted_spec.callback = audio_callback; // 音频解码的关键回调函数
    32     wanted_spec.userdata = is; // 传给上面回调函数的外带数据
    33  
    34     /*  打开音频设备,这里使用一个while来循环尝试打开不同的声道数(由上面 */
    35     /*  next_nb_channels数组指定)直到成功打开,或者全部失败 */
    36     while(SDL_OpenAudio(&wanted_spec, &spec) < 0){
    37         fprintf(stderr, "SDL_OpenAudio(%d channels): %s ", wanted_spec.channels, SDL_GetError());
    38         wanted_spec.channels = next_nb_channels[FFMIN(7, wanted_spec.channels)]; // FFMIN()由ffmpeg定义的宏,返回较小的数
    39         if(!wanted_spec.channels){
    40               fprintf(stderr, "No more channel to try ");
    41               return -1;
    42         }
    43         wanted_channel_layout = av_get_default_channel_layout(wanted_spec.channels);
    44     }
    45     /* 检查实际使用的配置(保存在spec,由SDL_OpenAudio()填充) */
    46     if(spec.format != AUDIO_S16SYS){
    47         fprintf(stderr, "SDL advised audio format %d is not supported ", spec.format);
    48         return -1;
    49     }
    50     if(spec.channels != wanted_spec.channels) {
    51         wanted_channel_layout = av_get_default_channel_layout(spec.channels);
    52         if(!wanted_channel_layout){
    53                 fprintf(stderr, "SDL advised channel count %d is not support ", spec.channels);
    54                 return -1;
    55         }
    56     }
    57     /* 把设置好的参数保存到大结构中 */
    58     is->audio_src_fmt = is->audio_tgt_fmt = AV_SAMPLE_FMT_S16;
    59     is->audio_src_freq = is->audio_tgt_freq = spec.freq;
    60     is->audio_src_channel_layout = is->audio_tgt_layout = wanted_channel_layout;
    61     is->audio_src_channels = is->audio_tat_channels = spec.channels;
    62  
    63     codec = avcodec_find_decoder(codecCtx>codec_id);
    64     if(!codec || (avcodec_open2(codecCtx, codec, NULL) < 0)){
    65         fprintf(stderr, "Unsupported codec! ");
    66         return -1;
    67     }
    68     ic->streams[stream_index]->discard = AVDISCARD_DEFAULT; //具体含义请查看“FFMPEG宏定义”部分
    69     is->audioStream = stream_index;
    70     is->audio_st = ic->streams[stream_index];
    71     is->audio_buf_size = 0;
    72     is->audio_buf_index = 0;
    73     memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    74     packet_queue_init(&is->audioq);
    75     SDL_PauseAudio(0); // 开始播放静音
    76 }

    audio_callback(): 回调函数,向SDL缓冲区填充数据

    1 void audio_callback(void *userdata, Uint8 *stream, int len){
    2     VideoState *is = (VideoState*)userdata;
    3     int len1, audio_data_size;
    4  
    5     /*   len是由SDL传入的SDL缓冲区的大小,如果这个缓冲未满,我们就一直往里填充数据 */
    6     while(len > 0){
    7         /*  audio_buf_index 和 audio_buf_size 标示我们自己用来放置解码出来的数据的缓冲区,*/
    8         /*   这些数据待copy到SDL缓冲区, 当audio_buf_index >= audio_buf_size的时候意味着我*/
    9         /*   们的缓冲为空,没有数据可供copy,这时候需要调用audio_decode_frame来解码出更
    10         /*   多的桢数据 */
    11         if(is->audio_buf_index >= is->audio_buf_size){
    12                 audio_data_size = audio_decode_frame(is);
    13                 /* audio_data_size < 0 标示没能解码出数据,我们默认播放静音 */
    14                 is(audio_data_size < 0){
    15                          is->audio_buf_size = 1024;
    16                          /* 清零,静音 */
    17                          memset(is->audio_buf, 0, is->audio_buf_size);
    18                 } else{
    19                           is->audio_buf_size = audio_data_size;
    20                  }
    21                  is->audio_buf_index = 0;
    22         }
    23         /*  查看stream可用空间,决定一次copy多少数据,剩下的下次继续copy */
    24         len1 = is->audio_buf_size - is->audio_buf_index;
    25         if(len1 > len)    len1 = len;
    26  
    27         memcpy(stream, (uint8_t*)is->audio_buf + is->audio_buf_index, len1);
    28         len -= len1;
    29         stream += len1;
    30         is->audio_buf_index += len1;
    31     }
    32 }

    audio_decode_frame():解码音频

    1 int audio_decode_frame(VideoState *is){
    2     int len1, len2, decoded_data_size;
    3     AVPacket *pkt = &is->audio_pkt;
    4     int got_frame = 0;
    5     int64_t dec_channel_layout;
    6     int wanted_nb_samples, resampled_data_size;
    7  
    8     for(;;){
    9       while(is->audio_pkt_size > 0){
    10         if(!is->audio_frame){
    11             if(!(is->audio_frame = avacodec_alloc_frame())){
    12                 return AVERROR(ENOMEM);
    13             }
    14         } else
    15           avcodec_get_frame_defaults(is->audio_frame);
    16  
    17         len1 = avcodec_decode_audio4(is->audio_st_codec, is->audio_frame, got_frame, pkt);
    18         /* 解码错误,跳过整个包 */
    19         if(len1 < 0){
    20            is->audio_pkt_size = 0;
    21            break;
    22         }
    23         is->audio_pkt_data += len1;
    24         is->audio_pkt_size -= len1;
    25         if(!got_frame)   continue;
    26         /* 计算解码出来的桢需要的缓冲大小 */
    27         decoded_data_size = av_samples_get_buffer_size(NULL,
    28                             is->audio_frame_channels,
    29                             is->audio_frame_nb_samples,
    30                             is->audio_frame_format, 1);
    31         dec_channel_layout = (is->audio_frame->channel_layout && is->audio_frame->channels
    32                    == av_get_channel_layout_nb_channels(is->audio_frame->channel_layout))
    33                    ? is->audio_frame->channel_layout : av_get_default_channel_layout(is->audio_frame->channels);                      
    34         wanted_nb_samples =  is->audio_frame->nb_samples;
    35         if (is->audio_frame->format != is->audio_src_fmt ||
    36             dec_channel_layout != is->audio_src_channel_layout ||
    37             is->audio_frame->sample_rate != is->audio_src_freq ||
    38             (wanted_nb_samples != is->audio_frame->nb_samples && !is->swr_ctx)) {
    39                 if (is->swr_ctx) swr_free(&is->swr_ctx);
    40                 is->swr_ctx = swr_alloc_set_opts(NULL,
    41                                                  is->audio_tgt_channel_layout,
    42                                                  is->audio_tgt_fmt,
    43                                                  is->audio_tgt_freq,
    44                                                  dec_channel_layout,
    45                                                  is->audio_frame->format,
    46                                                  is->audio_frame->sample_rate,
    47                                                  0, NULL);
    48                  if (!is->swr_ctx || swr_init(is->swr_ctx) < 0) {
    49                      fprintf(stderr, "swr_init() failed ");
    50                      break;
    51                  }
    52                  is->audio_src_channel_layout = dec_channel_layout;
    53                  is->audio_src_channels = is->audio_st->codec->channels;
    54                  is->audio_src_freq = is->audio_st->codec->sample_rate;
    55                  is->audio_src_fmt = is->audio_st->codec->sample_fmt;
    56          }
    57          /* 这里我们可以对采样数进行调整,增加或者减少,一般可以用来做声画同步 */
    58          if (is->swr_ctx) {
    59              const uint8_t **in = (const uint8_t **)is->audio_frame->extended_data;
    60              uint8_t *out[] = { is->audio_buf2 };
    61              if (wanted_nb_samples != is->audio_frame->nb_samples) {
    62                 if(swr_set_compensation(is->swr_ctx,
    63                   (wanted_nb_samples - is->audio_frame->nb_samples)*is->audio_tgt_freq/is->audio_frame->sample_rate,
    64                    wanted_nb_samples * is->audio_tgt_freq/is->audio_frame->sample_rate) < 0) {
    65                         fprintf(stderr, "swr_set_compensation() failed ");
    66                         break;
    67                    }
    68              }
    69              len2 = swr_convert(is->swr_ctx, out, 
    70                   sizeof(is->audio_buf2)/is->audio_tgt_channels/av_get_bytes_per_sample(is->audio_tgt_fmt), 
    71                   in, is->audio_frame->nb_samples);
    72              if (len2 < 0) {
    73                   fprintf(stderr, "swr_convert() failed ");
    74                   break;
    75              }
    76              if(len2 == sizeof(is->audio_buf2)/is->audio_tgt_channels/av_get_bytes_per_sample(is->audio_tgt_fmt)) {
    77                  fprintf(stderr, "warning: audio buffer is probably too small ");
    78                  swr_init(is->swr_ctx);
    79              }
    80              is->audio_buf = is->audio_buf2;
    81              resampled_data_size = len2*is->audio_tgt_channels*av_get_bytes_per_sample(is->audio_tgt_fmt);
    82            } else {
    83              resampled_data_size = decoded_data_size;
    84              is->audio_buf = is->audio_frame->data[0];
    85            }
    86            /*  返回得到的数据 */
    87            return resampled_data_size;
    88        }
    89        if (pkt->data) av_free_packet(pkt);
    90        memset(pkt, 0, sizeof(*pkt));
    91        if (is->quit) return -1;
    92        if (packet_queue_get(&is->audioq, pkt, 1) < 0) return -1;
    93        is->audio_pkt_data = pkt->data;
    94        is->audio_pkt_size = pkt->size;
    95  
    96      }
    97 }

    FFMPEG结构体

    channel_layout_map

    1 static const struct {
    2 const char *name;
    3 int nb_channels;
    4 uint64_t layout;
    5 } channel_layout_map[] = {
    6 { "mono", 1, AV_CH_LAYOUT_MONO },
    7 { "stereo", 2, AV_CH_LAYOUT_STEREO },
    8 { "2.1", 3, AV_CH_LAYOUT_2POINT1 },
    9 { "3.0", 3, AV_CH_LAYOUT_SURROUND },
    10 { "3.0(back)", 3, AV_CH_LAYOUT_2_1 },
    11 { "4.0", 4, AV_CH_LAYOUT_4POINT0 },
    12 { "quad", 4, AV_CH_LAYOUT_QUAD },
    13 { "quad(side)", 4, AV_CH_LAYOUT_2_2 },
    14 { "3.1", 4, AV_CH_LAYOUT_3POINT1 },
    15 { "5.0", 5, AV_CH_LAYOUT_5POINT0_BACK },
    16 { "5.0(side)", 5, AV_CH_LAYOUT_5POINT0 },
    17 { "4.1", 5, AV_CH_LAYOUT_4POINT1 },
    18 { "5.1", 6, AV_CH_LAYOUT_5POINT1_BACK },
    19 { "5.1(side)", 6, AV_CH_LAYOUT_5POINT1 },
    20 { "6.0", 6, AV_CH_LAYOUT_6POINT0 },
    21 { "6.0(front)", 6, AV_CH_LAYOUT_6POINT0_FRONT },
    22 { "hexagonal", 6, AV_CH_LAYOUT_HEXAGONAL },
    23 { "6.1", 7, AV_CH_LAYOUT_6POINT1 },
    24 { "6.1", 7, AV_CH_LAYOUT_6POINT1_BACK },
    25 { "6.1(front)", 7, AV_CH_LAYOUT_6POINT1_FRONT },
    26 { "7.0", 7, AV_CH_LAYOUT_7POINT0 },
    27 { "7.0(front)", 7, AV_CH_LAYOUT_7POINT0_FRONT },
    28 { "7.1", 8, AV_CH_LAYOUT_7POINT1 },
    29 { "7.1(wide)", 8, AV_CH_LAYOUT_7POINT1_WIDE },
    30 { "octagonal", 8, AV_CH_LAYOUT_OCTAGONAL },
    31 { "downmix", 2, AV_CH_LAYOUT_STEREO_DOWNMIX, },
    32 };

    FFMPEG宏定义

    Audio channel convenience macros

    1 #define AV_CH_LAYOUT_MONO              (AV_CH_FRONT_CENTER)
    2  #define AV_CH_LAYOUT_STEREO            (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
    3  #define AV_CH_LAYOUT_2POINT1           (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
    4  #define AV_CH_LAYOUT_2_1               (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
    5  #define AV_CH_LAYOUT_SURROUND          (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
    6  #define AV_CH_LAYOUT_3POINT1           (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
    7  #define AV_CH_LAYOUT_4POINT0           (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
    8  #define AV_CH_LAYOUT_4POINT1           (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
    9  #define AV_CH_LAYOUT_2_2               (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
    10  #define AV_CH_LAYOUT_QUAD              (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
    11  #define AV_CH_LAYOUT_5POINT0           (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
    12  #define AV_CH_LAYOUT_5POINT1           (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
    13  #define AV_CH_LAYOUT_5POINT0_BACK      (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
    14  #define AV_CH_LAYOUT_5POINT1_BACK      (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
    15  #define AV_CH_LAYOUT_6POINT0           (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
    16  #define AV_CH_LAYOUT_6POINT0_FRONT     (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
    17  #define AV_CH_LAYOUT_HEXAGONAL         (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
    18  #define AV_CH_LAYOUT_6POINT1           (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
    19  #define AV_CH_LAYOUT_6POINT1_BACK      (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
    20  #define AV_CH_LAYOUT_6POINT1_FRONT     (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
    21  #define AV_CH_LAYOUT_7POINT0           (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
    22  #define AV_CH_LAYOUT_7POINT0_FRONT     (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
    23  #define AV_CH_LAYOUT_7POINT1           (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
    24 #define AV_CH_LAYOUT_7POINT1_WIDE      (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
    25 #define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
    26 #define AV_CH_LAYOUT_OCTAGONAL         (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
    27 #define AV_CH_LAYOUT_STEREO_DOWNMIX    (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)

    SDL宏定义

    SDL_AudioSpec format

    1 AUDIO_U8           Unsigned 8-bit samples
    2 AUDIO_S8            Signed 8-bit samples
    3 AUDIO_U16LSB    Unsigned 16-bit samples, in little-endian byte order
    4 AUDIO_S16LSB    Signed 16-bit samples, in little-endian byte order
    5 AUDIO_U16MSB    Unsigned 16-bit samples, in big-endian byte order
    6 AUDIO_S16MSB    Signed 16-bit samples, in big-endian byte order
    7 AUDIO_U16           same as AUDIO_U16LSB (for backwards compatability probably)
    8 AUDIO_S16           same as AUDIO_S16LSB (for backwards compatability probably)
    9 AUDIO_U16SYS    Unsigned 16-bit samples, in system byte order
    10 AUDIO_S16SYS     Signed 16-bit samples, in system byte order

    git clone https://github.com/lnmcc/musicPlayer.git

  • 相关阅读:
    2021,6,10 xjzx 模拟考试
    平衡树(二)——Treap
    AtCoder Beginner Contest 204 A-E简要题解
    POJ 2311 Cutting Game 题解
    Codeforces 990G GCD Counting 题解
    NOI2021 SDPTT D2T1 我已经完全理解了 DFS 序线段树 题解
    第三届山东省青少年创意编程与智能设计大赛总结
    Luogu P6042 「ACOI2020」学园祭 题解
    联合省选2021 游记
    Codeforces 1498E Two Houses 题解 —— 如何用结论吊打标算
  • 原文地址:https://www.cnblogs.com/lidabo/p/3701074.html
Copyright © 2011-2022 走看看