GSoC 2022 Blender VSE: 第二、三周总结

本文章旨在于个人记录参加GSoC2022的编程进展。


工厂模式解读

我们继续上一周提到的关于reader的工厂模式的调用。关于工厂模式,我看到过一篇非常好的文章。它详细讲述了简单工厂模式、工厂方法以及抽象工厂模式的原理并给出了相应的示例代码。
C++实现工厂模式
为了确认reader与工厂方法有关,我们需要找到它的基函数以及一系列继承函数,通过这些函数的继承关系来分析它与工厂方法的关系。
上一篇文章提到的reader->read(len, eos, buf);是由std::shared_ptr<IReader> reader = ChannelMapper(*sound, specs).createReader();产生的。reader
我们进入ChannelMapper这个函数去查看它的内容。

class AUD_API ChannelMapper : public SpecsChanger
{
private:
	// delete copy constructor and operator=
	ChannelMapper(const ChannelMapper&) = delete;
	ChannelMapper& operator=(const ChannelMapper&) = delete;

public:
	/**
	 * Creates a new sound.
	 * \param sound The input sound.
	 * \param specs The target specifications.
	 */
	ChannelMapper(std::shared_ptr<ISound> sound, DeviceSpecs specs);

	virtual std::shared_ptr<IReader> createReader();
};

它的主要函数是一个构造函数和一个虚函数createReaderChannelMapper本身继承自SpecesChanger

ChannelMapper::ChannelMapper(std::shared_ptr<ISound> sound, DeviceSpecs specs) :
		SpecsChanger(sound, specs)
{
}

std::shared_ptr<IReader> ChannelMapper::createReader()
{
	std::shared_ptr<IReader> reader = getReader();
	return std::shared_ptr<IReader>(new ChannelMapperReader(reader, m_specs.channels));
}

ChannelMapper的构造函数调用了其父类的构造函数,createReader()函数调用了其基类函数的一个getReader方法,其后,返回了一个reader类。
这就有一点工厂方法那味了,接着阅读源码。
我们需要重点关注在ChannelMapper出现的几个函数以及其相关的类。一个是其父类SpecsChanger,一个是其在createReader中返回的一个Reader类。
先看SpecsChanger,需要重点关注的是getReader函数。当我进入到这个类,映入眼帘的不是重点关注的函数,而是它的一段注释

/**
 * This sound is a base class for all mixer factories.
 */

这说明我们没有找错,这确实是一个工厂方法!不过,mixer,是个啥?先不管,我们继续看代码。
但是我们进入到它的函数以后,就会发现

std::shared_ptr<IReader> SpecsChanger::getReader() const
{
	return m_sound->createReader();
}

还有一层封装?说好的SpecsChanger已经是mixer factory的基类了吗,怎么还有?只能硬着头皮向下看。m_sound是指向ISound的一个std::shared_ptr智能指针,它在类构造函数中被赋值。对于我们目前正在跟踪的代码来说,调用链大概长这样
AUD_readSound —> ChannelMapper —>SpecesChanger。最终m_sound被赋值。由于其指针是ISound的智能指针,我们姑且认为m_sound指向的也是ISound。所以我们需要深入查看ISound的代码
ISound的内部比较简单,它甚至似乎没有源文件,仅有两个声明的虚函数,getReader返回的是一个IReader的智能指针。

class AUD_API ISound
{
public:
	/**
	 * Destroys the sound.
	 */
	virtual ~ISound() {}

	/**
	 * Creates a reader for playback of the sound source.
	 * \return A pointer to an IReader object or nullptr if there has been an
	 *         error.
	 * \exception Exception An exception may be thrown if there has been
	 *            a more unexpected error during reader creation.
	 */
	virtual std::shared_ptr<IReader> createReader()=0;
};

我们使用VS做一个比较有意思的事情,就是查看createReader的符号。
在这里插入图片描述我们可以发现,类ChannelMapper赫然在列,而且有许多个函数继承了这个虚函数。也就是说,这个创建reader的方法是被广泛使用的!我们正在慢慢接近这些代码的真谛!
我们接下来去看上文中提到的reader的代码,reader代码需要关注的有两个部分一个是基类IReader,另外一个是ChannelMapperReader

class AUD_API IReader
{
public:
	/**
	 * Destroys the reader.
	 */
	virtual ~IReader() {}

	/**
	 * Tells whether the source provides seeking functionality or not.
	 * \warning This doesn't mean that the seeking always has to succeed.
	 * \return Always returns true for readers of buffering types.
	 */
	virtual bool isSeekable() const=0;

	/**
	 * Seeks to a specific position in the source.
	 * \param position The position to seek for measured in samples. To get
	 *        from a given time to the samples you simply have to multiply the
	 *        time value in seconds with the sample rate of the reader.
	 * \warning This may work or not, depending on the actual reader.
	 */
	virtual void seek(int position)=0;

	/**
	 * Returns an approximated length of the source in samples.
	 * \return The length as sample count. May be negative if unknown.
	 */
	virtual int getLength() const=0;

	/**
	 * Returns the position of the source as a sample count value.
	 * \return The current position in the source. A negative value indicates
	 *         that the position is unknown.
	 * \warning The value returned doesn't always have to be correct for readers,
	 *          especially after seeking.
	 */
	virtual int getPosition() const=0;

	/**
	 * Returns the specification of the reader.
	 * \return The Specs structure.
	 */
	virtual Specs getSpecs() const=0;

	/**
	 * Request to read the next length samples out of the source.
	 * The buffer supplied has the needed size.
	 */
	virtual void read(int& length, bool& eos, sample_t* buffer)=0;
};

查找虚函数read的符号
在这里插入图片描述我们会发现之前的ChannelMapperReader也在里面,ChannelMapperReader继承了EffectReader(EffectReader继承自IReader)并且重写了getSpeces()read()方法。

ChannelMapperReader::ChannelMapperReader(std::shared_ptr<IReader> reader,
												 Channels channels) :
		EffectReader(reader), m_target_channels(channels),
	m_source_channels(CHANNELS_INVALID), m_mapping(nullptr), m_map_size(0), m_mono_angle(0)
{
}

ChannelMapperReader的构造函数比较重要的是EffectReader的初始化,它将传入构造函数的reader参数传给父类EffectReader的构造函数

EffectReader::EffectReader(std::shared_ptr<IReader> reader)
{
	m_reader = reader;
}

EffectReader将其赋值给类变量m_reader。这个m_reader是比较重要的,因为在ChannelMapperReader的重要方法read中,m_reader将会起到实际上读取的作用。

void ChannelMapperReader::read(int& length, bool& eos, sample_t* buffer)
{
	Channels channels = m_reader->getSpecs().channels;
	// more...
	m_reader->read(length, eos, in);
	// more...
}

这个m_reader的赋值已经溯源过了,是来自于ChannelMapperReader的构造函数的调用,这个构造函数在ChannelMapper::createReader()中被调用。继续向上溯源,reader本身来自于SpecsChanger::getReader()函数,其内部调用m_sound->createReader()std::shared_ptr<ISound> m_sound)。这也就是说明,它的生成Reader的函数是一个类似于回调的形式,而且目前不知道这个createReader方法是对应哪一个子类的方法。
这里其实我有一点不解。按照一般的工厂模式,CreateReader理论上应当直接返回一个Reader类而且Reader类中应该有独立的Read而不是调用m_reader进行读取,而且这个m_reader依赖的是工厂类ISound内部的CreateReader回调。这样子感觉会产生一个问题。

std::shared_ptr<IReader> ChannelMapper::createReader()
{
	std::shared_ptr<IReader> reader = getReader();
	return std::shared_ptr<IReader>(new ChannelMapperReader(reader, m_specs.channels));
}

如果getReader返回的是其它Reader,比如,需要返回ChannelMapperReader却返回了FFMPEGReader,那么整个类的功能将会不正确。有什么机制保证了其返回正确?亦或者是这个返回的Reader是一个通用Reader?这一切,需要打断点去找到答案。我们在std::shared_ptr<IReader> reader = getReader();上打断点,看其执行。

在这里插入图片描述
在这里插入图片描述通过调用栈可以发现,最终ChannelMapper创造的reader实际上是FFMPEGReader。这个比较令人疑惑。为什么这几个Reader均继承自IReaderChannelMapper还需要向另外一个类去借Reader
关键在于ChannelMapper(std::shared_ptr<ISound> sound, DeviceSpecs specs)中传入的sound参数。在我们的例子中,这个类的构造函数是在AUD_readSound(AUD_Sound* sound, float* buffer, int length, int samples_per_second, short* interrupt)中调用的。AUD_Sound *sound作为ChannelMapper构造函数中的第一个参数。

typedef std::shared_ptr<aud::ISound> AUD_Sound

在AUD_Types.h中可以发现AUD_sound是一个ISound的别名。我们需要继续看,这个AUD_Sound *sound如何被传入。继续打断点。
在这里插入图片描述

AUD_API int AUD_readSound(AUD_Sound* sound, float* buffer, int length, int samples_per_second, short* interrupt) // 函数参数

        waveform->length = AUD_readSound(
            sound->playback_handle, waveform->data, length, SOUND_WAVE_SAMPLES_PER_SECOND, stop);

BKE_sound_read_waveform中,AUD_readSound被调用,传入的参数是sound->playback_handle,这个结构体成员我们在上一篇文章列举过,它是一个void*指针变量,现在我们要探究它具体是什么。

 sound->cache = AUD_Sound_cache(sound->handle);
  if (sound->cache) {
    sound->playback_handle = sound->cache;
  }
  else {
    sound->playback_handle = sound->handle;
  }

从以上源代码可以看出,sound->playback_handle实际上就是sound->handle(因为我们没有使用cache这个功能),sound->handle来源于sound->handle = AUD_Sound_file(fullpath);这一条语句。

AUD_API AUD_Sound* AUD_Sound_file(const char* filename)
{
	assert(filename);
	return new AUD_Sound(new File(filename));
}

我们看到,它返回了一个指向File的类的智能指针。这个File的实际上也是继承了ISound,可以认为是一个工厂类。它的createReader使用了FileManagercreateReader
关于智能指针的用法,我还不大熟悉,希望以后能够熟悉它的使用。

std::shared_ptr<IReader> File::createReader()
{
	if(m_buffer.get())
		return FileManager::createReader(m_buffer, m_stream);
	else
		return FileManager::createReader(m_filename, m_stream);
}

std::shared_ptr<IReader> FileManager::createReader(std::shared_ptr<Buffer> buffer, int stream)
{
	for(std::shared_ptr<IFileInput> input : inputs())
	{
		try
		{
			return input->createReader(buffer, stream);
		}
		catch(Exception&) {}
	}

	AUD_THROW(FileException, "The file couldn't be read with any installed file reader.");
}

其内部是一个迭代器,将inputs的内容取出,然后调用inputcreateReader函数,创建一个Reader

std::list<std::shared_ptr<IFileInput>>& FileManager::inputs()
{
	static std::list<std::shared_ptr<IFileInput>> inputs;
	return inputs;
}

void FileManager::registerInput(std::shared_ptr<IFileInput> input)
{
	inputs().push_back(input);
}

这里的inputs是一个返回STL的list容器引用的函数,第二个函数是向这个容器添加元素的一个函数。我们在这个函数中打断点。
在这里插入图片描述可以发现这个很明显是从AUD_initOnce开始初始化的

AUD_API void AUD_initOnce()
{
	PluginManager::loadPlugins();
	NULLDevice::registerPlugin();
}

void PluginManager::loadPlugins(const std::string& path)
{
	STATIC_PLUGIN_REGISTER(FFMPEG)
	STATIC_PLUGIN_REGISTER(SndFile)
	STATIC_PLUGIN_REGISTER(OpenALDevice)
	STATIC_PLUGIN_REGISTER(SDLDevice)
	STATIC_PLUGIN_REGISTER(WASAPIDevice)
	// more...
	
void FFMPEG::registerPlugin()
{
	std::shared_ptr<FFMPEG> plugin = std::shared_ptr<FFMPEG>(new FFMPEG);
	FileManager::registerInput(plugin);
	FileManager::registerOutput(plugin);
}
}

以上的代码已经显示的很清楚了,FFMPEGReader作为一个插件被加载进来,然后注册到STL的list容器中,再在FileManager调用CreateReader的时候将其逐个遍历并使用对应的Reader函数,当try语句没有出现错误,就可以确定是这个Reader,因此就可以返回。
这样子我们就回答了之前的问题,为什么它可以创造正确的Reader
AUDSpace的工厂模式看起来和一般的工厂方法(或者说是抽象工厂方法?)差不多,其实内部大有乾坤。最主要的原因在于其(似乎是通过回调)复用了其它的工厂类和产品类,而且它的继承更大,也更复杂。
总的来说,我认为在此应用的工厂方法,其主要的目的是为底层提供一个良好的抽象,我们可以发现,如此复杂的底层在AUD_readSound中仅仅需要std::shared_ptr<IReader> reader = ChannelMapper(*sound, specs).createReader();创建一个Reader以后便可以方便的调用而不用去了解底层的代码,可以说是非常的方便的。
紧接着,我们去阅读ChannelMapperReader以及File还有FFMPEGReader内部的源码,去观察它如何真正的将数据读出的。


Reader源码解析

AUD_readSound中,我们用到了reader->getSpecs()以及reader->read(len, eos, buf)两个函数,我们进入ChannelMapperReader查看这两个函数。

struct Specs
{
	/// Sample rate in Hz.
	SampleRate rate;

	/// Channel count.
	Channels channels;
};

Specs ChannelMapperReader::getSpecs() const
{
	Specs specs = m_reader->getSpecs();
	specs.channels = m_target_channels;
	return specs;
}

void ChannelMapperReader::read(int& length, bool& eos, sample_t* buffer)
{
	Channels channels = m_reader->getSpecs().channels;
	if(channels != m_source_channels)
	{
		m_source_channels = channels;
		calculateMapping();
	}

	if(m_source_channels == m_target_channels)
	{
		m_reader->read(length, eos, buffer);
		return;
	}

	m_buffer.assureSize(length * channels * sizeof(sample_t));

	sample_t* in = m_buffer.getBuffer();

	m_reader->read(length, eos, in);

	sample_t sum;

	for(int i = 0; i < length; i++)
	{
		for(int j = 0; j < m_target_channels; j++)
		{
			sum = 0;
			for(int k = 0; k < m_source_channels; k++)
				sum += m_mapping[j * m_source_channels + k] * in[i * m_source_channels + k];
			buffer[i * m_target_channels + j] = sum;
		}
	}
}

可以看到getSpeces是一个调用了ChannelMapperReader内部的Reader,它返回的是一些音频的数据,包括音频的波特率以及音频的声道。
read函数实际上仍然是,调用了内部的m_readerread函数。在m_target_channelsm_source_channels相同的时候直接采用m_reader->read,但是在两者不相等的时候还采用其它的一些变换。我的理解是ChannelMapper是将声道进行映射的一个类。比如原本的音频是7.1声道的,我们将其映射到单声道。如果源声道和目标声道相同的话,我们是不需要进行额外的变化的。但是源声道与目标声道不同的话,它是需要进行变化的。这里面主要涉及复杂的数学变换,我们暂且不去管它。之后可能会对这个做一个性能分析来确认它的性能是否需要改进。
在运行的过程中,我发现m_target_channels == CHANNELS_MONO并且m_source_channels == CHANNELS_SURROUND51,这个也符合我们对于其的猜想
其次是FFMPEGReader,因为ChannelMapperReader需要调用到这个Reader,所以我们也需要看其内部源码。
好吧,太难了,咱不看了。这个实际上暂时只需要了解功能即可,因为它是FFMPEG相关的,我现在还没有学相关的知识,就看不了了。如果有需要,我们再边学边看。(说句实话,我本来想选FFMPEG作为GSoC的项目,但是他们GSoC介绍页太丑了,就没选…)


波形绘制函数解析

在分析完成一堆读取函数以后,我们回到对于绘制函数的分析。上文已经提到绘制函数是draw_seq_waveform_overlay,它从BKE_sound_read_waveform获取到波形数据后会对其处理后再进行绘制。我们需要了解它处理了什么东西。由于这个函数有几百行,并且有非常多的注释,因此我就不将其完整的代码放上来而只放置我认为比较重要的代码。

/**
 * \param x1, x2, y1, y2: The starting and end X value to draw the wave, same for y1 and y2.
 * \param frames_per_pixel: The amount of pixels a whole frame takes up (x-axis direction).
 */
static void draw_seq_waveform_overlay(View2D *v2d,
                                      const bContext *C,
                                      SpaceSeq *sseq,
                                      Scene *scene,
                                      Sequence *seq,
                                      float x1,
                                      float y1,
                                      float x2,
                                      float y2,
                                      float frames_per_pixel)

传入绘制函数的有许多参数,我们需要稍微解释一下。
根据我的导师的解释,

View2D is basically description of editor 2D space - so it’s like matrix so our drawing code can use editors coordinate system (frames and channels in case of VSE)

View2D是对于编辑器2D空间的一个描述,通过这个结构就可以让绘制函数可以使用与编辑器的坐标系统。

bContext is huuuuuge rabbithole - imagine anything you can ask about UI is stored there, and bunch more like what is current active scene, what is current active editor, how many windows there are…

bContext是一个无底洞(离谱),想象一下关于UI的一切事情都被存储在这里。当前的活动scene,活动编辑器以及多少个窗口等都在里面。
Blender文档中也有关于context的相关描述。

Window manager context is easiest, these are just pointers to the screen, area, space data, region and region data.
Data context is more complicated, important to understand is that it is based on RNA pointers and collections.

具体关于Context的信息可以去查看Blender中关于Context的文档

SpaceSeq结构体根据注释是表示Sequencer(序列编辑器)的一个结构体。

There are 2 main data structures containing sequencer data: - Editing holds data that belongs to sequencer core, this is mainly strips. - SpaceSeq holds data that belongs to sequencer editor. This defines editor state and configuration like render size, overlays(概念见此) and various view settings.

SpaceSeq这个结构体是保存着序列编辑器的一些设置的结构体(渲染大小,各种视图设置等等)。

Scence的用法,根据我的导师的说法是Scene是和.blend文件有关的。它包含了.blender中被渲染的内容,像是对象,图片啥的。

I: I meet some problems about Scene structure. Can you tell me the usage of it? I haven’t seen many documents mention this.
Now I just know it may be relative to screen.
Mentor: Everything is relative to everything 🙂 But Scene is basically content of .blend file that can be rendered. So it may describe used objects, nodes, images, … but also sequencer content.

x1,y1之类的文件表示的是起始坐标和终止坐标,确定的是波形绘制的位置。frames_per_pixel根据名字和注释描述的是每一个像素所占有的帧的数量(也可以说是一帧占用多少个像素)。
了解完成传入的参数,我们开始看内部代码。

/* Make sure that the start drawing position is aligned to the pixels on the screen to avoid
     * flickering when moving around the strip.
     * To do this we figure out the fractional offset in pixel space by checking where the
     * window starts.
     * We then append this pixel offset to our strip start coordinate to ensure we are aligned to
     * the screen pixel grid. */
    float pixel_frac = v2d->cur.xmin / frames_per_pixel - floor(v2d->cur.xmin / frames_per_pixel);
    float x1_adj = clamp_frame_coord_to_pixel(x1, pixel_frac, frames_per_pixel);

    /* Offset x1 and x2 values, to match view min/max, if strip is out of bounds. */
    float x1_offset = max_ff(v2d->cur.xmin, x1_adj);
    float x2_offset = min_ff(v2d->cur.xmax, x2);

    /* Calculate how long the strip that is in view is in pixels. */
    int pix_strip_len = round((x2_offset - x1_offset) / frames_per_pixel);

    if (pix_strip_len < 2) {
      return;
    }

前面的代码做的是一些对齐以及计算序列的长度的一些工作,想要了解详细一些可以查看注释。
接下来是比较重要的波形读取函数,

bSound *sound = seq->sound;

    BLI_spin_lock(sound->spinlock);
    if (!sound->waveform) {
      /* Load the waveform data if it hasn't been loaded and cached already. */
      if (!(sound->tags & SOUND_TAGS_WAVEFORM_LOADING)) {
        /* Prevent sounds from reloading. */
        sound->tags |= SOUND_TAGS_WAVEFORM_LOADING;
        BLI_spin_unlock(sound->spinlock);
        sequencer_preview_add_sound(C, seq);
      }
      else {
        BLI_spin_unlock(sound->spinlock);
      }
      return; /* Nothing to draw. */
    }
    BLI_spin_unlock(sound->spinlock);

   SoundWaveform *waveform = sound->waveform;

sound->waveformNULL的时候,意味着波形没有被加载,因此需要执行加载函数。
spin_lock应该是一个自旋锁,防止waveform被其它的线程写入,不过为什么unlock可以被调用两次我不是特别理解,我对于自旋锁的了解不是很深。
函数draw_seq_waveform_overlay应该是每一次绘制都需要进行调用,此时上面的对于(sound->tags & SOUND_TAGS_WAVEFORM_LOADING)就对于异步加载非常的重要。当检测到sound的状态是正在加载的时候,它就会返回,这样子就可以避免重复加载。
紧接着是加载的重头戏,也是整个加载源码比较难以看懂的地方。

    FCurve *fcu = id_data_find_fcurve(&scene->id, seq, &RNA_Sequence, "volume", 0, NULL);

    WaveVizData *tri_strip_arr = MEM_callocN(sizeof(*tri_strip_arr) * pix_strip_len * 2,
                                             "tri_strip");
    WaveVizData *line_strip_arr = MEM_callocN(sizeof(*line_strip_arr) * pix_strip_len,
                                              "line_strip");

    WaveVizData *tri_strip_iter = tri_strip_arr;
    WaveVizData *line_strip_iter = line_strip_arr;

    /* The y coordinate for the middle of the strip. */
    float y_mid = (y1 + y2) / 2.0f;
    /* The length from the middle of the strip to the top/bottom. */
    float y_scale = (y2 - y1) / 2.0f;
    float volume = seq->volume;

  /* Value to keep track if the previous item to be drawn was a line strip. */
    int8_t was_line_strip = -1; /* -1 == no previous value. */

    float samples_per_frame = SOUND_WAVE_SAMPLES_PER_SECOND / FPS;

    /* How many samples do we have for each pixel? */
    float samples_per_pix = samples_per_frame * frames_per_pixel;

    float strip_start_offset = seq->startofs + seq->anim_startofs;
    float start_sample = 0;
    if (strip_start_offset != 0) {
     	/* If start offset is not zero, we need to make sure that we pick the same start sample as if
       * we simply scrolled the start of the strip off-screen. Otherwise we will get flickering
       * when changing start offset as the pixel alignment will not be the same for the drawn
       * samples. */
	    strip_start_offset = clamp_frame_coord_to_pixel(
	        x1 - strip_start_offset, pixel_frac, frames_per_pixel);
	    start_sample = fabsf(strip_start_offset - x1_adj) * samples_per_frame;
    }

从后文来看,tri_strip_arr以及line_strip_arr 可能是一个迭代器,y_mid是确认波形的中间位置,y_scale是根据上下限确定缩放大小的,volume是应该是blender的音频设置面板中设置音量的大小。was_line_strip暂时不清楚作用,我们先不去管它。SOUND_WAVE_SAMPLES_PER_SECOND 的值是250,意味着我们每秒采样250个数据点来绘制波形,那么,换算到帧率也就是SOUND_WAVE_SAMPLES_PER_SECOND / FPS,那么,换算到每像素采样多少个就是samples_per_frame * frames_per_pixel
strip_start_offset应该是表示序列(strip)的偏移,原理尚不清楚。if下面的注释没有看懂,但是大概知道它是一个位移然后防止闪烁的作用。

    start_sample += seq->sound->offset_time * SOUND_WAVE_SAMPLES_PER_SECOND;
    /* If we scrolled the start off-screen, then the start sample should be at the first visible
     * sample. */
    start_sample += (x1_offset - x1_adj) * samples_per_frame;

盲猜start_sample是一个记录起始采样个数的一个结构,比如从第500个开始采样就是说从第二秒开始采样。

for (int i = 0; i < pix_strip_len; i++) {
      float sample_offset = start_sample + i * samples_per_pix;
      int p = sample_offset;

      if (p < 0) {
        continue;
      }

      if (p >= waveform->length) {
        break;
      }

      float value_min = waveform->data[p * 3];
      float value_max = waveform->data[p * 3 + 1];
      float rms = waveform->data[p * 3 + 2];
			
      if (p + 1 < waveform->length) {
        /* Use simple linear interpolation. */
        float f = sample_offset - p;
        value_min = (1.0f - f) * value_min + f * waveform->data[p * 3 + 3];
        value_max = (1.0f - f) * value_max + f * waveform->data[p * 3 + 4];
        rms = (1.0f - f) * rms + f * waveform->data[p * 3 + 5];
        if (samples_per_pix > 1.0f) {
          /* We need to sum up the values we skip over until the next step. */
          float next_pos = sample_offset + samples_per_pix;
          int end_idx = next_pos;

          for (int j = p + 1; (j < waveform->length) && (j < end_idx); j++) {
            value_min = min_ff(value_min, waveform->data[j * 3]);
            value_max = max_ff(value_max, waveform->data[j * 3 + 1]);
            rms = max_ff(rms, waveform->data[j * 3 + 2]);
          }
        }
      }

      if (fcu && !BKE_fcurve_is_empty(fcu)) {
        float evaltime = x1_offset + (i * frames_per_pixel);
        volume = evaluate_fcurve(fcu, evaltime);
        CLAMP_MIN(volume, 0.0f);
      }

      value_min *= volume;
      value_max *= volume;
      rms *= volume;

      bool clipping = false;

      if (value_max > 1 || value_min < -1) {
        clipping = true;

        CLAMP_MAX(value_max, 1.0f);
        CLAMP_MIN(value_min, -1.0f);
      }

      bool is_line_strip = (value_max - value_min < 0.05f);

      if (!ELEM(was_line_strip, -1, is_line_strip)) {
        /* If the previously added strip type isn't the same as the current one,
         * add transition areas so they transition smoothly between each other. */
        if (is_line_strip) {
          /* This will be a line strip, end the tri strip. */
          tri_strip_iter->pos[0] = x1_offset + i * frames_per_pixel;
          tri_strip_iter->pos[1] = y_mid + value_min * y_scale;
          tri_strip_iter->clip = clipping;
          tri_strip_iter->rms_pos = tri_strip_iter->pos[1];
          tri_strip_iter->end = true;

          /* End of section. */
          tri_strip_iter++;

          /* Check if we are at the end.
           * If so, skip one point line. */
          if (i + 1 == pix_strip_len) {
            continue;
          }
        }
        else {
          /* This will be a tri strip. */
          line_strip_iter--;
          tri_strip_iter->pos[0] = line_strip_iter->pos[0];
          tri_strip_iter->pos[1] = line_strip_iter->pos[1];
          tri_strip_iter->clip = line_strip_iter->clip;
          tri_strip_iter->rms_pos = line_strip_iter->pos[1];
          tri_strip_iter++;

          /* Check if line had only one point. */
          line_strip_iter--;
          if (line_strip_iter < line_strip_arr || line_strip_iter->end) {
            /* Only one point, skip it. */
            line_strip_iter++;
          }
          else {
            /* End of section. */
            line_strip_iter++;
            line_strip_iter->end = true;
            line_strip_iter++;
          }
        }
      }

紧接着是一个非常长的一个for循环,这应该是一块难啃的硬骨头。
从开头的三行可以看得出来,这个函数非常明显是描述每一个像素的绘制行为的。start_sample也可以看的出来是一个初始的偏移量,而且从p的小于0这个偏移量可能会出现负值,这个表示的应该是不应该绘制的部分。p >= waveform->length部分可以认为是已经完成对于波形的读取了。value_minvalue_maxrms这三个变量在第一周的文章应该解释的比较清楚了,我们就省略它。

if (p + 1 < waveform->length)中的内容还蛮复杂的,但是大致可以看得出来是计算min, max, rms的中间值的。最后,将这三个值乘音量,再对max和min其进行一个限幅就可以得到最终的三个值。
if (!ELEM(was_line_strip, -1, is_line_strip))以下的区域有相关注释进行用处说明,

If the previously added strip type isn’t the same as the current one, add transition areas so they transition smoothly between each other.
它的直译就是如果先前添加的序列与当前的不同,添加一个过渡区域用于保证其可以平滑过渡

看不懂…
因此我将其运行起来看看,使用VS非常nb的热重载来测试它的功能

    draw_waveform(line_strip_iter, line_strip_end, GPU_PRIM_LINE_STRIP, false);
    draw_waveform(tri_strip_iter, tri_strip_end, GPU_PRIM_TRI_STRIP, false);
    draw_waveform(tri_strip_iter, tri_strip_end, GPU_PRIM_TRI_STRIP, true);

draw_seq_waveform_overlay中,最终绘制波形的有以上这三个函数。因为我搞不懂line和tri两个变量的作用,我觉得注释掉剩余两个,每一次只让一个函数绘制波形。
在只保留第一个函数的情况下,我们发现它没有任何的波形显示。

在这里插入图片描述
在保留第二个的情况下,存在波形显示
在这里插入图片描述
在保留第三个的情况下,也存在波形显示。但是相对于第二个有所不同。它与第二个波形不同,而且更加亮。
在这里插入图片描述
目前看来,第一个函数似乎没有什么作用?我们结合刚才看不懂的注释,向这个方向尝试以下试试。
wait!有重要发现,我发现第一个函数并不是什么都没有绘制的,实际上它还是有绘制了一些东西的,知识太小了我们没有发现而已。
在这里插入图片描述可以看见,那些非常细小的白线就是它绘制出来的。我们已经把序列放的很大了才得以看清,如果小一点,它大概就是一个点(fan是CSDN默认的水印)
在这里插入图片描述
然后我找到了之前没有特别留意的一行代码,bool is_line_strip = (value_max - value_min < 0.05f);这个表示当音频的响度足够小的时候,这个is_line_strip就会置为1。
这样子我就对于整个绘制有一个大概的思路了。

	was_line_strip = is_line_strip;

    if (is_line_strip) {
       line_strip_iter->pos[0] = x1_offset + i * frames_per_pixel;
       line_strip_iter->pos[1] = y_mid + value_min * y_scale;
       line_strip_iter->clip = clipping;
       line_strip_iter++;
     }
     else {
       tri_strip_iter->pos[0] = x1_offset + i * frames_per_pixel;
       tri_strip_iter->pos[1] = y_mid + value_min * y_scale;
       tri_strip_iter->clip = clipping;
       tri_strip_iter->rms_pos = y_mid + max_ff(-rms, value_min) * y_scale;
       tri_strip_iter++;

       tri_strip_iter->pos[0] = x1_offset + i * frames_per_pixel;
       tri_strip_iter->pos[1] = y_mid + value_max * y_scale;
       tri_strip_iter->clip = clipping;
       tri_strip_iter->rms_pos = y_mid + min_ff(rms, value_max) * y_scale;
       tri_strip_iter++;
     }

这个是上文for循环以外的一个判断。结合上文的for循环代码,我们可以大致描绘出整个绘制的思路。
当音频幅值太小的时候,我们让它直接绘制一条直线,当音频范围比较大的时候,我们让它等于音频的最小值,并且这是一条直线。当音频幅值较大的时候,我们就绘制一个范围,这个范围即是最大值和最小值。并且我们还绘制rms(也就是音频的能量)。这样子最终我们会得到一个由三个音频波形叠加而成的最终波形。
由于波形使用线来绘制与使用范围来绘制可能会造成过渡不平滑,因此代码中采取一些过渡效果让其看起来更加的平滑。
以上就是对于绘制函数的分析,紧接着我们看绘制线程的启动相关函数。因为我的任务就是使用多线程来进行性能提升,所以这个函数跟我们的关系很大。


绘制线程函数详解

sequencer_preview_add_sound(C, seq)这个函数就是启动波形读取的函数。

void sequencer_preview_add_sound(const bContext *C, Sequence *seq)
{
  wmJob *wm_job;
  PreviewJob *pj;
  ScrArea *area = CTX_wm_area(C);
  PreviewJobAudio *audiojob = MEM_callocN(sizeof(PreviewJobAudio), "preview_audio");
  wm_job = WM_jobs_get(CTX_wm_manager(C),
                       CTX_wm_window(C),
                       CTX_data_scene(C),
                       "Strip Previews",
                       WM_JOB_PROGRESS,
                       WM_JOB_TYPE_SEQ_BUILD_PREVIEW);

  /* Get the preview job if it exists. */
  pj = WM_jobs_customdata_get(wm_job);

  if (!pj) {
    pj = MEM_callocN(sizeof(PreviewJob), "preview rebuild job");

    pj->mutex = BLI_mutex_alloc();
    pj->scene = CTX_data_scene(C);

    WM_jobs_customdata_set(wm_job, pj, free_preview_job);
    WM_jobs_timer(wm_job, 0.1, NC_SCENE | ND_SEQUENCER, NC_SCENE | ND_SEQUENCER);
    WM_jobs_callbacks(wm_job, preview_startjob, NULL, NULL, preview_endjob);
  }

  audiojob->bmain = CTX_data_main(C);
  audiojob->sound = seq->sound;

  BLI_mutex_lock(pj->mutex);
  BLI_addtail(&pj->previews, audiojob);
  pj->total++;
  BLI_mutex_unlock(pj->mutex);

  if (!WM_jobs_is_running(wm_job)) {
    G.is_break = false;
    WM_jobs_start(CTX_wm_manager(C), wm_job);
  }

  ED_area_tag_redraw(area);
}

它是不会阻塞的,可以说是一个线程启动函数。它是用WM_jobs_customdata_get去获取一个线程,如果这个线程不存在的话,他就会注册它。在绘制第一个波形的时候,它是不存在的。在绘制接下来的波形的时候,这个线程是已经存在的,因此就不会重复创建这个线程。仅仅是将数据添加到链表中并,通过pj->total++来告知读取线程添加了新的需要读取的音频。但是,这样子有一个非常大的问题,这也是处理的瓶颈。它就是每一次只可以处理一个序列的音频,其它音频波形的读取也只能在一个读取完成以后才可以继续。我们需要对其进行并行化处理。

/* Only this runs inside thread. */
static void preview_startjob(void *data, short *stop, short *do_update, float *progress)
{
  PreviewJob *pj = data;
  PreviewJobAudio *previewjb;

  BLI_mutex_lock(pj->mutex);
  previewjb = pj->previews.first;
  BLI_mutex_unlock(pj->mutex);

  while (previewjb) {
    PreviewJobAudio *preview_next;
    bSound *sound = previewjb->sound;

    BKE_sound_read_waveform(previewjb->bmain, sound, stop);

    if (*stop || G.is_break) {
      BLI_mutex_lock(pj->mutex);
      previewjb = previewjb->next;
      BLI_mutex_unlock(pj->mutex);
      while (previewjb) {
        sound = previewjb->sound;

        /* Make sure we cleanup the loading flag! */
        BLI_spin_lock(sound->spinlock);
        sound->tags &= ~SOUND_TAGS_WAVEFORM_LOADING;
        BLI_spin_unlock(sound->spinlock);

        BLI_mutex_lock(pj->mutex);
        previewjb = previewjb->next;
        BLI_mutex_unlock(pj->mutex);
      }

      BLI_mutex_lock(pj->mutex);
      BLI_freelistN(&pj->previews);
      pj->total = 0;
      pj->processed = 0;
      BLI_mutex_unlock(pj->mutex);
      break;
    }

    BLI_mutex_lock(pj->mutex);
    preview_next = previewjb->next;
    BLI_freelinkN(&pj->previews, previewjb);
    previewjb = preview_next;
    pj->processed++;
    *progress = (pj->total > 0) ? (float)pj->processed / (float)pj->total : 1.0f;
    *do_update = true;
    BLI_mutex_unlock(pj->mutex);
  }
}

if (*stop || G.is_break)中的内容主要针对线程退出或者程序退出(这个存疑)的一些处理,主要用来防止内存泄露。
这些内容已经分析清楚了,接下来我们开始编程第一个特性。


Coding

现在是编程的时间了!我先说一下这个改进的思路。我们需要使用线程池(线程池还需要更多了解一下),在调用add_sound函数的时候启动这个线程池,然后在每一次需要读取新的波形的时候通过这个线程池启动新的线程来完成功能。这样子有多少个声音,就会有多少个读取线程,做到了并行化。

具体代码就先不放了,因为现在很粗糙,目前的测试结果是读取速度快了22%

blender演示

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值