昨天微博搜索空降话题——“iPhone 15折叠屏渲染曝光”。
有多少人和小优一样,以为苹果的折叠屏真的要来了,兴奋地进去了。结果事实根本不是我们想的那样!
简单来说,就是以@新浪热点为首的一群数码博主,转载了一段国外渲染器@4RMD关于折叠屏iPhone(代号iPhone 15 Flip)的自创视频。
虽然视频中用鼻子和眼睛展示了iPhone 15翻盖的方方面面,比如垂直折叠,6.8寸展开,搭载A17芯片,可以折叠20万次。外置摄像头在小副屏旁边.
但是!这些信息都来自于爆料的汇总,有些是果粉的猜测,有些甚至是谣言,无从考证真假。就连iPhone 15 Flip的型号名称都是自鸣得意的产物。
所以,整个视频充其量就是一个启示录衍生品。
其次,视频制作者只是一个为爱发电的概念设计师,并不是苹果官方,也不是苹果供应链的相关人士。也就是说流出的渲染图/视频可信度相当低。
还记得去年的“iPhone 13粉色”热搜吗?
当时很多果粉都信了,还挺看好猛男粉iPhone的。
结果没几天事情就有了转机。
iPhone粉色效果图的制作者出面承认,自己制作图纸只是一时兴起。没想到图片被转载,诞生了“iPhone 13加粉色”的谣言,甚至几个月后上市时就被编造出来了。
不过,新浪让这个“爆料衍生视频”成为热搜也不是没有道理。
今年设备的折叠趋势越来越强。我们熟悉的安卓厂商,比如三星、华为、OPPO、vivo、小米等。都推出了折叠屏手机,但苹果迟迟没有动作。
在这个节骨眼上,任何关于折叠屏iPhone的消息都能引起巨大的讨论,这就给了这个视频一个机会。稍微推波助澜一下就冲上了高级别的热搜。
值得一提的是,虽然折叠屏iPhone没有那么快,但是折叠屏iPad和Mac已经在路上了。
两个月前,分析公司CCS Insight在年度预测报告中提到,苹果可能会在2024年推出折叠屏iPad。
昨天,外媒TheElec报道称,苹果正在与三星LG合作开发折叠屏幕,展开状态为20.25英寸,折叠状态为15.3英寸。这么大的尺寸是为MacBook视觉开发的。
把这两条新闻放在一起看,苹果似乎是要先在大屏设备上测试水折叠技术,然后在小屏手机上推广。
为什么?
CCS Insight的研究主管本伍德(Ben Wood)对此进行了分析:
首先,折叠屏iPhone是个吃力不讨好的产品,做不好会招来很多非议。是真的。很多竖屏折叠手机都被形容为“漂亮的小废物”,看着没用。
其次,现在的iPhone已经是手机界的顶级价位了。折叠屏iPhone一旦做出来,定价一定要越来越高,才不会影响现有iPhone产品线的销售。
总结一下Ben Wood的观点:折叠屏iPhone是一个高价高风险的产品。苹果自己也觉得这个东西悬,所以短期内不愿意涉足。
那为什么苹果愿意做折叠屏Mac?
肖佑这样认为:
首先,Mac比较大,为了实现折叠形状,加铰链和调整组件位置的难度可能比iPhone小(仅小优愚见)。
其次,现有的MAC价格不是普通人能承受的,顶级的MAC早就有了。然后,一个折叠版放上更高的价格,以前买不起的现在也买不起了。这难道不是无关紧要的吗?自然不会碰其他在售苹果产品的蛋糕。
离屏渲染怎么优化
首先,通过fetch请求音频数据以获得ArrayBuffer对象。
fetchData() {
获取(this.url)。然后((响应)={
return response . array buffer();
}).然后((缓冲)={
this.decodeAudioData(缓冲区);//解码
ArrayBuffer看似处理二进制数据流,但JS没有办法直接处理(读写)其内容,必要时需要转换成TypedArray 。
接下来,通过audioContext对象,可以使用接口的decodeAudioData()方法异步解码音频文件中的ArrayBuffer 。
decodeAudioData(缓冲区){
const audioCtx=new(窗口。audio context | | window . webkitaudiocontext)();
audioCtx.decodeAudioData(
(音频缓冲区)={
const { sampleRate }=audiobuffer//获取采样率
const channel data=audio buffer . getchanneldata(0);
this . draw(channel data);//绘图
解码成功后会得到AudioBuffer对象。
它包含音频持续时间、长度、通道数量和采样率。
采样是将模拟声音信号转换为数字声音信号的过程。采样率是单位时间内采集音频信号的次数,用赫兹(Hz)或千赫兹(kHz)来度量。MP3的采样率一般为44.1kHz,即每秒44100次声音分析,记录每次分析的差值。采样越高,声音信息就越完整。
这里,总持续时间*采样率=总长度。总时长会与实际相差0.1-0.2秒,在音频dom上获得的实际音频时长为94.32秒。我们将把这个作为标准
然后调用getChannelData()方法返回TypedArray对象,该对象包含与通道关联的PCM数据。通道参数定义(0表示第一个通道)不能大于numberOfChannels 。
TypedArray是描述ArrayBuffer的类数组视图。虽然没有TypedArray的全局属性和构造函数,但是提供了基于其特定数据类型的一系列数组(可以理解为TypedArray是一个接口及其扩展类)
【更多音视频学习资料,点击下方链接免费领取这样你就可以先码起来不迷路啦~】
点击接收音视频开发基础知识与资料包。
知道音频时长后,确定绘制的波形画布的宽度,设置为每秒100像素,时长94.32秒,得到画布宽度为9432像素。
模板
div id='应用程序'
音频控件:src=' URL ' @ loaded metadata=' load data '/audio
canvas ref=' timeline 'width=' 360 px 'height='auto' /
canvas ref='wave'width='360px 'height='auto' /
/div
/模板
导出默认值{
unitwidth=' 360px像素'height='auto' /
总时间:0,
宽度=' 360像素'高度='自动'/
loadData() {
this . total time=e . target . duration;
this.width='360px 'height='auto' /
this.timelineCtx=this。$ refs . timeline . get context(' 2d ');
this.waveCtx=this。$ refs . wave . get context(' 2d ');
this . fetch data();
所解码的channelData的长度为4150273,画布的宽度为9432个像素,所以每个像素分配了440.02个数据,相当于channelData 每440.02个点渲染一个像素。
但是440.02不是整数,每100个像素会有两个数据偏差,每22000个像素会有一个像素。音频持续时间越长,偏差越大。况且这里的小数只有0.2,小数越大偏差越明显
所以接下来,四舍五入440.02,微调单位宽度=' 360px '高度=' auto'/
draw(channelData) {
const step=math . floor(channel data . length/(this . total time * this . unit width=' 360 px 'height='auto' /
this.unitwidth='360px 'height='auto' /
this.width='360px 'height='auto' /
this . draw timeline();//绘制时间线
this.drawWave(channelData,step);//绘制波形
画了时间轴,这里就不分析了。
drawTimeline() {
for(设I=0;I math . ceil(this . total time);i=1) {
this . timeline CTX . font=' 10px Arial '
this . timeline CTX . fill style=' rgba(0,0,0,1)'
this . timeline CTX . fill rect(this . unit width=' 360 px 'height='auto' /
如果(i 0) {
this.timelineCtx.fillText(
durationToTime(i)。拆分('')[0],
this.unitwidth='360px 'height='auto' /
绘制波形需要对整个通道数据进行遍历,每440 个数据,算出最大值和最小值,最大值与最小值的差值越大,则整个波形也越大
drawWave(channelData,step) {
设步进索引=0;
设xIndex=0;
设min=1;
设max=-1;
对于(设I=0;我通道数据长度;i=1) {
步进指数=1;
const item=channel data[I]| | 0;
如果(最小项目){
最小值=项目;
} else if (item max) {
最大=项目
if (stepIndex=step) {
xIndex=1;
this.waveCtx.fillStyle='rgba(0,0,0,0.3)'
const l=(max-min)* 40 * 0.8;
this.waveCtx.fillRect(xIndex,40 – (l/2),1,Math.max(1,l));
步进索引=0;
尝试下一段26 分钟的音频,发现波形并不能渲染出来。按照之前的方案,一段26 分钟的音频,画布的宽度已经高达16 万多像素,这时肯定需要将帆布分段\\ u200b
以2分钟为一段,将26 分钟多的视频分为14 段,算出对应宽度并定位布局
模板
div id='应用程序'
音频控件:src=' URL ' @ loaded metadata=' load data '/audio
div class='wave-box '
差异
v-for='画布中的项目'
:key='item.key '
:ref='`timeline${item.key} ` '
高度='20 '
:width='360px 'height='auto' /
:style='{ left: `${item.left}px`} '
' class='时间轴'
/画布
/div
差异
v-for='画布中的项目'
:key='item.key '
:ref='`wave${item.key} ` '
高度='80 '
:width='360px 'height='auto' /
:style='{ left: `${item.left}px`} '
'挥手'
/画布
/div
/div
/div
/模板
loadData(e) {
这个。总时间=e .目标。持续时间;
this.width='360px 'height='auto' /
const w=this.unitwidth='360px 'height='auto' /
const num=数学。ceil(这个。宽度=' 360像素'高度='自动'/
const canvas=[];
对于(设I=0;i numi=1) {
画布。推({
宽度=' 360像素,高度='自动'/
左:我* w,
this.canvas=画布;
这个. nextTick(()={
对于(设I=0;我这个。画布。长度;i=1) {
这个[`wave${i}Ctx`]=这个refs[`wave${i}`][0].获取上下文(“2d”);
this[`timeline${i}Ctx`]=this .$refs[`timeline${i}`][0].获取上下文(“2d”);
这个。获取数据();
【更多音视频学习资料,点击下方链接免费领取,先码住不迷路~】
点击领取音视频开发基础知识和资料包
时间轴分段渲染
时间轴渲染方案较原先做一点改版,每一秒时间先得到对应帆布画布的索引,在对应画布上渲染
drawTimeline() {
对于(设I=0;我数学。ceil(这个。总时间);i=1) {
const n=数学。地板(I/120);
这[`timeline${n}Ctx`] .font=” 10px Arial
这[`timeline${n}Ctx`] .fillStyle='rgba(0,0,0,1)'
这[`timeline${n}Ctx`] .fillRect(this.unitwidth='360px 'height='auto' /
如果(i 0) {
这[`timeline${n}Ctx`] .fillText(
持续时间(一).拆分('')[0],
this.unitwidth='360px 'height='auto' /
同理,渲染每个像素点前,得到对应的画布
drawWave(channelData,step) {
设步进索引=0;
设xIndex=0;
设min=1;
设max=-1;
对于(设I=0;我通道数据长度;i=1) {
步进指数=1;
const item=channel data[I]| | 0;
如果(最小项目){
最小值=项目;
} else if (item max) {
最大=项目
if (stepIndex=step) {
xIndex=1;
const n=数学。楼层(xIndex/(120 *这个。单位宽度=' 360像素'高度='自动'/
这个[`wave${n}Ctx`] .fillStyle='rgba(0,0,0,0.3)'
const l=(max-min)* 40 * 0.8;
这个[`wave${n}Ctx`] .填充rect(xIndex %(120 * this。单位宽度=' 360像素'高度='自动'/
步进索引=0;
最终效果
在绘制波形的时候,我发现浏览器会直接被阻塞卡死,这里我加上了时间跟状态(加载数据、解码、绘图、完成)
上面可交换的图像格式中可以看出,在3.7 秒时卡住,状态直接跳过了”绘图”
我们再给绘图加上日志,看花费了多少时间
draw(channelData) {
console.time('时长');
这个。绘制时间轴();
this.drawWave(channelData,step);
console.timeEnd('时长');
可以看到,整个绘图过程花费一秒多,在这一秒内,浏览器被阻塞无法做任何动作。所以我想将这些任务根据帆布的数量拆分开来,异步地一个个去执行
这里将渲染时间轴方法写成单个任务,每次执行渲染一个画布
绘制时间线(n) {
常数start=n * 120
const end=n===this。画布。长度-1?数学。ceil(这个。总时间):开始120;
对于(设i=开始我结束;i=1) {
这[`timeline${n}Ctx`] .font=” 10px Arial
这[`timeline${n}Ctx`] .fillStyle='rgba(0,0,0,1)'
这[`timeline${n}Ctx`] .fillRect(this.unitwidth='360px 'height='auto' /
如果(i % 120) {
这[`timeline${n}Ctx`] .fillText(
持续时间(一).拆分('')[0],
this.unitwidth='360px 'height='auto' /
同理,将波形渲染方法写成单个任务,并计算当前任务的通道数据数据的位置
drawWave(channelData,step,n) {
设步进索引=0;
设xIndex=0;
设min=1;
设max=-1;
const数据长度=120 * this。采样率;//每2分钟的数据长度时间* 采样频率
const start=n *数据长度
const end=n===this。画布。长度-1?数学。ceil(这个。总时间)*这个。采样率:起始数据长度;
对于(设i=开始我结束;i=1) {
步进指数=1;
const item=channel data[I]| | 0;
如果(最小项目){
最小值=项目;
} else if (item max) {
最大=项目
if (stepIndex=step) {
xIndex=1;
这个[`wave${n}Ctx`] .fillStyle='rgba(0,0,0,0.3)'
const l=(max-min)* 40 * 0.8;
这个[`wave${n}Ctx`] .fillRect(xIndex,40 – (l/2),1,Math.max(1,l));
步进索引=0;
最后利用设置超时一个个执行任务
draw(channelData) {
对于(设I=0;我这个。画布。长度;i=1) {
setTimeout(()={
这个。绘制时间线(一);
this.drawWave(channelData,step,I);
},I * 100);
从可交换的图像格式可以看到原本需要卡顿一秒,现在被分割成多个任务异步渲染,也将卡顿时间拆分,增加用户体验
这次再来挑战一小时54 分钟音频
上图看到即便做了异步的优化,但本质不能减少卡顿时间,长时间音频依然得花大量时间计算、渲染。
那如何去减少卡顿的时间?
我们知道卡顿是因为有大量的数据去计算波形的最大值、最小值,计算过程中会直接阻塞浏览器从而造成卡顿。减少卡顿必须得对这段计算进行”特殊处理”。
可以想到不阻塞浏览器有网络工作者和requestIdleCallback,熟悉反应的同学肯定知道,我们可以通过requestIdleCallback api去把计算任务放到浏览器空闲时间去做
下面我将一次渲染任务一分为10,每次渲染12 秒音频波形,浏览器空闲时间执行12 秒波形渲染,首先创建一个工作任务类
课程任务{
构造函数(ctx,{
数量、总目标、采样率、总时间、步长、通道数据、
这个数字=数字
这个。通道数据=通道数据;
这个。start=(num-1)* 120 *采样率;
this.end=num===totoal?(总时间-(num-1)* 120)*采样率:num * 120 *采样率;
这个。步进索引=0;
这个。min=-1;
这个。max=0;
这个。xindex=0;
这一步=一步
this.ctx=ctx
这个。次数=1;
this.maxTimes=10
这个。渲染长度=数学。ceil((这个。结束这一切。开始)/这个。最大次数);
if (this.times tiis.maxTimes) {
返回空
const start=(this。乘以-1)*这个。渲染长度。开始;
const end=this。倍===这个。最大次数?这个。结束:开始这个。渲染长度;
对于(设i=开始我结束;i=1) {
这个。步进指数=1;
常量项=this。通道数据[I]| | 0;
if (item this.min) {
this.min=item
} else if (item this.max) {
this.max=item
if (this.stepIndex=this.step) {
这个。xindex=1;
this.ctx.fillStyle='rgba(0,0,0,0.1)'
const l=(this。麦克斯-这个。min)* 40 * 0.8;
this.ctx.fillRect(this.xIndex,40 – (l/2),1,Math.max(1,l));
这个。步进索引=0;
这个。min=1;
这个。max=-1;
这个。次数=1;
还这个;
导出默认任务;
上面工作类作用就是每调用奔跑方法就渲染12 秒的波形,第11 次调用后会返回空告诉程序渲染下一条画布
【更多音视频学习资料,点击下方链接免费领取,先码住不迷路~】
点击领取音视频开发基础知识和资料包
drawWave(channelData,step) {
const drawWork=(截止日期)={
if (deadline.timeRemaining() 0) {
if (i=this.canvas.length) {
如果(!绘制){
绘制=新任务(这[`wave${i – 1}Ctx`],{
总计:this.canvas .长度,
sampleRate: this.sampleRate
totalTime: this.totalTime,
channelData,
画=画。run();
如果(!绘制){
if (i=this.canvas.length) {
requestIdleCallback(绘制工作);
if (requestIdleCallback) {
requestIdleCallback(drawWork,{ time out:1000 });
//不支持requestIdleCallback采用异步方案
this.status='完成;
效果明显好多了,但是任务分得越多越细,波形对应时间的误差也会越大。因为任务分10 段时,数据是直接除以10 的,不一定是步骤的倍数,所以会造成偏差,下面再做一步优化
这个。渲染长度=数学。ceil((这个。结束这一切。开始)/步骤/这个。最大次数)*步长同理时间轴也可以做时间切片优化,这里就不介绍了\\ u200b
这次换一个3小时43 分钟的音频,页面直接崩溃
查明原因是在解码阶段解码音频数据方法解析不了太大数据,下面就对请求到的缓冲器进行分段,分别去解码\\ u200b
方案有2种,一是使用承诺。所有得到排列数据
解码音频数据(缓冲区){
const audioCtx=new(窗口音频上下文| |窗口。webkitaudiocontext)();
const promise array=[];
常量大小=1024 * 1024 * 0.1
const num=数学。细胞(缓冲。字节长度/大小);
对于(设I=0;i numi=1) {
常数p=新承诺((解决)={
audioCtx.decodeAudioData(
buffer.slice(大小* i,大小*(I ^ 1)),
(音频缓冲区)={
const { sampleRate }=音频缓冲器
常量通道数据=音频缓冲区。getchanneldata(0);
这个。采样速率=采样速率;
解析(渠道数据);
无极阵。推(p);
Promise.all(promiseArray)。然后((res)={
控制台。日志(分辨率);
得到的结果
解码音频数据其实也有承诺写法,但回调函数语法兼容性更好,我只看铬对我没影响
另一种是递归,得到数组通道数据数据
解码音频数据(缓冲区){
const audioCtx=new(窗口音频上下文| |窗口。webkitaudiocontext)();
常量大小=1024 * 1024 * 0.1
const num=数学。细胞(缓冲。字节长度/大小);
常量通道数据=[];
让len=0
const decode=()={
audioCtx.decodeAudioData(
buffer.slice(大小* i,大小*(I ^ 1)),
(音频缓冲区)={
const { sampleRate }=音频缓冲器
这个。采样速率=采样速率;
const CD=音频缓冲器。getchanneldata(0);
channelData.push(光盘);
莱恩=cd.length//记录通道数据长度
如果(识别号){
控制台。日志(通道数据);
2种方案得到数据一样,用时上承诺。所有更优秀,但我更倾向于递归方案,因为还可以做后续优化
接着因为通道数据的数据结构变了,从原来TypedArray变成数组类型数组,再加个获取通道数据对应数据的方法
getChannelDataItem(index) {
if (this.cdl index) {
归还这个。渠道数据【这个。我][索引-这个。cdll];
这个。I=1;
for(;这个。我这个。频道数据。长度;this.i=1) {
这个。cdll=这个。CDL;
这个。CDL=这个。渠道数据【这个。我].长度;
if (this.cdl index) {
归还这个。渠道数据【这个。我][索引-这个。cdll];
这个方法里加一层缓存,减少计算量,因为传入的参数指数是稳定累加的
【更多音视频学习资料,点击下方链接免费领取,先码住不迷路~】
点击领取音视频开发基础知识和资料包
最终效果
但是问题来了,这解码音频数据解码时间也太长了,上图显示花费了25 秒,总不能让用户干等着,况且还没算上加载、渲染时间
所以这里再做一下优化,解码多少数据,优先渲染多少数据,不必等到所有数据解码后再渲染
解码音频数据(缓冲区){
const audioCtx=new(窗口音频上下文| |窗口。webkitaudiocontext)();
常量大小=1024 * 1024 * 0.1
const num=数学。细胞(缓冲。字节长度/大小);
常量通道数据=[];
设len=0;
let buffer data=new float 32 array();//储存多余数据
const decode=()={
audioCtx.decodeAudioData(
buffer.slice(大小* i,大小*(I ^ 1)),
(音频缓冲区)={
const { sampleRate }=音频缓冲器
这个。采样速率=采样速率;
const CD=音频缓冲器。getchanneldata(0);
channelData.push(光盘);
常数步长=数学。地板(这个。总时间*采样率/(100 *数学。ceil(这个。总时间)));
if (i===1) {
this.draw(null,step);//这里改成渲染时间轴
if (bufferData.length 0) {
const data=新的float 32数组(缓冲区数据。长度CD。长度);
data.set(bufferData,0);
data.set(cd,缓冲数据。长度);//合并数据
this.drawWave(data.slice(0,data.length – data.length % step),len,step);
len=(数据长度-数据长度%步长);
缓冲数据=数据。切片(数据。长度-数据。长度%步长,数据。长度);
} else { //第一次解码
this.drawWave(cd.slice(0,cd.length – cd.length % step),len,step);
len=(cd.length – cd.length %步长);//记录解码的累计长度
缓冲数据=CD。切片(CD。长度-CD。长度%步长。长度);//存储多余的数据
如果(识别号){
这里加了bufferData变量,主要是因为解码出来的数据长度假设为44500,而步骤为400 (400个数据渲染一个像素)最终渲染出111 个像素,多出来的100 个数据就被抛弃,造成了误差\\ u200b
所以这里加了bufferData来储存多余数据,下次译后拼接起来
接着修改牵引波方法
拉伸波(通道数据,长度,步长){
让我们抽签=新任务(这个,{
channelData,
x:镜头/步长,
unitwidth=' 360px像素像素,height='auto' /
const drawWork=(截止日期)={
如果(截止日期。剩余时间()10){
画=画。run();
如果(绘制){
requestIdleCallback(drawWork,{ time out:1000 });
if (requestIdleCallback) {
requestIdleCallback(drawWork,{ time out:1000 });
然后修改下工作类,之前是基于时间,每2分钟一个工作实例,再分成10 分运行计算。现在则是基于解码音频数据解码出来的一段channelData,每一段一个工作实例,持续调用奔跑方法结算数据、渲染\\ u200b
课程任务{
构造函数(虚拟机,{
channelData,
unitwidth=' 360px像素像素,height='auto' /
this.w=unitwidth='360px 'height='auto' /
这个。通道数据=通道数据;
this.vm=vm
this.x=x
这一步=一步
this.maxTimes=20
这个。渲染长度=数学。ceil(通道数据。长度/步长/这个。最大次数)*步长;
stepIndex=0
if (this.times this.maxTimes) {
返回空
const start=(this。乘以-1)*这个。渲染长度;
const end=开始这个。渲染长度;
对于(设i=开始我结束;i=1) {
这个。步进指数=1;
常量项=this。通道数据[I]| | 0;
if (item this.min) {
this.min=item
} else if (item this.max) {
this.max=item
if (this.stepIndex=this.step) {
这个。x=1;
const num=数学。地板(这个。x/这个。w);
if (this.vm[`wave${num}Ctx`]) {
this.vm[`wave${num}Ctx`] .fillStyle='rgba(0,0,0,0.1)'
const l=(this。麦克斯-这个。min)* 40 * 0.8;
this.vm[`wave${num}Ctx`] .fillRect((this.x % this.w),40 – (l/2),1,Math.max(1,l));
这个。步进索引=0;
这个。min=1;
这个。max=-1;
这个。次数=1;
还这个;
导出默认t;
这里的最大次数并不一定写死,最好能把单个任务控制在10ms左右
最终结果
相比下再也不用等待25 秒了。但是似乎还是有点卡顿?
用表演来查看下,是什么导致的。选中英国制药学会会员红色的点(表示帧数低)查看火焰图上耗时的函数,再点击具体代码位置
结果是储存bufferData花费了很多时间,打印下具体时间
那换成不储存,浪费就浪费误差就误差试试效果
仔细看是有好一点,具体的取舍还是看实际业务吧\\ u200b
如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区讨论!