|
@@ -0,0 +1,628 @@
|
|
|
|
|
+import { common, ConfigurationConstant } from '@kit.AbilityKit';
|
|
|
|
|
+import { CommonConstants } from '../common/constants/CommonConstants';
|
|
|
|
|
+import { VideoItem } from '../viewmodel/VideoItem';
|
|
|
|
|
+import { AppUtil, FileUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
|
|
|
|
|
+import { convertSpecialToWav } from '../common/util/MusicTagUtils';
|
|
|
|
|
+import { taskpool, util } from '@kit.ArkTS';
|
|
|
|
|
+import MediaTable from '../common/util/MediaTable';
|
|
|
|
|
+import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
|
|
|
|
|
+import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
+import { PlayStatus } from '../common/PlayStatus';
|
|
|
|
|
+import { generateOutputPath, getFileNameWithoutExtension, setRingTone } from './EditAudio';
|
|
|
|
|
+import PermissionUtil from '../common/util/PermissionUtil';
|
|
|
|
|
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
|
|
|
|
|
+import { Utility } from '../common/util/Utility';
|
|
|
|
|
+import { changeFileExtension, isSpecialAudioFormat, saveDb } from './ExtractAccompaniment';
|
|
|
|
|
+
|
|
|
|
|
+// 变速变调功能
|
|
|
|
|
+@Component
|
|
|
|
|
+export struct ChangeTone {
|
|
|
|
|
+ onResult = (_result: boolean, outPath: string) => {
|
|
|
|
|
+ }
|
|
|
|
|
+ onBack = () => {
|
|
|
|
|
+ }
|
|
|
|
|
+ onPlayOrPause = () => {
|
|
|
|
|
+ }
|
|
|
|
|
+ onSeeOutputPath = (outPutPath: string) => {
|
|
|
|
|
+ }
|
|
|
|
|
+ onSliderChange = (value: number) => {
|
|
|
|
|
+ }
|
|
|
|
|
+ @Prop currentPath: string;
|
|
|
|
|
+ @State isEditing: boolean = false;
|
|
|
|
|
+ @Link progressValue: number;
|
|
|
|
|
+ @Link currentTime: string;
|
|
|
|
|
+ @Link videoUrl: string; // 用于判断当前播放的歌曲是不是和编辑的歌一样,如果不是播放控制不跟随更新
|
|
|
|
|
+ @State PROGRESS_MAX_VALUE: number = 100;
|
|
|
|
|
+ @Link CONTROL_PlayStatus: number;
|
|
|
|
|
+ @State bundleName: string = '';
|
|
|
|
|
+ @State durationStr: string = '';
|
|
|
|
|
+ @State outputPath: string = '';
|
|
|
|
|
+ @State inputPath: string = '';
|
|
|
|
|
+ @Prop mVideoItem: VideoItem;
|
|
|
|
|
+ @StorageProp('topRectHeight') topRectHeight: number = 0;
|
|
|
|
|
+ @State isDarkMode: boolean = false;
|
|
|
|
|
+ @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
|
|
|
|
|
+ ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
|
|
|
|
|
+ @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
|
|
|
|
|
+ context = this.getUIContext().getHostContext() as common.UIAbilityContext;
|
|
|
|
|
+
|
|
|
|
|
+ // 变速变调相关参数
|
|
|
|
|
+ @State pitchSemiTones: number = 0; // 音调调节(半音为单位,-12到+12)
|
|
|
|
|
+ @State tempoChange: number = 1.0; // 速度调节(倍速,0.25到4.0)
|
|
|
|
|
+ @State isProcessing: boolean = false;
|
|
|
|
|
+
|
|
|
|
|
+ private table: MediaTable = new MediaTable(this.context);
|
|
|
|
|
+
|
|
|
|
|
+ onColorModeChange() {
|
|
|
|
|
+ this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async aboutToAppear() {
|
|
|
|
|
+ await PermissionUtil.activatePermission(this.mVideoItem.filePath);
|
|
|
|
|
+ this.bundleName = AppUtil.getBundleName();
|
|
|
|
|
+ const rawDuration = this.mVideoItem.duration || '00:00:00';
|
|
|
|
|
+ this.durationStr = rawDuration;
|
|
|
|
|
+ this.outputPath = generateOutputPath(this.mVideoItem.filePath, '变速变调', this.currentPath);
|
|
|
|
|
+ console.info('heanup ChangeTone outputPath = ' + this.outputPath);
|
|
|
|
|
+ await new Promise<void>((resolve, reject) => {
|
|
|
|
|
+ this.table.getRdbStore(this.context, (err: Error) => {
|
|
|
|
|
+ err ? reject(err) : resolve();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 变速变调处理
|
|
|
|
|
+ async changePitchAndTempo() {
|
|
|
|
|
+ if (this.isProcessing) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StrUtil.isEmpty(this.inputPath)) {
|
|
|
|
|
+ this.inputPath = this.mVideoItem.filePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (FileUtil.accessSync(this.outputPath)) {
|
|
|
|
|
+ this.outputPath = generateOutputPath(this.mVideoItem.filePath, '变速变调', this.currentPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.isProcessing = true;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const task = new taskpool.Task(
|
|
|
|
|
+ applyPitchAndTempo,
|
|
|
|
|
+ this.context,
|
|
|
|
|
+ this.inputPath,
|
|
|
|
|
+ this.outputPath,
|
|
|
|
|
+ this.pitchSemiTones,
|
|
|
|
|
+ this.tempoChange,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const result = await taskpool.execute(task, taskpool.Priority.HIGH);
|
|
|
|
|
+
|
|
|
|
|
+ if (result) {
|
|
|
|
|
+ await saveDb(this.context, this.outputPath);
|
|
|
|
|
+ this.onResult(true, this.outputPath);
|
|
|
|
|
+ this.showSuccess();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.onResult(false, this.outputPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('heanup ChangeTone failed:', error);
|
|
|
|
|
+ this.onResult(false, this.outputPath);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.isProcessing = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ showSuccess() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.getUIContext().getPromptAction().showDialog({
|
|
|
|
|
+ title: '处理成功',
|
|
|
|
|
+ message: '变速变调处理完成,保存路径为:\n\n' + this.outputPath + '\n\n',
|
|
|
|
|
+ buttons: [
|
|
|
|
|
+ {
|
|
|
|
|
+ text: '查看路径',
|
|
|
|
|
+ color: '#000000'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ text: '设为铃声',
|
|
|
|
|
+ color: '#000000'
|
|
|
|
|
+ },
|
|
|
|
|
+ ]
|
|
|
|
|
+ }, (err, data) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ console.error('heanup showDialog err: ' + err);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (data.index === 1) { // "设为铃声"按钮的索引为1
|
|
|
|
|
+ setRingTone(this.context, this.outputPath, FileUtil.getFileName(this.outputPath));
|
|
|
|
|
+ } else if (data.index === 0) { // 查看路径
|
|
|
|
|
+ this.onSeeOutputPath(this.outputPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ console.info('heanup showDialog success callback, click button: ' + data.index);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ let message = (error as BusinessError).message;
|
|
|
|
|
+ let code = (error as BusinessError).code;
|
|
|
|
|
+ console.error(`heanup showDialog args error code is ${code}, message is ${message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ build() {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ this.topTitleBar();
|
|
|
|
|
+ this.buildContent();
|
|
|
|
|
+ }
|
|
|
|
|
+ .height('100%')
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .backgroundColor($r('app.color.index_background'))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ buildContent() {
|
|
|
|
|
+ Column({ space: 10 }) {
|
|
|
|
|
+ // 文件信息显示
|
|
|
|
|
+ Row({ space: 5 }) {
|
|
|
|
|
+ Image(this.mVideoItem.pixelMapPath)
|
|
|
|
|
+ .height(55)
|
|
|
|
|
+ .width(55)
|
|
|
|
|
+ .alt($r('app.media.llq'))
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.6 })
|
|
|
|
|
+ .borderRadius(12)
|
|
|
|
|
+ .clip(true);
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Text(` ${this.mVideoItem.name || this.mVideoItem.fileName}`)
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'))
|
|
|
|
|
+ .textAlign(TextAlign.Start);
|
|
|
|
|
+ Text(` ${this.mVideoItem.artist || this.mVideoItem.size}`)
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'))
|
|
|
|
|
+ .textAlign(TextAlign.Start);
|
|
|
|
|
+ }
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .alignItems(HorizontalAlign.Start)
|
|
|
|
|
+ .width('100%');
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .justifyContent(FlexAlign.Start);
|
|
|
|
|
+
|
|
|
|
|
+ // 输出文件名设置
|
|
|
|
|
+ Column({ space: 10 }) {
|
|
|
|
|
+ Text('输出文件名:')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'));
|
|
|
|
|
+
|
|
|
|
|
+ TextArea({ text: getFileNameWithoutExtension(this.outputPath) })
|
|
|
|
|
+ .height('auto')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'))
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .onEditChange((isEditing: boolean) => {
|
|
|
|
|
+ console.info(`heanup isEditing ${isEditing}`);
|
|
|
|
|
+ this.isEditing = isEditing;
|
|
|
|
|
+ })
|
|
|
|
|
+ .onChange((val: string) => {
|
|
|
|
|
+ if (this.isEditing) {
|
|
|
|
|
+ console.info('heanup onChange val=' + val);
|
|
|
|
|
+ this.outputPath = generateOutputPath(this.mVideoItem.filePath, '', this.currentPath,
|
|
|
|
|
+ undefined, val);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .margin({ top: 5, bottom: 5 })
|
|
|
|
|
+ .justifyContent(FlexAlign.Start);
|
|
|
|
|
+
|
|
|
|
|
+ // 音调调节区域
|
|
|
|
|
+ Column({ space: 10 }) {
|
|
|
|
|
+ Text('音调调节 (半音):')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'));
|
|
|
|
|
+
|
|
|
|
|
+ // 音调滑块
|
|
|
|
|
+ Column({ space: 8 }) {
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Text('-12')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'));
|
|
|
|
|
+ Text(`当前: ${this.pitchSemiTones > 0 ? '+' : ''}${this.pitchSemiTones}`)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.themeColor)
|
|
|
|
|
+ .margin({ left: 20, right: 20 });
|
|
|
|
|
+ Text('+12')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'));
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .justifyContent(FlexAlign.SpaceBetween);
|
|
|
|
|
+
|
|
|
|
|
+ Slider({
|
|
|
|
|
+ value: this.pitchSemiTones + 12, // 偏移到0-24范围
|
|
|
|
|
+ min: 0,
|
|
|
|
|
+ max: 24,
|
|
|
|
|
+ step: 1,
|
|
|
|
|
+ style: SliderStyle.OutSet
|
|
|
|
|
+ })
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .trackThickness(4)
|
|
|
|
|
+ .selectedColor(this.themeColor)
|
|
|
|
|
+ .blockColor(this.themeColor)
|
|
|
|
|
+ .onChange((value: number, mode: SliderChangeMode) => {
|
|
|
|
|
+ this.pitchSemiTones = Math.round(value) - 12;
|
|
|
|
|
+ console.info(`heanup pitchSemiTones changed to: ${this.pitchSemiTones}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 快捷音调按钮
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Button(' -12 ', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.pitchSemiTones === -12 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.pitchSemiTones === -12 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.pitchSemiTones = -12; });
|
|
|
|
|
+ Button(' -7 ', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.pitchSemiTones === -7 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.pitchSemiTones === -7 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.pitchSemiTones = -7; });
|
|
|
|
|
+ Button(' -5 ', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.pitchSemiTones === -5 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.pitchSemiTones === -5 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.pitchSemiTones = -5; });
|
|
|
|
|
+ Button(' 0 ', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.pitchSemiTones === 0 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.pitchSemiTones === 0 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.pitchSemiTones = 0; });
|
|
|
|
|
+ Button(' +5 ', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.pitchSemiTones === 5 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.pitchSemiTones === 5 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.pitchSemiTones = 5; });
|
|
|
|
|
+ Button(' +7 ', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.pitchSemiTones === 7 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.pitchSemiTones === 7 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.pitchSemiTones = 7; });
|
|
|
|
|
+ Button(' +12 ', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.pitchSemiTones === 12 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.pitchSemiTones === 12 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.pitchSemiTones = 12; });
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .justifyContent(FlexAlign.SpaceEvenly);
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding({ top: 5, bottom: 5 });
|
|
|
|
|
+
|
|
|
|
|
+ // 速度调节区域
|
|
|
|
|
+ Column({ space: 10 }) {
|
|
|
|
|
+ Text('播放速度调节:')
|
|
|
|
|
+ .fontSize(16)
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .textAlign(TextAlign.Start)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'));
|
|
|
|
|
+
|
|
|
|
|
+ // 速度滑块
|
|
|
|
|
+ Column({ space: 8 }) {
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Text('0.25x')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'));
|
|
|
|
|
+ Text(`${this.tempoChange.toFixed(2)}x`)
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(this.themeColor)
|
|
|
|
|
+ .margin({ left: 20, right: 20 });
|
|
|
|
|
+ Text('4.0x')
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .fontColor($r('app.color.text_color'));
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .justifyContent(FlexAlign.SpaceBetween);
|
|
|
|
|
+
|
|
|
|
|
+ Slider({
|
|
|
|
|
+ value: this.tempoChange,
|
|
|
|
|
+ min: 0.25,
|
|
|
|
|
+ max: 4.0,
|
|
|
|
|
+ step: 0.05,
|
|
|
|
|
+ style: SliderStyle.OutSet
|
|
|
|
|
+ })
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .trackThickness(4)
|
|
|
|
|
+ .selectedColor(this.themeColor)
|
|
|
|
|
+ .blockColor(this.themeColor)
|
|
|
|
|
+ .onChange((value: number, mode: SliderChangeMode) => {
|
|
|
|
|
+ this.tempoChange = Math.round(value * 100) / 100; // 保留两位小数
|
|
|
|
|
+ console.info(`heanup tempoChange changed to: ${this.tempoChange}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 快捷速度按钮
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ Button('0.5x', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.tempoChange === 0.5 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.tempoChange === 0.5 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.tempoChange = 0.5; });
|
|
|
|
|
+ Button('0.75x', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.tempoChange === 0.75 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.tempoChange === 0.75 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.tempoChange = 0.75; });
|
|
|
|
|
+ Button('1.0x', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.tempoChange === 1.0 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.tempoChange === 1.0 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.tempoChange = 1.0; });
|
|
|
|
|
+ Button('1.25x', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.tempoChange === 1.25 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.tempoChange === 1.25 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.tempoChange = 1.25; });
|
|
|
|
|
+ Button('1.5x', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.tempoChange === 1.5 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.tempoChange === 1.5 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.tempoChange = 1.5; });
|
|
|
|
|
+ Button('2.0x', { type: ButtonType.Circle })
|
|
|
|
|
+ .fontSize(12)
|
|
|
|
|
+ .width(40)
|
|
|
|
|
+ .height(40)
|
|
|
|
|
+ .backgroundColor(this.tempoChange === 2.0 ? this.themeColor : '#E0E0E0')
|
|
|
|
|
+ .fontColor(this.tempoChange === 2.0 ? Color.White : $r('app.color.text_color'))
|
|
|
|
|
+ .onClick(() => { this.tempoChange = 2.0; });
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .justifyContent(FlexAlign.SpaceEvenly);
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding({ top: 10, bottom: 10 });
|
|
|
|
|
+
|
|
|
|
|
+ this.playButton();
|
|
|
|
|
+
|
|
|
|
|
+ // 处理按钮
|
|
|
|
|
+ Button({ type: ButtonType.Capsule, stateEffect: true }) {
|
|
|
|
|
+ Row({ space: 8 }) {
|
|
|
|
|
+ if (this.isProcessing) {
|
|
|
|
|
+ LoadingProgress()
|
|
|
|
|
+ .width(26)
|
|
|
|
|
+ .color(Color.Blue);
|
|
|
|
|
+ }
|
|
|
|
|
+ Text(this.isProcessing ? '处理中...' : '保存音频')
|
|
|
|
|
+ .fontSize(14)
|
|
|
|
|
+ .fontColor(Color.White);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .width(200)
|
|
|
|
|
+ .height(45)
|
|
|
|
|
+ .backgroundColor(this.isProcessing ? Color.Gray : this.themeColor)
|
|
|
|
|
+ .borderRadius(20)
|
|
|
|
|
+ .margin({ top: 20 })
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ if (!this.isProcessing) {
|
|
|
|
|
+ if (isSpecialAudioFormat(this.mVideoItem.filePath)) {
|
|
|
|
|
+ let newPath = changeFileExtension(this.mVideoItem.filePath, 'wav'); // 特色格式先转成wav格式
|
|
|
|
|
+ console.info('heanup newPath = ' + newPath);
|
|
|
|
|
+ this.inputPath = newPath;
|
|
|
|
|
+ if (!FileUtil.accessSync(newPath)) {
|
|
|
|
|
+ this.isProcessing = true;
|
|
|
|
|
+ const task = new taskpool.Task(convertSpecialToWav, this.mVideoItem.filePath, newPath);
|
|
|
|
|
+ taskpool.execute(task, taskpool.Priority.HIGH).then((data) => {
|
|
|
|
|
+ this.changePitchAndTempo();
|
|
|
|
|
+ }).catch((e: object) => {
|
|
|
|
|
+ console.info("heanup task catch e: " + e);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.changePitchAndTempo();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.changePitchAndTempo();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .enabled(!this.isProcessing);
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('100%')
|
|
|
|
|
+ .padding({ top: 20, bottom: 20 })
|
|
|
|
|
+ .alignItems(HorizontalAlign.Center);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ playButton() {
|
|
|
|
|
+ Row() {
|
|
|
|
|
+ Button({ type: ButtonType.Circle, stateEffect: true }) {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Image(this.CONTROL_PlayStatus === PlayStatus.PLAY && this.videoUrl == this.mVideoItem.filePath ?
|
|
|
|
|
+ $r('app.media.hm_pause')
|
|
|
|
|
+ : $r('app.media.hm_play2'))
|
|
|
|
|
+ .width(38)
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.5 })
|
|
|
|
|
+ .fillColor(Color.White)
|
|
|
|
|
+ .aspectRatio(CommonConstants.ASPECT_RATIO)
|
|
|
|
|
+ .onClick(async () => {
|
|
|
|
|
+ this.onPlayOrPause();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .backgroundColor(this.themeColor);
|
|
|
|
|
+
|
|
|
|
|
+ Text(this.videoUrl == this.mVideoItem.filePath ? this.currentTime : '00:00')
|
|
|
|
|
+ .fontSize($r('app.float.slider_font_size'))
|
|
|
|
|
+ .fontColor($r('app.color.text_color'))
|
|
|
|
|
+ .margin(3);
|
|
|
|
|
+ Slider({
|
|
|
|
|
+ value: this.videoUrl == this.mVideoItem.filePath ? this.progressValue : 0,
|
|
|
|
|
+ min: 0,
|
|
|
|
|
+ max: this.PROGRESS_MAX_VALUE,
|
|
|
|
|
+ step: 1,
|
|
|
|
|
+ style: SliderStyle.OutSet
|
|
|
|
|
+ })
|
|
|
|
|
+ .width('600px')
|
|
|
|
|
+ .trackThickness(3)
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .margin({ left: 1 })
|
|
|
|
|
+ .showSteps(false)
|
|
|
|
|
+ .showTips(true)
|
|
|
|
|
+ .onChange((value: number, mode: SliderChangeMode) => {
|
|
|
|
|
+ this.onSliderChange(value);
|
|
|
|
|
+ });
|
|
|
|
|
+ Text(this.durationStr)
|
|
|
|
|
+ .fontSize($r('app.float.slider_font_size'))
|
|
|
|
|
+ .fontColor($r('app.color.text_color'))
|
|
|
|
|
+ .margin(3)
|
|
|
|
|
+ .margin({ left: 2 });
|
|
|
|
|
+ }
|
|
|
|
|
+ .width('90%')
|
|
|
|
|
+ .margin({ top: 10 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Builder
|
|
|
|
|
+ topTitleBar() {
|
|
|
|
|
+ Column() {
|
|
|
|
|
+ Row({ space: 15 }) {
|
|
|
|
|
+ // 左侧滑动按钮
|
|
|
|
|
+ Button({ type: ButtonType.Circle, stateEffect: true }) {
|
|
|
|
|
+ SymbolGlyph($r('sys.symbol.chevron_left'))
|
|
|
|
|
+ .attributeModifier(new SymbolGlyphFancyModifier(25, '', ''));
|
|
|
|
|
+ }
|
|
|
|
|
+ .attributeModifier(new ButtonFancyModifier(40, 40))
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 })
|
|
|
|
|
+ .animation({ duration: 300, curve: Curve.Ease })
|
|
|
|
|
+ .onClick(() => {
|
|
|
|
|
+ this.onBack();
|
|
|
|
|
+ })
|
|
|
|
|
+ .attributeModifier(new ShadowModifier())
|
|
|
|
|
+ .zIndex(0);
|
|
|
|
|
+
|
|
|
|
|
+ Text('变速变调')
|
|
|
|
|
+ .margin({ left: 3, right: 10 })
|
|
|
|
|
+ .fontColor($r('app.color.text_color'))
|
|
|
|
|
+ .fontSize(19)
|
|
|
|
|
+ .maxLines(1)
|
|
|
|
|
+ .textOverflow({ overflow: TextOverflow.MARQUEE }) // 超长滚动
|
|
|
|
|
+ .layoutWeight(1)
|
|
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.6 });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding({ top: this.topRectHeight, left: 10, right: 10, bottom: 12 })
|
|
|
|
|
+ .width('100%');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 变速变调的核心方法
|
|
|
|
|
+@Concurrent
|
|
|
|
|
+async function applyPitchAndTempo(
|
|
|
|
|
+ context: Context,
|
|
|
|
|
+ inputPath: string,
|
|
|
|
|
+ outPath?: string,
|
|
|
|
|
+ pitchSemiTones: number = 0,
|
|
|
|
|
+ tempoChange: number = 1.0
|
|
|
|
|
+): Promise<boolean> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ inputPath = FileUtil.getFilePath(inputPath);
|
|
|
|
|
+ // 生成输出路径
|
|
|
|
|
+ let outputPath = outPath ?? (() => {
|
|
|
|
|
+ const dir = inputPath.substring(0, inputPath.lastIndexOf('/') + 1);
|
|
|
|
|
+ const fullName = inputPath.substring(inputPath.lastIndexOf('/') + 1);
|
|
|
|
|
+ const dotIndex = fullName.lastIndexOf('.');
|
|
|
|
|
+ const name = dotIndex === -1 ? fullName : fullName.substring(0, dotIndex);
|
|
|
|
|
+ const ext = dotIndex === -1 ? '' : fullName.substring(dotIndex);
|
|
|
|
|
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
|
|
|
+ return `${dir}${name}_${timestamp}_pitch_tempo${ext}`;
|
|
|
|
|
+ })();
|
|
|
|
|
+
|
|
|
|
|
+ console.info(`heanup 开始变速变调处理,输入: ${inputPath}, 输出: ${outputPath}`);
|
|
|
|
|
+ console.info(`heanup 音调变化: ${pitchSemiTones} 半音, 速度变化: ${tempoChange} 倍`);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算音调变化因子 (2^(semitones/12))
|
|
|
|
|
+ const pitchFactor = Math.pow(2, pitchSemiTones / 12);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取文件扩展名
|
|
|
|
|
+ const fileExt = inputPath.split('.').pop()?.toLowerCase() || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 根据文件扩展名获取音频编码器(直接内联逻辑)
|
|
|
|
|
+ let audioCodec: string = 'libmp3lame'; // 默认使用mp3编码器
|
|
|
|
|
+ switch (fileExt) {
|
|
|
|
|
+ case 'mp3':
|
|
|
|
|
+ audioCodec = 'libmp3lame';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'flac':
|
|
|
|
|
+ audioCodec = 'flac';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'wav':
|
|
|
|
|
+ audioCodec = 'pcm_s16le';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'aac':
|
|
|
|
|
+ case 'm4a':
|
|
|
|
|
+ audioCodec = 'aac';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'ogg':
|
|
|
|
|
+ audioCodec = 'libvorbis';
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ audioCodec = 'libmp3lame';
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建FFmpeg命令 - 使用rubberband滤镜实现高质量变速变调
|
|
|
|
|
+ const commands = [
|
|
|
|
|
+ 'ffmpeg',
|
|
|
|
|
+ '-i', inputPath,
|
|
|
|
|
+ '-filter:a', `rubberband=pitch=${pitchFactor}:tempo=${tempoChange}`,
|
|
|
|
|
+ '-c:a', audioCodec,
|
|
|
|
|
+ '-ar', '44100',
|
|
|
|
|
+ '-y', outputPath
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.info(`heanup 执行变速变调命令: ${commands.join(' ')}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 执行FFmpeg命令
|
|
|
|
|
+ await FFmpeg.execute(commands, {
|
|
|
|
|
+ logCallback: (logLevel, logMessage) =>
|
|
|
|
|
+ console.log(`[heanup] [${logLevel}] 变速变调: ${logMessage}`),
|
|
|
|
|
+ progressCallback: (message) =>
|
|
|
|
|
+ console.log(`[heanup] [progress] 变速变调: ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.info(`heanup 变速变调成功: ${outputPath}`);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`heanup 变速变调失败: ${error}`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`heanup 主流程错误: ${error}`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|