使用.NET Core搭建分布式音频效果处理服务(三)完成音频合成效果处理程序

使用.NET Core搭建分布式音频效果处理服务(三)完成音频合成效果处理程序

上一节我们已经介绍了FFmpeg在Net Core中的简单应用,这一节我们将根据之前的功能需求和解决方案,进行项目的详细设计工作。

 

画个流程图

先阐述一下流程,如下图:

使用.NET Core搭建分布式音频效果处理服务(三)完成音频合成效果处理程序

整个流程其实非常简单,客户端(无论桌面软件、还是原生APP、还是HTML网页)通过一个统一的接口进行调用,我们这里定义这个接口名称叫AudioSynthesisSync吧,为何名称后面还要加个同步(也是命名规范),目前来说这个接口就属于同步的,异步方式后续再一一解答。

当得到指定的两个(前景和背景)参数后,服务器去自动下载(或本地缓存)参数指定的音频文件,将这两个音频带入到ffmpeg处理程序中进行音频合成和处理(处理效果包括指定时间剪辑、淡入和淡出、拼接多个音频、合并两个音频、音量缩放,具体参数参见ffmpeg官方文档),等待音频处理完成后得到一个新的文件,将这个文件上传到指定云(笔者这边用的是阿里云OSS),上传完成后将阿里云的云文件访问地址存储并返回到请求的客户端,整个流程结束。

反观上面整个流程,其实就三个步骤的操作,下载文件,处理文件,上传文件。ok,那么我们只需要一个Controller即可,这里我们声明为MediaApiController吧。

 

创建默认构造函数

大家现在都码砖都喜欢用依赖注入的方式去构建某个类,笔者也不例外,依赖注入的优势很多很多,你可以使用netcore自带,asp.net core,autofac,unity等优秀的框架做依赖注入,笔者这里不做详细阐述。

笔者这里使用autofac注入了大量乱七八糟的类(说是乱七八糟,实际是笔者创建的一个easyHub的框架,日后会一一分享出来),有注释。

 1 public class MediaApiController: BaseApiController
 2 
 3 {
 4    // 消息总线
 5     private readonly IMsgBusService _iMsgBusService;
 6    // 数据库操作工厂
 7     private readonly IDataOpService _dataOpService;
 8    // 缓存操作工厂
 9     private readonly ICacheAsyncService _iCacheAsyncService;
10    // 宿主环境
11     private readonly IHostingEnvironment _ihostingEnvironment;
12    // 音频处理单元
13     private readonly AudioHandlerWorkUnit _audioHandlerWorkUnit;
14 
15     public MediaApiController(IDataOpService iDataOpService,
16 
17     ICacheAsyncService iCacheAsyncService,
18 
19     IMsgBusService imsgBusService,
20 
21     IHostingEnvironment iHostingEnvironment,
22 
23     IDurationMath durationMath)
24 
25     {
26 
27         _dataOpService = iDataOpService;
28 
29         _iMsgBusService = imsgBusService;
30 
31         _iCacheAsyncService = iCacheAsyncService;
32 
33         _ihostingEnvironment = iHostingEnvironment;
34 
35         _audioHandlerWorkUnit = new AudioHandlerWorkUnit(iDataOpService: _dataOpService,
36 
37         iCacheAsyncService: _iCacheAsyncService,
38 
39         imsgBusService: _iMsgBusService,
40 
41         iHostingEnvironment: _ihostingEnvironment,
42 
43         iDurationMath: durationMath
44 
45         );
46 
47     }
48 
49 }

 

在MediaApiController构造函数中,笔者将需要使用的中间服务一股脑的全部注入到控制器中,其中iDataOpService接口实现了数据库的操作,iCacheAsyncService实现了缓存操作,imsgBusService实现了消息队列操作,iHostingEnvironment是netcore存储的宿主环境参数和变量,durationMath实现了处理耗时,AudioHandlerWorkUnit是音频处理的主要方法类。

当然,还有细心的朋友发现了,笔者这边不是继承于Controller类型,而是继承于BaseApiController类型,在net api中可没有这个类型的啊。的确,这是笔者自定义的一个类型,当然父类肯定继承于Controller下,为何笔者喜欢这样中间再多继承一层,不是多次一举吗?不是,笔者这边简单的介绍一下:

Controller的功能和特性这里不做阐述,如果我们要在netcore中实现web请求,那么必须继承于该Controller类型。笔者喜欢使用AOP编程模式进行码砖,这样能遵循开闭原则,并且便于维护,在BaseApiController中,笔者重写了OnActionExecuting,OnActionExecuted,OnActionExecutionAsync三个主要方法,便于在请求处理过程中,对出和入进行过滤、处理时间的计算、返回内容的重构等等进行统一规范。(项目源码会在日后新开的EasyHub框架中详细介绍)

 

创建音频信息模型

在我们创建接口之前,需要规范来回传输数据结构上面的一些信息,比如音频文件名、格式、持续时间、编码率等等一些基础信息。

因此建立一个模型叫AudioInfo

 1 public class AudioInfo
 2 
 3 {
 4 
 5     public string filename {
 6         get;
 7         set;
 8     }
 9 
10     public int nb_streams {
11         get;
12         set;
13     }
14 
15     public int nb_programs {
16         get;
17         set;
18     }
19 
20     public string format_name {
21         get;
22         set;
23     }
24 
25     public string format_long_name {
26         get;
27         set;
28     }
29 
30     public string start_time {
31         get;
32         set;
33     }
34 
35     public double duration {
36         get;
37         set;
38     }
39 
40     public long size {
41         get;
42         set;
43     }
44 
45     public int bit_rate {
46         get;
47         set;
48     }
49 
50     public int probe_score {
51         get;
52         set;
53     }
54 
55 }

通过ffprobe命令获取一个音频文件后,数据实例化样本如下

[format, {
    "filename": "text.mp3",
    "nb_streams": 1,
    "nb_programs": 0,
    "format_name": "mp3",
    "format_long_name": "MP2/3 (MPEG audio layer 2/3)",
    "start_time": "0.000000",
    "duration": "25.568875",
    "size": "409102",
    "bit_rate": "128000",
    "probe_score": 51
}]

 

各项参数意思不用过多解释相信大家从单词上就够能明白。

 

创建接口

接下来我们创建一个接口,命名为AudioSynthesisSync。

 1      /// <summary>
 2         /// 同步合成两个音频文件并上传到阿里云
 3         /// </summary>
 4         /// <param name="frontFileUrl">输入文件1,一般是前景读音</param>
 5         /// <param name="backgounedAudioIndex">背景音乐文件</param>
 6         /// <remarks>
 7         /// 背景音乐文件,需要在应用程序根目录下面创建StaticResurces文件夹
 8         /// 并将数据库BackGroundAudioListModels中AudioUrl路径文件名相对应
 9         /// 合成时间将受到音频时长、CPU性能严重相关而定。接口适合较短(10秒内的音频合成)
10         /// </remarks>
11         /// <returns>合成后音频文件的URL地址</returns>
12         [HttpGet]
13         [Route("AudioSynthesisSync")]
14         public JsonResult AudioSynthesisSync(string frontFileUrl, int backgounedAudioIndex)
15         {
16             if (string.IsNullOrEmpty(frontFileUrl))
17                 return new JsonResult("文件不能为空") {StatusCode = ClientStatusCode.ClientParameterError};
18             if (frontFileUrl.Contains("https://"))
19                 return new JsonResult("不支持https加密协议") {StatusCode = ClientStatusCode.ClientParameterError};
20             if (!frontFileUrl.Contains("http://"))
21                 return new JsonResult("文件必须存在于网络") {StatusCode = ClientStatusCode.ClientParameterError};
22 
23             var mixInfo = _audioHandlerWorkUnit.SynthesisAudio(frontFileUrl, backgounedAudioIndex, "");
24 
25             if (mixInfo.GetType() == typeof(JsonResult))
26             {
27                 // 存在错误的时候直接返回错误
28                 return mixInfo;
29             }
30 
31             return new JsonResult(new Dictionary<string, object>
32             {
33                 {"web_url", mixInfo.WebUrl},
34                 {
35                     "duration", new Dictionary<string, object>()
36                     {
37                         {"download", mixInfo.downloadDuration.GetTotalDuration()},
38                         {"synthesis", mixInfo.synthesisDuration.GetTotalDuration()},
39                         {"upload", mixInfo.uploadDuration.GetTotalDuration()}
40                     }
41                 },
42                 {
43                     "fileInfo", new Dictionary<string, object>()
44                     {
45                         {"front_audio", mixInfo.synthesisAudioinfo.FrontAudioInfo},
46                         {"back_audio", mixInfo.synthesisAudioinfo.BackAudioInfo},
47                         {"synthesis_audio", mixInfo.synthesisAudioinfo.SynthesisInfo}
48                     }
49                 }
50             }) {StatusCode = ClientStatusCode.Ok};
51         }

 

而SynthesisAudio函数的源码如下(源码过长,有兴趣的朋友可以自行实现自己想要的音频处理逻辑和效果,全然当此段为参考范本)

使用.NET Core搭建分布式音频效果处理服务(三)完成音频合成效果处理程序使用.NET Core搭建分布式音频效果处理服务(三)完成音频合成效果处理程序
  1 public dynamic SynthesisAudio(string frontFileUrl, int backgounedAudioIndex, string taskName)
  2         {
  3             // 将当前任务添加到队列列表池中,用于限制本机最大队列数量,防止单机队列过多而死机
  4             CurrentQueueTask.Add(taskName);
  5 
  6             ProcessState.CurrentAudioProcessingState = AudioProcessingState.StartHandler;
  7             _iCacheAsyncService.SetDatabase(0);
  8             _iCacheAsyncService.SetStringAsync(taskName,
  9                 JsonConvert.SerializeObject(new ProgressPrompt()
 10                 {
 11                     Remarks = ProcessState.GetState(),
 12                     Progress = 10
 13                 }),
 14                 TimeSpan.FromDays(KeyExpire));
 15 
 16             var totalDuration = new DurationMath();
 17             var synthesisDuration = new DurationMath();
 18             var downloadDuration = new DurationMath();
 19             var uploadDuration = new DurationMath();
 20             var backgroundInfo = new BackGroundAudio();
 21 
 22             string aliyunReturnUrl;
 23             MixedInfo synthesisAudioinfo;
 24 
 25             try
 26             {
 27                 #region 获取前景和背景音频文件
 28 
 29                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.DownloadAudio;
 30                 downloadDuration.Start();
 31 
 32                 using (var r = GetFrontFileAndBackGroundAudio(frontFileUrl, backgounedAudioIndex))
 33                 {
 34                     if (!r.IsExceptionReturn)
 35                     {
 36                         backgroundInfo = (BackGroundAudio) r.ReturnObjects;
 37 
 38                         if (backgroundInfo == null)
 39                         {
 40                             _iCacheAsyncService.SetStringAsync(taskName,
 41                                 JsonConvert.SerializeObject(new ProgressPrompt()
 42                                 {
 43                                     Remarks =
 44                                         $"InExpcetion_{DateTime.Now}_{taskName}_request_audio_index_not_found_for_{backgounedAudioIndex}",
 45                                     Progress = 10
 46                                 }),
 47                                 TimeSpan.FromDays(KeyExpire));
 48 
 49                             return new JsonResult($"请求的背景音乐索引不存在{backgounedAudioIndex}")
 50                                 {StatusCode = ClientStatusCode.ClientParameterError};
 51                         }
 52 
 53                         backgroundInfo.AudioUrl = AppDomain.CurrentDomain.BaseDirectory
 54                                                   + "StaticResources/"
 55                                                   + backgroundInfo.AudioUrl;
 56 
 57                         if (!System.IO.File.Exists(backgroundInfo.AudioUrl))
 58                         {
 59                             _iCacheAsyncService.SetStringAsync(taskName,
 60                                 JsonConvert.SerializeObject(new ProgressPrompt()
 61                                 {
 62                                     Remarks =
 63                                         $"InExpcetion_{DateTime.Now}_{taskName}_request_audio_path_not_found_for_{backgroundInfo.AudioUrl}",
 64                                     Progress = 100
 65                                 }),
 66                                 TimeSpan.FromDays(KeyExpire));
 67 
 68                             return new JsonResult($"背景音频文件物理路径不存在{backgroundInfo.AudioUrl}")
 69                                 {StatusCode = ClientStatusCode.ClientParameterError};
 70                         }
 71                     }
 72                     else
 73                     {
 74                         _iCacheAsyncService.SetStringAsync(taskName,
 75                             JsonConvert.SerializeObject(new ProgressPrompt()
 76                             {
 77                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
 78                                 Progress = 100
 79                             }),
 80                             TimeSpan.FromDays(KeyExpire));
 81 
 82                         return new JsonResult($"文件获取失败,详见错误日志({r.ExceptionCode})输出")
 83                             {StatusCode = ClientStatusCode.ServerHandlerError};
 84                     }
 85                 }
 86 
 87                 _iCacheAsyncService.SetStringAsync(taskName,
 88                     JsonConvert.SerializeObject(new ProgressPrompt()
 89                     {
 90                         Remarks = ProcessState.GetState(),
 91                         Progress = 30
 92                     }),
 93                     TimeSpan.FromDays(KeyExpire));
 94 
 95                 downloadDuration.Stop();
 96 
 97                 #endregion
 98 
 99                 #region 音频合成
100 
101                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.SynthesisAudio;
102                 synthesisDuration.Start();
103                 using (var r = GetCustomMixedTwoAudio(frontFileUrl, backgroundInfo.AudioUrl))
104                 {
105                     if (!r.IsExceptionReturn)
106                     {
107                         synthesisAudioinfo = (MixedInfo) r.ReturnObjects;
108                     }
109                     else
110                     {
111                         _iCacheAsyncService.SetStringAsync(taskName,
112                             JsonConvert.SerializeObject(new ProgressPrompt()
113                             {
114                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
115                                 Progress = 100
116                             }),
117                             TimeSpan.FromDays(KeyExpire));
118 
119                         return new JsonResult($"音频合成失败,详见错误日志({r.ExceptionCode})输出")
120                         {
121                             StatusCode = ClientStatusCode.ServerHandlerError
122                         };
123                     }
124                 }
125 
126                 _iCacheAsyncService.SetStringAsync(taskName,
127                     JsonConvert.SerializeObject(new ProgressPrompt()
128                     {
129                         Remarks = ProcessState.GetState(),
130                         Progress = 60
131                     }),
132                     TimeSpan.FromDays(KeyExpire));
133 
134                 synthesisDuration.Stop();
135 
136                 #endregion
137 
138                 #region 上传到阿里云
139 
140                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.UploadAudio;
141                 uploadDuration.Start();
142 
143                 using (var r = UploadTheAliyun(synthesisAudioinfo))
144                 {
145                     if (!r.IsExceptionReturn)
146                     {
147                         aliyunReturnUrl = (string) r.ReturnObjects;
148                     }
149                     else
150                     {
151                         _iCacheAsyncService.SetStringAsync(taskName,
152                             JsonConvert.SerializeObject(new ProgressPrompt()
153                             {
154                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
155                                 Progress = 100
156                             }),
157                             TimeSpan.FromDays(KeyExpire));
158 
159                         return new JsonResult($"上传阿里云失败,详见错误日志({r.ExceptionCode})输出")
160                         {
161                             StatusCode = ClientStatusCode.ServerHandlerError
162                         };
163                     }
164                 }
165 
166                 _iCacheAsyncService.SetStringAsync(taskName,
167                     JsonConvert.SerializeObject(new ProgressPrompt()
168                     {
169                         Remarks = ProcessState.GetState(),
170                         Progress = 80
171                     }),
172                     TimeSpan.FromDays(KeyExpire));
173                 uploadDuration.Stop();
174 
175                 #endregion
176 
177                 #region 存储到数据库
178 
179                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.UpdateDatabase;
180                 var dataOptionException = "";
181                 _dataOpService.DataOperatedCallBackEvent += (sender, args) =>
182                 {
183                     if (!string.IsNullOrEmpty(args.Exceptions))
184                     {
185                         _iCacheAsyncService.SetStringAsync(taskName,
186                             JsonConvert.SerializeObject(new ProgressPrompt()
187                             {
188                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
189                                 Progress = 100
190                             }),
191                             TimeSpan.FromDays(KeyExpire));
192 
193                         dataOptionException = args.Exceptions;
194                     }
195                 };
196 
197                 // 将异步最终结果放入到数据库中,便于多次查询
198                 var addResult = _dataOpService.AddEntity(new AudioSynthesisAsyncResult
199                 {
200                     HandlerResult = JsonConvert.SerializeObject(synthesisAudioinfo),
201                     TaskName = taskName,
202                     web_url = aliyunReturnUrl,
203                     Duration = JsonConvert.SerializeObject(new Dictionary<string, object>()
204                     {
205                         {"download", downloadDuration.GetTotalDuration()},
206                         {"synthesis", synthesisDuration.GetTotalDuration()},
207                         {"upload", uploadDuration.GetTotalDuration()}
208                     })
209                 }, new MediaContext(new DatabaseConfig())).Result;
210 
211                 if (!string.IsNullOrEmpty(dataOptionException))
212                 {
213                     _iCacheAsyncService.SetStringAsync(taskName,
214                         JsonConvert.SerializeObject(new ProgressPrompt()
215                         {
216                             Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
217                             Progress = 100
218                         }),
219                         TimeSpan.FromDays(KeyExpire));
220 
221                     return new JsonResult("数据库操作失败,详见错误日志(Error)输出")
222                     {
223                         StatusCode = ClientStatusCode.ServerHandlerError
224                     };
225                 }
226 
227                 #endregion
228 
229                 if (addResult > 0)
230                 {
231                     ProcessState.CurrentAudioProcessingState = AudioProcessingState.InCompleted;
232                     _iCacheAsyncService.SetStringAsync(taskName,
233                         JsonConvert.SerializeObject(new ProgressPrompt()
234                         {
235                             Remarks = ProcessState.GetState(),
236                             Progress = 100
237                         }),
238                         TimeSpan.FromDays(KeyExpire));
239                 }
240 
241                 // 屏蔽返回字符串中详细的IO地址
242                 synthesisAudioinfo.FrontAudioInfo.filename =
243                     Path.GetFileName(synthesisAudioinfo.FrontAudioInfo.filename);
244                 synthesisAudioinfo.BackAudioInfo.filename =
245                     Path.GetFileName(synthesisAudioinfo.BackAudioInfo.filename);
246                 synthesisAudioinfo.SynthesisInfo.filename =
247                     Path.GetFileName(synthesisAudioinfo.SynthesisInfo.filename);
248                 totalDuration.Stop();
249 
250                 CurrentQueueTask.Remove(taskName);
251                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.EmptyHandler;
252                 return new AudioSynthesisSyncResult
253                 {
254                     WebUrl = aliyunReturnUrl,
255                     downloadDuration = downloadDuration,
256                     synthesisDuration = synthesisDuration,
257                     uploadDuration = uploadDuration,
258                     synthesisAudioinfo = synthesisAudioinfo
259                 };
260             }
261             catch (Exception e)
262             {
263                 Console.WriteLine(e);
264                 CurrentQueueTask.Remove(taskName);
265                 return e;
266             }
267         }
View Code

 

 

本节总结

先画个图:

使用.NET Core搭建分布式音频效果处理服务(三)完成音频合成效果处理程序

画的比较简单:-)

当客户端请求该接口的时候,逻辑服务器收到请求,并通过ffmpeg进行处理,当ffmpeg处理完成后,将新的文件上传到云OSS,通过逻辑服务器再将云URL将返回给请求客户端,很简单。

 

注意问题

按照如上的代码,很快便实现了这个项目的需求,可能最多也就三天时间吧(包括测试)。心里暗示,嗯,功能我完成了,可以向上汇报了。

可细心的朋友、或有过多年WEB服务器工作经验的朋友就会发现一个至关重要的问题:这个功能接口的TPS非常的慢。为何这么说,单机的性能是固定的,而用户所录制的音频时长却不是固定的,假如就算限制为3分钟的录音(微信最长才60秒),那么服务器会面临1s-180s区间不同的处理耗时,即使用上目前最好的多路CPU,恐怕也不可能在180s的处理需求中达到毫秒级的处理响应吧。

我们这样来假设一个场景(嗯,有点机器学习的味道o(∩_∩)o),用户录音为10秒,背景声音为15秒(前后加点听觉缓冲:渐入和渐出),笔者多次测试过,包括用上E-2680 v2版的CPU,忽略下载和上传时间,FFMPEG处理时间仍然需要3秒(FFMPEG的处理模型是如何工作的我不清楚,加入-threads参数也无济于事)。因此可得到这样一个公式:

使用.NET Core搭建分布式音频效果处理服务(三)完成音频合成效果处理程序

如果这台服务器大部分时间都耗在处理一个任务上,那么出现多个请求都在这个接口上呢,那么时间将会更加的长,笔者试过一次并发10个请求(对于处理时间以秒为单位的状况,10个并发对单机是很吓人的),结果最后一个请求结果得到的时间是48秒,哈哈,客户端肯定是无法等待这么长的时间的,而且这个10个请求中,有2个请求出现了运行时错误...

这样的问题很严重,就算功能实现了也决不能部署到生产环境中,也是笔者开辟这个系列的主要解决目标。也许你有更好的思路或者建议,欢迎大家一起讨论。下一节开始介绍笔者的思路。

 

感谢阅读

上一篇:用Python预测某某国际平台概率分析(目录)v0.41


下一篇:使用.NET Core搭建分布式音频效果处理服务(六)让Middleware自动Invoke