Przeglądaj źródła

长按新增剪辑音频 提前伴奏 格式转化 人声分离 设为铃声

onecold 9 miesięcy temu
rodzic
commit
082dcae39d

+ 2 - 2
entry/src/main/ets/common/constants/CommonConstants.ets

@@ -161,10 +161,10 @@ export class CommonConstants {
   static readonly pathDir:string = 'file://docs/storage/Users/currentUser/'
 
 
-  static readonly musicBgList = [$r('app.media.ic_avatar7'),
+  static readonly musicBgList = [$r('app.media.ic_avatar10'),
     $r('app.media.ic_avatar2'),$r('app.media.avatar'),
     $r('app.media.ic_avatar4'),$r('app.media.ic_avatar5'),
-    $r('app.media.ic_avatar6'),$r('app.media.ic_avatar1'),
+    $r('app.media.ic_avatar8'),$r('app.media.ic_avatar1'),
     $r('app.media.ic_avatar8'),$r('app.media.ic_avatar9'),$r('app.media.ic_avatar10')
   ]
 

+ 108 - 0
entry/src/main/ets/common/util/MusicTagUtils.ets

@@ -592,6 +592,7 @@ export async function convertDsfToWav(
   inputPath: string,
   outputPath: string
 ): Promise<boolean> {
+  inputPath = FileUtil.getFilePath(inputPath)
   // 1. 输入文件验证(增强型检查)
   try {
     const stats = await fs.stat(inputPath);
@@ -640,4 +641,111 @@ export async function convertDsfToWav(
     try { await fs.unlink(outputPath);  } catch {} // 静默清理
     return false;
   }
+}
+
+
+/**
+ * 将 DSF/DFF/AIFF/AIF/APE 转换为 WAV 格式
+ * @param inputPath 输入文件路径(支持 dsf/dff/aiff/aif/ape)
+ * @param outputPath 输出 WAV 文件路径
+ * @returns true: 转换成功;false: 转换失败
+ */
+@Concurrent
+export async function convertSpecialToWav(
+  inputPath: string,
+  outputPath: string
+): Promise<boolean> {
+  // 1. 输入文件验证
+  inputPath = FileUtil.getFilePath(inputPath)
+  try {
+    const stats = await fs.stat(inputPath);
+    if (!stats.isFile()  || stats.size  < 1024) { // 检查是否为有效文件(>1KB)
+      console.error(' 输入文件无效或过小');
+      return false;
+    }
+  } catch (error) {
+    console.error(` 文件验证失败: ${JSON.stringify(error)}`);
+    return false;
+  }
+
+  // 2. 提取扩展名并适配格式参数
+  const ext = inputPath.split('.').pop()?.toLowerCase()  || '';
+  let ffmpegParams: string[] = [];
+  let formatName = '';
+
+  switch (ext) {
+    case 'dsf':
+    case 'dff':
+      // DSD格式(DSF/DFF):24位PCM+88.2kHz采样率
+      formatName = ext.toUpperCase();
+      ffmpegParams = [
+        "-c:a", "pcm_s24le",    // 24位高精度PCM
+        "-ar", "88200",         // 兼容性采样率(避免96kHz设备不支持)
+        "-ac", "2",             // 立体声
+        "-fflags", "+genpts"    // 修复时间戳
+      ];
+      break;
+    case 'aiff':
+    case 'aif':
+      // AIFF格式:16位小端PCM+44.1kHz(WAV标准)
+      formatName = ext.toUpperCase();
+      ffmpegParams = [
+        "-c:a", "pcm_s16le",    // 16位小端PCM(WAV标准编码)
+        "-ar", "44100",         // CD标准采样率
+        "-ac", "2",
+        "-sample_fmt", "s16"    // 强制采样格式,解决高位深问题
+      ];
+      break;
+    case 'ape':
+      // APE格式:无损解码+24位PCM(保留原音质)
+      formatName = "APE";
+      ffmpegParams = [
+        "-c:a", "pcm_s24le",    // APE无损解码为24位PCM
+        "-ar", "48000",         // 常用采样率(兼容多数场景)
+        "-ac", "2",
+        "-af", "aformat=channel_layouts=stereo" // 强制立体声布局
+      ];
+      break;
+    default:
+      console.error(` 不支持的格式: ${ext},仅支持dsf/dff/aiff/aif/ape`);
+      return false;
+  }
+
+  // 3. 构建FFmpeg命令
+  const commands = [
+    "ffmpeg",
+    "-i", inputPath,
+    ...ffmpegParams,
+    "-f", "wav",              // 强制输出WAV格式
+    "-y",                     // 覆盖输出文件
+    outputPath
+  ];
+
+  // 4. 执行转换与结果验证
+  try {
+    await FFmpeg.execute(commands,  {
+      logCallback: (_, msg) => console.debug(`[${formatName}]  ${msg}`),
+      progressCallback: (msg) => {
+        const progress = FFProgressMessageParser.parse(msg);
+        // console.log(`[${formatName}]  转换进度: ${progress.percent.toFixed(1)}%`);
+      }
+    });
+
+    // 5. 输出文件完整性检查(WAV文件至少100KB,根据原文件大小调整)
+    const outStats = await fs.stat(outputPath);
+    if (outStats.size  < 100 * 1024) { // 100KB阈值(APE/DSF原文件较大,输出应更大)
+      await fs.unlink(outputPath);
+      throw new Error(`输出WAV文件过小(${outStats.size} 字节),可能转换异常`);
+    }
+
+    console.log(`[${formatName}]  转换成功: ${outputPath}(大小: ${(outStats.size  / (1024 * 1024)).toFixed(2)}MB)`);
+    return true;
+  } catch (error) {
+    console.error(`[${formatName}]  转换失败: ${error instanceof Error ? error.message  : '未知错误'}`);
+    // 清理无效输出文件
+    if (await fs.access(outputPath).then(()  => true).catch(() => false)) {
+      await fs.unlink(outputPath);
+    }
+    return false;
+  }
 }

+ 17 - 1
entry/src/main/ets/common/util/Utility.ets

@@ -78,6 +78,8 @@ interface FFprobeFormat {
 
 interface FFprobeStream {
   // Define stream properties as needed
+  codec_name?: string;
+  sample_fmt?: string;
   codec_type?: string;  // 流类型,如"audio"、"video"
   sample_rate?: string; // 采样率
   bit_rate?: string;    // 比特率
@@ -110,7 +112,7 @@ interface StreamDisposition {
   still_image?: number;
 }
 
-interface FFprobeMetadata {
+export interface FFprobeMetadata {
   streams: FFprobeStream[];
   format: FFprobeFormat;
 }
@@ -259,6 +261,20 @@ export class Utility {
     return `${sampleRateKHz} KHz`;
   }
 
+  static isMerge(localList:Array<VideoItem>,packName:string):boolean{
+    if(ArrayUtil.isEmpty(localList))
+      return false
+    for(let i=0;i<localList.length;i++){
+
+      if(!localList[i].filePath.includes(packName)){
+        return false
+      }
+
+    }
+    return true
+  }
+
+
   /**
    * 格式化媒体格式类型显示
    * @param mimeType 媒体格式类型字符串

+ 1 - 1
entry/src/main/ets/controller/AvSessionController.ets

@@ -108,7 +108,7 @@ export class AvSessionController {
       return;
     }
     BackgroundTaskManager.startContinuousTask(this.context);
-    const imagePixMap = await ImageUtil.getPixelMapFromMedia($r('app.media.ic_avatar7'));
+    const imagePixMap = await ImageUtil.getPixelMapFromMedia($r('app.media.ic_avatar8'));
     let metadata: avSession.AVMetadata = {
       assetId: `${curSource.filePath}`,
       title: curSource.name,

+ 650 - 0
entry/src/main/ets/view/AIVoiceSeparation.ets

@@ -0,0 +1,650 @@
+import { common, ConfigurationConstant } from '@kit.AbilityKit';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { VideoItem } from '../viewmodel/VideoItem';
+import { LazyDataSource } from '../common/util/LazyDataSource';
+import { AppUtil, FileUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
+import fs from '@ohos.file.fs';
+import {
+  convertSpecialToWav,
+} from '../common/util/MusicTagUtils';
+import { taskpool, util } from '@kit.ArkTS';
+import { Utility } from '../common/util/Utility';
+import { DialogHelper } from '@pura/harmony-dialog';
+import MediaTable from '../common/util/MediaTable';
+import { TrackProgress } from '@abner/track';
+import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
+import Logger from '../common/util/Logger';
+import { BusinessError } from '@kit.BasicServicesKit';
+import { PlayStatus } from '../common/PlayStatus';
+import { ringtone } from '@kit.RingtoneKit';
+import { uniformTypeDescriptor } from '@kit.ArkData';
+import { generateOutputPath, setRingTone } from './EditAudio';
+import { SwipeRefresher, SegmentButton } from '@kit.ArkUI';
+import { SegmentButtonOptions, SegmentButtonItemTuple } from '@kit.ArkUI';
+import { changeFileExtension, isSpecialAudioFormat } from './ExtractAccompaniment';
+import PermissionUtil from '../common/util/PermissionUtil'
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
+
+// 人声分离功能
+@Component
+export struct AIVoiceSeparation {
+  onResult = (_result: boolean, accompanimentPath: string, vocalPath: string) => {
+  }
+  onBack = () => {
+  }
+  onPlayOrPause = () => {
+  }
+  onPlayPath = (path:string) => {
+  }
+  onSeeOutputPath = (outPutPath:string) => {
+
+  }
+  onSliderChange = (value:number) => {
+  }
+  @State isEditing:boolean = false
+  @Prop currentPath: string;
+  @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 accompanimentPath: string = '';
+  @State vocalPath: 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 separationQuality: number = 2; // 0-低, 1-中, 2-高
+  @State isProcessing: boolean = false;
+  @State showResult: boolean = false;
+
+  // SegmentButton选项
+  @State qualityOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [{ text: '低质量' }, { text: '中等质量' }, { text: '高质量' }] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor:$r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding:{top:10,bottom:10},
+    multiply: false
+  });
+  @State @Watch('onQualityChange') selectedQualityIndex: number[] = [this.separationQuality];
+
+  private table: MediaTable = new MediaTable(this.context);
+
+  // 音质变化监听
+  onQualityChange() {
+    if (this.selectedQualityIndex.length  > 0) {
+      this.separationQuality  = this.selectedQualityIndex[0];
+    }
+  }
+  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.accompanimentPath  = generateOutputPath(this.mVideoItem.filePath,' 伴奏',this.currentPath)
+    this.vocalPath  = generateOutputPath(this.mVideoItem.filePath,' 人声',this.currentPath)
+    console.info('onecold  this.accompanimentPath  ='+this.accompanimentPath)
+    await new Promise<void>((resolve, reject) => {
+      this.table.getRdbStore(this.context,   (err:Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+  }
+
+  // 人声分离
+  async separateVoice() {
+    if (this.isProcessing)   {
+      return;
+    }
+    if(StrUtil.isEmpty(this.inputPath)){
+      this.inputPath  =this.mVideoItem.filePath
+    }
+    this.accompanimentPath  = generateOutputPath(this.inputPath,' 伴奏',this.currentPath)
+    this.vocalPath  = generateOutputPath(this.inputPath,' 人声',this.currentPath)
+    this.isProcessing   = true;
+    this.showResult  = false;
+
+    try {
+      const task = new taskpool.Task(
+        separateVoiceAndAccompaniment,
+        this.context,
+        this.inputPath,
+        this.accompanimentPath,
+        this.vocalPath,
+        this.separationQuality,
+        this.currentPath
+      );
+
+      const result = await taskpool.execute(task,   taskpool.Priority.HIGH);
+
+      if (result) {
+        await this.saveDb(this.context,  this.accompanimentPath)
+        await this.saveDb(this.context,  this.vocalPath)
+        this.onResult(true,   this.accompanimentPath,  this.vocalPath);
+        this.showResult  = true;
+        this.showSuccess();
+      } else {
+        this.onResult(false,   this.accompanimentPath,  this.vocalPath);
+      }
+    } catch (error) {
+      console.error('Voice   separation failed:', error);
+      this.onResult(false,   this.accompanimentPath,  this.vocalPath);
+    } finally {
+      this.isProcessing   = false;
+    }
+  }
+
+  async saveDb(context: Context, outputPath: string): Promise<boolean> {
+    try {
+      const table: MediaTable = new MediaTable(context);
+
+      await new Promise<void>((resolve, reject) => {
+        table.getRdbStore(context,   (err: Error | null) => {
+          if (err) {
+            reject(new Error(`获取数据库连接失败: ${err.message}`));
+          } else {
+            resolve();
+          }
+        });
+      });
+
+      const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+        context,
+        outputPath,
+        CommonConstants.TYPE_LOCAL,
+        true
+      );
+      if (!mediaItem) {
+        console.error(`  获取媒体元数据失败: ${outputPath}`);
+        return false;
+      }
+
+      await new Promise<void>((resolve, reject) => {
+        table.insert(
+          mediaItem,
+          (err: Error | null) => {
+            if (err) {
+              reject(new Error(`数据库插入失败: ${err.message}`));
+            } else {
+              resolve();
+            }
+          },
+          ''
+        );
+      });
+
+      console.log(`  媒体入库成功: ${outputPath}`);
+      return true;
+
+    } catch (error) {
+      console.error(`  媒体入库失败: ${error instanceof Error ? error.message   : error}`);
+      return false;
+    }
+  }
+
+  showSuccess() {
+    try {
+      this.getUIContext().getPromptAction().showDialog({
+        title: '分离成功',
+        message: '人声分离完成,已生成两个文件!',
+        buttons: [
+          {
+            text: '查看文件',
+            color: '#000000'
+          },
+          {
+            text: '设为铃声',
+            color: '#000000'
+          },
+        ]
+      }, (err, data) => {
+        if (err) {
+          console.error('showDialog   err: ' + err);
+          return;
+        }
+        if (data.index   === 1) {
+          setRingTone(this.context,  this.accompanimentPath,FileUtil.getFileName(this.accompanimentPath))
+        }else if (data.index   === 0){
+          this.onSeeOutputPath(this.accompanimentPath)
+        }
+        console.info('showDialog   success callback, click button: ' + data.index);
+      });
+    } catch (error) {
+      let message = (error as BusinessError).message;
+      let code = (error as BusinessError).code;
+      console.error(`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: 20 }) {
+      // 文件信息显示
+      Row({ space: 10 }) {
+        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%')
+      .margin({ top: 5})
+      .justifyContent(FlexAlign.Start)
+
+      // 分离质量选择
+      Column({ space: 10 }) {
+        Text('分离质量:')
+          .fontSize(16)
+          .width('90%')
+          .textAlign(TextAlign.Start)
+          .fontColor($r('app.color.text_color'))
+
+        SegmentButton({
+          options: this.qualityOptions,
+          selectedIndexes: $selectedQualityIndex
+        })
+          .width('90%')
+
+        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('onecold  this.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.separateVoice();
+                }).catch((e:object)=>{
+                  console.info("task1  catch e: " + e);
+                })
+              }else{
+                this.separateVoice();
+              }
+            } else {
+              this.separateVoice();
+            }
+          }
+        })
+        .enabled(!this.isProcessing)
+
+        // 结果显示区域
+        if (this.showResult)  {
+          Column({ space: 15 }) {
+            Text('分离结果:')
+              .fontSize(16)
+              .width('90%')
+              .textAlign(TextAlign.Start)
+              .fontColor($r('app.color.text_color'))
+
+            // 伴奏文件显示
+            Row({ space: 10 }) {
+              SymbolGlyph($r('sys.symbol.music'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .padding({ left: 5 })
+                .borderRadius(8)
+
+              Column({ space: 5 }) {
+                Text('伴奏文件')
+                  .fontSize(14)
+                  .fontColor($r('app.color.text_color'))
+
+                Row({ space: 10 }) {
+
+                  Text(this.accompanimentPath)
+                    .fontSize(14)
+                    .fontColor($r('app.color.text_color'))
+                    .borderRadius(10)
+                }
+              }
+              .layoutWeight(1)
+              .alignItems(HorizontalAlign.Start)
+            }
+            .width('90%')
+            .padding(10)
+            .backgroundColor($r('app.color.index_background'))
+            .clickEffect({level:ClickEffectLevel.HEAVY,scale:0.8})
+            .borderRadius(10)
+            .onClick(()=>{
+              this.onPlayPath(this.accompanimentPath)
+            })
+
+            // 人声文件显示
+            Row({ space: 10 }) {
+              SymbolGlyph($r('sys.symbol.person_2'))
+                .fontSize(20)
+                .fontColor([this.themeColor])
+                .alignSelf(ItemAlign.Center)
+                .padding({ left: 5 })
+                .borderRadius(8)
+
+              Column({ space: 5 }) {
+                Text('人声文件')
+                  .fontSize(14)
+                  .fontColor($r('app.color.text_color'))
+
+                Row({ space: 10 }) {
+                  Text(this.vocalPath)
+                    .fontSize(14)
+                    .fontColor($r('app.color.text_color'))
+                    .layoutWeight(1)
+                }
+              }
+              .layoutWeight(1)
+              .alignItems(HorizontalAlign.Start)
+            }
+            .width('90%')
+            .padding(10)
+            .backgroundColor($r('app.color.index_background'))
+            .clickEffect({level:ClickEffectLevel.HEAVY,scale:0.8})
+            .borderRadius(10)
+            .onClick(()=>{
+              this.onPlayPath(this.vocalPath)
+            })
+          }
+          .width('100%')
+          .padding({ top: 20 })
+        }
+      }
+      .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||this.videoUrl==this.accompanimentPath
+            ||this.videoUrl==this.vocalPath)  ?
+            $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.videoUrl==this.accompanimentPath
+        ||this.videoUrl==this.vocalPath)?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.videoUrl==this.accompanimentPath
+          ||this.videoUrl==this.vocalPath)?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('AI人声分离')
+          .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 separateVoiceAndAccompaniment(
+  context: Context,
+  inputPath: string,
+  accompanimentPath: string,
+  vocalPath: string,
+  quality: number = 2,
+): Promise<boolean> {
+  try {
+    // 生成输出路径
+    let accPath = accompanimentPath
+    let vocPath = vocalPath
+    inputPath = FileUtil.getFilePath(inputPath);
+    console.info(`onecold      开始人声分离,输入: ${inputPath}`);
+
+    // 根据质量设置参数
+    let qualityParams: string[] = [];
+    let codec = 'libmp3lame';
+
+    switch (quality) {
+      case 0: // 低质量
+        qualityParams = ['-ar', '22050', '-b:a', '64k'];
+        break;
+      case 1: // 中等质量
+        qualityParams = ['-ar', '44100', '-b:a', '128k'];
+        break;
+      case 2: // 高质量
+      default:
+        qualityParams = ['-ar', '48000', '-b:a', '192k'];
+        break;
+    }
+
+    try {
+      // 第一步:提取伴奏(使用中置声道消除方法)
+      console.info(`onecold      第一步:提取伴奏`);
+      
+      // 获取文件扩展名(统一转为小写,避免大小写问题)
+      const fileExt = inputPath.split('.').pop()?.toLowerCase()  || '';
+
+      // 定义格式相关参数(通过 switch case 动态调整)
+      let inputDecoder: string[] = []; // 输入解码器参数
+      let channelFix: string = '';     // 声道预处理滤镜
+      let outputCodec: string = codec; // 输出编码器
+
+      switch (fileExt) {
+        case 'flac':
+          // FLAC 格式特殊处理:使用 FLAC 解码器,强制立体声,输出 FLAC 格式
+          inputDecoder = ['-c:a', 'flac']; // 显式指定 FLAC 解码器
+          channelFix = 'aformat=channel_layouts=stereo,'; // 强制立体声布局
+          outputCodec = 'flac'; // 输出保持 FLAC 无损格式
+          break;
+        case 'wav':
+          // WAV 格式:无需特殊解码器,默认处理
+          channelFix = 'aformat=channel_layouts=stereo,'; // 确保立体声
+          break;
+        case 'mp3':
+          // MP3 格式:默认处理(使用 libmp3lame 编码器)
+          break;
+        default:
+        // 其他格式:使用默认参数(如 MP3、AAC 等)
+          console.warn(`onecold      未优化格式: ${fileExt},使用默认配置`);
+          break;
+      }
+
+      // 构建中置声道消除命令(动态拼接参数)
+      const accompanimentCommands = [
+        'ffmpeg',
+        ...inputDecoder, // 动态解码器参数(如 FLAC 的 ['-c:a', 'flac'])
+        '-i', inputPath,
+        '-af', `${channelFix}pan=stereo|c0=c0-c1|c1=c1-c0`, // 动态声道滤镜
+        '-c:a', outputCodec, // 动态输出编码器(如 FLAC 保留原格式)
+        ...qualityParams,
+        '-y', accPath
+      ];
+
+      console.info(`onecold      使用中置声道消除方法提取伴奏(格式: ${fileExt})`);
+
+      // 执行 FFmpeg 命令提取伴奏
+      await FFmpeg.execute(accompanimentCommands,  {
+        logCallback: (logLevel, logMessage) =>
+        console.log(`[${logLevel}]      提取伴奏:    ${logMessage}`),
+        progressCallback: (message) =>
+        console.log(`[progress]      提取伴奏:    ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+      });
+
+      console.info(`onecold      伴奏提取成功: ${accPath}`);
+
+      // 第二步:提取人声(使用相反的声道处理)
+      console.info(`onecold      第二步:提取人声`);
+
+      // 优化的人声提取命令
+      const vocalCommands = [
+        'ffmpeg',
+        ...inputDecoder,
+        '-i', inputPath,
+        '-af', `${channelFix}pan=stereo|c0=0.5*c0+0.5*c1|c1=0.5*c1+0.5*c0`,
+        '-c:a', codec, ...qualityParams, '-y', vocPath
+      ];
+
+      await FFmpeg.execute(vocalCommands,  {
+        logCallback: (logLevel, logMessage) =>
+          console.log(`[${logLevel}]      人声提取:    ${logMessage}`),
+        progressCallback: (message) =>
+          console.log(`[progress]      人声提取:    ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+      });
+
+      console.info(`onecold      人声分离成功`);
+      console.info(`onecold      伴奏文件: ${accPath}`);
+      console.info(`onecold      人声文件: ${vocPath}`);
+      return true;
+
+    } catch (error) {
+      console.error(`onecold      人声分离失败: ${error}`);
+
+      // 备用方案:使用频段分离提取人声
+      try {
+        console.info(`onecold      尝试备用方案:频段分离`);
+
+        const backupVocalCommands = [
+          'ffmpeg',
+          '-i', inputPath,
+          '-af',
+          // 人声主要在中高频段,使用带通滤波器
+          'bandpass=f=300:width_type=h:width=3000,' +  // 300-3300Hz 是人声主要频段
+          'acompressor=threshold=0.08:ratio=8:attack=50:release=500,' +
+          'volume=1.5',  // 适当提高音量
+          '-c:a', codec, ...qualityParams, '-y', vocPath
+        ];
+
+        await FFmpeg.execute(backupVocalCommands,  {
+          logCallback: (logLevel, logMessage) =>
+            console.log(`[${logLevel}]      备用方案:    ${logMessage}`),
+        });
+
+        console.info(`onecold      备用方案成功`);
+        return true;
+
+      } catch (secondError) {
+        console.error(`onecold      备用方案失败: ${secondError}`);
+        return false;
+      }
+    }
+
+  } catch (error) {
+    console.error(`onecold      主流程错误: ${error}`);
+    return false;
+  }
+}

+ 651 - 0
entry/src/main/ets/view/AudioFormatConverter.ets

@@ -0,0 +1,651 @@
+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 fs from '@ohos.file.fs';
+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 {  SegmentButton } from '@kit.ArkUI';
+import { SegmentButtonOptions, SegmentButtonItemTuple } from '@kit.ArkUI';
+import { saveDb } from './ExtractAccompaniment';
+import PermissionUtil from '../common/util/PermissionUtil'
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
+
+// 音频格式转换功能
+@Component
+export struct AudioFormatConverter {
+  onResult = (_result: boolean, outPath: string) => {
+  }
+  onBack = () => {
+  }
+  onPlayOrPause = () => {
+  }
+  onSeeOutputPath = (outPutPath: string) => {
+  }
+  onSliderChange = (value: number) => {
+  }
+  @State isEditing:boolean = false
+  @Prop currentPath: string;
+  @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 targetFormat: string = 'mp3';
+  @State isProcessing: boolean = false;
+
+  // 前5个格式选项
+  @State formatOptions1: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [
+      { text: 'MP3' },
+      { text: 'FLAC' },
+      { text: 'WAV' },
+      { text: 'AAC' },
+    ] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor: $r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding: { top: 8, bottom: 8 },
+    multiply: false
+  });
+
+  // 后4个格式选项
+  @State formatOptions2: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [
+      { text: 'WMA' },
+      { text: 'MP2' },
+      { text: 'AC3' },
+      { text: 'OPUS' }
+    ] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor: $r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding: { top: 8, bottom: 8 },
+    multiply: false
+  });
+  
+  @State @Watch('onFormatChange') selectedFormatIndex: number[] = [0];
+  // 新增状态用于跟踪当前选中的格式组
+  @State selectedFormatGroup: number = 1; // 1表示第一组,2表示第二组
+
+  private table: MediaTable = new MediaTable(this.context);
+
+  // 格式变化监听
+  onFormatChange() {
+    if (this.selectedFormatIndex.length > 0) {
+      const index = this.selectedFormatIndex[0];
+      let formats: string[];
+      if (this.selectedFormatGroup === 1) {
+        // 第一组格式: mp3, flac, wav, aac, m4a
+        formats = ['mp3', 'flac', 'wav', 'aac'];
+      } else {
+        // 第二组格式: wma, ogg, ac3, opus
+        formats = ['wma', 'mp2', 'ac3', 'opus'];
+      }
+      
+      if (index >= 0 && index < formats.length) {
+        this.targetFormat = formats[index];
+        // 当目标格式改变时,更新输出路径
+        this.outputPath = generateOutputPath(this.mVideoItem.filePath, '转换',this.currentPath, this.targetFormat);
+      }
+    }
+  }
+
+  onColorModeChange() {
+    this.isDarkMode  = this.currentMode  === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
+  }
+
+  async aboutToAppear() {
+    this.bundleName  = AppUtil.getBundleName();
+    const rawDuration = this.mVideoItem.duration  || '00:00:00';
+    this.durationStr  = rawDuration;
+    await PermissionUtil.activatePermission(this.mVideoItem.filePath)
+    this.outputPath  = generateOutputPath(this.mVideoItem.filePath,  '转换', this.currentPath,this.targetFormat)
+    console.info('onecold  this.outputPath  =' + this.outputPath)
+    await new Promise<void>((resolve, reject) => {
+      this.table.getRdbStore(this.context,  (err: Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+  }
+  
+  aboutToDisappear(): void {
+
+  }
+
+  // 音频格式转换
+  async convertAudioFormat() {
+    if (this.isProcessing)  {
+      return;
+    }
+    if (StrUtil.isEmpty(this.inputPath))  {
+      this.inputPath  = this.mVideoItem.filePath
+    }
+    if (FileUtil.accessSync(this.outputPath))  {
+      this.outputPath  = generateOutputPath(this.inputPath,  '转换', this.currentPath,this.targetFormat)
+    }
+    this.isProcessing  = true;
+
+    try {
+      const task = new taskpool.Task(
+        convertAudioFormatTask,
+        this.context,
+        this.inputPath,
+        this.targetFormat,
+        this.outputPath
+      );
+
+      // 添加进度监听
+      task.onReceiveData((type:  string, data: string | number) => {
+        console.info(`onecold  收到转换进度: ${type} - ${data}`);
+
+        if (type === 'progress') {
+          // 这里可以更新UI进度
+        }
+      });
+
+      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('Audio  format conversion 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('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('showDialog  success callback, click button: ' + data.index);
+      });
+    } catch (error) {
+      let message = (error as BusinessError).message;
+      let code = (error as BusinessError).code;
+      console.error(`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: 20 }) {
+      // 文件信息显示
+      Row({ space: 10 }) {
+        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%')
+      .margin({ top: 5 })
+      .justifyContent(FlexAlign.Start)
+
+      // 目标格式选择
+      Column({ space: 10 }) {
+        Text('目标格式:')
+          .fontSize(16)
+          .width('90%')
+          .textAlign(TextAlign.Start)
+          .fontColor($r('app.color.text_color'))
+
+        // 第一组格式按钮 (MP3, FLAC, WAV, AAC, M4A)
+        SegmentButton({
+          options: this.formatOptions1,
+          selectedIndexes: $selectedFormatIndex
+        })
+          .width('90%')
+          .visibility(this.selectedFormatGroup === 1 ? Visibility.Visible : Visibility.Hidden)
+          .opacity(this.selectedFormatGroup === 1 ? 1 :0)
+          .animation({
+            duration: 500,
+            curve: 'ease-in-out' // 可选动画曲线
+          })
+          .onTouch(() => {
+            // 确保当前组是第一组
+            if (this.selectedFormatGroup !== 1) {
+              this.selectedFormatGroup = 1;
+              // 重置选中索引以触发onFormatChange
+              this.selectedFormatIndex = [0];
+            }
+          })
+
+        // 第二组格式按钮 (WMA, OGG, AC3, OPUS)
+        SegmentButton({
+          options: this.formatOptions2,
+          selectedIndexes: $selectedFormatIndex
+        })
+          .width('90%')
+          .visibility(this.selectedFormatGroup === 2 ? Visibility.Visible : Visibility.Hidden)
+          .opacity(this.selectedFormatGroup === 2 ? 1 :0)
+          .animation({
+            duration: 500,
+            curve: 'ease-in-out' // 可选动画曲线
+          })
+          .onTouch(() => {
+            // 确保当前组是第二组
+            if (this.selectedFormatGroup !== 2) {
+              this.selectedFormatGroup = 2;
+              // 重置选中索引以触发onFormatChange
+              this.selectedFormatIndex = [0];
+            }
+          })
+
+        // 组切换按钮
+        Row() {
+          Button("常用格式")
+            .backgroundColor(this.selectedFormatGroup === 1 ? this.themeColor : $r('app.color.index_background'))
+            .fontColor(this.selectedFormatGroup === 1 ? Color.White : $r('app.color.text_color'))
+            .borderRadius(20)
+            .fontSize(14)
+            .onClick(() => {
+              if (this.selectedFormatGroup !== 1) {
+                this.selectedFormatGroup = 1;
+                this.selectedFormatIndex = [0];
+              }
+            })
+          
+          Button("更多格式")
+            .backgroundColor(this.selectedFormatGroup === 2 ? this.themeColor : $r('app.color.index_background'))
+            .fontColor(this.selectedFormatGroup === 2 ? Color.White : $r('app.color.text_color'))
+            .borderRadius(20)
+            .fontSize(14)
+            .onClick(() => {
+              if (this.selectedFormatGroup !== 2) {
+                this.selectedFormatGroup = 2;
+                this.selectedFormatIndex = [0];
+              }
+            })
+        }
+        .width('90%')
+        .justifyContent(FlexAlign.SpaceBetween)
+        .margin({ top: 10 })
+
+        // 输出文件名设置
+        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)
+            .maxLines(3)
+            .fontColor($r('app.color.text_color'))
+            .width('90%')
+            .onEditChange((isEditing: boolean) => {
+              console.info(`onecold isEditing ${isEditing}`);
+              this.isEditing = isEditing;
+
+            })
+            .onChange((val: string) => {
+              if(this.isEditing){
+                console.info('onecold onChange val=' + val)
+                this.outputPath  = generateOutputPath(this.mVideoItem.filePath,'',this.currentPath,
+                  this.targetFormat, val )
+              }
+
+            })
+        }
+        .width('100%')
+        .margin({ top: 5, bottom: 5 })
+        .justifyContent(FlexAlign.Start)
+
+        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)  {
+            this.convertAudioFormat();
+          }
+        })
+        .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%')
+  }
+}
+
+
+
+// 生成新路径的方法
+function generateNewPath(filePath: string, newName: string, targetFormat?: string): string {
+  const lastSlashIndex = filePath.lastIndexOf('/');
+  const dir = filePath.substring(0, lastSlashIndex + 1);
+  
+  // 如果提供了目标格式,则使用目标格式作为扩展名
+  const ext = targetFormat ? `.${targetFormat}` : filePath.substring(filePath.lastIndexOf('.'));
+  
+  // 检查新名称是否已经包含扩展名
+  if (newName.endsWith(ext)) {
+    // 如果已经包含扩展名,直接使用新名称
+    return `${dir}${newName}`;
+  } else {
+    // 如果不包含扩展名,添加目标格式的扩展名
+    // 但需要检查是否已经有其他扩展名,如果有则替换
+    if (newName.includes('.')) {
+      const newNameWithoutExt = newName.substring(0, newName.lastIndexOf('.'));
+      return `${dir}${newNameWithoutExt}${ext}`;
+    } else {
+      // 没有扩展名,直接添加
+      return `${dir}${newName}${ext}`;
+    }
+  }
+}
+
+// 格式配置接口
+interface FormatConfig {
+  audioParams: string[];
+  description: string;
+}
+
+
+// 音频格式转换的核心方法
+@Concurrent
+async function convertAudioFormatTask(
+  context: Context,
+  inputPath: string,
+  targetFormat: string,
+  outPath?: string
+): 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 = targetFormat.toLowerCase();
+      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+      return `${dir}${name}_${timestamp}.${ext}`;
+    })();
+
+    console.info(`onecold  开始音频格式转换,输入: ${inputPath}, 目标格式: ${targetFormat}, 输出: ${outputPath}`);
+
+    // 根据目标格式设置编码参数
+    const formatLower = targetFormat.toLowerCase();
+    let formatConfig: FormatConfig ;
+
+    switch (formatLower) {
+      case 'mp3':
+        formatConfig = {
+          audioParams: ['-c:a', 'libmp3lame', '-b:a', '192k'],
+          description: 'MP3格式,兼容性好,文件较小'
+        } as FormatConfig;
+        break;
+      case 'flac':
+        formatConfig = {
+          audioParams: ['-c:a', 'flac', '-compression_level', '5'],
+          description: 'FLAC无损格式,音质最佳'
+        } as FormatConfig;
+        break;
+      case 'wav':
+        formatConfig = {
+          audioParams: ['-c:a', 'pcm_s16le'],
+          description: 'WAV无损格式,兼容性极佳'
+        } as FormatConfig;
+        break;
+      case 'aac':
+        formatConfig = {
+          audioParams: ['-c:a', 'aac', '-b:a', '192k'],
+          description: 'AAC格式,高质量压缩'
+        } as FormatConfig;
+        break;
+      case 'wma':
+        formatConfig = {
+          audioParams: ['-c:a', 'wmav2', '-b:a', '192k'],
+          description: 'WMA格式,微软设备兼容'
+        } as FormatConfig;
+        break;
+      case 'ape':
+        formatConfig = {
+          audioParams: ['-c:a', 'ape', '-compression_level', '1000'],
+          description: 'APE无损格式,高压缩率'
+        } as FormatConfig;
+        break;
+      case 'mp2':
+        formatConfig = {
+          audioParams: ['-c:a', 'mp2', '-b:a', '256k'],
+          description: 'MP2格式,广播和电视常用'
+        } as FormatConfig;
+        break;
+      case 'ac3':
+        formatConfig = {
+          audioParams: ['-c:a', 'ac3', '-b:a', '192k'],
+          description: 'AC3格式,多声道支持'
+        } as FormatConfig;
+        break;
+      case 'opus':
+        formatConfig = {
+          audioParams: ['-c:a', 'libopus', '-b:a', '128k'],
+          description: 'Opus格式,低延迟高音质'
+        } as FormatConfig;
+        break;
+      default:
+        formatConfig = {
+          audioParams: ['-c:a', 'libmp3lame', '-b:a', '192k'],
+          description: '默认MP3格式'
+        } as FormatConfig;
+    }
+
+    // 构建FFmpeg命令
+    const commands = [
+      'ffmpeg',
+      '-i', inputPath,
+      ...formatConfig.audioParams,
+      '-y', outputPath
+    ];
+
+    try {
+      console.info(`onecold  转换命令: ${commands.join('  ')}`);
+
+      // 执行 FFmpeg 命令
+      await FFmpeg.execute(commands,  {
+        logCallback: (logLevel, logMessage) =>
+        console.log(`[${logLevel}  ${logMessage}`),
+        progressCallback: (message) =>
+        console.log(`[progress]   ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+      });
+      
+      console.info(`onecold  格式转换成功: ${outputPath}`);
+      return true;
+    } catch (error) {
+      console.error(`onecold  格式转换失败: ${error}`);
+      return false;
+    }
+
+  } catch (error) {
+    console.error(`onecold  主流程错误: ${error}`);
+    return false;
+  }
+}
+
+function fileExists(path: string): boolean {
+  try {
+    // 这里假设有一个FileUtil.accessSync 方法
+    // 实际项目中可能是fs.existsSync(path) 或其他API
+    return fs.accessSync(path)
+  } catch (e) {
+    return false;
+  }
+}

+ 1094 - 0
entry/src/main/ets/view/EditAudio.ets

@@ -0,0 +1,1094 @@
+import { common, ConfigurationConstant } from '@kit.AbilityKit';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { VideoItem } from '../viewmodel/VideoItem';
+import { LazyDataSource } from '../common/util/LazyDataSource';
+import { AppUtil, FileUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
+import fs from '@ohos.file.fs';
+import { changeMusicCover, findLocalCoverImage, getApiLyric, repairAudioMetadata,
+  searchCover,
+  syncLyricToDB} from '../common/util/MusicTagUtils';
+import { taskpool, util } from '@kit.ArkTS';
+import { extractHwMediaMetadata, FFMpegTags, FFprobeMetadata, Utility } from '../common/util/Utility';
+import { DialogHelper } from '@pura/harmony-dialog';
+import MediaTable from '../common/util/MediaTable';
+import { TrackProgress } from '@abner/track';
+import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
+import Logger from '../common/util/Logger';
+import { BusinessError } from '@kit.BasicServicesKit';
+import { PlayStatus } from '../common/PlayStatus';
+import { ringtone } from '@kit.RingtoneKit';
+import { uniformTypeDescriptor } from '@kit.ArkData';
+import PermissionUtil from '../common/util/PermissionUtil'
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
+
+// 音频编辑功能
+@Component
+export struct EditAudio {
+  onResult = (_result: boolean,outPath:string) => {
+  }
+  onPlayOrPause = () => {
+  }
+  onBack = () => {
+  }
+  onSeeOutputPath = (outPutPath:string) => {
+
+  }
+  onSliderChange = (value:number) => {
+  }
+  @State isEditing:boolean = false
+  @Prop currentPath: string;
+  @StorageProp('isLandscape')   isLandscape: 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 startTimeStr:string = '00:00'
+  @State endTimeStr:string = '00:00'
+  // @State dataSource: LazyDataSource<VideoItem> = new LazyDataSource([])
+  @Prop mVideoItem: VideoItem
+  @StorageProp('topRectHeight') topRectHeight: number = 0;
+  @State isDarkMode: boolean = false
+  @State isProcessing: 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
+
+  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()
+    // 原始时间格式转换(如 "03:37" → "00:03:37.00")
+    const rawDuration = this.mVideoItem.duration  || '00:00:00';
+    this.durationStr  = convertSecondsToTime(convertTimeToSeconds(rawDuration));
+
+    // 初始化结束时间(标准化格式)
+    this.endTimeStr  = this.durationStr;
+    console.info('onecold this.durationStr ='+this.durationStr)
+    this.outputPath = generateOutputPath(this.mVideoItem.filePath,'剪辑',this.currentPath)
+    await new Promise<void>((resolve, reject) => {
+      this.table.getRdbStore(this.context,  (err:Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+  }
+
+  //导出音频
+  async exportAudio(){
+    if (this.isProcessing)  {
+      return;
+    }
+    this.isProcessing  = true;
+    // 1. 主线程获取音频信息(普通异步函数,无 @Concurrent)
+    const streamInfo = await getAudioStreamInfo(this.mVideoItem.filePath);
+    const task = new taskpool.Task(
+      trimAudio,
+      this.context,
+      this.mVideoItem.filePath,
+      this.startTimeStr,
+      this.endTimeStr,
+      0,
+      this.outputPath,
+      streamInfo.codecName,
+      streamInfo.sampleRate,
+      streamInfo.sampleFmt,
+    );
+    taskpool.execute(task, taskpool.Priority.HIGH).then((result) => {
+      this.isProcessing  = false;
+      if(result){//导出成功后提示
+        this.onResult(true,this.outputPath)
+        this.showSuccess()
+      }else{
+        this.onResult(false,this.outputPath)
+      }
+
+    }).catch((error: Error) => {
+      console.error('trimAudio sync failed:', error);
+    });
+  }
+
+
+  async exportAudioForDSF(){
+    if(FileUtil.accessSync(this.outputPath)){
+      this.outputPath = generateOutputPath(this.mVideoItem.filePath,'剪辑',this.currentPath)
+    }
+    // 1. 主线程获取音频信息(普通异步函数,无 @Concurrent)
+    const streamInfo = await getAudioStreamInfo(this.mVideoItem.filePath);
+    const task = new taskpool.Task(
+      handleDSDNative,
+      this.context,
+      this.mVideoItem.filePath,
+      this.startTimeStr,
+      this.endTimeStr,
+      this.outputPath,
+    );
+    taskpool.execute(task, taskpool.Priority.HIGH).then((result) => {
+
+      if(result){//导出成功后提示
+        this.onResult(true,this.outputPath)
+        this.showSuccess()
+      }else{
+        this.onResult(false,this.outputPath)
+      }
+
+    }).catch((error: Error) => {
+      console.error('exportAudioForDSF sync failed:', error);
+    });
+  }
+
+  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('showDialog err: ' + err);
+          return;
+        }
+
+        // 根据点击的按钮索引处理不同逻辑
+        if (data.index  === 1) { // "设为铃声"按钮的索引为1
+          // ToastUtil.showToast('设置铃声成功')
+          setRingTone(this.context, this.outputPath,FileUtil.getFileName(this.outputPath))
+        }else if (data.index  === 0){//查看路径
+          this.onSeeOutputPath(this.outputPath)
+        }
+        console.info('showDialog success callback, click button: ' + data.index);
+      });
+    } catch (error) {
+      let message = (error as BusinessError).message;
+      let code = (error as BusinessError).code;
+      console.error(`showDialog args error code is ${code}, message is ${message}`);
+    }
+
+  }
+
+
+  build() {
+    Scroll(){
+      Column(){
+        this.topTitleBar()
+        this.buildContent()
+      }
+      .height(this.isLandscape ? 'auto' :'98%')
+    }
+    .height('100%')
+    .width('100%')
+    .backgroundColor($r('app.color.index_background'))
+  }
+
+
+  @Builder
+  buildContent() {
+    Column({ space: 20 }) {
+      // 文件名显示
+      Row({ space: 10 }) {
+        Image(this.mVideoItem.pixelMapPath)
+          .height(55)
+          .width(55)
+          .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.6})
+          .borderRadius(12)
+          .alt($r('app.media.llq'))
+          .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%')
+      .margin({ top: 5})
+      .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(`onecold isEditing ${isEditing}`);
+            this.isEditing = isEditing;
+
+          })
+          .onChange((val: string) => {
+            if(this.isEditing){
+              console.info('onecold onChange val=' + val)
+              this.outputPath  = generateOutputPath(this.mVideoItem.filePath,'',this.currentPath,
+                undefined, val )
+            }
+
+          })
+      }
+      .width('100%')
+      .margin({ top: 5, bottom: 5 })
+      .justifyContent(FlexAlign.Start)
+
+      Column(){
+        TrackProgress({
+          pointerWidth: 20,
+          leftPointerBgColor: Color.Orange,
+          rightPointerBgColor: Color.Orange,
+          trackSelectColor: Color.Orange,
+          trackBgBorder: { width: 1, color: Color.Orange, radius: 5 },
+          onLeftProgress: (progress: number) => {
+            console.log("onecold ===左侧指针进度:" + progress)
+            // 将进度比例转换为时间(秒)
+            const totalSeconds = convertTimeToSeconds(this.durationStr);
+            const newStartSeconds = Math.floor(progress*0.01  * totalSeconds);
+            this.startTimeStr  = convertSecondsToTime(newStartSeconds);
+          },
+          onRightProgress: (progress: number) => {
+            console.log("===右侧指针进度:" + progress)
+            const totalSeconds = convertTimeToSeconds(this.durationStr);
+            const newEndSeconds = Math.floor(progress*0.01  * totalSeconds);
+            this.endTimeStr  = convertSecondsToTime(newEndSeconds);
+          }
+        })
+      }
+      .width('90%')
+
+      // 开始时间设置区域
+      Row() {
+        Text('开始时间:')
+          .fontSize(16)
+          .fontColor($r('app.color.text_color'))
+
+        Button('-', { type: ButtonType.Circle })
+          .width(40)
+          .height(40)
+          .fontSize(20)
+          .backgroundColor(this.themeColor) // 使用主题色
+          .onClick(() => {
+            const seconds = convertTimeToSeconds(this.startTimeStr);
+            if (seconds > 0.1) { // 避免负数
+              this.startTimeStr  = convertSecondsToTime(seconds - 0.1);
+            }
+
+          })
+          .gesture(
+            LongPressGesture({ repeat: true, duration: 50 })
+              .onAction((event?: GestureEvent) => {
+                const seconds = convertTimeToSeconds(this.startTimeStr);
+                if (seconds > 0.1) {
+                  this.startTimeStr  = convertSecondsToTime(seconds - 0.1);
+                }
+              }))
+
+        Text(this.startTimeStr) // 假设startTime属性存在
+          .fontSize(16)
+          .fontColor($r('app.color.text_color'))
+          .width(80)
+          .textAlign(TextAlign.Center)
+
+        Button('+', { type: ButtonType.Circle })
+          .width(40)
+          .height(40)
+          .fontSize(20)
+          .backgroundColor(this.themeColor) // 使用主题色
+          .onClick(() => {
+            const startSeconds = convertTimeToSeconds(this.startTimeStr);
+            const endSeconds = convertTimeToSeconds(this.endTimeStr);
+            // 限制:开始时间不超过结束时间-0.1秒
+            if (startSeconds < endSeconds - 0.1) {
+              this.startTimeStr  = convertSecondsToTime(startSeconds + 0.1);
+            }
+          })
+          .gesture(
+            LongPressGesture({ repeat: true, duration: 50 })
+              .onAction((event?: GestureEvent) => {
+                const startSeconds = convertTimeToSeconds(this.startTimeStr);
+                const endSeconds = convertTimeToSeconds(this.endTimeStr);
+                // 限制:开始时间不超过结束时间-0.1秒
+                if (startSeconds < endSeconds - 0.1) {
+                  this.startTimeStr  = convertSecondsToTime(startSeconds + 0.1);
+                }
+              }))
+      }
+      .width('90%')
+      .justifyContent(FlexAlign.SpaceBetween)
+
+      // 结束时间设置区域
+      Row() {
+        Text('结束时间:')
+          .fontSize(16)
+          .fontColor($r('app.color.text_color'))
+
+        Button('-', { type: ButtonType.Circle, stateEffect: true })
+          .width(40)
+          .height(40)
+          .fontSize(20)
+          .backgroundColor(this.themeColor) // 使用主题色
+          .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
+          .onClick(() => {
+            const startSeconds = convertTimeToSeconds(this.startTimeStr);
+            const endSeconds = convertTimeToSeconds(this.endTimeStr);
+            // 限制:结束时间不小于开始时间+0.1秒
+            if (endSeconds > startSeconds + 0.1) {
+              this.endTimeStr  = convertSecondsToTime(endSeconds - 0.1);
+            }
+          })
+          .gesture(
+            LongPressGesture({ repeat: true, duration: 50 })
+              .onAction((event?: GestureEvent) => {
+                const startSeconds = convertTimeToSeconds(this.startTimeStr);
+                const endSeconds = convertTimeToSeconds(this.endTimeStr);
+                // 限制:结束时间不小于开始时间+0.1秒
+                if (endSeconds > startSeconds + 0.1) {
+                  this.endTimeStr  = convertSecondsToTime(endSeconds - 0.1);
+                }
+              }))
+
+        Text(this.endTimeStr) // 假设endTime属性存在
+          .fontSize(16)
+          .fontColor($r('app.color.text_color'))
+          .width(80)
+          .textAlign(TextAlign.Center)
+
+        Button('+', { type: ButtonType.Circle, stateEffect: true })
+          .width(40)
+          .height(40)
+          .fontSize(20)
+          .backgroundColor(this.themeColor) // 使用主题色
+          .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
+          .onClick(() => {
+            const totalDuration = convertTimeToSeconds(this.durationStr);
+            const currentEnd = convertTimeToSeconds(this.endTimeStr);
+            if (currentEnd < totalDuration - 0.1) { // 避免超出总时长
+              this.endTimeStr  = convertSecondsToTime(currentEnd + 0.1);
+            }
+          })
+          .gesture(
+            LongPressGesture({ repeat: true, duration: 50 })
+              .onAction((event?: GestureEvent) => {
+                const totalDuration = convertTimeToSeconds(this.durationStr);
+                const currentEnd = convertTimeToSeconds(this.endTimeStr);
+                if (currentEnd < totalDuration - 0.1) {
+                  this.endTimeStr  = convertSecondsToTime(currentEnd + 0.1);
+                }
+              }))
+      }
+      .width('90%')
+      .justifyContent(FlexAlign.SpaceBetween)
+
+      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)
+        .enabled(!this.isProcessing)
+        .backgroundColor(this.isProcessing  ? Color.Gray : this.themeColor)
+        .borderRadius(20)
+        .margin({top:20})
+        .clickEffect({ level: ClickEffectLevel.MIDDLE,scale:0.6 })
+        .onClick(() => {
+          if (this.isProcessing)  {
+            return
+          }
+          // 实现导出逻辑
+          if(this.mVideoItem.filePath.toLowerCase().endsWith('.dsf')){
+            this.exportAudioForDSF();
+          }else{
+            this.exportAudio();
+          }
+
+        })
+    }
+    .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')
+        // .blockColor('rgba(255,255,255,1)')
+        // .trackColor('rgba(255,255,255,0.3)')
+        // .selectedColor($r('app.color.index_background'))
+        .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%')
+  }
+
+  @Builder
+  oldTopTitleBar() {
+    // 顶部安全区和自定义标题栏
+    Column() {
+      // 顶部安全区
+      Blank()
+        .height(this.topRectHeight)
+        .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
+      // 自定义标题栏(Stack实现绝对居中)
+      Stack() {
+        // 居中标题
+        Text('剪辑音频')
+          .fontSize(18)
+          .fontColor(Color.White)
+          .align(Alignment.Center)
+        // 左右按钮
+        Row() {
+          Image($r('app.media.left_back_white'))
+            .width(26)
+            .height(26)
+            .margin({ left: 12, right: 8 })
+            .onClick(() => {
+               this.onBack()
+            })
+          Blank().flexGrow(1)
+          Blank().width(32)
+        }
+        .height(48)
+        .width('100%')
+        .alignItems(VerticalAlign.Center)
+      }
+      .height(48)
+      .width('100%')
+      .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
+    }
+  }
+
+}
+
+//剪辑音频
+// 辅助函数:通过ffprobe获取音频流信息
+// 定义音频流信息接口(鸿蒙支持interface)
+interface AudioStreamInfo {
+  codecName: string;
+  sampleRate: string;
+  sampleFmt: string;
+}
+
+async function getAudioStreamInfo(inputPath: string): Promise<AudioStreamInfo> {
+  return new Promise((resolve, reject) => {
+    inputPath = FileUtil.getFilePath(inputPath);
+    const probeCommands = [
+      'ffprobe',
+      '-v', 'error',
+      '-select_streams', 'a:0',
+      '-show_entries', 'stream=codec_name,sample_rate,sample_fmt',
+      '-of', 'json',
+      inputPath
+    ];
+
+    let outputJson = "";
+
+    FFmpeg.execute(probeCommands,  {
+      logCallback: (logLevel: number, logMessage: string) => {
+        console.log(`[${logLevel}]  ${logMessage}`);
+      },
+      outputCallback: (message: string) => {
+        outputJson += message;
+      },
+    }).then(() => {
+      try {
+        const info: FFprobeMetadata = JSON.parse(outputJson);
+        const stream = info.streams?.[0];
+
+        if (!stream) {
+          reject(new Error("未找到音频流"));
+          return;
+        }
+
+        resolve({
+          codecName: stream.codec_name  || "",
+          sampleRate: stream.sample_rate  || "44100",
+          sampleFmt: stream.sample_fmt  || "s16"
+        });
+      } catch (e) {
+        console.error(`onecold  解析音频信息失败: ${e}`);
+        reject(new Error(`解析音频信息失败`));
+      }
+    }).catch((err: Error) => {
+      console.error(`onecold  FFprobe执行错误: ${err.message}`);
+      reject(err);
+    });
+  });
+}
+
+// 主函数:跨格式音频剪辑(修复流层duration和duration_ts)
+@Concurrent
+export async function trimAudio(
+  context: Context,
+  inputPath: string,
+  startTime: string,
+  endTime: string,
+  streamIndex: number = 0,
+  outPath?: string,
+  codecName?: string,
+  sampleRate?: string,
+  sampleFmt?: string
+): Promise<string> {
+  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}_trimmed${ext}`;
+  })();
+  // 步骤2:从路径提取文件扩展名
+  const getFileExtension = (path: string): string => {
+    const lastDotIndex = path.lastIndexOf('.');
+    if (lastDotIndex === -1 || lastDotIndex === path.length  - 1) {
+      return ''; // 无扩展名
+    }
+    return path.substring(lastDotIndex  + 1).toLowerCase(); // 如"mp3"
+  };
+  const format  = getFileExtension(inputPath);
+  console.info(` onecold  format: ${format}`);
+  console.info(` onecold  codecName: ${codecName}`);
+  console.info(` onecold  sampleFmt: ${sampleFmt}`);
+  console.info(` onecold  sampleRate: ${sampleRate}`);
+  codecName = codecName ||format
+  sampleFmt = sampleFmt ||'s32'
+  sampleRate = sampleRate ||'48000'
+  try {
+
+    // 步骤2:根据格式动态生成FFmpeg命令
+    const baseCommands = [
+      'ffmpeg',
+      '-i', inputPath,
+      '-map', `0:${streamIndex}`,
+      '-ss', startTime,
+      '-to', endTime,
+      '-fflags', '+genpts',           // 所有格式通用:重新生成时间戳
+      '-reset_timestamps', '1',       // 所有格式通用:重置时间戳起点为0
+      '-map_metadata', '-1',          // 所有格式通用:清除原始元数据
+    ];
+
+    let formatSpecificCommands: string[] = [];
+
+    switch (codecName.toLowerCase())  {
+      // 1. FLAC格式:必须重新编码以重置帧时间戳
+      case 'flac':
+        formatSpecificCommands = [
+          '-c:a', 'flac',                // 重新编码FLAC
+          '-sample_fmt', sampleFmt,      // 保持原始采样格式(如s32)
+          '-ar', sampleRate,             // 保持原始采样率(如48000)
+        ];
+        break;
+
+      // 2. MP3格式:禁用XING帧头,优先流复制,失败则重新编码
+      case 'mp3':
+        formatSpecificCommands = [
+          '-c:a', 'copy',                // 优先流复制(快速)
+          '-write_xing', '0',            // 禁用XING帧头(关键修复)
+        ];
+        // 若流复制失败(如MP3帧损坏),降级为重新编码
+        formatSpecificCommands.push('-c:a',  'libmp3lame'); // 备选:重新编码
+        break;
+
+      // 3. AAC格式(常见于MP4/M4A):流复制+MP4优化
+      case 'aac':
+        formatSpecificCommands = [
+          '-c:a', 'copy',                // 流复制
+          '-movflags', '+faststart',     // MP4格式优化:确保时长正确
+        ];
+        break;
+
+      // 4. WAV格式:无损流复制(无压缩帧头问题)
+      case 'pcm_s16le': // WAV的PCM编码
+      case 'pcm_s32le':
+        formatSpecificCommands = [
+          '-c:a', 'copy',                // 直接流复制
+        ];
+        break;
+      // 新增:DSF格式处理(codecName为dsd_lsbf/dsd_msbf/dsf)
+      case 'dsd_lsbf':
+      case 'dsd_msbf':
+      case 'dsf':
+        // 方案1:转码为PCM(WAV容器,兼容性最佳)
+        formatSpecificCommands = [
+          '-c:a', 'pcm_s24le',    // 24位PCM编码
+          '-ar', '48000',      // 保持原始采样率(如352800Hz)
+          '-sample_fmt', 's24',   // 采样格式
+          '-f', 'wav',            // 输出为WAV
+        ];
+        // 若原扩展为.dsf,强制修改输出扩展为.wav(避免格式混淆)
+        if (outputPath.endsWith('.dsf')  && !outPath) {
+          outputPath = outputPath.replace('.dsf',  '.wav');
+        }
+        break;
+      // 5. 其他格式(如OGG/ALAC):默认重新编码为原格式
+      default:
+        formatSpecificCommands = [
+          '-c:a', codecName,             // 使用原编码器重新编码
+          '-sample_fmt', sampleFmt,
+          '-ar', sampleRate,
+        ];
+        break;
+    }
+
+    // 拼接完整命令
+    const commands = [...baseCommands, ...formatSpecificCommands, outputPath];
+
+    // 步骤3:执行FFmpeg剪辑
+    await FFmpeg.execute(commands,  {
+      logCallback: (logLevel, logMessage) =>
+      console.log(`[${logLevel}]  ${logMessage}`),
+      progressCallback: (message) =>
+      console.log(`[progress]  ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+    });
+
+    // 步骤4:媒体入库(保持原逻辑)
+    console.info(` 音频剪辑成功,保存路径: ${outputPath}`);
+    const table: MediaTable = new MediaTable(context);
+    await new Promise<void>((resolve, reject) => {
+      table.getRdbStore(context,  (err: Error) => err ? reject(err) : resolve());
+    });
+    if (Utility.isMeidaByExtension(outputPath))  {
+      const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+        context, outputPath, CommonConstants.TYPE_LOCAL, true
+      );
+      table.insert(mediaItem,  () => {}, '');
+    }
+
+    return outputPath;
+
+  } catch (error) {
+    const errorMsg = error instanceof Error ? error.message  : String(error);
+    console.error(` 音频剪辑失败: ${errorMsg}`);
+    return '';
+  }
+}
+
+// 秒数转智能时间格式(自动省略小时00)
+function convertSecondsToTime(totalSeconds: number): string {
+  const hours = Math.floor(totalSeconds  / 3600);
+  const minutes = Math.floor((totalSeconds  % 3600) / 60);
+  const seconds = totalSeconds % 60;
+
+  // 格式化秒数,去掉末尾的 .0
+  const formatSeconds = (sec: number): string => {
+    return Number.isInteger(sec)  ?
+    sec.toString().padStart(2,  '0') :
+    sec.toFixed(1).padStart(4,  '0');
+  };
+
+  // 格式化为 MM:SS.s 或 HH:MM:SS.s(小时为0时省略)
+  if (hours > 0) {
+    return `${hours.toString().padStart(2,  '0')}:${minutes.toString().padStart(2,  '0')}:${formatSeconds(seconds)}`;
+  } else {
+    return `${minutes.toString().padStart(2,  '0')}:${formatSeconds(seconds)}`;
+  }
+}
+
+// 时间字符串解析(兼容 HH:MM:SS.s 和 MM:SS.s 格式)
+function convertTimeToSeconds(timeStr: string): number {
+  const parts = timeStr.split(':');
+
+  // 根据冒号数量判断格式
+  switch (parts.length)  {
+    case 1: // 仅秒数(如 "37.5")
+      return parseFloat(parts[0]);
+    case 2: // MM:SS.s 格式
+      return parseInt(parts[0]) * 60 + parseFloat(parts[1]);
+    case 3: // HH:MM:SS.s 格式
+      return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]);
+    default:
+      throw new Error(`Invalid time format: ${timeStr}`);
+  }
+}
+
+/**
+ * 自动生成输出路径,支持自定义文件名和灵活的后缀规则
+ * @param inputPath 输入文件路径(如 `/data/storage/audio.mp3` )
+ * @param suffix 可选后缀(如 "_trimmed"),customName存在时无效
+ * @param downloadPath 下载目录路径
+ * @param outputExt 可选输出后缀名(如 ".mp3")
+ * @param customName 自定义文件名(存在时忽略时间戳和suffix)
+ * @returns 输出路径(customName存在时格式:`/data/storage/customName.ext` )
+ */
+export function generateOutputPath(
+  inputPath: string,
+  suffix: string = "",
+  downloadPath: string,
+  outputExt?: string,
+  customName?: string
+): string {
+  // 解析路径基础组件
+  const dir = downloadPath.endsWith('/')  ? downloadPath : downloadPath + '/';
+  const fullName = safeDecode(inputPath.substring(inputPath.lastIndexOf('/')  + 1));
+  const dotIndex = fullName.lastIndexOf('.');
+
+  if(StrUtil.isNotEmpty(outputExt)&&!outputExt?.startsWith('.')){
+    outputExt = '.'+outputExt//兼容输出扩展名有没有包含.
+  }
+  // 确定最终文件名和扩展名
+  const finalName = customName ?? (dotIndex === -1 ? fullName : fullName.substring(0,  dotIndex));
+  const ext = outputExt ?? (dotIndex === -1 ? '' : fullName.substring(dotIndex));
+  let timestamp:string=''
+  // 核心逻辑分支
+  let basePath: string;
+  if (customName !== undefined) {
+    // 模式1:完全自定义(忽略时间戳和suffix)
+    basePath = `${dir}${finalName}${ext}`;
+  } else {
+    // 模式2:自动生成带时间戳的路径
+    timestamp = `${new Date().getFullYear()}${(new Date().getMonth() + 1).toString().padStart(2, '0')}${new Date().getDate().toString().padStart(2, '0')}_${new Date().getHours().toString().padStart(2, '0')}${new Date().getMinutes().toString().padStart(2, '0')}`;
+    basePath = `${dir}${finalName}_${timestamp}${suffix}${ext}`;
+  }
+
+  // 处理文件重名冲突
+  let counter = 0;
+  let outputPath = basePath;
+  while (fileExists(outputPath)) {
+    counter++;
+    outputPath = customName !== undefined
+      ? `${dir}${finalName}_${counter}${ext}`  // 自定义模式追加数字
+      : `${dir}${finalName}_${timestamp}${suffix}_${counter}${ext}`; // 自动模式追加数字
+  }
+
+  return outputPath;
+}
+/**
+ * 获取文件名(不含后缀),自动处理URL编码
+ * @param filePath 文件路径(如 `/data/测试%20文件.txt` 或 `example.mkv` )
+ * @returns 纯文件名(如 `测试 文件` 或 `example`)
+ */
+export function getFileNameWithoutExtension(filePath: string): string {
+  // 1. 提取最后一个'/'后的内容(兼容Windows路径)
+  const fileNameWithExt = filePath.substring(filePath.lastIndexOf('/')  + 1);
+
+  // 2. 解码URL编码(如 %20 → 空格)
+  const decodedName = decodeURIComponent(fileNameWithExt);
+
+  // 3. 去除后缀(找到最后一个点号)
+  const lastDotIndex = decodedName.lastIndexOf('.');
+
+  // 4. 返回结果(无后缀时返回完整名称)
+  return lastDotIndex === -1 ? decodedName : decodedName.substring(0,  lastDotIndex);
+}
+
+function safeDecode(encodedStr: string) {
+  if (/%[0-9A-Fa-f]{2}/.test(encodedStr)) {
+    return decodeURIComponent(encodedStr);
+  }
+  return encodedStr; // 非编码字符串直接返回
+}
+
+function fileExists(path: string): boolean {
+  try {
+    // 这里假设有一个FileUtil.accessSync 方法
+    // 实际项目中可能是fs.existsSync(path) 或其他API
+    return fs.accessSync(path)
+  } catch (e) {
+    return false;
+  }
+}
+
+
+
+// 专门处理DSF格式的函数
+@Concurrent
+async function handleDSFAudio(
+  context: Context,
+  inputPath: string,
+  startTime: string,
+  endTime: string,
+  outputPath: string
+): Promise<string> {
+  try {
+    inputPath = FileUtil.getFilePath(inputPath);
+    // 方案1:转换为高精度PCM WAV格式(推荐)
+    const wavOutputPath = outputPath.replace('.dsf',  '.wav');
+
+    console.info(`onecold  DSF转码开始: ${inputPath} -> ${wavOutputPath}`);
+
+    // 将DSF转换为WAV进行剪辑
+    const convertCommands = [
+      'ffmpeg',
+      '-i', inputPath,
+      '-ss', startTime,
+      '-to', endTime,
+      '-c:a', 'pcm_s24le',     // 24位PCM,保持高音质
+      '-ar', '48000',           // 设置采样率(DSF通常很高,需要降低)
+      '-ac', '2',               // 立体声
+      '-f', 'wav',               // 输出为WAV格式
+      '-y',
+      wavOutputPath
+    ];
+
+    await FFmpeg.execute(convertCommands,  {
+      logCallback: (logLevel, logMessage) =>
+      console.log(`[${logLevel}]  DSF转码: ${logMessage}`),
+      progressCallback: (message) =>
+      console.log(`[progress]  DSF转码: ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+    });
+
+    console.info(`onecold  DSF转码成功: ${wavOutputPath}`);
+
+    // 媒体入库
+    const table: MediaTable = new MediaTable(context);
+    await new Promise<void>((resolve, reject) => {
+      table.getRdbStore(context,  (err: Error) => err ? reject(err) : resolve());
+    });
+
+    if (Utility.isMeidaByExtension(wavOutputPath))  {
+      const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+        context, wavOutputPath, CommonConstants.TYPE_LOCAL, true
+      );
+      table.insert(mediaItem,  () => {}, '');
+    }
+
+    return wavOutputPath;
+  } catch (error) {
+    const errorMsg = error instanceof Error ? error.message  : String(error);
+    console.error(`DSF 转码失败: ${errorMsg}`);
+    return ''
+    // 方案2:如果转码失败,尝试使用DSD原生处理
+    // return await handleDSDNative(context, inputPath, startTime, endTime, outputPath);
+  }
+}
+
+// 备选方案:DSD原生处理(如果FFmpeg支持)
+@Concurrent
+async function handleDSDNative(
+  context: Context,
+  inputPath: string,
+  startTime: string,
+  endTime: string,
+  outputPath: string
+): Promise<string> {
+  try {
+    inputPath = FileUtil.getFilePath(inputPath);
+    // 尝试使用DSD原生支持
+    const dsdCommands = [
+      'ffmpeg',
+      '-i', inputPath,
+      '-ss', startTime,
+      '-to', endTime,
+      '-c:a', 'copy',           // 尝试直接复制
+      '-f', 'dsf',               // 强制输出为DSF格式
+      '-y',
+      outputPath
+    ];
+
+    await FFmpeg.execute(dsdCommands,  {
+      logCallback: (logLevel, logMessage) =>
+      console.log(`[${logLevel}]  DSD原生: ${logMessage}`),
+      progressCallback: (message) =>
+      console.log(`[progress]  DSD原生: ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+    });
+
+    console.info(`onecold  DSD原生剪辑成功: ${outputPath}`);
+
+    // 媒体入库
+    const table: MediaTable = new MediaTable(context);
+    await new Promise<void>((resolve, reject) => {
+      table.getRdbStore(context,  (err: Error) => err ? reject(err) : resolve());
+    });
+
+    if (Utility.isMeidaByExtension(outputPath))  {
+      const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+        context, outputPath, CommonConstants.TYPE_LOCAL, true
+      );
+      table.insert(mediaItem,  () => {}, '');
+    }
+
+    return outputPath;
+  } catch (error) {
+    const errorMsg = error instanceof Error ? error.message  : String(error);
+    console.error(`DSD 原生剪辑失败: ${errorMsg}`);
+
+    // 最终方案:转换为FLAC格式
+    const flacOutputPath = outputPath.replace('.dsf',  '.flac');
+
+    const flacCommands = [
+      'ffmpeg',
+      '-i', inputPath,
+      '-ss', startTime,
+      '-to', endTime,
+      '-c:a', 'flac',
+      '-compression_level', '5',   // 高质量FLAC
+      '-ar', '48000',
+      '-ac', '2',
+      '-y',
+      flacOutputPath
+    ];
+
+    await FFmpeg.execute(flacCommands,  {
+      logCallback: (logLevel, logMessage) =>
+      console.log(`[${logLevel}]  DSF转FLAC: ${logMessage}`),
+      progressCallback: (message) =>
+      console.log(`[progress]  DSF转FLAC: ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+    });
+    const table: MediaTable = new MediaTable(context);
+    // 媒体入库
+    await new Promise<void>((resolve, reject) => {
+      const table: MediaTable = new MediaTable(context);
+      table.getRdbStore(context,  (err: Error) => err ? reject(err) : resolve());
+    });
+
+    if (Utility.isMeidaByExtension(flacOutputPath))  {
+      const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+        context, flacOutputPath, CommonConstants.TYPE_LOCAL, true
+      );
+      table.insert(mediaItem,  () => {}, '');
+    }
+
+    return flacOutputPath;
+  }
+}
+
+//设置铃声
+export async  function setRingTone(context: Context,path:string, name:string) {
+  if (StrUtil.isEmpty(path) || StrUtil.isEmpty(name)) {
+    return
+  }
+  // 确定后的逻辑
+  let ringtoneTypeList: Array<ringtone.RingtoneType> = ringtone.getSupportedRingtoneTypes();
+  LogUtil.info('onecold getSupportedRingtoneTypes : ' + JSON.stringify(ringtoneTypeList));
+  let dataTypeList: Array<uniformTypeDescriptor.UniformDataType> =
+    ringtone.getSupportedDataTypes(ringtone.RingtoneType.NOTIFICATION);
+  LogUtil.info('onecold getSupportedDataTypes: ' + JSON.stringify(dataTypeList));
+
+
+  //let fileName: string = audioPath.substring(audioPath.lastIndexOf('/') + 1, audioPath.lastIndexOf('.'));
+  await ringtone.startRingtoneSetting(context as common.UIAbilityContext
+    ,path, name).then(res => {
+    LogUtil.info('onecold setFlag :' + res);
+  });
+}
+
+

+ 697 - 0
entry/src/main/ets/view/ExtractAccompaniment.ets

@@ -0,0 +1,697 @@
+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 { SwipeRefresher, SegmentButton } from '@kit.ArkUI';
+import { SegmentButtonOptions, SegmentButtonItemTuple } from '@kit.ArkUI';
+import PermissionUtil from '../common/util/PermissionUtil'
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
+import { Utility } from '../common/util/Utility';
+
+// 伴奏提取功能
+@Component
+export struct ExtractAccompaniment {
+  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 extractionQuality: number = 2; // 0-低, 1-中, 2-高
+  @State isProcessing: boolean = false;
+  
+  // SegmentButton选项
+  @State qualityOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [{ text: '低质量' }, { text: '中等质量' }, { text: '高质量' }] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor:$r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding:{top:10,bottom:10},
+    multiply: false
+  });
+  @State @Watch('onQualityChange') selectedQualityIndex: number[] = [this.extractionQuality];
+  
+  private table: MediaTable = new MediaTable(this.context);
+  
+  // 音质变化监听
+  onQualityChange() {
+    if (this.selectedQualityIndex.length > 0) {
+      this.extractionQuality = this.selectedQualityIndex[0];
+    }
+  }
+  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('onecold this.outputPath ='+this.outputPath)
+    await new Promise<void>((resolve, reject) => {
+      this.table.getRdbStore(this.context,  (err:Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+  }
+      // 提取伴奏
+  async extractAccompaniment() {
+    if (this.isProcessing)  {
+      return;
+    }
+    if(StrUtil.isEmpty(this.inputPath)){
+      this.inputPath =this.mVideoItem.filePath
+    }
+    if(FileUtil.accessSync(this.outputPath)){
+      this.outputPath = generateOutputPath(this.inputPath,'伴奏',this.currentPath)
+    }
+    this.isProcessing  = true;
+
+    try {
+      const task = new taskpool.Task(
+        extractAudioAccompaniment,
+        this.context,
+        this.inputPath,
+        this.outputPath,
+        this.extractionQuality,
+      );
+
+      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('Extract  accompaniment 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('showDialog  err: ' + err);
+          return;
+        }
+        // 根据点击的按钮索引处理不同逻辑
+        if (data.index  === 1) { // "设为铃声"按钮的索引为1
+          // ToastUtil.showToast('设置铃声成功')
+          setRingTone(this.context, this.outputPath,FileUtil.getFileName(this.outputPath))
+        }else if (data.index  === 0){//查看路径
+          this.onSeeOutputPath(this.outputPath)
+        }
+        console.info('showDialog  success callback, click button: ' + data.index);
+      });
+    } catch (error) {
+      let message = (error as BusinessError).message;
+      let code = (error as BusinessError).code;
+      console.error(`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: 20 }) {
+        // 文件信息显示
+        Row({ space: 10 }) {
+          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%')
+        .margin({ top: 5})
+        .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(`onecold isEditing ${isEditing}`);
+              this.isEditing = isEditing;
+
+            })
+            .onChange((val: string) => {
+              if(this.isEditing){
+                console.info('onecold 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)
+
+          SegmentButton({
+            options: this.qualityOptions,
+            selectedIndexes: $selectedQualityIndex
+          })
+          .width('90%')
+
+          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('onecold this.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.extractAccompaniment();
+                    }).catch((e:object)=>{
+                      console.info("task1 catch e: " + e);
+                    })
+                  }else{
+                    this.extractAccompaniment();
+                  }
+
+                }else{
+                  this.extractAccompaniment();
+                }
+
+              }
+            })
+            .enabled(!this.isProcessing)
+        }
+        .width('100%')
+        .padding({ top: 20, bottom: 20 })
+        .alignItems(HorizontalAlign.Center)
+      }
+    }
+
+  // @Builder
+  // methodBuilder(){
+  //   Column({ space: 5 }) {
+  //
+  //     Text('提取方法:')
+  //       .fontSize(16)
+  //       .width('90%')
+  //       .textAlign(TextAlign.Start)
+  //     GridRow({ columns:  6, gutter: 5 }) {
+  //       GridCol() {
+  //         Button('中置声道提取')
+  //           .width(100)
+  //           .height(35)
+  //           .fontSize(10)
+  //           .backgroundColor(this.method  === 0 ? this.themeColor  : Color.Gray)
+  //           .onClick(() => {
+  //             this.method  = 0;
+  //           })
+  //       }
+  //       .margin({top:10})
+  //       .span({ xs: 2, sm: 2, md: 2 })
+  //
+  //       GridCol() {
+  //         Button('立体声分离')
+  //           .width(100)
+  //           .height(35)
+  //           .fontSize(10)
+  //           .backgroundColor(this.method  === 1 ? this.themeColor  : Color.Gray)
+  //           .onClick(() => {
+  //             this.method  = 1;
+  //           })
+  //       }
+  //       .margin({top:10})
+  //       .span({ xs: 2, sm: 2, md: 2 })
+  //
+  //       GridCol() {
+  //         Button('高通滤波器')
+  //           .width(100)
+  //           .height(35)
+  //           .fontSize(10)
+  //           .backgroundColor(this.method  === 2 ? this.themeColor  : Color.Gray)
+  //           .onClick(() => {
+  //             this.method  = 2;
+  //           })
+  //       }
+  //       .margin({top:10})
+  //       .span({ xs: 2, sm: 2, md: 2 })
+  //
+  //       GridCol() {
+  //         Button('中置声道消除')
+  //           .width(100)
+  //           .height(35)
+  //           .fontSize(10)
+  //           .backgroundColor(this.method  === 3 ? this.themeColor  : Color.Gray)
+  //           .onClick(() => {
+  //             this.method  = 3;
+  //           })
+  //       }
+  //       .margin({top:10})
+  //       .span({ xs: 2, sm: 2, md: 2 })
+  //
+  //       GridCol() {
+  //         Button('Spleeter模型分离')
+  //           .width(110)
+  //           .height(35)
+  //           .fontSize(10)
+  //           .backgroundColor(this.method  === 4 ? this.themeColor  : Color.Gray)
+  //           .onClick(() => {
+  //             this.method  = 4;
+  //           })
+  //       }
+  //       .margin({top:10})
+  //       .span({ xs: 2, sm: 2, md: 2 })
+  //     }
+  //     .width('90%')
+  //
+  //   }
+  // }
+
+  @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')
+        // .blockColor('rgba(255,255,255,1)')
+        // .trackColor('rgba(255,255,255,0.3)')
+        // .selectedColor($r('app.color.index_background'))
+        .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%')
+  }
+}
+
+// 1. 定义提取方法配置的接口类型
+interface ExtractionMethod {
+  name: string;
+  commands: string[]; // ffmpeg命令数组
+}
+
+
+// 伴奏提取的核心方法
+@Concurrent
+async function extractAudioAccompaniment(
+  context: Context,
+  inputPath: string,
+  outPath?: string,
+  quality: number = 3
+): 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}_accompaniment${ext}`;
+    })();
+
+    console.info(`onecold    开始提取伴奏,输入: ${inputPath}, 输出: ${outputPath}`);
+
+    // 根据质量设置参数(逻辑不变)
+    let qualityParams: string[] = [];
+    let codec = 'libmp3lame'; // 默认编码器(MP3)
+
+    switch (quality) {
+      case 0: // 低质量
+        qualityParams = ['-ar', '22050', '-b:a', '64k', '-ac', '1'];
+        break;
+      case 1: // 中等质量
+        qualityParams = ['-ar', '44100', '-b:a', '128k'];
+        break;
+      case 2: // 高质量
+      default:
+        qualityParams = ['-ar', '48000', '-b:a', '192k'];
+        break;
+    }
+
+    // 获取文件扩展名(统一转为小写,避免大小写问题)
+    const fileExt = inputPath.split('.').pop()?.toLowerCase()  || '';
+
+    // 定义格式相关参数(通过 switch case 动态调整)
+    let inputDecoder: string[] = []; // 输入解码器参数
+    let channelFix: string = '';     // 声道预处理滤镜
+    let outputCodec: string = codec; // 输出编码器
+
+    switch (fileExt) {
+      case 'flac':
+        // FLAC 格式特殊处理:使用 FLAC 解码器,强制立体声,输出 FLAC 格式
+        inputDecoder = ['-c:a', 'flac']; // 显式指定 FLAC 解码器
+        channelFix = 'aformat=channel_layouts=stereo,'; // 强制立体声布局
+        outputCodec = 'flac'; // 输出保持 FLAC 无损格式
+        break;
+      case 'wav':
+        // WAV 格式:无需特殊解码器,默认处理
+        channelFix = 'aformat=channel_layouts=stereo,'; // 确保立体声
+        break;
+      case 'mp3':
+        // MP3 格式:默认处理(使用 libmp3lame 编码器)
+        break;
+      default:
+      // 其他格式:使用默认参数(如 MP3、AAC 等)
+        console.warn(`onecold    未优化格式: ${fileExt},使用默认配置`);
+        break;
+    }
+
+    // 构建中置声道消除命令(动态拼接参数)
+    const accompanimentMethod: ExtractionMethod = {
+      name: '中置声道消除',
+      commands: [
+        'ffmpeg',
+        ...inputDecoder, // 动态解码器参数(如 FLAC 的 ['-c:a', 'flac'])
+        '-i', inputPath,
+        '-af', `${channelFix}pan=stereo|c0=c0-c1|c1=c1-c0`, // 动态声道滤镜
+        '-c:a', outputCodec, // 动态输出编码器(如 FLAC 保留原格式)
+        ...qualityParams,
+        '-y', outputPath
+      ]
+    };
+
+    try {
+      console.info(`onecold    使用方法: ${accompanimentMethod.name} (格式: ${fileExt})`);
+
+      // 执行 FFmpeg 命令
+      await FFmpeg.execute(accompanimentMethod.commands,  {
+        logCallback: (logLevel, logMessage) =>
+        console.log(`[${logLevel}]    ${accompanimentMethod.name}:    ${logMessage}`),
+        progressCallback: (message) =>
+        console.log(`[progress]    ${accompanimentMethod.name}:    ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+      });
+
+      console.info(`onecold    提取成功: ${outputPath}`);
+      return true;
+    } catch (error) {
+      console.error(`onecold    提取失败: ${error}`);
+      return false;
+    }
+
+  } catch (error) {
+    console.error(`onecold    主流程错误: ${error}`);
+    return false;
+  }
+}
+
+
+
+// 伴奏提取质量描述
+function getQualityDescription(quality: number): string {
+  switch (quality) {
+    case 0:
+      return '低质量(文件小,适合语音)';
+    case 1:
+      return '中等质量(平衡大小与音质)';
+    case 2:
+    default:
+      return '高质量(音质好,文件较大)';
+  }
+}
+
+export async function saveDb(context: Context, outputPath: string): Promise<boolean> {
+  try {
+    // 媒体入库
+    const table: MediaTable = new MediaTable(context);
+
+    // 等待数据库连接就绪(修复回调转Promise逻辑)
+    await new Promise<void>((resolve, reject) => {
+      table.getRdbStore(context,  (err: Error | null) => { // 补充err可能为null的类型定义
+        if (err) {
+          reject(new Error(`获取数据库连接失败: ${err.message}`));
+        } else {
+          resolve();
+        }
+      });
+    });
+
+    // 获取媒体元数据
+    const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+      context,
+      outputPath,
+      CommonConstants.TYPE_LOCAL,
+      true
+    );
+    if (!mediaItem) {
+      console.error(` 获取媒体元数据失败: ${outputPath}`);
+      return false;
+    }
+
+    // 插入数据库(回调转Promise,确保异步完成)
+    await new Promise<void>((resolve, reject) => {
+      table.insert(
+        mediaItem,
+        (err: Error | null) => { // 补充insert回调的错误参数
+          if (err) {
+            reject(new Error(`数据库插入失败: ${err.message}`));
+          } else {
+            resolve();
+          }
+        },
+        '' // 空字符串参数(根据table.insert 定义传入,若无用可考虑移除)
+      );
+    });
+
+    console.log(` 媒体入库成功: ${outputPath}`);
+    return true;
+
+  } catch (error) {
+    console.error(` 媒体入库失败: ${error instanceof Error ? error.message  : error}`);
+    return false;
+  }
+}
+
+/**
+ * 转换文件路径扩展名(保留原路径其他所有信息)
+ * @param inputPath 输入路径(如 `/data/music/song.dsf` )
+ * @param newFormat 新格式(如 "mp3",不需要包含点)
+ * @returns 仅修改扩展名的新路径(如 `/data/music/song.mp3` )
+ * @throws 当格式包含非法字符时抛出错误
+ */
+export function changeFileExtension(inputPath: string, newFormat: string): string {
+  // 格式验证(只允许字母数字)
+  if (!/^[a-z0-9]+$/i.test(newFormat))  {
+    throw new Error(`Invalid format: "${newFormat}" (only alphanumeric characters allowed)`);
+  }
+
+  // 处理无扩展名情况
+  const lastDotIndex = inputPath.lastIndexOf('.');
+  if (lastDotIndex === -1) {
+    return `${inputPath}.${newFormat.toLowerCase()}`;
+  }
+
+  // 替换扩展名
+  return `${inputPath.substring(0,  lastDotIndex)}.${newFormat.toLowerCase()}`;
+}
+
+/**
+ * 判断文件路径是否为特殊音频格式(dsf/dff/aiff/aif)
+ * @param filePath 文件完整路径(如:/storage/music/test.dsf )
+ * @returns true: 是特殊格式;false: 非特殊格式
+ */
+export function isSpecialAudioFormat(filePath: string): boolean {
+  if (!filePath) return false; // 空路径直接返回false
+
+  // 提取文件扩展名(统一转为小写,处理大小写不敏感)
+  const ext = filePath.split('.').pop()?.toLowerCase();
+
+  // 检查扩展名是否在特殊格式列表中
+  const specialFormats = ['dsf', 'dff', 'aiff', 'aif','ape'];
+  return specialFormats.includes(ext  || '');
+}

+ 609 - 0
entry/src/main/ets/view/ExtractAudioFromVideo.ets

@@ -0,0 +1,609 @@
+import { common, ConfigurationConstant } from '@kit.AbilityKit';
+import { CommonConstants } from '../common/constants/CommonConstants';
+import { VideoItem } from '../viewmodel/VideoItem';
+import { AppUtil, FileUtil,  StrUtil } from '@pura/harmony-utils';
+import { taskpool, util } from '@kit.ArkTS';
+import { Utility } from '../common/util/Utility';
+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 { SegmentButtonItemTuple, SegmentButtonOptions, SwipeRefresher } from '@kit.ArkUI';
+import { SegmentButton } from '@kit.ArkUI';
+import PermissionUtil from '../common/util/PermissionUtil'
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from '../common/util/AttributeModifierUtil';
+
+// 视频音频提取功能
+@Component
+export struct ExtractAudioFromVideo {
+  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 extractionQuality: number = 2; // 0-低, 1-中, 2-高
+  @State audioFormat: number = 0; // 0-MP3, 1-WAV, 2-AAC
+  @State isProcessing: boolean = false;
+
+  // SegmentButton选项
+  @State audioFormatOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [{ text: 'MP3' }, { text: 'WAV' }, { text: 'AAC' }] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor:$r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding:{top:10,bottom:10},
+    multiply: false
+  });
+  @State qualityOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [{ text: '低质量' }, { text: '中等质量' }, { text: '高质量' }] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor:$r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding:{top:10,bottom:10},
+    multiply: false
+  });
+  @State @Watch('onAudioFormatChange') selectedAudioFormatIndex: number[] = [this.audioFormat];
+  @State @Watch('onQualityChange') selectedQualityIndex: number[] = [this.extractionQuality];
+
+  private table: MediaTable = new MediaTable(this.context);
+
+  onColorModeChange() {
+    this.isDarkMode  = this.currentMode  === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
+  }
+
+  // 音频格式变化监听
+  onAudioFormatChange() {
+    if (this.selectedAudioFormatIndex.length > 0) {
+      this.audioFormat = this.selectedAudioFormatIndex[0];
+      this.outputPath  = generateOutputPath(this.inputPath,'提取音频',this.currentPath,
+        this.getAudioFormatExtension(this.audioFormat))
+    }
+  }
+
+  // 音质变化监听
+  onQualityChange() {
+    if (this.selectedQualityIndex.length > 0) {
+      this.extractionQuality = this.selectedQualityIndex[0];
+    }
+  }
+
+  // 添加generateNewPath函数
+  generateNewPath(inputPath: string, fileName: string): string {
+    const dir = inputPath.substring(0, inputPath.lastIndexOf('/') + 1);
+    const dotIndex = fileName.lastIndexOf('.');
+    const name = dotIndex === -1 ? fileName : fileName.substring(0, dotIndex);
+    const ext = this.getAudioFormatExtension(this.audioFormat);
+    return `${dir}${name}${ext}`;
+  }
+
+  // 获取音频格式扩展名
+  getAudioFormatExtension(format: number): string {
+    switch (format) {
+      case 0: return '.mp3';
+      case 1: return '.wav';
+      case 2: return '.aac';
+      default: return '.mp3';
+    }
+  }
+
+  async aboutToAppear() {
+    this.bundleName   = AppUtil.getBundleName();
+    const rawDuration = this.mVideoItem.duration   || '00:00:00';
+    this.durationStr   = rawDuration;
+    await PermissionUtil.activatePermission(this.mVideoItem.filePath)
+    this.outputPath  = generateOutputPath(this.mVideoItem.filePath,'提取音频',this.currentPath,
+      this.getAudioFormatExtension(this.audioFormat))
+    console.info('onecold  this.outputPath  ='+this.outputPath)
+    await new Promise<void>((resolve, reject) => {
+      this.table.getRdbStore(this.context,   (err:Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+  }
+
+  // 提取视频音频
+  async extractVideoAudio() {
+    if (this.isProcessing)   {
+      return;
+    }
+    if(StrUtil.isEmpty(this.inputPath)){
+      this.inputPath  =this.mVideoItem.filePath
+    }
+    if(FileUtil.accessSync(this.outputPath)){
+      this.outputPath  = generateOutputPath(this.inputPath,'提取音频',this.currentPath,
+        this.getAudioFormatExtension(this.audioFormat))
+    }
+
+    this.isProcessing   = true;
+
+    try {
+      const task = new taskpool.Task(
+        extractAudioFromVideo,
+        this.context,
+        this.inputPath,
+        this.outputPath,
+        this.extractionQuality,
+        this.audioFormat
+      );
+
+      const result = await taskpool.execute(task,   taskpool.Priority.HIGH);
+
+      if (result) {
+        await this.saveDb(this.context,  this.outputPath)
+        this.onResult(true,   this.outputPath);
+        this.showSuccess();
+      } else {
+        this.onResult(false,   this.outputPath);
+      }
+    } catch (error) {
+      console.error('Extract  video audio failed:', error);
+      this.onResult(false,   this.outputPath);
+    } finally {
+      this.isProcessing   = false;
+    }
+  }
+
+  async saveDb(context: Context, outputPath: string): Promise<boolean> {
+    try {
+      // 媒体入库
+      const table: MediaTable = new MediaTable(context);
+
+      // 等待数据库连接就绪(修复回调转Promise逻辑)
+      await new Promise<void>((resolve, reject) => {
+        table.getRdbStore(context,  (err: Error | null) => { // 补充err可能为null的类型定义
+          if (err) {
+            reject(new Error(`获取数据库连接失败: ${err.message}`));
+          } else {
+            resolve();
+          }
+        });
+      });
+
+      // 获取媒体元数据
+      const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+        context,
+        outputPath,
+        CommonConstants.TYPE_LOCAL,
+        true
+      );
+      if (!mediaItem) {
+        console.error(` 获取媒体元数据失败: ${outputPath}`);
+        return false;
+      }
+
+      // 插入数据库(回调转Promise,确保异步完成)
+      await new Promise<void>((resolve, reject) => {
+        table.insert(
+          mediaItem,
+          (err: Error | null) => { // 补充insert回调的错误参数
+            if (err) {
+              reject(new Error(`数据库插入失败: ${err.message}`));
+            } else {
+              resolve();
+            }
+          },
+          '' // 空字符串参数(根据table.insert 定义传入,若无用可考虑移除)
+        );
+      });
+
+      console.log(` 媒体入库成功: ${outputPath}`);
+      return true;
+
+    } catch (error) {
+      console.error(` 媒体入库失败: ${error instanceof Error ? error.message  : error}`);
+      return 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('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('showDialog   success callback, click button: ' + data.index);
+      });
+    } catch (error) {
+      let message = (error as BusinessError).message;
+      let code = (error as BusinessError).code;
+      console.error(`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: 20 }) {
+      // 文件信息显示
+      Row({ space: 10 }) {
+        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%')
+      .margin({ top: 5})
+      .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)
+          .maxLines(3)
+          .fontColor($r('app.color.text_color'))
+          .width('90%')
+          .onEditChange((isEditing: boolean) => {
+            console.info(`onecold isEditing ${isEditing}`);
+            this.isEditing = isEditing;
+
+          })
+          .onChange((val: string) => {
+            if(this.isEditing){
+              console.info('onecold onChange val=' + val)
+              this.outputPath  = generateOutputPath(this.mVideoItem.filePath,'',this.currentPath,
+                this.getAudioFormatExtension(this.audioFormat), val )
+            }
+
+
+          })
+      }
+      .width('100%')
+      .margin({ top: 5, bottom: 5 })
+      .justifyContent(FlexAlign.Start)
+
+      // 音频格式选择
+      Column({ space: 10 }) {
+        Text('音频格式:')
+          .fontSize(16)
+          .width('90%')
+          .textAlign(TextAlign.Start)
+
+        SegmentButton({
+          options: this.audioFormatOptions,
+          selectedIndexes: $selectedAudioFormatIndex
+        })
+        .width('90%')
+
+        // 音质选择
+        Column({ space: 10 }) {
+          Text('音质质量:')
+            .fontSize(16)
+            .width('90%')
+            .textAlign(TextAlign.Start)
+
+          SegmentButton({
+            options: this.qualityOptions,
+            selectedIndexes: $selectedQualityIndex
+          })
+          .width('90%')
+
+          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)   {
+              this.extractVideoAudio();
+            }
+          })
+          .enabled(!this.isProcessing)
+        }
+        .width('100%')
+        .padding({ top: 20, bottom: 20 })
+        .alignItems(HorizontalAlign.Center)
+      }
+    }
+    .width('100%')
+  }
+
+  @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 extractAudioFromVideo(
+  context: Context,
+  inputPath: string,
+  outPath?: string,
+  quality: number = 2,
+  audioFormat: number = 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}_audio${ext}`;
+    })();
+
+    console.info(`onecold     开始提取视频音频,输入: ${inputPath}, 输出: ${outputPath}`);
+
+    // 根据音频格式和质量设置参数
+    let qualityParams: string[] = [];
+    let outputExtension: string = '';
+    let codec: string = '';
+
+    // 设置音频格式相关参数
+    switch (audioFormat) {
+      case 0: // MP3
+        codec = 'libmp3lame';
+        outputExtension = '.mp3';
+        break;
+      case 1: // WAV
+        codec = 'pcm_s16le';
+        outputExtension = '.wav';
+        break;
+      case 2: // AAC
+        codec = 'aac';
+        outputExtension = '.aac';
+        break;
+      default:
+        codec = 'libmp3lame';
+        outputExtension = '.mp3';
+        break;
+    }
+
+    // 确保输出路径有正确的扩展名
+    if (!outputPath.toLowerCase().endsWith(outputExtension))  {
+      const basePath = outputPath.lastIndexOf('.')  > outputPath.lastIndexOf('/')
+        ? outputPath.substring(0,   outputPath.lastIndexOf('.'))
+        : outputPath;
+      outputPath = `${basePath}${outputExtension}`;
+    }
+
+    // 根据质量设置参数
+    switch (quality) {
+      case 0: // 低质量
+        qualityParams = ['-ar', '22050', '-b:a', '64k'];
+        break;
+      case 1: // 中等质量
+        qualityParams = ['-ar', '44100', '-b:a', '128k'];
+        break;
+      case 2: // 高质量
+      default:
+        qualityParams = ['-ar', '48000', '-b:a', '192k'];
+        break;
+    }
+
+    try {
+      console.info(`onecold     提取视频音频,格式: ${audioFormat === 0 ? 'MP3' : audioFormat === 1 ? 'WAV' : 'AAC'}`);
+
+      // 构建FFmpeg命令 - 提取音频流,不重新编码视频
+      const commands = [
+        'ffmpeg',
+        '-i', inputPath,
+        '-vn',           // 不处理视频流
+        '-acodec', codec, // 音频编码器
+        ...qualityParams,
+        '-y', outputPath
+      ];
+
+      // 执行 FFmpeg 命令
+      await FFmpeg.execute(commands,   {
+        logCallback: (logLevel, logMessage) =>
+        console.log(`[${logLevel}]     视频音频提取:    ${logMessage}`),
+        progressCallback: (message) =>
+        console.log(`[progress]     视频音频提取:    ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+      });
+
+      console.info(`onecold     视频音频提取成功: ${outputPath}`);
+      return true;
+    } catch (error) {
+      console.error(`onecold     视频音频提取失败: ${error}`);
+      return false;
+    }
+
+  } catch (error) {
+    console.error(`onecold     主流程错误: ${error}`);
+    return false;
+  }
+}
+
+// 音频格式描述
+function getAudioFormatDescription(format: number): string {
+  switch (format) {
+    case 0:
+      return 'MP3(通用格式,文件较小)';
+    case 1:
+      return 'WAV(无损格式,文件较大)';
+    case 2:
+    default:
+      return 'AAC(高质量,文件较小)';
+  }
+}
+
+// 音频质量描述
+function getQualityDescription(quality: number): string {
+  switch (quality) {
+    case 0:
+      return '低质量(文件小,适合语音)';
+    case 1:
+      return '中等质量(平衡大小与音质)';
+    case 2:
+    default:
+      return '高质量(音质好,文件较大)';
+  }
+}

+ 546 - 33
entry/src/main/ets/view/LocalMusic.ets

@@ -101,6 +101,12 @@ import { WebdavManager, WebDavAuthInfo as WebDavManagerAuthInfo,
   buildHttpHeadersWithWebDav,
   WebDavAuthItem} from '../common/util/WebdavManager';
 import PermissionUtil from '../common/util/PermissionUtil'
+import { EditAudio, setRingTone } from './EditAudio';
+import { ExtractAccompaniment } from './ExtractAccompaniment';
+import { ExtractAudioFromVideo } from './ExtractAudioFromVideo';
+import { AudioFormatConverter } from './AudioFormatConverter';
+import { MergeAudio } from './MergeAudio';
+import { AIVoiceSeparation } from './AIVoiceSeparation';
 const TAG = 'LocalMusic';
 
 /**
@@ -175,6 +181,12 @@ const ITEM_HEIGHT_BIG: number = 78; // 列表项大高度
 @Preview
 @Component
 export struct LocalMusic {
+  @State isEditAudio:boolean = false
+  @State isExtractAcc:boolean = false
+  @State isConverter:boolean = false
+  @State isAIVoiceSeparation:boolean = false
+  @State isExtractAudio:boolean = false
+  @State isMergeAudio:boolean = false
   @State isCopyFileToDownLoad: boolean = false
   @State offHeight: number = 75
   @State currentSongList: Array<VideoItem> = []//当前歌单
@@ -1783,22 +1795,47 @@ export struct LocalMusic {
   }
 
   async showTips() {
-    DialogHelper.showTipsDialog({
-      title: '使用提示:自动导入教程',
-
-      content: '用系统自带的文件管理器将:\n\n1、音频文件放入' + '\n\n我的设备/Download/'
-        + this.appName + '\n\n2、无需繁琐的勾选和分类步骤。\n\n3、每个子文件夹将被组成一个分组。' +
-        '\n\n4、歌词文件的文件名必须和目标歌曲文件名相同(不包含后缀名)。\n\n5、歌词文件和目标歌曲文件必须同个目录。' +
-        '\n\n6、电脑上Download的目录是\nDownload/' + this.packName + '。\n\n7、放好文件后如果没显示点击刷新按钮。\n',
-      onAction: (action) => {
-        if (action == DialogAction.TWO) {
-          PreferencesUtil.putSync('isFirstMusic', false)
-          router.pushUrl({
-            url: 'pages/ScanFilePage'
-          });
+    try {
+      this.getUIContext().getPromptAction().showDialog({
+        title: '导入指南',
+        message:'用系统自带的文件管理器将:\n\n1、音频文件放入' + '\n\n我的设备/Download/'
+          + this.appName + '\n\n2、无需繁琐的勾选和分类步骤。\n\n3、每个子文件夹将被组成一个分组。' +
+          '\n\n4、歌词文件的文件名必须和目标歌曲文件名相同(不包含后缀名)。\n\n5、歌词文件和目标歌曲文件必须同个目录。' +
+          '\n\n6、电脑上Download的目录是\nDownload/' + this.packName + '。\n\n7、放好文件后如果没显示点击刷新按钮。\n',
+        buttons: [
+          {
+            text: '知道了',
+            color: '#000000'
+          },
+        ]
+      }, (err, data) => {
+        if (err) {
+          console.error('showDialog err: ' + err);
+          return;
         }
-      }
-    })
+        console.info('showDialog success callback, click button: ' + data.index);
+      });
+    } catch (error) {
+      let message = (error as BusinessError).message;
+      let code = (error as BusinessError).code;
+      console.error(`showDialog args error code is ${code}, message is ${message}`);
+    };
+    // DialogHelper.showTipsDialog({
+    //   title: '使用提示:自动导入教程',
+    //
+    //   content: '用系统自带的文件管理器将:\n\n1、音频文件放入' + '\n\n我的设备/Download/'
+    //     + this.appName + '\n\n2、无需繁琐的勾选和分类步骤。\n\n3、每个子文件夹将被组成一个分组。' +
+    //     '\n\n4、歌词文件的文件名必须和目标歌曲文件名相同(不包含后缀名)。\n\n5、歌词文件和目标歌曲文件必须同个目录。' +
+    //     '\n\n6、电脑上Download的目录是\nDownload/' + this.packName + '。\n\n7、放好文件后如果没显示点击刷新按钮。\n',
+    //   onAction: (action) => {
+    //     if (action == DialogAction.TWO) {
+    //       PreferencesUtil.putSync('isFirstMusic', false)
+    //       router.pushUrl({
+    //         url: 'pages/ScanFilePage'
+    //       });
+    //     }
+    //   }
+    // })
 
 
   }
@@ -2097,6 +2134,10 @@ export struct LocalMusic {
 
   //重命名的对话框
   showReNameDialog(item: VideoItem, index: string, filePath: string) {
+    if(!item.filePath.includes(this.packName)){
+      ToastUtil.showToast('请把该音频导入到本应用的DownLoad文件下,其他路径音频无权限重命名')
+      return
+    }
     DialogHelper.showTextInputDialog({
       title: '重命名',
       text: item.type === CommonConstants.TYPE_IS_DIR ? item.name : item.fileName,
@@ -2483,19 +2524,38 @@ export struct LocalMusic {
                 this.isOneKeyTags = !this.isOneKeyTags
               })
 
-            Button('乱码修复', { type: ButtonType.Capsule, stateEffect: true })
-              .width(90)
+            Button('合并', { type: ButtonType.Circle, stateEffect: true })
+              .width(50)
               .height(50)
-              .fontSize(13)
+              .fontSize(11)
               .margin({ top: 10, bottom: 10, right: 6 })
               .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
               .backgroundColor(this.themeColor)
-              .bindContentCover($$this.isFixMessy, this.FixMessyViewBuilder(), {
+              .bindContentCover($$this.isMergeAudio, this.MergeAudioBuilder(), {
                 transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
               })
               .onClick(() => {
-                this.isFixMessy = !this.isFixMessy
+                if(ArrayUtil.isNotEmpty(this.selectedFiles)&&Utility.isMerge(this.selectedFiles,this.packName)){
+                  this.isMergeAudio = !this.isMergeAudio
+                }else{
+                  ToastUtil.showToast('选中的文件包含了不是本应用的DownLoad文件下,其他路径音频无权限合并')
+                }
               })
+
+
+            // Button('乱码修复', { type: ButtonType.Capsule, stateEffect: true })
+            //   .width(90)
+            //   .height(50)
+            //   .fontSize(13)
+            //   .margin({ top: 10, bottom: 10, right: 6 })
+            //   .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
+            //   .backgroundColor(this.themeColor)
+            //   .bindContentCover($$this.isFixMessy, this.FixMessyViewBuilder(), {
+            //     transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+            //   })
+            //   .onClick(() => {
+            //     this.isFixMessy = !this.isFixMessy
+            //   })
             // Button('剪切', { type: ButtonType.Circle, stateEffect: true })
             //   .width('35%')
             //   .height(60)
@@ -2636,6 +2696,374 @@ export struct LocalMusic {
     .height('100%')
   }
 
+  //剪辑音频
+  @Builder
+  EditAudioBuilder(item:VideoItem) {
+    Scroll() {
+      Column() {
+        EditAudio(
+          {
+            mVideoItem:item,
+            currentPath:this.currentPath,
+            videoUrl:this.videoUrl,
+            CONTROL_PlayStatus:this.CONTROL_PlayStatus,
+            currentTime:this.currentTime,
+            progressValue:this.progressValue,
+            onBack:()=>{
+              this.isEditAudio = !this.isEditAudio
+            },
+            onSeeOutputPath:(outPutPath:string)=>{
+              this.isEditAudio = !this.isEditAudio
+              this.longItemFilePath = ''
+              this.modeType = 0
+              this.getSortedFiles(FileUtil.getParentPath(outPutPath))
+            },
+            onPlayOrPause:()=>{
+              if(this.videoUrl==item.filePath){
+                this.playOrPause()
+              }else{//如果不是当前播放的,则播放该文件
+                this.videoUrl==item.filePath
+                this.doPlay(item,0,false,true)
+              }
+
+            },
+            onSliderChange:(value:number)=>{
+              if(this.videoUrl!=item.filePath){
+                this.videoUrl==item.filePath
+                return
+              }
+              this.isSeekTo = true;
+              this.mDestroyPage = false;
+              this.showLoadIng();
+              LogUtils.getInstance().LOGI("slider-->seekValue start:" + value);
+              let seekValue = value * (this.mIjkMediaPlayer.getDuration() / 100);
+              this.seekTo(seekValue + "");
+              LogUtils.getInstance().LOGI("slider-->seekValue end:" + seekValue);
+              this.isSeekTo = false;
+
+            },
+            onResult:(result: boolean,outPath:string)=>{
+              if(result){//如果剪辑成功,更新数据
+
+                this.doUpdateData()
+              }else{
+                ToastUtil.showToast('剪辑失败')
+              }
+
+            }
+          }
+        )
+      }
+    }
+    .width('100%')
+    .height('100%')
+  }
+
+  //提取伴奏
+  @Builder
+  ConverterBuilder(item:VideoItem) {
+    Scroll() {
+      Column() {
+        AudioFormatConverter(
+          {
+            mVideoItem:item,
+            videoUrl:this.videoUrl,
+            currentPath:this.currentPath,
+            CONTROL_PlayStatus:this.CONTROL_PlayStatus,
+            currentTime:this.currentTime,
+            progressValue:this.progressValue,
+            onBack:()=>{
+              this.isConverter = !this.isConverter
+              this.longItemFilePath = ''
+            },
+            onPlayOrPause:()=>{
+              if(this.videoUrl==item.filePath){
+                this.playOrPause()
+              }else{//如果不是当前播放的,则播放该文件
+                this.videoUrl==item.filePath
+                this.doPlay(item,0,false,true)
+              }
+
+            },
+            onSeeOutputPath:(outPutPath:string)=>{
+              this.isConverter = !this.isConverter
+              this.longItemFilePath = ''
+              this.modeType = 0
+              this.getSortedFiles(FileUtil.getParentPath(outPutPath))
+            },
+            onSliderChange:(value:number)=>{
+              if(this.videoUrl!=item.filePath){
+                this.videoUrl==item.filePath
+                return
+              }
+              this.isSeekTo = true;
+              this.mDestroyPage = false;
+              this.showLoadIng();
+              let seekValue = value * (this.mIjkMediaPlayer.getDuration() / 100);
+              this.seekTo(seekValue + "");
+              this.isSeekTo = false;
+
+            },
+            onResult:(result: boolean,outPath:string)=>{
+              if(result){//如果成功,更新数据
+                this.doUpdateData()
+              }else{
+                ToastUtil.showToast('格式转化失败')
+              }
+
+            }
+          }
+        )
+      }
+    }
+    .width('100%')
+    .height('100%')
+  }
+
+  //提取伴奏
+  @Builder
+  ExtractAccBuilder(item:VideoItem) {
+    Scroll() {
+      Column() {
+        ExtractAccompaniment(
+          {
+            mVideoItem:item,
+            currentPath:this.currentPath,
+            videoUrl:this.videoUrl,
+            CONTROL_PlayStatus:this.CONTROL_PlayStatus,
+            currentTime:this.currentTime,
+            progressValue:this.progressValue,
+            onBack:()=>{
+              this.isExtractAcc = !this.isExtractAcc
+              this.longItemFilePath = ''
+            },
+            onPlayOrPause:()=>{
+              if(this.videoUrl==item.filePath){
+                this.playOrPause()
+              }else{//如果不是当前播放的,则播放该文件
+                this.videoUrl==item.filePath
+                this.doPlay(item,0,false,true)
+              }
+
+            },
+            onSeeOutputPath:(outPutPath:string)=>{
+              this.isExtractAcc = !this.isExtractAcc
+              this.longItemFilePath = ''
+              this.modeType = 0
+              this.getSortedFiles(FileUtil.getParentPath(outPutPath))
+            },
+            onSliderChange:(value:number)=>{
+              if(this.videoUrl!=item.filePath){
+                this.videoUrl==item.filePath
+                return
+              }
+              this.isSeekTo = true;
+              this.mDestroyPage = false;
+              this.showLoadIng();
+              let seekValue = value * (this.mIjkMediaPlayer.getDuration() / 100);
+              this.seekTo(seekValue + "");
+              this.isSeekTo = false;
+
+            },
+            onResult:(result: boolean,outPath:string)=>{
+              if(result){//如果成功,更新数据
+                this.doUpdateData()
+              }else{
+                ToastUtil.showToast('提取伴奏失败')
+              }
+
+            }
+          }
+        )
+      }
+    }
+    .width('100%')
+    .height('100%')
+  }
+
+  //AI人声分离
+  @Builder
+  AiBuilder(item:VideoItem) {
+    Scroll() {
+      Column() {
+        AIVoiceSeparation(
+          {
+            mVideoItem:item,
+            videoUrl:this.videoUrl,
+            currentPath:this.currentPath,
+            CONTROL_PlayStatus:this.CONTROL_PlayStatus,
+            currentTime:this.currentTime,
+            progressValue:this.progressValue,
+            onBack:()=>{
+              this.isAIVoiceSeparation = !this.isAIVoiceSeparation
+              this.longItemFilePath = ''
+            },
+            onPlayOrPause:()=>{
+              if(this.videoUrl==item.filePath||this.isPlaying){
+                this.playOrPause()
+              }else{//如果不是当前播放的,则播放该文件
+                this.videoUrl==item.filePath
+                this.doPlay(item,0,false,true)
+              }
+
+            },
+            onPlayPath:(path:string)=>{
+              this.videoUrl==path
+              const itemP = Utility.getItemByFilePath(this.videoLocalList,path)
+              if(itemP)
+                this.doPlay(itemP,0,false,true)
+            },
+            onSeeOutputPath:(outPutPath:string)=>{
+              this.isAIVoiceSeparation = !this.isAIVoiceSeparation
+              this.longItemFilePath = ''
+              this.modeType = 0
+              this.getSortedFiles(FileUtil.getParentPath(outPutPath))
+            },
+            onSliderChange:(value:number)=>{
+              if(this.videoUrl!=item.filePath){
+                this.videoUrl==item.filePath
+                return
+              }
+              this.isSeekTo = true;
+              this.mDestroyPage = false;
+              this.showLoadIng();
+              let seekValue = value * (this.mIjkMediaPlayer.getDuration() / 100);
+              this.seekTo(seekValue + "");
+              this.isSeekTo = false;
+
+            },
+            onResult:(result: boolean,outPath:string)=>{
+              if(result){//如果成功,更新数据
+                this.doUpdateData()
+              }else{
+                ToastUtil.showToast('Ai人声分离失败')
+              }
+
+            }
+          }
+        )
+      }
+    }
+    .width('100%')
+    .height('100%')
+  }
+
+
+  //提取视频中的音频
+  @Builder
+  extractAudioFromVideoBuilder(item:VideoItem) {
+    Scroll() {
+      Column() {
+        ExtractAudioFromVideo(
+          {
+            mVideoItem:item,
+            currentPath:this.currentPath,
+            videoUrl:this.videoUrl,
+            CONTROL_PlayStatus:this.CONTROL_PlayStatus,
+            currentTime:this.currentTime,
+            progressValue:this.progressValue,
+            onBack:()=>{
+              this.isExtractAudio = !this.isExtractAudio
+              this.longItemFilePath = ''
+            },
+            onPlayOrPause:()=>{
+              if(this.videoUrl==item.filePath){
+                this.playOrPause()
+              }else{//如果不是当前播放的,则播放该文件
+                this.videoUrl==item.filePath
+                this.doPlay(item,0,false,true)
+              }
+
+            },
+            onSeeOutputPath:(outPutPath:string)=>{
+              this.isExtractAudio = !this.isExtractAudio
+              this.longItemFilePath = ''
+              this.modeType = 0
+              this.getSortedFiles(FileUtil.getParentPath(outPutPath))
+            },
+            onSliderChange:(value:number)=>{
+              if(this.videoUrl!=item.filePath){
+                this.videoUrl==item.filePath
+                return
+              }
+              this.isSeekTo = true;
+              this.mDestroyPage = false;
+              this.showLoadIng();
+              let seekValue = value * (this.mIjkMediaPlayer.getDuration() / 100);
+              this.seekTo(seekValue + "");
+              this.isSeekTo = false;
+
+            },
+            onResult:(result: boolean,outPath:string)=>{
+              if(result){//如果成功,更新数据
+                this.doUpdateData()
+              }else{
+                ToastUtil.showToast('提取音频失败')
+              }
+
+            }
+          }
+        )
+      }
+    }
+    .width('100%')
+    .height('100%')
+  }
+
+
+  //合并音频
+  @Builder
+  MergeAudioBuilder() {
+    Scroll() {
+      Column() {
+        MergeAudio(
+          {
+            videoUrl:this.videoUrl,
+            CONTROL_PlayStatus:this.CONTROL_PlayStatus,
+            currentPath:this.currentPath,
+            selectedFiles:this.selectedFiles,
+            currentTime:this.currentTime,
+            progressValue:this.progressValue,
+            onBack:()=>{
+              this.isMergeAudio = !this.isMergeAudio
+              this.longItemFilePath = ''
+            },
+            // onPlayPath:(path:string)=>{
+            //   if(this.videoUrl==item.filePath){
+            //     this.playOrPause()
+            //   }else{//如果不是当前播放的,则播放该文件
+            //     this.videoUrl==item.filePath
+            //     this.doPlay(item,0,false,true)
+            //   }
+            //
+            // },
+            onSeeOutputPath:(outPutPath:string)=>{
+              this.isMergeAudio = !this.isMergeAudio
+              this.longItemFilePath = ''
+              this.modeType = 0
+              this.getSortedFiles(FileUtil.getParentPath(outPutPath))
+            },
+            onResult:(result: boolean,outPath:string)=>{
+              if(result){//如果成功,更新数据
+                this.doUpdateData()
+              }else{
+                if(this.selectedFiles.length>=3){
+                  ToastUtil.showToast('合并数量太多导致失败')
+                }else{
+                  ToastUtil.showToast('合并失败')
+                }
+
+              }
+
+            }
+          }
+        )
+      }
+    }
+    .width('100%')
+    .height('100%')
+  }
+
   asyncCurrentPathData() {
     if (this.modeType == 0) {
       workerInstance.postMessage({
@@ -4842,6 +5270,77 @@ export struct LocalMusic {
                 this.longItemFilePath = ''
                 this.selectedFiles.push(item)
               })
+
+            MenuItem({
+              symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.cut')),
+              content: $r('app.string.edit_audio')
+            })
+              .bindContentCover($$this.isEditAudio, this.EditAudioBuilder(item), {
+                transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+              })
+              .onClick(() => {
+                this.isEditAudio = !this.isEditAudio
+              })
+            MenuItem({
+              symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.music')),
+              content: $r('app.string.extract_accompaniment')
+            })
+              .bindContentCover($$this.isExtractAcc, this.ExtractAccBuilder(item), {
+                transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+              })
+              .onClick(() => {
+                this.isExtractAcc = !this.isExtractAcc
+              })
+            MenuItem({
+              symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.repeat')),
+              content: $r('app.string.converter_audio')
+            })
+              .bindContentCover($$this.isConverter, this.ConverterBuilder(item), {
+                transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+              })
+              .onClick(() => {
+                this.isConverter = !this.isConverter
+              })
+
+            MenuItem({
+              symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.ai_edit')),
+              content: $r('app.string.vocal_separation')
+            })
+              .bindContentCover($$this.isAIVoiceSeparation, this.AiBuilder(item), {
+                transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+              })
+              .onClick(() => {
+                this.isAIVoiceSeparation = !this.isAIVoiceSeparation
+              })
+
+
+            if(Utility.isVideoByExtension(item.filePath)){
+              MenuItem({
+                symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.identify_song')),
+                content: $r('app.string.extract_audio')
+              })
+                .bindContentCover($$this.isExtractAudio, this.extractAudioFromVideoBuilder(item), {
+                  transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+                })
+                .onClick(() => {
+                  this.isExtractAudio = !this.isExtractAudio
+                })
+            }
+
+
+            MenuItem({
+              symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.phone')),
+              content: $r('app.string.set_ring')
+            })
+              .onClick(async () => {
+                  await PermissionUtil.activatePermission(item.filePath)
+                // if(item.filePath.includes(this.packName)){
+                  setRingTone(this.context, item.filePath,item.name)
+                // }else{
+                //   ToastUtil.showToast('请把该音频导入到本应用的DownLoad文件下,其他路径音频无权限设置铃声')
+                // }
+                this.longItemFilePath = ''
+              })
             MenuItem({
               symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.rename')),
               content: $r('app.string.rename')
@@ -6045,6 +6544,18 @@ export struct LocalMusic {
     );
 
     if (result) {
+      if(!item.filePath.includes(this.packName)){
+        //内嵌成功的歌路径如果不是包含包名,则入库
+        let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, tempOutPath,
+          CommonConstants.TYPE_LOCAL, this.autoParseMusicName);
+        this.table.insert(newItem, (id: number) => {
+          //加入数据库
+          if(this.modeType==0){
+            this.deleteCache(this.currentPath)
+            this.getSortedFiles(this.currentPath)
+          }
+        });
+      }
       this.table.updateMediaInfo(item.filePath, this.titleStr, this.artistStr, this.ablumStr,this.lyricConStr,
         this.yearStr,this.genreStr,this.trackStr,this.albumArtistStr,this.composerStr, this.lyricistStr,this.commentStr,this.discStr,
         (success: boolean, error?: string) => {
@@ -6053,8 +6564,6 @@ export struct LocalMusic {
           if (success) {
             console.log(" onecold 编辑信息成功,数据库已同步");
             ToastUtil.showToast('内嵌音乐标签成功')
-            // 关闭进度条
-            DialogHelper.closeLoading();
             this.isShowMoreView = false
             this.name = this.titleStr
             if (item.filePath == this.currentSong?.filePath) {
@@ -6080,7 +6589,8 @@ export struct LocalMusic {
             console.error(" onecold  编辑信息数据库失败原因: " + error);
           }
 
-
+          // 关闭进度条
+          DialogHelper.closeLoading();
         });
       console.log(' onecold 元数据标签更新成功,');
 
@@ -8082,7 +8592,7 @@ export struct LocalMusic {
   @State videoHeight: string = '100%';
   @State initAspectRatio: number = 1;
   @State videoAspectRatio: number = this.initAspectRatio;
-  private videoUrl: string = '';
+  @State videoUrl: string = '';
   private last: number = 0;
   @State videoParentAspectRatio: number = this.initAspectRatio;
   private mIjkMediaPlayer = IjkMediaPlayer.getInstance();
@@ -11784,15 +12294,18 @@ export struct LocalMusic {
 
   private async play(url: string,startOffset?:number) {
     console.info('onecold this.url 1= '+url)
-    let hasPermission = await PermissionUtil.activatePermission(url)
-    console.info('onecold this.hasPermission 1= '+hasPermission)
-    if(!url.includes(this.packName)){
-      const file = fs.openSync(url)
-      this.videoUrl = file.path
-      url= this.videoUrl
-    }
-    console.info('onecold this.hasPermission 1= '+hasPermission)
-    console.info('onecold this.videoUrl 1= '+this.videoUrl)
+    if(!url.toLowerCase().startsWith('http')){
+      let hasPermission = await PermissionUtil.activatePermission(url)
+      console.info('onecold this.hasPermission 1= '+hasPermission)
+      if(!url.includes(this.packName)){
+        const file = fs.openSync(url)
+        this.videoUrl = file.path
+        url= this.videoUrl
+      }
+      console.info('onecold this.hasPermission 1= '+hasPermission)
+      console.info('onecold this.videoUrl 1= '+this.videoUrl)
+    }
+
 
     let that = this;
     that.showLoadIng();

+ 627 - 0
entry/src/main/ets/view/MergeAudio.ets

@@ -0,0 +1,627 @@
+import { CommonConstants } from "../common/constants/CommonConstants"
+import { VideoItem } from "../viewmodel/VideoItem"
+import { common, ConfigurationConstant } from "@kit.AbilityKit"
+import { SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions } from "@kit.ArkUI"
+import MediaTable from "../common/util/MediaTable"
+import { AppUtil, FileUtil, ToastUtil } from "@pura/harmony-utils"
+import { generateOutputPath, getFileNameWithoutExtension, setRingTone } from "./EditAudio"
+import { JSON, taskpool } from "@kit.ArkTS"
+import { Utility } from "../common/util/Utility"
+import { FFmpeg, FFProgressMessageParser } from "@sj/ffmpeg"
+import PermissionUtil from '../common/util/PermissionUtil'
+import { ButtonFancyModifier, ShadowModifier, SymbolGlyphFancyModifier } from "../common/util/AttributeModifierUtil"
+
+// 音频合并功能
+@Component
+export struct MergeAudio {
+  onResult = (_result: boolean, outPath: string) => {
+  }
+  onBack = () => {
+  }
+  // onPlayOrPause = () => {
+  // }
+  onSeeOutputPath = (outPutPath:string) => {
+
+  }
+  @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 = '';
+  @StorageProp('isLandscape')   isLandscape: boolean = false;
+  @Prop selectedFiles: Array<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 mergeMethod: number = 0; // 0-顺序合并, 1-交叉淡化, 2-重叠合并
+  @State outputFormat: number = 0; // 0-MP3, 1-WAV, 2-AAC
+  @State isProcessing: boolean = false;
+
+  // SegmentButton选项
+  @State outputFormatOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [{ text: 'MP3' }, { text: 'WAV' }, { text: 'AAC' }] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor:$r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding:{top:10,bottom:10},
+    multiply: false
+  });
+  @State mergeMethodOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
+    buttons: [{ text: '顺序合并' },  { text: '重叠合并' }] as SegmentButtonItemTuple,
+    // buttons: [{ text: '顺序合并' }, { text: '交叉淡化' }, { text: '重叠合并' }] as SegmentButtonItemTuple,
+    backgroundColor: $r('app.color.index_background'),
+    selectedBackgroundColor:$r('app.color.start_window_background'),
+    selectedFontColor: $r('app.color.text_color'),
+    buttonPadding:{top:10,bottom:10},
+    multiply: false
+  });
+  @State @Watch('onOutputFormatChange') selectedOutputFormatIndex: number[] = [this.outputFormat];
+  @State @Watch('onMergeMethodChange') selectedMergeMethodIndex: number[] = [this.mergeMethod];
+
+  private table: MediaTable = new MediaTable(this.context);
+
+  onColorModeChange() {
+    this.isDarkMode   = this.currentMode   === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
+  }
+
+  // 输出格式变化监听
+  onOutputFormatChange() {
+    if (this.selectedOutputFormatIndex.length  > 0) {
+      this.outputFormat  = this.selectedOutputFormatIndex[0];
+      this.outputPath  = generateOutputPath(this.selectedFiles[0].filePath,'合并音频',this.currentPath)
+    }
+  }
+
+
+  // 合并方法变化监听
+  onMergeMethodChange() {
+    if (this.selectedMergeMethodIndex.length  > 0) {
+      this.mergeMethod  = this.selectedMergeMethodIndex[0];
+    }
+  }
+
+  // 生成新路径
+  // generateNewPath(inputPath: string, fileName: string): string {
+  //   const dir = inputPath.substring(0,  inputPath.lastIndexOf('/')  + 1);
+  //   const dotIndex = fileName.lastIndexOf('.');
+  //   const name = dotIndex === -1 ? fileName : fileName.substring(0,  dotIndex);
+  //   const ext = this.getOutputFormatExtension(this.outputFormat);
+  //   return `${dir}${name}${ext}`;
+  // }
+
+  // 获取输出格式扩展名
+  getOutputFormatExtension(format: number): string {
+    switch (format) {
+      case 0: return '.mp3';
+      case 1: return '.wav';
+      case 2: return '.aac';
+      default: return '.mp3';
+    }
+  }
+
+  async aboutToAppear() {
+    this.bundleName    = AppUtil.getBundleName();
+    // 计算总时长
+    const totalDuration = this.calculateTotalDuration();
+    this.durationStr    = totalDuration;
+    for (let index = 0; index < this.selectedFiles.length; index++) {
+      await PermissionUtil.activatePermission(this.selectedFiles[index].filePath)
+    }
+    this.outputPath   = generateOutputPath(this.selectedFiles[0]?.filePath,'合并音频',this.currentPath)
+    console.info('onecold   this.outputPath   ='+this.outputPath)
+    await new Promise<void>((resolve, reject) => {
+      this.table.getRdbStore(this.context,    (err:Error) => {
+        err ? reject(err) : resolve();
+      });
+    });
+  }
+
+  // 计算总时长
+  calculateTotalDuration(): string {
+    let totalSeconds = 0;
+    this.selectedFiles.forEach(file  => {
+      const duration = file.duration  || '00:00:00';
+      const parts = duration.split(':');
+      if (parts.length  === 3) {
+        totalSeconds += parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
+      }
+    });
+
+    const hours = Math.floor(totalSeconds  / 3600);
+    const minutes = Math.floor((totalSeconds  % 3600) / 60);
+    const seconds = totalSeconds % 60;
+    return `${hours.toString().padStart(2,  '0')}:${minutes.toString().padStart(2,  '0')}:${seconds.toString().padStart(2,  '0')}`;
+  }
+
+  // 合并音频
+  async mergeAudioFiles() {
+    if (this.isProcessing)    {
+      return;
+    }
+
+    if (this.selectedFiles.length  < 2) {
+      ToastUtil.showToast(' 请选择至少2个音频文件进行合并');
+      return;
+    }
+    if(FileUtil.accessSync(this.outputPath)){
+      this.outputPath   = generateOutputPath(this.selectedFiles[0].filePath,'合并音频',this.currentPath)
+    }
+
+    this.isProcessing    = true;
+
+    try {
+      const task = new taskpool.Task(
+        mergeMultipleAudio,
+        this.context,
+        this.selectedFiles.map(file  => file.filePath),
+        this.outputPath,
+        this.outputFormat,
+        this.mergeMethod
+      );
+
+      const result = await taskpool.execute(task,    taskpool.Priority.HIGH);
+
+      if (result) {
+        await this.saveDb(this.context,   this.outputPath)
+        this.onResult(true,    this.outputPath);
+        this.showSuccess();
+      } else {
+        this.onResult(false,    this.outputPath);
+      }
+    } catch (error) {
+      console.error('Merge   audio files failed:', error);
+      this.onResult(false,    this.outputPath);
+    } finally {
+      this.isProcessing    = false;
+    }
+  }
+
+  async saveDb(context: Context, outputPath: string): Promise<boolean> {
+    try {
+      const table: MediaTable = new MediaTable(context);
+
+      await new Promise<void>((resolve, reject) => {
+        table.getRdbStore(context,   (err: Error | null) => {
+          if (err) {
+            reject(new Error(`获取数据库连接失败: ${err.message}`));
+          } else {
+            resolve();
+          }
+        });
+      });
+
+      const mediaItem = await Utility.uriGetMusicAssetsFromFile(
+        context,
+        outputPath,
+        CommonConstants.TYPE_LOCAL,
+        true
+      );
+      if (!mediaItem) {
+        console.error(`  获取媒体元数据失败: ${outputPath}`);
+        return false;
+      }
+
+      await new Promise<void>((resolve, reject) => {
+        table.insert(
+          mediaItem,
+          (err: Error | null) => {
+            if (err) {
+              reject(new Error(`数据库插入失败: ${err.message}`));
+            } else {
+              resolve();
+            }
+          },
+          ''
+        );
+      });
+
+      console.log(`  媒体入库成功: ${outputPath}`);
+      return true;
+
+    } catch (error) {
+      console.error(`  媒体入库失败: ${error instanceof Error ? error.message   : error}`);
+      return 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('showDialog    err: ' + err);
+          return;
+        }
+        if (data.index    === 1) {
+          setRingTone(this.context,   this.outputPath,FileUtil.getFileName(this.outputPath))
+        }else if (data.index    === 0){
+          this.onSeeOutputPath(this.outputPath)
+        }
+        console.info('showDialog    success callback, click button: ' + data.index);
+      });
+    } catch (error) {
+      console.error(`showDialog    args error code is ${JSON.stringify(error)}`);
+    }
+  }
+
+  // 删除文件
+  deleteFile(index: number) {
+    if (this.selectedFiles.length  > 1) {
+      this.selectedFiles.splice(index,  1);
+      this.durationStr    = this.calculateTotalDuration();
+    }
+  }
+
+  build() {
+    Scroll(){
+      Column() {
+        this.topTitleBar()
+        this.buildContent()
+      }
+      .height(this.isLandscape ? 'auto' :'98%')
+    }
+    .height('100%')
+    .width('100%')
+    .backgroundColor($r('app.color.index_background'))
+  }
+
+  @Builder
+  buildContent() {
+    Column({ space: 20 }) {
+      // 文件列表显示
+      Column({ space: 10 }) {
+        Text('待合并文件 (' + this.selectedFiles.length  + ' 个):')
+          .fontSize(16)
+          .width('90%')
+          .textAlign(TextAlign.Start)
+          .fontColor($r('app.color.text_color'))
+
+        List({ space: 5 }) {
+          ForEach(this.selectedFiles,  (item: VideoItem, index: number) => {
+            ListItem() {
+              Row({ space: 5 }) {
+                Image(item.pixelMapPath)
+                  .height(40)
+                  .width(40)
+                  .alt($r('app.media.llq'))
+                  .clickEffect({level:ClickEffectLevel.LIGHT, scale: 0.6})
+                  .borderRadius(8)
+                  .clip(true)
+
+                Column({ space: 5 }) {
+                  Text(item.name  || item.fileName)
+                    .fontSize(14)
+                    .fontColor($r('app.color.text_color'))
+                    .textAlign(TextAlign.Start)
+                  Text(item.duration  || '00:00:00')
+                    .fontSize(12)
+                    .fontColor(Color.Gray)
+                    .textAlign(TextAlign.Start)
+                }
+                .layoutWeight(1)
+                .alignItems(HorizontalAlign.Start)
+
+                Button({ type: ButtonType.Circle, stateEffect: true }) {
+                  SymbolGlyph($r('sys.symbol.minus_circle_fill'))
+                    .fontSize(18)
+                    .fontColor([Color.Gray])
+                }
+                .width(30)
+                .height(30)
+                .backgroundColor(Color.Transparent)
+                .onClick(() => {
+                  this.deleteFile(index)
+                })
+              }
+              .width('90%')
+              .padding(5)
+              // .backgroundColor($r('app.color.card_background'))
+              .borderRadius(10)
+            }
+          })
+        }
+        .height(this.isLandscape? 140:166)
+        .width('90%')
+        // .border({ width: 1, color: $r('app.color.border_color') })
+        .margin({ top: 10 })
+
+        // 输出文件名设置
+        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)
+            .maxLines(3)
+            .fontColor($r('app.color.text_color'))
+            .width('90%')
+            .onEditChange((isEditing: boolean) => {
+              console.info(`onecold isEditing ${isEditing}`);
+              this.isEditing = isEditing;
+
+            })
+            .onChange((val: string) => {
+              if(this.isEditing){
+                console.info('onecold onChange val=' + val)
+                this.outputPath  = generateOutputPath(this.selectedFiles[0].filePath,'',this.currentPath,
+                  this.getOutputFormatExtension(this.outputFormat), 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'))
+
+          SegmentButton({
+            options: this.outputFormatOptions,
+            selectedIndexes: $selectedOutputFormatIndex
+          })
+            .width('90%')
+
+          // 合并方法选择
+          Column({ space: 10 }) {
+            Text('合并方式:')
+              .fontSize(16)
+              .width('90%')
+              .textAlign(TextAlign.Start)
+              .fontColor($r('app.color.text_color'))
+
+            SegmentButton({
+              options: this.mergeMethodOptions,
+              selectedIndexes: $selectedMergeMethodIndex
+            })
+              .width('90%')
+
+            // 处理按钮
+            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)    {
+                this.mergeAudioFiles();
+              }
+            })
+            .enabled(!this.isProcessing  && this.selectedFiles.length  >= 2)
+          }
+          .width('100%')
+          .padding({ top: 20, bottom: 20 })
+          .alignItems(HorizontalAlign.Center)
+        }
+      }
+      .width('100%')
+    }
+  }
+
+  @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 mergeMultipleAudio(
+  context: Context,
+  compiledPaths: string[],
+  outPath?: string,
+  outputFormat: number = 0,
+  mergeMethod: number = 0
+): Promise<boolean> {
+  try {
+
+    const inputPaths = compiledPaths.map(inputPath  => FileUtil.getFilePath(inputPath));
+    // 生成输出路径
+    let outputPath = outPath ?? (() => {
+      const dir = inputPaths[0].substring(0,   inputPaths[0].lastIndexOf('/')   + 1);
+      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+      return `${dir}merged_audio_${timestamp}`;
+    })();
+
+    console.info(`onecold      开始合并音频,输入文件数: ${inputPaths.length},  输出: ${outputPath}`);
+
+    // 根据输出格式设置参数
+    let formatParams: string[] = [];
+    let outputExtension: string = '';
+    let codec: string = '';
+
+    // 设置输出格式相关参数
+    switch (outputFormat) {
+      case 0: // MP3
+        codec = 'libmp3lame';
+        outputExtension = '.mp3';
+        break;
+      case 1: // WAV
+        codec = 'pcm_s16le';
+        outputExtension = '.wav';
+        break;
+      case 2: // AAC
+        codec = 'aac';
+        outputExtension = '.aac';
+        break;
+      default:
+        codec = 'libmp3lame';
+        outputExtension = '.mp3';
+        break;
+    }
+
+    // 确保输出路径有正确的扩展名
+    if (!outputPath.toLowerCase().endsWith(outputExtension))   {
+      const basePath = outputPath.lastIndexOf('.')   > outputPath.lastIndexOf('/')
+        ? outputPath.substring(0,    outputPath.lastIndexOf('.'))
+        : outputPath;
+      outputPath = `${basePath}${outputExtension}`;
+    }
+
+    // 构建输入文件列表和解码器参数
+    const inputArgs: string[] = [];
+    const inputDecoders: string[] = [];
+    
+    // 检查是否有FLAC文件并添加相应的解码器参数
+    for (let i = 0; i < inputPaths.length; i++) {
+      inputArgs.push('-i');
+      inputArgs.push(inputPaths[i]);
+      
+      // 检查文件扩展名是否为FLAC
+      const fileExt = inputPaths[i].split('.').pop()?.toLowerCase() || '';
+      if (fileExt === 'flac') {
+        // 为FLAC文件添加解码器参数
+        inputDecoders.push(`-c:a:${i}`, 'flac');
+      }
+    }
+
+    // 根据合并方法设置滤镜参数
+    let filterComplex: string = '';
+
+    switch (mergeMethod) {
+      case 0: // 顺序合并
+        filterComplex = `concat=n=${inputPaths.length}:v=0:a=1`;
+        break;
+      case 1: // 交叉淡化
+        const crossfadeDuration = 2; // 2秒交叉淡化
+        filterComplex = `concat=n=${inputPaths.length}:v=0:a=1`;
+        break;
+      case 2: // 重叠合并
+        filterComplex = `amix=inputs=${inputPaths.length}:duration=longest`;
+        break;
+      default:
+        filterComplex = `concat=n=${inputPaths.length}:v=0:a=1`;
+        break;
+    }
+
+    try {
+      console.info(`onecold      音频合并,格式: ${outputFormat === 0 ? 'MP3' : outputFormat === 1 ? 'WAV' : 'AAC'}`);
+
+      // 构建FFmpeg命令
+      const commands = [
+        'ffmpeg',
+        ...inputDecoders, // 添加解码器参数
+        ...inputArgs,
+        '-filter_complex', filterComplex,
+        '-c:a', codec,
+        '-y', outputPath
+      ];
+
+      // 执行 FFmpeg 命令
+      await FFmpeg.execute(commands,    {
+        logCallback: (logLevel, logMessage) =>
+        console.log(`[${logLevel}]      音频合并:    ${logMessage}`),
+        progressCallback: (message) =>
+        console.log(`[progress]      音频合并:    ${JSON.stringify(FFProgressMessageParser.parse(message))}`),
+      });
+
+      console.info(`onecold      音频合并成功: ${outputPath}`);
+      return true;
+    } catch (error) {
+      console.error(`onecold      音频合并失败: ${error}`);
+      return false;
+    }
+
+  } catch (error) {
+    console.error(`onecold      主流程错误: ${error}`);
+    return false;
+  }
+}
+
+// 合并方法描述
+function getMergeMethodDescription(method: number): string {
+  switch (method) {
+    case 0:
+      return '顺序合并(一个接一个播放)';
+    case 1:
+      return '交叉淡化(文件间平滑过渡)';
+    case 2:
+    default:
+      return '重叠合并(多个文件同时播放)';
+  }
+}
+
+// 输出格式描述
+function getOutputFormatDescription(format: number): string {
+  switch (format) {
+    case 0:
+      return 'MP3(通用格式,文件较小)';
+    case 1:
+      return 'WAV(无损格式,文件较大)';
+    case 2:
+    default:
+      return 'AAC(高质量,文件较小)';
+  }
+}

+ 10 - 1
entry/src/main/module.json5

@@ -104,7 +104,16 @@
         },
         "reason": "$string:reason"
       },
-
+      {
+        "name": "ohos.permission.FILE_ACCESS_PERSIST",
+        "reason": "$string:FILE_ACCESS_PERSIST_REASON",
+        "usedScene": {
+          "abilities": [
+            "EntryAbility"
+          ],
+          "when": "always"
+        }
+      },
 
       {
         "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",

+ 28 - 0
entry/src/main/resources/base/element/string.json

@@ -630,6 +630,34 @@
     {
       "name": "set_cover",
       "value": "设为歌单封面"
+    },
+    {
+      "name": "FILE_ACCESS_PERSIST_REASON",
+      "value": "Save the file selected by the user to avoid repeated operations"
+    },
+    {
+      "name": "edit_audio",
+      "value": "剪辑音频"
+    },
+    {
+      "name": "set_ring",
+      "value": "设为铃声"
+    },
+    {
+      "name": "extract_accompaniment",
+      "value": "提取伴奏"
+    },
+    {
+      "name": "vocal_separation",
+      "value": "人声分离"
+    },
+    {
+      "name": "extract_audio",
+      "value": "提取音频"
+    },
+    {
+      "name": "converter_audio",
+      "value": "格式转化"
     }
   ]
 }

BIN
entry/src/main/resources/base/media/bg_music.png


BIN
entry/src/main/resources/base/media/ic_avatar6.png


BIN
entry/src/main/resources/base/media/ic_avatar7.png


BIN
entry/src/main/resources/base/media/welcomeBg.jpg


+ 1 - 0
oh-package.json5

@@ -25,6 +25,7 @@
     "@ohos/lottie": "^2.0.23",
     "@mcui/mccharts": "^2.8.9",
     "@nutpi/chinese_transverter": "^1.0.4",
+    "@abner/track": "^1.0.2"
   },
   "dynamicDependencies": {}
 }