尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

树莓派+音频检测,打造自动跳过电视广告的DIY系统

树莓派+音频检测,打造自动跳过电视广告的DIY系统 最近折腾完一个挺有意思的项目Build a TV Commercial Killer。单看标题像是个暴力拆电视的活儿但圈内人懂它是说造一套能自动识别电视广告、自动跳过或静音的系统。看电视时最烦的从来不是广告本身而是正看到关键剧情突然切入一条大音量的洗脑广告你手忙脚乱去找遥控器它已经播完一半了。与其每次都手动切不如让机器替我盯着。我做的是用树莓派加USB声卡和红外发射器搭出一台能监听电视音频、识别广告边界、自动按遥控器跳过键的装置。它解决的就是电视广告必须被忍受这件事适合家里有IPTV机顶盒、有线电视或者智能电视而且愿意花一个周末折腾点DIY硬件的朋友。这套方案从硬件到算法都是常用组件原理不深但把细节做扎实才能真正用得顺手。1. 项目到底是什么先把电视广告杀手拆开看很多人第一次看到这个标题会以为是要做一个杀掉电视广告的插件或者App。其实在自嘲和夸张的外壳下面它是一个典型的家庭自动化项目。动手之前我先花了两天把需求拆清楚这是整件事里最重要的一步比选硬件还关键。1.1 从标题拆解核心需求Build a TV Commercial Killer可以拆成三个词来看。Build说明这是一个自己动手做的项目而不是买现成的去广告盒子TV Commercial目标非常明确是传统电视播放渠道里的商业广告包括有线电视、IPTV机顶盒、运营商宽带电视里的插播广告Killer在这个语境下不是杀死电视而是让广告失去强行打扰你的能力。从这个拆解里能得出几个实际需求第一系统必须能自动检测广告的开始和结束第二检测到广告后要能自动执行跳过、静音或者切换信号源之类的操作第三广告结束后要能自动恢复原状态让用户无感。除此之外扩展需求还包括统计广告时长、记录误判日志、学习不同遥控器的按键编码等等。一开始我差点把需求做成只要是广告就永久屏蔽后来意识到不现实。真正合理的定位是自动跳过而非彻底消灭因为广告在电视播出链路里和正片是混在同一个音视频流里的你不可能像网页广告那样用内容过滤器直接把它过滤掉只能从播放行为层面做自动化干预。1.2 三条主流技术路线的取舍做广告检测我调研下来主要有三条技术路线各有各的道理也各有各的坑。第一种是静音检测。利用电视广告切换时普遍存在的短暂静音、音量突变、响度变化来做触发信号。优点是实现最简单只要一根音频线加一个USB声卡就能干缺点是误判率高节目本身一旦出现安静场景或者转场就可能被当成广告切掉。第二种是音频指纹。提前采集常播广告的声音提取特征指纹存在本地运行时持续比对当前播放的声音一旦匹配到广告指纹就触发跳过。优点是准确率高甚至能精确识别出这一条广告是哪家的、播到第几秒缺点是前期需要建库而且每家运营商播的广告不一样指纹库要持续维护。第三种是视频图像识别。通过HDMI采集卡或者网络机顶盒的画面输出用图像算法识别台标切换、画面亮度突变、播放进度条等特征。优点是信息量最大缺点是要额外买HDMI采集卡处理量也大树莓派跑起来会比较吃力。我把三种方案做过对比最终选的是组合方案以静音检测音量突变检测作为主触发器以简化版音频指纹作为二次确认。这样既保证了实时性又大幅降低了误判。纯粹做静音检测我实测误触发率会高到让人想拆机纯粹做指纹库又会被运营商更换广告拖垮。1.3 为什么是树莓派加USB声卡加红外发射器确定技术路线之后硬件选型就顺理成章了。我选了树莓派4B作为主控理由很直接它跑完整套Python音频处理流程非常轻松GPIO能直接接红外发射管而且生态成熟遇到问题搜一下就有答案。有人会问为什么不用ESP32这种更便宜的板子我试过问题在于ESP32上的音频采集和FFT要写更底层的代码装不了PyAudio和SciPy调试成本高出一大截省下来那几十块钱不值得。音频采集我用的是USB声卡不是树莓派板载的3.5mm口。原因有两个一是树莓派板载音频接口的底噪和驱动支持都不理想二是USB声卡免驱、即插即用而且可以方便地接Line In。红外遥控模块用的是最基础的红外发射管加三极管驱动电路整套下来成本很低。2. 硬件选型与整机结构硬件是这套系统的骨架选错一个组件后面就会在某个环节反复折腾。我把自己最终确定的配置列出来附上选型理由大家可以直接按这个来也可以根据手头设备调整。2.1 核心组件清单与选型理由下表是我这套电视广告杀手的核心组件。预算大概是几百元级别前提是你手头已经有一块树莓派如果没有的话整体成本会稍微高一些。组件规格/型号作用选型理由主控板树莓派 4B2GB及以上运行采集与检测程序性能充裕可跑Python生态GPIO方便音频采集免驱USB声卡CM108芯片为例采集机顶盒/电视音频免驱稳定接口可扩展连接线3.5mm转双莲花头音频线从电视/机顶盒取音频信号接线方便同时做物理隔离红外发射红外发射管三极管电阻模拟遥控器发送按键指令成本低通用性强遥控码接收红外接收头VS1838B学习原装遥控器编码学习型方案不需要拆遥控器供电5V/3A电源适配器树莓派供电稳定供电避免USB声卡电流声如果你家电视只有HDMI输出没有模拟音频口我建议加一个HDMI音频分离器把音频分离成光纤或者模拟信号再接进USB声卡。否则就得用HDMI采集卡那成本和复杂度会上升不少。2.2 音频采集回环采集才是关键我在这套系统上踩过的第一个大坑就是音频采集方式。一开始我图省事直接用桌面麦克风放在电视机旁边让树莓派听电视声音。结果白天环境噪声一多检测逻辑几乎崩溃风扇声、说话声、窗外的声音全被当成音频特征。后来改成物理回环采集从IPTV机顶盒的音频输出接口莲花头或者3.5mm耳机口拉一根线直接接到USB声卡的Line In。这样树莓派采集到的就是纯正的、不经过空气传播的电视伴音和正片、广告之间的音量关系、静音间隙都保留得非常干净。实测下来回环采集的误判定率比环境麦克风低了一个数量级。有一点要提醒电视耳机口的输出电平和USB声卡的Line In并不总能直接匹配。如果发现采进来的波形削顶或者音量偏小就在线路上串联一个10k欧姆左右的电阻做衰减必要时接一个音量旋钮手动校准。我一开始偷懒直接怼进Mic口结果全是削波失真根本没法用。2.3 遥控模块红外、HDMI-CEC、IP控制怎么选整套系统里另一个关键的硬件模块是执行动作的部分。检测到广告之后系统需要一个方式把跳过/静音/返回指令发送给电视或者机顶盒。最常见的是红外遥控。红外方案兼容性极好不管是IPTV机顶盒、老电视还是智能盒子基本都带红外接收。缺点是需要学习原装遥控器的编码而且发射管要对准设备。HDMI-CEC能直接通过HDMI线缆发送控制指令电视支持的话非常省事但对机顶盒这类HDMI源设备的控制能力有限。IP控制是另一种思路比如部分智能电视开放了网络控制端口可以在局域网里直接发送指令但不同品牌协议千差万别。我最终选了红外作为主力并且做了学习型处理用红外接收头先学习原装遥控器的跳过键和静音键再通过GPIO驱动红外发射管转发。这样哪怕换一台机顶盒我也只需要重新学一次不用改代码。3. 核心原理与算法细节它凭什么判断广告来了硬件只是耳朵和手真正决定这套系统好不好用的是判断广告的算法。很多DIY项目做到最后发现问题从来不在硬件而在于怎么让程序在正片安静片段和广告开始之间做出正确区分。3.1 广告在信号层面到底有什么特征要检测广告先要知道广告在音频信号上有什么共性。我总结了四个很明显的特征。第一是切换间隙。电视播出链路里从正片切到广告或从一条广告切到下一条往往存在几百毫秒到一两秒的静音或者弱音片段这是由播出系统的切换机制决定的。第二是响度更高。广告为了让人注意到普遍会把响度压得比正片更高平均响度差异经常超过3dB到6dB。第三是重复性。同一时段里同一个广告会反复播放内容完全一样音频指纹高度一致。第四是节奏差异。广告的剪辑节奏普遍比正片快音频的能量变化频率更高。这四点是算法设计的依据。静音检测和音量突变检测吃的是前两点音频指纹吃的是第三点节奏特征一般作为辅助用得少一些。3.2 基于RMS的静音边界检测怎么算、怎么判断最简单也最基础的是RMS均方根音量检测。流程是把音频流切成固定大小的小块每块算一个RMS值再拿这个值和过去几秒的背景音量做对比。如果当前RMS远低于背景值就认为进入了静音片段。我用的参数是这样的采样率16000Hz块大小4096个采样点也就是约每秒4次判断。每个块算完RMS后放进一个环形缓冲区取前两秒的中位数作为背景音量。静音判定条件是当前块的RMS低于背景音量的四分之一并且连续8个块约2秒都低于这个阈值才认为出现了一个完整静音间隙。这里有个细节为什么不能只看单块RMS就判定因为视频里的正常转场、人物语气停顿都可能出现几百毫秒的弱音。单块判定会把它们全误判成广告边界。连续多块判定虽然会让反应慢半秒但换来的稳定是值得的。我在代码里设了一个静音计数器信号低于阈值就加一高于阈值就清零只有计数超过设定的窗口长度才触发。3.3 音量突变与响度异常广告的大嗓门特征静音间隙解决的是广告切换的检测但有些广告在内容里根本没有明显静音间隙尤其是一条广告接一条广告连续轰炸的时候中间可能只隔了一帧。这种情况下靠静音检测会漏判。这时候需要用音量突变检测。广告的声音普遍比正片响我做了两个指标一是当前10秒平均RMS相对前90秒平均RMS的提升幅度提升超过4dB就标记为响度异常二是短时峰值出现频率广告的峰值密度通常明显高于正片。把这两个指标结合起来可以识别出节目里的安静对话突然变成高密度响度轰炸的时刻。不过这个环节最容易误判。有些正片本身就是高动态范围的大片爆炸、飙车片段响度很高峰值密度也大。所以我在代码里给响度异常加了一个条件只有它和静音间隙检测配合出现也就是在响度异常发生前的大约3到5秒内刚好也有过一个静音边界才能判定为广告开始。单独出现响度异常时不触发只记日志。3.4 简化版音频指纹让机器记住常播的广告静音加响度已经能挡住大部分误判但还有一个更恶心的情况电视剧每集开头有固定片头或者某个综艺节目有大量回放蒙太奇这些内容节奏和音量也接近广告。要对付这种情况得用音频指纹。完整版的音频指纹系统很复杂像Dejavu、Chromaprint那样的库能生成足够鲁棒的指纹。但对我来说太重了我写了一个简化版每秒对音频块做一次FFT把频谱切成12个频带每个频带取平均能量得到一个12维向量再量化成10级得到一个哈希指纹。这套指纹不追求跨设备的鲁棒性只针对我自己家里的音频链路够用就好。使用时做两件事第一在系统空闲时采集当前正在播放的广告音频自动入库第二运行时把实时指纹和库里每一条广告指纹做相似度匹配如果连续匹配超过15秒就认定当前正在播广告触发跳过。指纹匹配我直接用的余弦相似度阈值设到0.85以上才算命中。这个方案对付同一条广告反复播非常有效实测首播时漏过第二次再播就能精准命中。3.5 状态机与防误判策略检测逻辑不能是简单的if静音 then跳过否则系统会神经质。我把它做成了一个五状态状态机WATCHING正片、TRANSITION检测到疑似边界、AD确认广告、ACTED已执行跳过、RECOVERING恢复中。状态机的流转规则是这样的WATCHING状态下如果检测到静音间隙进入TRANSITIONTRANSITION持续2到3秒如果这段时间内又出现响度突变或者指纹命中就进入ADAD状态下先确认已经连续超过3秒判定为广告然后触发红外指令进入ACTED同时启动一个最短屏蔽时间比如90秒避免同一条广告里重复触发。屏蔽时间结束后回到WATCHING但录音继续。如果TRANSITION后几秒内没有其他佐证就退回WATCHING只记一条疑似日志。这个状态机的设计用意就是给每次判定留出举证和复核的空间。单个信号可能说明不了问题但两个独立特征在同一时间窗内同时出现可信度就高得多。我在调试时发现加上状态机之后误触率从每小时好几次下降到几乎一天一次效果非常明显。4. 完整实操从零搭一个能自动跳广告的装置原理讲完进入动手环节。下面是我实际搭建这套系统的完整过程每一步都有对应操作照着做基本能跑起来。整个流程大约需要一整天其中一半时间是在等系统下载依赖和采集声卡数据。4.1 系统准备与依赖安装我用的是树莓派官方Raspberry Pi OS Lite也就是不带桌面版的那个镜像烧写和SSH登录就不展开了网上资料很多。开机后先更新系统然后安装依赖。音频处理这块核心是PyAudio和NumPy红外控制这块我用的工具是ir-ctl和LIRC相关的库。sudo apt update sudo apt upgrade -y sudo apt install -y python3-pip python3-numpy python3-scipy \ libportaudio2 portaudio19-dev alsa-utils ir-ctl pip3 install pyaudio numpy scipy第5步的pip3 install pyaudio可能报错提示缺少portaudio头文件所以我提前装了libportaudio2和portaudio19-dev。装完之后用arecord -l检查USB声卡是否被系统识别如果能看到类似card 1: Device [USB Audio Device]的输出说明声卡驱动正常。4.2 学习原装遥控器的红外码要让设备能按遥控器得先让它学会你家遥控器的编码。我用的是红外接收头接GPIO ir-ctl抓码的方式。接线是这样的红外接收头VS1838B的OUT脚接树莓派的GPIO23GND接GNDVCC接3.3V。然后运行下面的命令捕获原始红外脉冲# 捕获原始红外码保存为文本 ir-ctl -d /dev/lirc0 --receive vol_up.txt这时候把原装遥控器的音量/跳过/静音键对着接收头按几下vol_up.txt里就会记录下对应的脉冲序列。ir-ctl生成的原始码可以直接用来重放。我额外加了一个红外发射管接在GPIO22上通过三极管驱动发射管正极接5V集电极接GPIO22发射极串联100欧姆电阻到GND。发送时就是读回之前存好的脉冲文件然后发射。# 回放红外码发送按键 ir-ctl -d /dev/lirc1 --sendvol_up.txt注意接收和发射需要不同的lirc设备节点树莓派上/dev/lirc0通常是接收头对应的设备/dev/lirc1是发射设备具体看dmesg的输出。别搞反了否则你按了半天程序还在忙着发呆。4.3 音频采集与静音检测主循环音频采集我用PyAudio完成。为了减少底层延迟我用的是回调模式在回调函数里只做一件事把原始音频数据塞进一个队列。真正处理RMS和状态机的是主线程避免在音频回调里做重计算导致卡顿。下面是我精简过的主循环代码核心就是RMS计算、背景音量和静音判定import pyaudio import numpy as np import queue import math CHUNK 4096 RATE 16000 SILENCE_THRESHOLD 0.25 # 当前块RMS低于背景音量的比例 SILENCE_BLOCKS 8 # 连续多少块算一次静音事件 q queue.Queue() pa pyaudio.PyAudio() stream pa.open( formatpyaudio.paInt16, channels1, rateRATE, inputTrue, input_device_index1, # USB声卡的设备编号用 arecord -l 查 frames_per_bufferCHUNK, stream_callbacklambda in_data, frame_count, time_info, status: ( q.put(np.frombuffer(in_data, dtypenp.int16).copy()), pyaudio.paContinue ) ) stream.start_stream() def rms(frame): return np.sqrt(np.mean(frame.astype(np.float32) ** 2)) background_levels [] silence_count 0 while True: data q.get() level rms(data) if level 1: continue # 忽略纯静音块避免把无信号误判为边界 background_levels.append(level) if len(background_levels) 30: # 保留约2秒背景音量 background_levels.pop(0) bg np.median(background_levels) if level max(bg * SILENCE_THRESHOLD, 5): silence_count 1 else: if silence_count SILENCE_BLOCKS: print(detected silence gap) # 在这里触发状态机的 TRANSITION silence_count 0这里有一个关键点背景音量不能用均值要用中位数因为窗口里如果混进了广告的大音量高分贝均值会被拉高导致静音判定基准漂移。中位数抗高音量突刺的能力要强得多。我调试时吃过这个亏换成中位数之后系统稳定了很多。4.4 音频指纹入库与实时匹配指纹部分的代码相对独立。入库时我把每一秒的音频做成一个12维频谱向量然后把连续16个向量16秒打成一个指纹片段和广告ID一起存进本地SQLite表。运行时同样截取最近16秒的向量序列逐一和库里的指纹做相似度计算只要有一条广告的相似度连续超过阈值就返回广告ID。指纹具体做法对每1024个采样点的窗口做FFT把0到8kHz分成12个对数频带取每带能量。每秒做一个指纹向量。归一化之后直接和参考向量做内积得到余弦相似度。这个过程我用NumPy实现耗时基本可以忽略。刚开始建库时我都是手动标注的看到广告来了按下学习按钮程序记录接下来60秒的音频指纹存成一条广告。这个办法笨但非常有效。跑了一周之后库里累计了大约20条广告覆盖了我家运营商电视的常见广告后续命中率越来越高。4.5 状态机与动作执行整合状态机的实现就是把前面的检测信号汇总然后按规则决定要不要发红外指令。我把每类信号都封装成一个事件函数例如on_silence_gap()、on_loudness_jump()、on_ad_fingerprint()状态机只消费这些事件。触发广告确认后发送红外跳过键的代码如下import subprocess def send_ir_command(key_name): # key_name 是之前保存的遥控码文件路径 subprocess.run([ ir-ctl, -d, /dev/lirc1, f--send/home/pi/remote_codes/{key_name}.txt ], timeout5)这里要提醒红外发送命令必须放到独立线程或者子进程里跑。一开始我直接在检测到广告的主循环里调用subprocess.run结果卡了大约0.8秒音频缓冲区堆了一堆数据检测节奏全乱了。改成subprocess.Popen后台发送之后问题就解决了。4.6 部署为开机自启服务整套程序调试稳定后我写了一个systemd服务让它开机自动运行。服务文件内容很简单[Unit] DescriptionTV Commercial Killer Afternetwork.target [Service] ExecStart/usr/bin/python3 /home/pi/tv_commercial_killer/main.py WorkingDirectory/home/pi/tv_commercial_killer Restartalways Userpi [Install] WantedBymulti-user.target然后把服务文件放到/etc/systemd/system/tv_killer.service执行sudo systemctl enable --now tv_killer就能开机自启。日志我用的是标准输出加上文件重定向调试时直接看journalctl -u tv_killer -f非常方便。5. 实战中遇到的坑和排查经验这个项目做到第四天才算真正能用前面三天全在和噪声、误判、遥控器编码较劲。下面这些坑是我自己踩过的写出来帮大家省点时间。5.1 常见问题速查表现象可能原因解决办法完全检测不到静音事件USB声卡没选对或音频线没接好用arecord -l确认设备编号测试录音确认波形频繁误判广告来了背景音量用均值而非中位数换成中位数做基准问题大幅缓解红外指令偶尔发不出去GPIO发射驱动不足或发射管朝向不对三极管驱动加限流电阻发射管对准机顶盒接收窗正片低语被当成静音静音阈值太低调低SILENCE_BLOCKS并要求同时出现响度突变佐证指纹命中率很低入库指纹和实时采音频段不平齐入库时用16秒滑动窗口匹配时做多个偏移对齐系统运行几个小时就卡死PyAudio回调里做了重计算回调只放队列处理全部挪到主线程5.2 我最想分享的三个调节细节第一音量阈值的校准不能靠听感要看数据。我写了个小脚本统计24小时内RMS的分布直方图然后根据直方图确定静音阈值和响度突变阈值。靠耳朵觉得这个音量应该算静音完全不行因为电视节目之间音量差异太大了。第二静音检测最适合检测广告与广告之间的切换而不是正片与广告的切换。因为正片切广告时经常是先播完片尾再切片尾音乐还在静音判断会滞后。所以我额外设置了一个节目前10秒检测策略如果识别出片尾字幕特有的音频节奏变化就提前进入准备状态。这个细节让整套系统的响应速度提升明显。第三红外发射的位置比编码本身更易导致失败。我一开始把发射管放在机顶盒侧面设置成每广告必发结果发现成功率只有七成。后来把发射管固定在机顶盒正前方约5厘米位置成功率立刻到了99%以上。如果你用的是带长线的发射管多调整角度和距离比改代码有效。5.3 后续还能怎么扩展这套系统现在只是自动跳过广告这个核心。对我来说下一步想做的扩展有三个方向第一是加入广告时长统计把每天拦截了多少分钟的广告汇总成周报顺便看看运营商广告投放的规律第二是增加多房间支持用两台设备分别监控客厅和卧室的机顶盒数据汇总到一台中央服务器第三是优化指纹库分享机制把入库的广告指纹脱敏后导出来和网上同好交换这样大家都能缩短建库时间。我个人在使用这套系统时最大的体会是它并不会让广告彻底消失而是把手动快进变成了自动跳过。你省下的每一次弯腰找遥控器背后都是一堆细节判断在替你把关。真正难的不是写代码而是理解你家里那台机顶盒的脾气它的切换静音多长、广告响度比正片高多少、遥控器编码是哪种格式这些数据比任何通用算法都更值钱。如果你也想做一个我建议先把日志跑到一千条再调算法数据比感觉可靠。
返回列表