2024年7月


《FFmpeg开发实战:从零基础到短视频上线》一书在第10章介绍了轻量级流媒体服务器MediaMTX,通过该工具可以测试RTSP/RTMP等流媒体协议的推拉流。不过MediaMTX的功能实在是太简单了,无法应用于真实直播的生产环境,真正能用于生产环境的流媒体服务器还要看SRS或者ZLMediaKit。

ZLMediaKit是一款国产的开源流媒体服务器,支持RTSP、RTMP、SRT等主流直播协议,它的安装说明参见之前的文章《Linux环境安装ZLMediaKit实现视频推流》。结合ZLMediaKit与ffmpeg实现RTSP/RTMP协议的推流功能,已在《Linux环境安装ZLMediaKit实现视频推流》一文中详细介绍,这里单独讲解如何通过ZLMediaKit与ffmpeg实现SRT协议的推流功能。
ZLMediaKit在编译和启动的时候已经默认支持SRT,查看ZLMediaKit的配置文件config.ini,找到srt部分的配置信息如下,可见ZLMediaKit默认把9000端口分配给SRT协议。

[srt]
latencyMul=4
pktBufSize=8192
port=9000
timeoutSec=5

除此以外,ZLMediaKit无需另外调整什么配置,只要在启动之后运行下面的ffmpeg命令即可将视频文件向SRT地址推流。注意,务必确保Linux服务器上的FFmpeg已经集成了libsrt库,否则ffmpeg无法向srt地址推流,详细的集成步骤参见之前的文章《Linux环境给FFmpeg集成libsrt和librist》。

ffmpeg -re -stream_loop -1 -i "/usr/local/src/test/cctv5.ts" -c copy -f mpegts 'srt://127.0.0.1:9000?streamid=#!::r=live/test,m=publish'

注意,上面命令中的srt地址后半段为“r=live/test,m=publish”,其中“r=live/test”表示SRT的服务名称叫做“live/test”,而“m=publish”表示该地址属于发布功能也就是给推流方使用。
ZLMediaKit对视频源文件的封装格式也有要求,不仅要求源文件为ts格式,还要求推流格式也为ts格式,所以ffmpeg命令中添加了“-f mpegts”表示转换成mpeg的ts流格式。如果源文件不是ts格式,或者没转成mpegts格式,后续通过ffplay播放srt链接都会报下面的错误。

non-existing PPS 0 referenced

此外,ZLMediaKit支持的音视频编码标准罗列在src/Extension/Frame.h中,详细的音视频支持标准如下所示。

#define CODEC_MAP(XX) \
    XX(CodecH264,  TrackVideo, 0, "H264", PSI_STREAM_H264, MOV_OBJECT_H264)          \
    XX(CodecH265,  TrackVideo, 1, "H265", PSI_STREAM_H265, MOV_OBJECT_HEVC)          \
    XX(CodecAAC,   TrackAudio, 2, "mpeg4-generic", PSI_STREAM_AAC, MOV_OBJECT_AAC)   \
    XX(CodecG711A, TrackAudio, 3, "PCMA", PSI_STREAM_AUDIO_G711A, MOV_OBJECT_G711a)  \
    XX(CodecG711U, TrackAudio, 4, "PCMU", PSI_STREAM_AUDIO_G711U, MOV_OBJECT_G711u)  \
    XX(CodecOpus,  TrackAudio, 5, "opus", PSI_STREAM_AUDIO_OPUS, MOV_OBJECT_OPUS)    \
    XX(CodecL16,   TrackAudio, 6, "L16", PSI_STREAM_RESERVED, MOV_OBJECT_NONE)       \
    XX(CodecVP8,   TrackVideo, 7, "VP8", PSI_STREAM_VP8, MOV_OBJECT_VP8)             \
    XX(CodecVP9,   TrackVideo, 8, "VP9", PSI_STREAM_VP9, MOV_OBJECT_VP9)             \
    XX(CodecAV1,   TrackVideo, 9, "AV1", PSI_STREAM_AV1, MOV_OBJECT_AV1)             \
    XX(CodecJPEG,  TrackVideo, 10, "JPEG", PSI_STREAM_JPEG_2000, MOV_OBJECT_JPEG)

由此可见,如果待推流的视频文件不属于上面的音视频编码标准,将无法通过SRT服务地址正常推流。
运行ffmpeg的SRT推流命令之后,ZLMediaKit输出以下的日志信息,可见其SRT推流功能正常运行。

[MediaServer] [576478-event poller 0] SrtSession.cpp:103 onRecv | 1-11(127.0.0.1:33630) 
[MediaServer] [576478-event poller 0] SrtTransportImp.cpp:166 operator() | test(127.0.0.1:33630) 允许 srt 推流
[MediaServer] [576478-event poller 0] Decoder.cpp:143 onTrack | Got track: H264
[MediaServer] [576478-event poller 0] Decoder.cpp:143 onTrack | Got track: mpeg4-generic
[MediaServer] [576478-event poller 0] Decoder.cpp:97 onStream | Add track finished
[MediaServer] [576478-event poller 0] MediaSink.cpp:161 emitAllTrackReady | All track ready use 172ms
[MediaServer] [576478-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:fmp4://__defaultVhost__/live/test
[MediaServer] [576478-event poller 0] MultiMediaSourceMuxer.cpp:551 onAllTrackReady | stream: schema://__defaultVhost__/app/stream , codec info: mpeg4-generic[48000/2/16] H264[1280/720/25] 
[MediaServer] [576478-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:rtmp://__defaultVhost__/live/test
[MediaServer] [576478-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:rtsp://__defaultVhost__/live/test
[MediaServer] [576478-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:ts://__defaultVhost__/live/test
[MediaServer] [576478-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:hls://__defaultVhost__/live/test

接着按照《FFmpeg开发实战:从零基础到短视频上线》一书“1.3  Windows系统安装FFmpeg”的介绍,在个人电脑上安装FFmpeg并打开MSYS的命令行,运行下面的ffplay命令,期望从SRT地址拉流播放。注意,务必确保电脑上的FFmpeg已经集成了libsrt库,否则ffplay无法播放srt链接,详细的集成步骤参见之前的文章《Windows环境给FFmpeg集成libsrt》。

ffplay -i 'srt://124.xxx.xxx.xxx:9000?streamid=#!::r=live/test,m=request'

上面的SRT拉流地址与之前的推流地址大同小异,除了把内网IP换成外网IP之外,就是把链接末尾的“m=publish”改成了“m=request”,其中request表示请求也就是用于拉流方。
ffplay运行后弹出播放器窗口,正常播放视频画面和声音。同时观察ZLMediaKit的服务日志如下所示:

[MediaServer] [576478-event poller 0] SrtSession.cpp:103 onRecv | 2-16(112.5.138.145:57022) 
[MediaServer] [576478-event poller 0] SrtTransport.cpp:731 onShutdown | peer close connection
[MediaServer] [576478-event poller 0] SrtSession.cpp:118 onError | 2-16(112.5.138.145:57022) 6(peer close connection)
[MediaServer] [576478-event poller 0] SrtTransportImp.cpp:14 ~SrtTransportImp | test(112.5.138.145:57022) srt 播放器(__defaultVhost__/live/test)断开,耗时(s):16

从以上日志可见,ZLMediaKit通过SRT协议成功实现了视频直播的SRT推拉流功能。

更多详细的FFmpeg开发知识参见
《FFmpeg开发实战:从零基础到短视频上线》
一书。

傅里叶级数和信号频谱

对于一个确定的时域信号,我们只需要知道它的函数表达式就可以在任意时刻确定一个信号,但是各种场景下中我们需要的往往并不是这样的解析式,因为这些复杂的式子首先难以快速准确地获得,另外难以进行快速进行分析,其中所蕴含的信息也难以提取。因此需要一种更高效的工具来进行信号的分析。

傅里叶级数的三角形式


傅里叶曾提出可以采用三角函数的线性组合表示一个时域上连续周期信号的想法。后面经过数学家对相关问题的研究得到如下结论:
当周期信号满足 $Dirichlet$ 条件,可以**唯一的**用三角函数线性组合来表示,如周期为 $T$ 频率为 $\displaystyle\omega = {2\pi\over T}$ 可以展开成如下式子:
$$
f(t) = a_0 + \sum_{n = 1}^{\infty}\big[a_n \cos{(n\omega t)} + b_n \sin{(n\omega t)}\big]
$$
其中
$$
\begin{aligned}
a_0 & = {1\over T} \int_{t_0}^{t_0+T} f(t) \text{d}t\\
a_n & = {2\over T} \int_{t_0}^{t_0+T} f(t) \cos{(n\omega t)}\text{d}t\\
b_n & = {2\over T} \int_{t_0}^{t_0+T} f(t) \sin{(n\omega t)}\text{d}t\\
\end{aligned}
$$
$Dirichlet$ 条件为:
1. 一个周期内间断点有限
2. 一个周期内信号绝对可积
3. 极大值和极小值的数目是有限的

对于常见的信号,
\(Dirichlet\)
条件都是满足的,可以不加验证的使用


如果使用三角公式将同频正余弦合并可以得到下面两种形式:
$$
\begin{aligned}
f(t) & = c_0 + \sum_{n = 0}^{\infty}c_n\cos{(n\omega t + \varphi_n)} \\
f(t) & = d_0 + \sum_{n = 0}^{\infty}d_n\sin{(n\omega t + \theta_n)} \\
\end{aligned}
$$

傅里叶级数不同表示形式下,量值之间的关系:

\[\large
\begin{cases}
& a_0 = b_0 = c_0\\
\\
& c_n = d_n = \sqrt{a_n^2 + b_n^2}\\
\\
& a_n = c_n\cos{\varphi_n} = d_n\sin{\theta_n}\\
\\
& b_n = -c_n\sin(\varphi_n) = d_n\cos{\theta_n}\\
\\
& \tan{\theta_n} = \displaystyle{a_n \over b_n}\\
\\
& \tan{\varphi_n} = \displaystyle-{b_n \over a_n}\\
\end{cases}
\]

利用上述的数学工具,我们很容易能够将一个连续的周期信号转变为三角函数,并且这个表达式中含有三个未知量————幅值、相位和频率,并且三者之间存在密切的关系。三个未知量,至少需要两个关系来描述,容易发现最方便构造数学关系的是幅值和频率以及相位与频率的关系,因为频率一般是个单调的函数,而另外两者则未必。于是分别得到幅度和频率的关系和相位和频率的关系,并分别将他们绘制出来就可以得到一组曲线————幅频曲线和相频曲线,分别称为幅度谱和相位谱。绘制过程中我们发现实际上信号包含的频率只在某些特定频率处取值,这就意味着我们绘制出来的图像是个离散图像,幅度谱和相位谱在这个频率上都为一个有限值,绘制出来只有一根根线,因此称为谱线,将每一根谱线顶端连起来,我们就可以看到谱线的大致走势。

傅里叶级数的复指数形式

上述表达式是一个三角函数表达式,当我们需要求解某一个具体周期信号的时候需要分别求出
\(a_0\)

\(a_n\)

\(b_n\)
,这个过程是繁琐的。
能否找到一个统一的表达式,使得能够同时求出这三者?
答案是利用欧拉公式

\[e^{j\theta} = \cos(\theta) + j \sin(\theta)
\]

欧拉公式可以在复数域上把三角函数表示为指数函数。
因此我们可以将上面的表达式转换成复指数的形式,具体过程如下:

\[\begin{aligned}
f(t) = &a_{0}+ \sum_{n = 1}^{\infty} \left( a_{n} \frac{e^{jn\omega t} + e^{-jn\omega t}}{2} + b_{n} \frac{e^{jn\omega t} - e^{-jn\omega t}}{2j}\right)\\
=&a_0 + \sum_{n = 1}^{\infty} ({a_n - jb_n \over 2}e^{jn\omega t} + {a_n + j b_n \over 2}e^{-jn\omega t})\\
\end{aligned}
\]

接下来,我们回过去看
\(a_n\)

\(b_n\)
的原始定义(积分表达式),如果
\(n\)
是一个整数,而不像前文那样定义为自然数, 则容易得到
\(a_n\)
是偶函数,
\(b_n\)
是奇函数。
于是我们不妨定义函数

\[F(n\omega) = {a_n - jb_n \over 2}
\]

结合上面的奇偶性分析有

\[\begin{aligned}
F(-n\omega) & = {a_{-n} - jb_{-n}\over 2} \\
& = {a_{n} + jb_{n}\over 2}
\end{aligned}
\]

我们发现
\(F(n\omega)\)

\(F(-n\omega)\)
是共轭的,恰好为复数表示的傅里叶级数的同一频次的两项。如果我们定义
\(F(0) = a_0\)
,我们便可以将傅里叶级数的复数形式写成下面这样:

\[f(t) = \sum_{n = -\infty}^{\infty} (F(n\omega)e^{jn\omega t})
\]

其中:

\[F_n(n\omega) = {1\over T} \int_{t_0}^{t_0 + T} f(t) e^{-jn\omega t}\text{d}t \\
\]

这样我们求解傅里叶级数将更加方便,不像三角形式那样需要求解好几个式子,但是
代价就是需要进行复变函数的积分,可能较为繁琐

傅里叶指数形式与傅里叶级数中相关参数的关系:

\[\large
\begin{cases}
& F_0 = c_0 = d_0 = a_0\\
\\
& F_n = |F_n|e^{j\varphi_n} = \displaystyle{a_n - jb_n \over 2} \\
\\
& F_{-n} = |F_{-n}|e^{-j\varphi_n} = \displaystyle{a_n + jb_n \over 2} \\
\\
& |F_n| = |F_{-n}| = \displaystyle{1\over 2}c_n = \displaystyle{1\over2}d_n = {1\over2}\displaystyle\sqrt{a_n^2 + b_n^2}\\
\\
& |F_n| + |F_{-n}| = c_n \\
\\
& a_n = F_n + F_{-n} \\
\\
& b_n = j( F_n + F_{-n} ) \\
\\
& c_n^2 = d_n^2 = a_n^2 + b_n^2 = 4F_n F_{-n}
\end{cases}
\]

由于
\(F(n\omega)\)

\(F(-n\omega)\)
是共轭的,所以复指数形式的谱图中幅度谱是左右对称的偶函数,相位谱是奇函数,并且一般位于二四象限。


值得注意的是在幅度谱中复指数的谱图中与傅里叶级数谱图对应位置相比,前者高度为后者的
一半
,也就是正频率项和复频率项相加即为实数形式的谱图。

注意复频率的出现主要是数学上的结果,并不具备实际意义。

傅里叶级数与函数对称性的关系

周期函数的对称性主要分为两类:

  1. 对整周期对称,如奇函数和偶函数。
  2. 对半周期对称,奇谐函数。

偶函数

定义:

\[f(t) = f(-t)\\
\]

图像上:关于坐标轴对称

对于偶函数存在下列结论:

\[\large
\begin{cases}
a_{n}= \frac{4}{T}\displaystyle\int_{0}^{\frac{T}{2}}f(t)\cos{ (n\omega t )}\text{d}t \\ \\
b_{n} = 0 \\ \\
c_{n} = d_{n} = a_{n} = 2 F_{n} \\ \\
F_{n} = F_{-n} = \displaystyle\frac{a_{n}}{2} \\ \\
\varphi_{n} = 0 \\ \\
\theta_{n} = \displaystyle\frac{\pi}{2} \\ \\
\end{cases}
\]

结论:偶函数的傅里叶级数展开中仅包含余弦项,不包含正弦项。并且复指数形式为实函数。

奇函数

定义:

\[f(t) = -f(-t)
\]

图像上:关于原点对称

奇函数相关的结论 :

\[\large
\begin{cases}
a_{0} = 0,a_{n} = 0 \\ \\
b_{n} = \frac{4}{T} \displaystyle \int_{0}^{\frac {T}{2}} f(t)\sin(n\omega t) \text{d}t\\ \\
c_{n} = d_{n} = b_{n} = 2 j F_{n}\\ \\
F_{n} = -F_{-n} = - \frac{1}{2} j b_{n}\\ \\
\varphi_{n} = -\frac{\pi}{2}\\ \\
\theta_{n} = 0\\ \\
\end{cases}
\]

结论:奇函数的
\(F_{n}\)
为虚函数。奇函数的傅里叶级数中不存在余弦项,只存在正弦项。若是奇函数再加上一个直流分量,则除了
\(a_{0}\)
不为
\(0\)
,其他结论不存在任何变化。

(三)奇谐函数

定义:

\[f(t) = -f\left( t \pm \frac{T}{2} \right)
\]

图像上:平移半周期再沿着x轴翻转后与原函数重合

奇谐函数相关结论:

\[\begin{cases}
a_{0} = 0 \\ \\
a_{n} = b_{n} = 0 \qquad (n \text{为偶数}) \\ \\
a_{n} = \displaystyle \frac{4}{T} \displaystyle \int_{0}^{\frac{T}{2}} f(t) \sin(n \omega t ) \text{d} t \qquad (n \text{为奇数})\\ \\
a_{n} = \displaystyle \frac{4}{T} \displaystyle \int_{0}^{\frac{T}{2}} f(t) \cos(n \omega t ) \text{d} t \qquad (n \text{为奇数})\\ \\
\end{cases}
\]

结论:
奇谐函数不存在偶数频次的谐波,仅存在奇次谐波的正弦和余弦项。

或许你在思考为什么上面的分类中半周期对称性仅仅说了奇谐函数,为什么没有说偶谐函数呢?
这个问题问得好,因为仿照奇谐函数的定义写出偶谐函数,我们可以发现偶谐函数就是周期是原函数一半的周期函数,原函数确定的情况下傅里叶级数是唯一的,因此我们把它放在讨论中的第一类,也就是整周期对称性。

帕塞瓦尔定理

帕塞瓦尔定理是个广泛存在于信号分析各种变换之间的定理,一般结论是,能量信号(能量是个有限值)在时域上的能量和频域上的能量是相等的,功率信号(功率是个有限值)在时域上的功率和频域上的功率是相等的。

对于这里的连续周期信号,往往能量不是有限的,功率是个有限值,也就是是功率信号,它的功率等于傅里叶级数展开后各分量有效值的平方和。

典型周期信号的傅里叶变换

(一)周期矩形脉冲信号

设周期矩形脉冲信号
\(f(t)\)
的脉冲宽度为
\(t\)
周期为
\(T\)
,脉冲幅度为
\(E\)
,则他的傅里叶级数展开形式如下:

\[\large
\begin{cases}
a_{0} = \frac{{E\tau}}{T}\\ \\
a_{n} = \frac{{2 E \tau}}{T} \text{Sa}\left( \frac{{n \pi \tau}}{T} \right) = \frac{{E \tau \omega}}{\pi} \text{Sa}\left( \frac{{n\omega\tau}}{2} \right)\\ \\
b_{n} = 0\\ \\
F_{n} = \frac{{E \tau}}{T} \text{Sa} \left( \frac{{n\omega \tau}}{2} \right)\\ \\
c_{n} = a_{n}\\ \\
c_{0} = a_{0} \\
\end{cases}
\]

结论:

  1. 周期矩形脉冲的频谱是离散的,重复周期周期越大,谱线越靠近。
  2. 直流分量,和各频次分量的大小与脉幅和脉宽成正比,与重复周期成反比
  3. 周期信号包含无穷多谱线,其中能量主要集中在第一次过零点内,也就是
    \(\omega < \displaystyle{\frac{{2\pi}}{\tau}}\)

    我们称这样的区域为频带,频带宽度为
    \(B_{\omega} = \displaystyle\frac{{2\pi}}{\tau}\)
    或者
    \(B_{f } = \displaystyle\frac{1}{\tau}\)
    ,频带宽度只与脉宽有关,并且成反比。

对称方波信号也是矩形信号的一种特殊情况:

  1. 它是正负交替的信号,其直流分量
    \(a_{0}\)
    等于零
  2. 他的脉宽恰好等于周期的一半,即
    \(\tau = \displaystyle\frac{T}{2}\)

对称方波的傅里叶级数形式为:

\[\begin{aligned}
f(t) & = \frac{{2E}}{\pi} \sum_{n = 1}^{\infty} \frac{1}{n} \sin\left( \frac{{n\pi}}{2} \right) \cos({n \omega t})\\
& = \frac{{2E}}{\pi} \left[ \cos({\omega t}) + \frac{1}{3} \cos(3\omega t +\pi) + \frac{1}{5} \cos({5\omega t}) + \cdots \right]
\end{aligned}
\]

对称方波的谐波幅度以
\(\displaystyle\frac{1}{n}\)
收敛。

(二)周期锯齿脉冲信号

峰值和谷值分别为
\(\displaystyle \pm\frac{E}{2}\)
,周期为
\(T\)
,信号是奇函数,因此傅里叶级数仅存在正弦分量。 谐波幅度以
\(\displaystyle\frac{1}{n}\)
的规律收敛。

\[f(t) = \frac{E}{\pi} \sum_{n=1}^{\infty} (-1)^{n+1} \frac{1}{n} \sin({n \omega t}).
\]

(三)周期三角脉冲信号

峰值为
\(E\)
谷值为
\(0\)
,周期为
\(T\)
,是偶函数,仅存在余弦分量。

\[\begin{aligned}
f(t) & = \frac{E}{2} + \frac{4E}{\pi^{2}} \left[ \cos(\omega t) + \frac{1}{3^{2}}\cos({3\omega t}) + \frac{1}{5^{2}} \cos({5 \omega t}) +\cdots \right]\\ \\
& = \frac{E}{2} + \frac{4E}{\pi^{2}}\sum_{n=1}^{\infty} \frac{1}{n^{2}} \sin^{2}\left( {\frac{n\pi}{2}} \right) \cos({n \omega t}) \\
\end{aligned}
\]

(四)周期半波余弦信号

偶函数,仅存在直流、基波和偶次谐波频率分量,谐波的幅度以
\(\displaystyle \frac{1}{n^{2}}\)
规律收敛。

\[\begin{aligned}
f(t) & = \frac{E}{2} + \frac{E}{2} \left[ \cos({\omega t}) + \frac{4}{3\pi} \cos({2 \omega t}) - \frac{4}{15\pi} \cos({4 \omega t}) +\cdots \right] \\ \\
& = \frac{E}{\pi} - \frac{2E}{\pi} \sum_{n=1}^{\infty} \frac{1}{(n^{2}-1)} \cos\left( \frac{{n\pi}}{2} \right) \cos(n \omega t).\\
\end{aligned}
\]

(五)周期全波余弦信号

周期全波余弦只包含直流分量和偶次谐波分量,谐波的幅度以
\(\displaystyle{\frac{1}{n^{2}}}\)
规律收敛 。

\[\begin{aligned}
f(t) & = \frac{2e}{\pi} + \frac{4E}{\pi} \left[ \frac{1}{3}\cos({2 \omega t}) - \frac{1}{15}\cos({4 \omega t}) + \frac{1}{35} \cos({6 \omega t}) \right]\\ \\
& = \frac{2E}{\pi} + \frac{4E}{\pi} \sum_{n=1}^{\infty} (-1)^{n+1} \frac{1}{(4n^{2} -1 )} \cos(2n \omega t)\\
\end{aligned}
\]

对于上述周期函数,周期矩形脉冲信号是最重要的一个
,需要进行仔细研究。

傅里叶级数举例和有限项逼近是带来的误差

实际工程中,我们无法做到将傅里叶级数展开到无穷项,我们只能展开到有限项。有限项算出的结果与真实值之间到底存在多少的误差是我们关心的。

误差即为后面无穷项之和:

\[\epsilon_{N}(t) = \sum_{n = N}^{\infty}[ a_{n} \cos(n\omega t) + b_{n} \sin(n \omega t)]
\]

方均误差为:

\[\begin{aligned}
E_{N} = \overline{ \epsilon_{n}^2(t) } & = \frac{1}{T} \int_{t_{0}}^{t_{0} + T} \epsilon_{N}(t)^2 \text{d}t \\
& = \overline{f^2(t)} - \bigg[a_{0}^2 + \frac{1}{2} \sum_{n=1}^{N}(a_{n}^2 + b_{n}^2) \bigg]\\
\end{aligned}
\]

下面,我们选择周期为2,幅值为1的周期矩形脉冲信号。

成分 图像
展开至基波
展开至3次谐波
展开至5次谐波
展开至7次谐波
展开至9次谐波
展开至11次谐波

通过观察上面的一组图像,我们可以发现如下规律:

  1. 傅里叶级数取的次数越多,最后的波形越接近原信号
  2. 高频信号主要影响的是信号中变化快速的部分;如信号为脉冲信号时,高频信号主要影响的是脉冲的跳变沿
  3. 低频信号主要影响的是信号中缓慢变化的部分;如信号为脉冲信号时,低频信号主要影响的时脉冲的顶部
  4. 当信号中任意频谱分量的幅值或者相位发生变化时,输出波形一般会失真,如在图像处理的时候,我们发现两张图片频谱的幅度不变,相位谱图互换后叠加,图像本身基本未发生改变,但是细节上稍微有些不同。


另外我们注意到一个有趣但又让人苦恼的地方。周期矩形脉冲信号的跳变边沿处有一个小的峰起,并且无论取多少项,这个峰起并没有因取的傅里叶级数项数变多而明显减小。
关于这样一个问题,信号分析中称其为
\(Gibbs\)
现象

。具体内容为:
在进行有限项傅里叶级数逼近时,随着采用更多的傅里叶级数成分或分量,傅里叶级数在接近(完整)跳跃的约
\(9%\)
的跳跃点附近显示出振荡行为中的第一个超调,并且该振荡不会消失,而是越来越接近该点,使得振荡积分接近零(即振荡能量为零)

如何让SQL Server像MySQL一样拥有慢查询日志(Slow Query Log慢日志)

SQL Server一直以来被人
诟病
的一个问题是缺少了像MySQL的慢日志功能,程序员和运维无法知道数据库过去历史的慢查询语句。

因为SQLServer默认是不捕获过去历史的长时间阻塞的SQL语句,导致大家都认为SQL Server没有历史慢日志功能

其实SQLServer提供了扩展事件让用户自己去捕获过去历史的长时间阻塞的SQL语句,但是因为不是默认出厂配置并且设置扩展事件对初级用户有一定难度,这里可以说不得不是一个遗憾,希望后续版本的SQL Server可以默认设置好慢日志的相关扩展事件,用初级用户也可以快速上手。


话不多说,这个文章主要讲述设置慢日志的扩展事件的步骤,并且把慢日志提供第三方程序读取以提供报表功能。

扩展事件介绍

SQL Server 扩展事件(Extended Events,简称 XE)是从 SQL Server 2008 开始引入的一种轻量级、高度可定制的事件处理系统,
旨在帮助数据库管理员和开发人员更好地监控、调试和优化 SQL Server 的性能。
扩展事件可以用于捕获和分析 SQL Server 内部发生的各种事件,以便识别和解决性能瓶颈和问题。

扩展事件优点包括轻量级、统一事件处理框架和集成性。事件设计对系统性能影响最小,确保在高负载环境下也能稳定运行。
扩展事件可以与 SQL Server Profiler 和 SQL Server Audit 结合使用,为用户提供全面的诊断和监控工具。


实验步骤

创建环境所需的数据库和表

--窗口1--建表

USEtestdbGO

CREATE TABLE Account(id INT, name NVARCHAR(200))INSERT INTO [dbo].[Account]
SELECT 1,'Lucy'
UNION ALL
SELECT 2,'Tom'
UNION ALL
SELECT 3,'Marry'

--查询
SELECT * FROM [dbo].[Account]

创建扩展事件

输入扩展事件名称

不要使用模版

事件库搜索block,选择blocked_process_report

确认事件

选择你需要的字段

这里选择client_app_name、client_hostname、database_id、database_name、plan_handle、query_hash、request_id、session_id、sql_text字段

当然你可以勾选自己想要的字段,这里只是抛砖引玉

直接下一步

这里需要注意的是,扩展事件日志不能全量保存,所以用户需要考虑好保留多长时间的扩展事件,假设一天可以产生的扩展事件大小为1GB,那么每个扩展事件文件大小1GB,最多5个扩展事件文件意味着你不能查询到5天之前的数据

比如你不能查询到前面第8天的扩展事件,扩展事件是滚动利用的。

扩展事件创建情况预览

小提示
:你可以点击script生成这个扩展事件的create脚本,那么其他服务器就不用这样用界面去创建这么繁琐了。

生成出来的扩展事件

CREATE EVENT SESSION [slowquerylog]
ONSERVERADDEVENT sqlserver.blocked_process_report
(ACTION
(
sqlserver.client_app_name,
sqlserver.client_hostname,
sqlserver.database_id,
sqlserver.database_name,
sqlserver.plan_handle,
sqlserver.query_hash,
sqlserver.request_id,
sqlserver.session_id,
sqlserver.sql_text
)
)
ADDTARGET package0.event_file
(
SET filename = N'E:\DBExtentEvent\slowquerylog.xel')WITH(
STARTUP_STATE
= ON);GO

完成

你可以勾选

a.扩展事件创建完成之后立刻启动

b.查看实时捕获的数据

立刻启动扩展事件

一定要设置locked process threshold,否则无办法捕获慢SQL语句,这个选项类似于MySQL的long_query_time参数

locked process threshold是SQL Server2005推出的一个选项,下面设置阻塞10秒就会记录

--窗口2--locked process threshold是SQL Server2005推出的一个选项

--设置阻塞进程阈值
sp_configure 'show advanced options', 1;GO  
RECONFIGURE;GOsp_configure'blocked process threshold', 10;   --10秒GO  
RECONFIGURE;GO  

执行一个update语句,不要commit

--窗口3
USEtestdb;GO

BEGIN tran
updateAccountset name ='Test'
where ID = 2

--commit

查询数据

--窗口4
USEtestdb;GO

--这个查询会被窗口3中的事务阻塞
SELECT * FROMAccountWHERE ID = 2

执行完毕之后,你可以看到扩展事件已经记录下来了

双击查看详细的会话里面的语句

可以很清楚的看到谁是被blocked的语句,谁是主动blocking的语句也就是源头

同时可以看到扩展事件已经记录到xel文件


使用其他编程语言制作慢查询日志报表

微软提供了使用 SQL Server Management Studio (SSMS) 和 T-SQL 查询扩展事件 XEL 文件内容的 API。

我们可以使用 sys.fn_xe_file_target_read_file 函数来读取 XEL 文件中的内容。
然后,你可以将这些数据导出为其他编程语言可以处理的格式

SQL语句

--查询扩展事件 XEL 文件内容
SELECTevent_data.value('(event/@name)[1]', 'VARCHAR(50)') ASevent_name,
event_data.value(
'(event/@timestamp)[1]', 'DATETIME2') ASevent_timestamp,
event_data.value(
'(event/data[@name="duration"]/value)[1]', 'INT') ASduration,
event_data.value(
'(event/action[@name="client_app_name"]/value)[1]', 'VARCHAR(255)') ASclient_app_name,
event_data.value(
'(event/action[@name="client_hostname"]/value)[1]', 'VARCHAR(255)') ASclient_hostname,
event_data.value(
'(event/action[@name="database_name"]/value)[1]', 'VARCHAR(255)') ASdatabase_name,
event_data.value(
'(event/action[@name="sql_text"]/value)[1]', 'VARCHAR(MAX)') ASsql_textFROMsys.fn_xe_file_target_read_file('E:\DBExtentEvent\slowquerylog*.xel', NULL, NULL, NULL) AStCROSSAPPLY
t.event_data.nodes(
'event') ASXEvent(event_data);



使用 Python 读取 XEL 文件内容
使用 pandas 库和pyodbc驱动程序从 SQL Server 导出数据并在 Python 中进行处理。
以下是一个示例脚本

importpyodbcimportpandas as pd#设置数据库连接
conn =pyodbc.connect('DRIVER={SQL Server};'
    'SERVER=your_server_name;'
    'DATABASE=your_database_name;'
    'UID=your_username;'
    'PWD=your_password')#查询 XEL 文件内容
query = """SELECT 
event_data.value('(event/@name)[1]', 'VARCHAR(50)') AS event_name,
event_data.value('(event/@timestamp)[1]', 'DATETIME2') AS event_timestamp,
event_data.value('(event/data[@name="duration"]/value)[1]', 'INT') AS duration,
event_data.value('(event/action[@name="client_app_name"]/value)[1]', 'VARCHAR(255)') AS client_app_name,
event_data.value('(event/action[@name="client_hostname"]/value)[1]', 'VARCHAR(255)') AS client_hostname,
event_data.value('(event/action[@name="database_name"]/value)[1]', 'VARCHAR(255)') AS database_name,
event_data.value('(event/action[@name="sql_text"]/value)[1]', 'VARCHAR(MAX)') AS sql_text
FROM
sys.fn_xe_file_target_read_file('E:\DBExtentEvent\slowquerylog*.xel', NULL, NULL, NULL) AS t
CROSS APPLY
t.event_data.nodes('event') AS XEvent(event_data);
""" #使用 pandas 读取数据 df =pd.read_sql(query, conn)#关闭数据库连接 conn.close()#显示数据 print(df)#将数据保存为 CSV 文件 df.to_csv('slowquerylog.csv', index=False)

这里的一个问题是,你不能直接读取XEL文件,本身XEL文件是一个二进制文件,必须挂接到在线SQL Server实例(任何SQL Server实例都可以,不一定是生产库的那一台SQL Server实例)

另外一个方法是使用 PowerShell 中的 Microsoft.SqlServer.XEvent.Linq.QueryableXEventData 类直接解析 XEL 文件,不用挂接到SQL Server实例

读取 XEL 文件的内容,然后导出CSV文件,让其他编程语言读取

Step 1: 创建 PowerShell 脚本 ReadXELFile.ps1

#加载所需的程序集
Add-Type -Path "C:\Program Files\Microsoft SQL Server\140\SDK\Assemblies\Microsoft.SqlServer.XEvent.Linq.dll"

#定义XEL文件路径
$xelFilePath = "E:\DBExtentEvent\slowquerylog*.xel"

#创建XEventData对象
$events = New-Object Microsoft.SqlServer.XEvent.Linq.QueryableXEventData($xelFilePath)#初始化一个空数组来存储事件数据
$eventDataList = @()#遍历每个事件并提取所需的字段
foreach ($event in $events) {$eventData = New-Object PSObject -Property @{
EventName
= $event.Name
Timestamp
= $event.Timestamp
Duration
= $event.Fields["duration"].Value
ClientAppName
= $event.Actions["client_app_name"].Value
ClientHostname
= $event.Actions["client_hostname"].Value
DatabaseName
= $event.Actions["database_name"].Value
SqlText
= $event.Actions["sql_text"].Value
}
$eventDataList += $eventData}#将事件数据导出为CSV文件 $eventDataList | Export-Csv -Path "E:\DBExtentEvent\slowquerylog.csv" -NoTypeInformation

Step 2: Python 脚本 ReadCSVFile.py读取导出的 CSV 文件

importpandas as pd#定义CSV文件路径
csv_file_path = "E:\\DBExtentEvent\\slowquerylog.csv"

#使用pandas读取CSV文件
df =pd.read_csv(csv_file_path)#显示数据
print(df)

这个方法需要使用powershell,对于powershell不熟悉的朋友也是一个问题


总结

本文介绍了SQL Server的扩展捕获慢查询语句的功能,也就是我们所说的慢日志

另外,一定要设置
“blocked process threshold
”参数,否则设置了扩展事件也没有效果

总体来说,SQL Server作为一个企业级数据库,确实不像MySQL这种开源数据库简单直接

需要设置比较繁琐的扩展事件,对新手用户不太友好,门槛比较高,但是因为扩展事件功能非常强大

除了捕获慢查询还可以捕获死锁,索引缺失等性能问题,所以这个是在所难免的

本文版权归作者所有,未经作者同意不得转载。

最近在书里看到的,让c语言去模拟其他语言里有的命名函数参数。觉得比较有意思所以记录一下。

目标

众所周知c语言里是没有命名函数参数这种东西的,形式参数虽然有自己的名字,但传递的时候并不能通过这个名字来指定参数的值。

而支持命名参数的语言,比如python里,我们能让代码达到这种效果:

def k_func(a, b, c):
    print(f'{a=}, {b=}, {c=}')

k_func(1, 2, 3)        # Output: a=1, b=2, c=3
k_func(c=1, b=3, a=2)  # Output: a=2, b=3, c=1

我们想要的类似于
k_func(c=1, b=3, a=2)
的效果,至少表现形式上要接近。虽说c的语法并不支持这样的表达式,但我们有办法模拟。

实现

我们假设有这样一个c语言的函数,现在我们想模拟命名参数传递:

int func(const char *text, unsigned int length, int width, int height, double weight)
{
    int printed = 0;
    printed += printf("text: %s\n", text);
    printed += printf("length: %d\n", length);
    printed += printf("width X height: %d X %d\n", width, height);
    printed += printf("weight: %g\n", weight);
    return printed;
}

模拟的关键在于如何完成名字到参数的映射。而且我们函数的五个参数有四种不同的类型,所以这个映射还得是异构的。

在不借助第三方库的情况下,第一个能想到的应该是enum加
void*
数组。enum可以完成名字到数组索引的映射,
void*
可以保存异构的数据。

这个方案的缺点很多:如果想要在length和width之间加个参数,我们很可能就需要修改所有的映射代码;比如
void*
可以接受任何数据类型的指针,所以我们几乎没有办法确保类型安全,想象一下如果有人给text传了个int的指针会发生什么。所以这个方案并不推荐。

能容纳异构的数据,同时还能给这些数据名字的东西,实际上在c里非常常见,那就是结构体。我们要选择的就是基于结构体的方案。

首先我们来定义这个结构体:

typedef struct func_arguments {
    const char *text;
    unsigned int length;
    int width;
    int height;
    double weight;
} func_arguments;

字段的顺序是无所谓的,你可以根据情况来任意调整。现在我们可以根据字段名来设置值了。现在还缺一环,只有结构体是没用的,我们需要把结构体的字段传递给函数才行。所以我们要写一个帮助函数:

int func_wrapper(func_arguments fa)
{
    // 根据需要还可以加入参数校验等逻辑
    return func(fa.text, fa.length, fa.width, fa.height, fa.weight);
}

我们需要的工具基本上都在这了,然而现在和命名参数传递还有不少差距,因为我们需要这样写代码:

func_arguments fa;
fa.text = "text";
fa.length = 4;
fa.width = fa.height = 8;
fa.weight = 10.0;
func_wrapper(fa);

不仅形式上差远了,代码还很繁琐,所以我们还得借助一下c99的复合字面量+指定初始化器的新语法来模拟命名参数传递:

func_wrapper((func_arguments){ .text = "text", .length = 4, .width = 8, .height = 8, .weight = 10.0 });

c99允许在初始化时使用
.字段名
的形式给字段设置值,没有指定的字段则初始化成零值,c99还允许符合要求的字面量类型转换成数组/结构体。利用新语法我们就能写出上面的代码了。

现在形式上确实很接近了,但还是显得有点啰嗦。这时候就得依赖宏了。c的宏可以实现文本替换和简单的类型分发,所以可以用它来把一些看起来不合法的表达式转换成正常的c语言代码。

首先声明,不要滥用宏,尤其是像下面那样,这里只是充当一下记录而不是教你生产实践。

用宏可以这样写:

#define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })

这里用了另一个c99的新语法,可变长参数宏,三个点意味着宏可以接受用逗号分隔的任意个参数,而
__VA_ARGS__
会原样替换成这些参数。因此我们只需要让宏的参数里是正确的指定初始化器就行了:

k_func(.text="text", .length=4);
// 宏完成替换后等价于 func_wrapper((func_arguments){ .text = "text", .length = 4 });

是不是很神奇?

有人可能会担心我们参数传递时复制了整个结构体,这会不会带来效率问题?通常这不会带来什么问题,现在编译器一般都能做到省略大部分不必要的复制,另外如果对象比较小的话复制通常也不会带来太大的开销,什么叫小很难定义,以我个人的经验来看尺寸比两个cacheline小的通常都可以算是“小”。

如果还是不放心,也可以简单得把参数类型改成结构体指针:

int func_wrapper(const func_arguments *fa)
{
    return func(fa->text, fa->length, fa->width, fa->height, fa->weight);
}

#define k_func(...) func_wrapper(&(func_arguments){ __VA_ARGS__ })

使用方法是一样的。注意宏里的
&
,这会分配一个auto生命周期的func_arguments变量(通常在栈上)然后再取它的指针。现在你可以不用担心了。不过我一般不推荐这么写,除非你经过性能测试后发现参数复制真的导致了性能问题。

缺陷

奇迹和魔法都不是免费的,所以上面像变魔术的代码也是有代价的。

第一个缺陷比较小,那就是字段名字前必须加上点。如果这么写:
printf("%d\n", k_func(.text="text", length=4))
,注意length前我们不小心把点给漏了。编译器会爆出一个不明所以的报错:

test.c: In function ‘main’:
test.c:31:45: error: ‘length’ undeclared (first use in this function)
   31 |         printf("%d\n", k_func(.text="text", length=4));
      |                                             ^~~~~~
test.c:27:52: note: in definition of macro ‘k_func’
   27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
      |                                                    ^~~~~~~~~~~
test.c:31:45: note: each undeclared identifier is reported only once for each function it appears in
   31 |         printf("%d\n", k_func(.text="text", length=4));
      |                                             ^~~~~~
test.c:27:52: note: in definition of macro ‘k_func’
   27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
      |                                                    ^~~~~~~~~~~

它会告诉你
length
这个名字从来没被定义过而不是告诉你漏了个
.
。平时看东西不认真的人就要受罪了,因为要找这个点得花上一番功夫。

第二个缺陷是没有语法提示和自动补全。毕竟宏可以把任何符合规则的文本替换进去,至于替换完怎么样就管不到了,所以想要对
k_func
的参数进行提示和补全是不太容易的,而且实验下来目前没有ide和编辑器能帮我把字段名自动补全。不过问题倒也不大,因为万一写错了的话编译器会给出准确的报错,只不过开发效率会降低一些。

第三个缺陷在于可以写出这样的东西:
printf("%d\n", k_func(.length=10, .text="text", .length=4))
。我们指定了两次length字段的值,语法上这是允许的,length的值会被最右边的那个覆盖掉。但这显然不符合我们的要求,而且前面说了因为没有自动补全和语法提示,我们不小心把height写成了weight也很难察觉到。更糟糕的是这种覆盖在gcc下需要指定
-Wextra
才能看到一个不痛不痒的警告。同样的情况在python下会直接收到一个语法错误的异常
keyword argument repeated

前两个缺陷还有办法克服,最后一个是没有任何办法的,只能靠更高级别的警告设置和人力检查了。

总结

正常来说我们做到
func_wrapper
那步就足够,后面的宏没啥意义纯粹是为了在形式上模拟python的命名参数而做的。

除了用结构体包装,写一个包装函数并调换参数的顺序或者给出默认值也是常见的做法,但这种做法很快会让接口的数量失控,大量相似的接口会让代码变得臭不可闻,所以我更推荐用结构体。

最后学习新语法还是很有用的,因为很多新语法好好利用的话可以有效提升扩展性和你的开发效率。

最近在刷短视频的时候,偶尔能看到一些真人转动漫风的作品,看起来给人一种新鲜感,流量都还不错,简简单单跳个舞,就能获得上千个点赞
~

那么,这种视频是怎么制作的?

本期给大家介绍一款
AI
转绘工具
Diffutoon
,可以将逼真的视频转换成动画风格,不仅能够处理高分辨率和快速运动的视频,还能确保整个视频的风格保持一致

Diffutoon
最新中文版:

百度网盘:
https://pan.baidu.com/s/1gKpZMMRpiB6OiTPc4cjNHA?pwd=tbjs

工作原理

Diffutoon
的核心是基于深度学习的图像转换模型,这些模型经过大量的动漫图像和视频数据训练,能够识别并模仿动漫风格的特征,包括颜色、线条、阴影和纹理等

1.
对输入的视频进行帧提取,将视频分解成单帧图像

2.
使用训练好的深度学习模型对每一帧图像进行风格转换,使图像呈现出动漫的视觉效果

3.
对转换后的图像合成处理,通过插帧技术提高视频的流畅度

4.
将处理好的帧图像重新合成视频,生成最终的动漫风格效果

功能特点

· 高度自动化:能够自动处理视频文件,无需人工干预,极大提高了视频动漫化的效率

· 结构引导:使用
ControlNet
模型提取视频中的关键轮廓信息,在动画过程中保留原内容的核心特征

· 一致性增强:集成
AnimateDiff
运动模块,增强帧时间一致性,确保动漫视频播放流畅,视觉连贯

· 自动着色:能够根据视频内容和风格要求自动选择合适的颜色进行填充

· 超分辨率:采用专门的控制网络进行视频的上色处理,即使输入低分辨率的视频,也能够生成高质量的动漫风格视频

使用方法

1.
在“稳定扩散”处选择模型“
aingdiffusion_v12.SafeTensors

2.
在“动画差异”处选择模型“
mm_sd_v15_v2.ckpt

3.
在“输入视频文件”处复制待处理视频所在的路径,例如
C:\dongman\1.mp4

4.
设置输入视频的高度和宽度,短视频的高宽一般是
1024

768

5.
设置开始帧
0
,结束帧设为视频的秒数乘以
30
,例如
10
秒视频的结束帧就是
300

6.
“输出视频文件路径”
保持默认值
output
,设置帧数为
30

7.
在“
ControlNet
单元”处设置

单元
0
:控制网为“
control_v11f1e_sd15_tile.pth
”,处理器为“
tile

单元
1
:控制网为“
control_v11p_sd15_lineart.pth
”,处理器为“
lineart

8.
点击
运行按钮

稍微等待一段时间,转绘后的视频保存在项目路径的
output
文件夹下

注意事项

①项目安装路径不要包含中文

②推荐使用
GTX1070
以上显卡运行此项目

③使用过程中若不慎关闭软件后台,请重新打开,并刷新网页