GSoC 2022 Blender VSE: 第一周总结

本文章旨在于个人记录参加GSoC2022的编程进展。
我参加了2022年的谷歌编程之夏,主要负责blender的VSE(视频编辑器)中的波形绘制优化。优化主要分为三个部分:

  • 多线程绘制,增加处理速度
  • 绘制时追踪playhead handle(视频播放头),优先绘制视频播放头的部分
  • 将波形数据缓存至.blender文件中,使得下一次加载时不再卡顿

以下是更多的技术细节,可能会比较混乱,因为是以笔记的形式记录的

Video Sequence Editor(VSE)的可能与项目相关的几个文件:

  • source/blender/sequencer/intern/strip_add.c添加strip(VSE序列)
  • source/blender/sequencer/intern/sound.cSound Strip实用功能,用于音频处理
  • source/blender/editors/space_sequencer/sequencer_draw.c绘制时间线和预览
  • source/blender/editors/space_sequence/sequencer_preview.c负责在sound strip中绘制waveform(波形)
    更多可以参考:VSE文件说明

在了解更多代码前,需要了解VSE界面的布局,以下是官网文档的一些图片
Sequencer这个图像显示着Sequencer(序列编辑器)的布局。其中蓝色和绿色的条状组件便是Strip(序列)

关于什么是Editor,用户文档中有这样的描述。

Blender provides a number of different editors for displaying and modifying different aspects of data. An Editor is contained inside an Area which determines the size and placement of the editor with in the Blender window. Every area in Blender may contain any type of editor.
Blender提供了一系列不同编辑器来显示和修改不同类型的数据。编辑器被包含在决定着其在窗口中尺寸和布局的Area中。每一个area可以包含任意类型的编辑器

Area
以上黄色框框包含的就是area,当然,也可以认为黄色方框内就是一个editor。

typedef struct Editing {
  /** Pointer to the current list of seq's being edited (can be within a meta strip). */
  ListBase *seqbasep;
  ListBase *displayed_channels;
  void *_pad0;
  /** Pointer to the top-most seq's. */
  ListBase seqbase;
  ListBase metastack;
  ListBase channels; /* SeqTimelineChannel */
  // more ... 
} Editing;

这个Editing结构体保存着属于sequence core的数据。其中ListBase是一个链表结构体,seqbase是指向保存strips的一个链表

// The sequence structure is the basic struct used by any strip. each of the strips uses a different sequence structure.
typedef struct Sequence {
  struct Sequence *next, *prev;
  /** SEQ_NAME_MAXSTR - name, set by default and needs to be unique, for RNA paths. */
  char name[64];
  /** The length of the contents of this strip - before handles are applied. */
  int len;

  Strip *strip;

  /** The linked "bSound" object. */
  struct bSound *sound;
  void *scene_sound;
  float volume;
	// more...
} Sequence;

Sequence结构体储存着序列(Strip)属性。其中,Sequence.Strip定义了一些在序列中使用的数据和属性,包括序列中的图像和视频的地址等
这里面需要注意的是,Sequence并不是代表Sequencer的数据结构而是Strip的数据结构。文档上也给出了相应的说明

This is quite confusing part of sequencer code - What is usually called a strip is represented by data structure called Sequence. There is also data structure with name Strip which defines data and some properties used by Sequence.

比较需要关注的是bSound这个结构体,它与我的任务相关

typedef struct bSound {
  ID id;

  /**
   * The path to the sound file.
   */
  /** 1024 = FILE_MAX. */
  char filepath[1024];

  /**
   * The packed file.
   */
  struct PackedFile *packedfile;

  /**
   * The handle for audaspace.
   */
  void *handle;

  /**
   * Deprecated; used for loading pre 2.5 files.
   */
  struct PackedFile *newpackedfile;
  struct Ipo *ipo;

  float volume;
  float attenuation;
  float pitch;
  float min_gain;
  float max_gain;
  float distance;
  short flags;
  /** Runtime only, always reset in readfile. */
  short tags;
  char _pad[4];
  double offset_time;

  /* Unused currently. */
  // int type;
  // struct bSound *child_sound;

  /**
   * The audaspace handle for cache.
   */
  void *cache;

  /**
   * Waveform display data.
   */
  void *waveform;

  /**
   * The audaspace handle that should actually be played back.
   * Should be cache if cache != NULL; otherwise its handle
   */
  void *playback_handle;

  /** Spin-lock for asynchronous loading of sounds. */
  void *spinlock;
  /* XXX unused currently (SOUND_TYPE_LIMITER) */
  /* float start, end; */

  /* Description of Audio channels, as of eSoundChannels*/
  int audio_channels;

  int samplerate;

} bSound;

里面的waveform需要我们进行关注,因为波形的绘制与waveform的处理息息相关。我们需要通过这个线索找到waveform的绘制过程
使用VS自带的查找所有引用功能,找到了不少关于waveform的引用,我们开始排查
在这里插入图片描述可以看到,在最底下的waveform调用属于sequence_draw.c,这个文件的功能在上文提到的是绘制时间线和预览。因此,很有可能是我们的绘制波形的功能。我们先看它。
调用函数名称为draw_seq_waveform_overlay 从名字来看和波形绘制非常有关系,我们打断点测试。
在这里插入图片描述从调用栈中可以看出,函数先绘制了一系列基础图像后,开始绘制时间线(timeline),然后绘制序列(strip),然后最后到达waveform的绘制。

	SoundWaveform *waveform = sound->waveform;

	/* Waveform could not be built. */
	if (waveform->length == 0) {
	  return;
	}

	typedef struct SoundWaveform {
	  int length;
	  float *data;
	} SoundWaveform;

sound是一个bSound结构体指针,它的waveform数据被赋值到了SoundWaveform 这个结构体中。我们发现,它是一个具有长度和浮点数组指针的一个结构体。在调试窗口中可以发现,waveform->data确实是一个浮点数组。
在这里插入图片描述
这些浮点数组的意义是什么,我们暂且不考虑。我们进一步验证,将生成的浮点数据写入到txt文件中,需要的时候再读出来,观测波形的变化。

// 读取
{
    #  include "stdio.h"
      FILE *sound_file = fopen("C:\\Users\\xs\\Desktop\\datadispose\\1.txt", "r");
      int i = 0;
      int return_value = 0;
      if (sound_file != NULL) {
        fseek(sound_file, 0, SEEK_SET);
        while (return_value != -1) {
      		fscanf_s(sound_file, "%f", &(waveform->data[i++]));
      		waveform->data[i-1] /= 8;
         return_value = fscanf_s(sound_file, ",");
        }
        fclose(sound_file);
        waveform->length = i/3;
        state = 1;
      }
      else {
		// blender提供的音频读取并生成波形的函数
        waveform->length = AUD_readSound(
            sound->playback_handle, waveform->data, length, SOUND_WAVE_SAMPLES_PER_SECOND, stop);
      }
    }
// 写入
{
    #include "stdio.h"
    if (state == 0) {
      FILE *sound_file = fopen("C:\\Users\\xs\\Desktop\\datadispose\\1.txt", "w");
      int count = 0;
      for (int i = 0; i < waveform->length * 3; ++i) {
        if (i == waveform->length - 1)
          fprintf_s(sound_file, "%f", waveform->data[i]);
        else
          fprintf_s(sound_file, "%f,", waveform->data[i]);

        count++;
      }
      fclose(sound_file);
}

上边代码的读取我做了一个手脚,我将读到的数据除以8,这样子对比前后波形可以更好的确定这个waveform的功能。
请添加图片描述
请添加图片描述上图为除以8以后的波形,下图是使用blender默认加载函数得到的波形。两者可以看出很大的区别。因此,我们可以确定waveform确实是储存的波形数据。
值得一提的是,我的写入的for循环采用的是waveform->length * 3作为终止条件而不是waveform->length。这是因为我采用后者生成的波形只有总波形的大约1/3。
在这里插入图片描述
由此可以推测,应该是3个float数据对应的一个波形。下面,我们最重要的是了解这个浮点数如何对应的最终的波形。
第一个猜测是waveform本身就是代表了音频的数据。首先上网搜集一些资料,发现主流音频格式似乎都是用整形来储存音频数据的,但是也有一些资料说浮点数也可以表示音频数据
为什么通常的声音格式,每个采样点都是用整形? - 诗云的回答 - 知乎
为了更加确认这件事情,我们需要查看blender的代码。
Blender的波形读取代码是int AUD_readSound(AUD_Sound* sound, float* buffer, int length, int samples_per_second, short* interrupt),我们进入到函数内部去查看内部代码。

AUD_API int AUD_readSound(AUD_Sound* sound, float* buffer, int length, int samples_per_second, short* interrupt)
{
	float* buf;
	int len;
	float min, max, power, overallmax;
	bool eos;
	for(int i = 0; i < length; i++) {
	// more...
	len = floor(samplejump * (i+1)) - floor(samplejump * i);
	
	std::shared_ptr<IReader> reader = ChannelMapper(*sound, specs).createReader();
	
	aBuffer.assureSize(len * AUD_SAMPLE_SIZE(specs));
	buf = aBuffer.getBuffer();
	
	reader->read(len, eos, buf);
	max = min = *buf;
	power = *buf * *buf;
	for(int j = 1; j < len; j++)
	{
		if(buf[j] < min)
			min = buf[j];
		if(buf[j] > max)
			max = buf[j];
		power += buf[j] * buf[j];
	}
	buffer[i * 3] = min;
	buffer[i * 3 + 1] = max;
	buffer[i * 3 + 2] = std::sqrt(power / len);
	
	if(overallmax < max)
		overallmax = max;
	if(overallmax < -min)
		overallmax = -min;
	
	if(eos)
	{
		length = i;
		break;
	}
	if(overallmax > 1.0f)
	{
		for(int i = 0; i < length * 3; i++)
		{
			buffer[i] /= overallmax;
		}
	}
	
	return length;
}

以上是函数中比较关键的部分。从这里面的代码可以看出为什么之前需要使用waveform->length * 3作为终止条件以及每一组浮点数(三个浮点数)所代表的意义。
浮点数组以三个为一组,其中分别为最小值、最大值以及能量。从循环for(int j = 1; j < len; j++)中的循环体可以看出这些值是如何求出的。其中能量需要稍微说明一下。在信号与系统中,我们把能量表述为信号平方对于时间的积分。对于离散来说,这就意味着对信号平方进行求和。至于为什么能量表示为这样子,我们可以理解为对于自然界一些能量的抽象。具体可以参照奥本海姆的信号与系统这本书。

if(overallmax < max)
	overallmax = max;
if(overallmax < -min)
	overallmax = -min;

if(overallmax > 1.0f)
{
	for(int i = 0; i < length * 3; i++)
	{
		buffer[i] /= overallmax;
	}
}

overallmax取所有片段max和min绝对值中的最大值。如果overallmax大于1,则需要对于片段进行标准化。
除此之外,reader->read(len, eos, buf);涉及到工厂模式的一些知识,
(一周时间写到这就到了,因为笔者忙于期末考试所以内容较少,我们下周总结见[狗头])

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值