Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	entry/src/main/ets/view/LocalMusic.ets
chendeben 9 месяцев назад
Родитель
Сommit
6afb7d9ac9
28 измененных файлов с 5318 добавлено и 127 удалено
  1. 2 2
      AppScope/app.json5
  2. 2 2
      entry/src/main/ets/common/constants/CommonConstants.ets
  3. 115 0
      entry/src/main/ets/common/util/MusicTagUtils.ets
  4. 1 1
      entry/src/main/ets/common/util/PermissionUtil.ets
  5. 27 9
      entry/src/main/ets/common/util/Utility.ets
  6. 24 5
      entry/src/main/ets/common/util/WebdavManager.ets
  7. 1 1
      entry/src/main/ets/controller/AvSessionController.ets
  8. 6 2
      entry/src/main/ets/dialog/WebDavAccountDialog.ets
  9. 51 23
      entry/src/main/ets/pages/NewIndex.ets
  10. 9 0
      entry/src/main/ets/pages/UserCenter.ets
  11. 6 4
      entry/src/main/ets/pages/WebDavMainPage.ets
  12. 650 0
      entry/src/main/ets/view/AIVoiceSeparation.ets
  13. 651 0
      entry/src/main/ets/view/AudioFormatConverter.ets
  14. 35 25
      entry/src/main/ets/view/DeleteComptent.ets
  15. 1094 0
      entry/src/main/ets/view/EditAudio.ets
  16. 697 0
      entry/src/main/ets/view/ExtractAccompaniment.ets
  17. 609 0
      entry/src/main/ets/view/ExtractAudioFromVideo.ets
  18. 613 43
      entry/src/main/ets/view/LocalMusic.ets
  19. 627 0
      entry/src/main/ets/view/MergeAudio.ets
  20. 58 9
      entry/src/main/ets/view/TagsContentCover.ets
  21. 1 0
      entry/src/main/ets/viewmodel/WebDavAccount.ets
  22. 10 1
      entry/src/main/module.json5
  23. 28 0
      entry/src/main/resources/base/element/string.json
  24. BIN
      entry/src/main/resources/base/media/bg_music.png
  25. BIN
      entry/src/main/resources/base/media/ic_avatar6.png
  26. BIN
      entry/src/main/resources/base/media/ic_avatar7.png
  27. BIN
      entry/src/main/resources/base/media/welcomeBg.jpg
  28. 1 0
      oh-package.json5

+ 2 - 2
AppScope/app.json5

@@ -2,8 +2,8 @@
   "app": {
     "bundleName": "com.xgplayer.ttmusic.hm",
     "vendor": "example",
-    "versionCode": 20251023,
-    "versionName": "1.6.6",
+    "versionCode": 20251101,
+    "versionName": "1.6.7",
     "icon": "$media:app_icon",
     "label": "$string:app_name",
     "multiAppMode": {

+ 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')
   ]
 

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

@@ -40,6 +40,7 @@ export async function repairAudioMetadata(
   overwrite: boolean = false,
   outputPath: string = ''
 ): Promise<boolean> {
+  inputPath = FileUtil.getFilePath(inputPath)
   // 确保metadata是对象类型
   if (typeof metadata !== 'object' || metadata === null) {
     console.error('元数据参数必须是一个对象');
@@ -52,6 +53,9 @@ export async function repairAudioMetadata(
 
   if (outputPath && outputPath.length > 0) {
     finalOutputPath = outputPath;
+    if(FileUtil.accessSync(finalOutputPath)){
+
+    }
   } else if (overwrite) {
     // 使用临时文件方式处理覆盖,保持原文件扩展名
     const timestamp = new Date().getTime();
@@ -203,6 +207,8 @@ export async function changeMusicCover(
   overwrite: boolean = false,
   outputPath: string = ''
 ): Promise<boolean> {
+  inputPath = FileUtil.getFilePath(inputPath)
+  console.info(`onecold changeMusicCover inputPath: ${inputPath} coverImagePath: ${coverImagePath} overwrite: ${overwrite} outputPath: ${outputPath}`);
   // 确定输出路径
   let finalOutputPath: string = '';
   let useTempFile: boolean = false;
@@ -558,6 +564,7 @@ export async function syncLyricToDB(context:Context,filePath: string,lyric:strin
         err ? reject(err) : resolve();
       });
     });
+
     table.updateMediaInfo(filePath, '', '', '',lyric,
       '','','','','', '','','',
       (success: boolean, error?: string) => {
@@ -589,6 +596,7 @@ export async function convertDsfToWav(
   inputPath: string,
   outputPath: string
 ): Promise<boolean> {
+  inputPath = FileUtil.getFilePath(inputPath)
   // 1. 输入文件验证(增强型检查)
   try {
     const stats = await fs.stat(inputPath);
@@ -637,4 +645,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;
+  }
 }

+ 1 - 1
entry/src/main/ets/common/util/PermissionUtil.ets

@@ -26,7 +26,6 @@ class PermissionUtil {
           let policies: Array<fileShare.PolicyInfo> = [policyInfo];
           let results = await fileShare.checkPersistentPermission(policies);
           for (let i = 0; i < results.length; i++) {
-            console.log('onecold activatePermission 激活权限成功');
             if (results[i]) {
               let info: fileShare.PolicyInfo = {
                 uri: policies[i].uri,
@@ -34,6 +33,7 @@ class PermissionUtil {
               };
               let policy: Array<fileShare.PolicyInfo> = [info];
               await fileShare.activatePermission(policy);
+              console.log('onecold activatePermission 激活权限成功');
             }
           }
           let fd = fs.openSync(uri);

+ 27 - 9
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 媒体格式类型字符串
@@ -652,7 +668,9 @@ export class Utility {
   static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number,autoParseMusicName?:boolean): Promise<VideoItem> {
     await ReqPermissionUtil.persistPermission(uri);
     if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
-      return  new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
+      const cueItem = new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
+      cueItem.fileName = FileUtil.getFileName(uri)
+      return  cueItem
     }
     return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName)
 
@@ -985,10 +1003,10 @@ export class Utility {
             const hasCover = metadata.streams.some(stream  =>
             stream.disposition?.attached_pic  === 1
             );
-            console.info('readMetaInfoFFmpeg asset hasCover: ', hasCover);
+            console.info('onecold readMetaInfoFFmpeg asset hasCover: ', hasCover);
             let md5Name = await MD5.digestSync(inputPath)
             let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
-            console.info('readMetaInfoFFmpeg asset imagePath: ', imagePath);
+            console.info('onecold readMetaInfoFFmpeg asset imagePath: ', imagePath);
             // 如果有封面图片,则提取
             try {
               if (hasCover) {
@@ -1013,22 +1031,22 @@ export class Utility {
                 videoItem.pixelMapPath  = imagePath;
               }
             } catch (error) {
-              console.warn(' 提取封面图片失败:', error.message);
+              console.warn('onecold 提取封面图片失败:', error.message);
             }
             if(sampleRate&&format.bit_rate){
               videoItem.md5Str = determineAudioQuality( videoItem.mimeType,Number(format.bit_rate),Number(sampleRate))
             }
-            console.info('Successfully  parsed metadata:', videoItem);
+            console.info('onecold Successfully  parsed metadata:', videoItem);
             resolve(videoItem);
           })
 
 
         } catch (error) {
-          console.error('Failed  to parse metadata:', error);
-          reject(new Error('Failed to parse metadata: ' + error.message));
+          console.error('onecold Failed  to parse metadata:', error);
+          reject(new Error('onecold Failed to parse metadata: ' + error.message));
         }
       }).catch((error: Error) => {
-        console.error(`Execution  failed with error: ${error.message}`);
+        console.error(`onecold Execution  failed with error: ${error.message}`);
         reject(error);
       });
     });

+ 24 - 5
entry/src/main/ets/common/util/WebdavManager.ets

@@ -159,6 +159,7 @@ export class WebdavManager {
         account.password = resultSet.getString(resultSet.getColumnIndex('password'));
         account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
         account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
+        account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
         account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
 
         resultSet.close();
@@ -375,6 +376,7 @@ export class WebdavManager {
         account.password = resultSet.getString(resultSet.getColumnIndex('password'));
         account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
         account.coverPath = resultSet.getString(resultSet.getColumnIndex('coverPath'));
+        account.webType = resultSet.getLong(resultSet.getColumnIndex('webType'));
         account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
 
         resultSet.close();
@@ -407,7 +409,8 @@ export class WebdavManager {
       account TEXT,
       password TEXT,
       enableHttps INTEGER DEFAULT 0,
-      coverPath TEXT
+      coverPath TEXT,
+      webType INTEGER DEFAULT 0
     )`;
 
     return this.dataBaseUtil.executeSql(createTableSql)
@@ -436,6 +439,18 @@ export class WebdavManager {
       // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
       Logger.info(TAG, 'coverPath字段可能已存在或添加失败,继续正常运行');
     }
+    
+    try {
+      // 添加webType字段,用于标识WebDAV账户类型
+      Logger.info(TAG, '检查数据库表结构,尝试添加webType字段...');
+      const addWebTypeColumnSql = `ALTER TABLE ${this.webDavTable} ADD COLUMN webType INTEGER DEFAULT 0`;
+
+      await this.dataBaseUtil.executeSql(addWebTypeColumnSql);
+      Logger.info(TAG, 'webType字段添加成功,数据库升级完成');
+    } catch (error) {
+      // 如果添加字段失败(可能字段已存在),记录但不阻止应用启动
+      Logger.info(TAG, 'webType字段可能已存在或添加失败,继续正常运行');
+    }
   }
 
   // 从数据库查询所有账户
@@ -469,8 +484,9 @@ export class WebdavManager {
         account.account = resultSet.getString(resultSet.getColumnIndex('account'));
         account.password = resultSet.getString(resultSet.getColumnIndex('password'));
         account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
-        // 设置coverPath为默认值undefined,稍后会尝试更新
+        // 设置coverPath和webType为默认值undefined,稍后会尝试更新
         account.coverPath = undefined;
+        account.webType = 0;
 
         this.webDavAccounts.push(account);
       }
@@ -528,7 +544,8 @@ export class WebdavManager {
     account: string,
     password: string,
     enableHttps: boolean,
-    coverPath?: string
+    coverPath?: string,
+    webType: number = 0
   ): Promise<void> {
     try {
       const values: relationalStore.ValuesBucket = {
@@ -545,7 +562,8 @@ export class WebdavManager {
         'account': account,
         'password': password,
         'enableHttps': enableHttps ? 1 : 0,
-        'coverPath': coverPath || null
+        'coverPath': coverPath || null,
+        'webType': webType
       };
 
       await this.dataBaseUtil.insertData(this.webDavTable, values);
@@ -578,7 +596,8 @@ export class WebdavManager {
         'account': account.account,
         'password': account.password,
         'enableHttps': account.enableHttps ? 1 : 0,
-        'coverPath': account.coverPath || null
+        'coverPath': account.coverPath || null,
+        'webType': account.webType
       };
 
       const predicates = new relationalStore.RdbPredicates(this.webDavTable);

+ 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,

+ 6 - 2
entry/src/main/ets/dialog/WebDavAccountDialog.ets

@@ -1,4 +1,4 @@
-import { ToastUtil } from '@pura/harmony-utils';
+import { StrUtil, ToastUtil } from '@pura/harmony-utils';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { WebDavAccount } from '../viewmodel/WebDavAccount';
 import { ImagePickerUtil } from '../common/util/ImagePickerUtil';
@@ -189,7 +189,11 @@ export struct WebDavAccountDialog {
           .maxLines(1)
           .type(InputType.Number)
           .onChange((value: string) => {
-            this.port = parseInt(value) || 80;
+            if(this.enableHttps){
+              this.port = parseInt(value) || 443;
+            }else{
+              this.port = parseInt(value) || 80;
+            }
           });
       }
       .alignItems(VerticalAlign.Center);

+ 51 - 23
entry/src/main/ets/pages/NewIndex.ets

@@ -675,41 +675,65 @@ struct NewIndex {
       .fontColor($r('app.color.text_color'))
       .align(Alignment.Start)
   }
-
+  @State ratioL: number = 1;
+  @State isRefreshing: boolean = false;
+  @State maxRefreshingHeight: number = 100.0;
+  private contentNode?: ComponentContent<Object> = undefined;
   /**
    * 动态生成抽屉菜单内容
    * 根据 isShowSponsorship 展示不同菜单项
    */
   @Builder
   getDrawerView() {
+    Refresh({ refreshing: $$this.isRefreshing, refreshingContent: this.contentNode }) {
+      List({ space: 0, scroller: this.scroller }) {
+        ListItemGroup({ header: this.buildUserInfoCard() }) {
+          if(this.tabSelectedIndexes[0]==0){//分类
+            this.buildTabCate()
+          }else if(this.tabSelectedIndexes[0]==1){//歌单
+            this.buildPlaylistTab()
+          }else if(this.tabSelectedIndexes[0]==2){//网盘
+            this.buildCloudStorageTab()
+          }
 
-    List({ space: 0, scroller: this.scroller }) {
-      ListItemGroup({ header: this.buildUserInfoCard() }) {
-        if(this.tabSelectedIndexes[0]==0){//分类
-          this.buildTabCate()
-        }else if(this.tabSelectedIndexes[0]==1){//歌单
-          this.buildPlaylistTab()
-        }else if(this.tabSelectedIndexes[0]==2){//网盘
-          this.buildCloudStorageTab()
         }
 
       }
+      .scrollBar(BarState.Off)
+      .width('86%')
+      .borderRadius(20)
+      .margin({
+        bottom: 20,
+        left: 10,
+        right: 10,
+        top: 10
+      })
+      .alignListItem(ListItemAlign.Center)
+      .layoutWeight(1)
+      .backgroundColor($r('app.color.left_draw_bg'))
 
+      .edgeEffect(EdgeEffect.None) // 必须设置列表为滑动到边缘无效果
     }
-    .scrollBar(BarState.Off)
-    .width('86%')
-    .borderRadius(20)
-    .margin({
-      bottom: 20,
-      left: 10,
-      right: 10,
-      top: 10
+    .pullDownRatio(this.ratioL)
+    .pullToRefresh(true)
+    .refreshOffset(0)
+    .onOffsetChange((offset: number) => {
+      // 越接近最大距离,下拉跟手系数越小。
+      this.ratioL = 1 - Math.pow((offset / this.maxRefreshingHeight), 3);
+    })
+    .onStateChange((refreshStatus: RefreshStatus) => {
+      console.info('onecold Refresh onStatueChange state is ' + refreshStatus);
+    })
+    .onRefreshing(async () => {
+      if(this.tabSelectedIndexes[0]==0){//分类
+        this.isRefreshing = false
+      }else if(this.tabSelectedIndexes[0]==1){//歌单
+        this.loadPlaylistList()
+      }else if(this.tabSelectedIndexes[0]==2){//网盘
+        this.loadWebDavAccounts()
+      }
     })
-    .alignListItem(ListItemAlign.Center)
-    .layoutWeight(1)
-    .backgroundColor($r('app.color.left_draw_bg'))
 
-    .edgeEffect(EdgeEffect.None) // 必须设置列表为滑动到边缘无效果
   }
 
 
@@ -1214,8 +1238,8 @@ struct NewIndex {
             // 账户封面或默认图标
             Stack() {
               Image(account.coverPath?account.coverPath:$r('app.media.cloudDisk'))
-                  .width(20)
-                  .height(20)
+                  .width(25)
+                  .height(25)
                   .borderRadius(4)
                   .fillColor(this.themeColor)
                   .objectFit(ImageFit.Cover)
@@ -1500,8 +1524,10 @@ struct NewIndex {
       } else {
         LogUtil.warn('heanup NewIndex', '歌单表未初始化')
       }
+      this.isRefreshing = false
     } catch (error) {
       LogUtil.error('heanup NewIndex', `加载歌单列表失败: ${(error as Error).message}`)
+      this.isRefreshing = false
     }
   }
 
@@ -1542,8 +1568,10 @@ struct NewIndex {
       await this.webdavManager.queryWebDavAccountsFromDB()
       // 强制触发UI更新
       this.webDavAccounts = this.webdavManager.getAllWebDavAccounts().slice()
+      this.isRefreshing = false
       LogUtil.info('heanup NewIndex', `成功加载 ${this.webDavAccounts.length} 个WebDAV账户`)
     } catch (error) {
+      this.isRefreshing = false
       LogUtil.error('heanup NewIndex', `加载WebDAV账户列表失败: ${(error as Error).message}`)
     }
   }

+ 9 - 0
entry/src/main/ets/pages/UserCenter.ets

@@ -43,6 +43,8 @@ async function isHasWX(): Promise<boolean> {
   }
 }
 
+
+
 async function createWeChatOrder(app_id: string, mch_id: string, description: string, amount: number,
   out_trade_no_pre: string, uid: number, plan_id: string): Promise<string> {
   let requestUrl = CommonConstants.WX_PAY_API
@@ -164,6 +166,13 @@ export struct UserCenter {
       isVipOnly: true,
       hasCome: true
     },
+    {
+      icon: $r('app.media.music_red'),
+      title: '提取伴奏',
+      description: '长按音乐可提取伴奏',
+      isVipOnly: true,
+      hasCome: true
+    },
     {
       icon: $r('app.media.skip'),
       title: '跳过头尾',

+ 6 - 4
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -192,9 +192,9 @@ export struct WebDavMainPage {
         // 更新可见文件夹列表
         this.updateVisibleFolders();
 
-        promptAction.showToast({
-          message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
-        });
+        // promptAction.showToast({
+        //   message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
+        // });
         break;
       case WebdavManagerStates.LoadFilesInfoFailed:
         this.isLoading = false;
@@ -617,9 +617,11 @@ export struct WebDavMainPage {
         if (this.webdavManager.currentPath !== '') {
           Row({ space: 8 }) {
             Button({ type: ButtonType.Circle }) {
-              Image(this.webdavManager.canGoBack()?$r('app.media.back'):$r('app.media.cloudDisk'))
+              Image(this.webdavManager.canGoBack()?$r('app.media.back'):this.selectedAccount.coverPath)
                 .width(15)
                 .height(15)
+                .borderRadius(10)
+                .alt($r('app.media.cloudDisk'))
                 .fillColor(Color.White)
             }
             .width(20)

+ 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;
+  }
+}

+ 35 - 25
entry/src/main/ets/view/DeleteComptent.ets

@@ -1,13 +1,13 @@
 import { common } from '@kit.AbilityKit';
 import { CommonConstants } from '../common/constants/CommonConstants';
 import { VideoItem } from '../viewmodel/VideoItem';
-import { ArrayUtil, FileUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
+import { AppUtil, ArrayUtil, FileUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
 import MediaTable from '../common/util/MediaTable';
 import PlaylistTable from '../common/util/PlaylistTable';
 import { EventConstants } from '../common/constants/EventConstants';
 import { emitter } from '@kit.BasicServicesKit';
 import { taskpool } from '@kit.ArkTS';
-
+import PermissionUtil from '../common/util/PermissionUtil'
 
 // 批量删除
 @Component
@@ -15,6 +15,7 @@ export struct DeleteComptent {
   @State isDeleteYuan: boolean = true
   @State isDeletePicture: boolean = true
   @State isDeleteLrc: boolean = true
+  @State packName: string = ''
   onDeleteResult = (_result: boolean) => {
   }
   onCancel = () => {
@@ -25,6 +26,7 @@ export struct DeleteComptent {
 
 
   async aboutToAppear() {
+    this.packName = AppUtil.getBundleName()
     this.isDeleteYuan = PreferencesUtil.getBooleanSync('isDeleteYuan', true);
     this.isDeletePicture = PreferencesUtil.getBooleanSync('isDeletePicture', true);
     this.isDeleteLrc = PreferencesUtil.getBooleanSync('isDeleteLrc', true)
@@ -165,11 +167,14 @@ export struct DeleteComptent {
     const task = new taskpool.Task(
       deleteMultipleFiles,
       JSON.stringify(this.selectedFiles),
-      this.isDeleteYuan,this.isDeletePicture,this.isDeleteLrc,this.context
+      this.isDeleteYuan,this.isDeletePicture,this.isDeleteLrc,this.context,this.packName
     );
     taskpool.execute(task, taskpool.Priority.HIGH).then((result) => {
       if(result){
-        this.onDeleteResult(true)
+        setTimeout(() => {
+          this.onDeleteResult(true)
+        },500)
+
       }else {
         this.onDeleteResult(false)
       }
@@ -189,7 +194,8 @@ async function deleteMultipleFiles(
   isDeleteYuan:boolean,
   isDeletePicture:boolean,
   isDeleteLrc:boolean,
-  context:Context
+  context:Context,
+  packName:string
 ) {
 
   const selectedFiles: VideoItem[] = JSON.parse(selectedFilesStr)
@@ -220,28 +226,12 @@ async function deleteMultipleFiles(
 
 
       } else {
-        // 先从所有歌单中移除这首歌(如果存在的话)
-        LogUtil.info('heanup DeleteComptent', `准备从歌单中移除歌曲: ${item.name}, filePath: ${item.filePath}`);
-        if (item.filePath) {
-          try {
-            const affectedPlaylists = await playlistTable.removeSongFromAllPlaylists(item.filePath);
-            LogUtil.info('heanup DeleteComptent', `removeSongFromAllPlaylists 返回了 ${affectedPlaylists.length} 个受影响的歌单`);
-            if (affectedPlaylists.length > 0) {
-              hasPlaylistChanges = true;
-              LogUtil.info('heanup DeleteComptent', `歌曲已从 ${affectedPlaylists.length} 个歌单中移除: ${item.name}`);
-            } else {
-              LogUtil.info('heanup DeleteComptent', `歌曲不在任何歌单中: ${item.name}`);
-            }
-          } catch (error) {
-            LogUtil.error('heanup DeleteComptent', `从歌单移除歌曲失败: ${(error as Error).message}`);
-          }
-        } else {
-          LogUtil.warn('heanup DeleteComptent', `歌曲 ${item.name} 的 filePath 为空`);
-        }
 
         table.deleteData(item, async () => {
           if(isDeleteYuan){
-            await FileUtil.unlink(item.filePath)
+            if(item.filePath.toLowerCase().includes(packName)){
+              await FileUtil.unlink(item.filePath)
+            }
 
           }
           console.info(`onecold 封面文件=: ${item.pixelMapPath}`);
@@ -256,7 +246,7 @@ async function deleteMultipleFiles(
           console.info(`onecold 歌词文件=: ${lyricPath}`);
           if(isDeleteLrc) {
 
-            if (lyricPath && FileUtil.accessSync(lyricPath)) {
+            if (lyricPath&&item.filePath.toLowerCase().includes(packName) && FileUtil.accessSync(lyricPath)) {
               await FileUtil.unlink(lyricPath);
               console.info( `onecold 歌词文件已删除: ${lyricPath}`);
             }
@@ -267,6 +257,26 @@ async function deleteMultipleFiles(
 
 
         });
+        // 先从所有歌单中移除这首歌(如果存在的话)
+        LogUtil.info('heanup DeleteComptent', `准备从歌单中移除歌曲: ${item.name}, filePath: ${item.filePath}`);
+        if (item.filePath) {
+          try {
+            const affectedPlaylists = await playlistTable.removeSongFromAllPlaylists(item.filePath);
+            LogUtil.info('heanup DeleteComptent', `removeSongFromAllPlaylists 返回了 ${affectedPlaylists.length} 个受影响的歌单`);
+            if (affectedPlaylists.length > 0) {
+              hasPlaylistChanges = true;
+              LogUtil.info('heanup DeleteComptent', `歌曲已从 ${affectedPlaylists.length} 个歌单中移除: ${item.name}`);
+            } else {
+              LogUtil.info('heanup DeleteComptent', `歌曲不在任何歌单中: ${item.name}`);
+            }
+          } catch (error) {
+            LogUtil.error('heanup DeleteComptent', `从歌单移除歌曲失败: ${(error as Error).message}`);
+          }
+        } else {
+          LogUtil.warn('heanup DeleteComptent', `歌曲 ${item.name} 的 filePath 为空`);
+        }
+
+
 
 
       }

+ 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 '高质量(音质好,文件较大)';
+  }
+}

+ 613 - 43
entry/src/main/ets/view/LocalMusic.ets

@@ -91,8 +91,18 @@ import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
 import { showCreatePlaylistDialog } from '../dialog/PlaylistDialog';
 import { convertPlaylistSongsToVideoItems, emptyView,updateAllSongsSortOrder } from '../pages/PlaylistDetailPage';
 import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
-import { WebdavManager, WebDavAuthItem} from '../common/util/WebdavManager';
-import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
+import { Song } from '../viewmodel/Song';
+import { SongType } from '../common/enums/SongType';
+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';
 
 /**
@@ -204,6 +214,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> = []//当前歌单
@@ -1120,6 +1136,7 @@ export struct LocalMusic {
     const documentViewPicker = new picker.DocumentViewPicker()
     let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD })
     this.rootPath = new fileUri.FileUri(documentSaveResult[0]).path
+    PreferencesUtil.putSync('download_path', this.rootPath+'/')
     this.currentPath = this.rootPath
     this.lockPath = this.rootPath + '/' + LocalMusic.STR_LOCK_VIDEO
     this.favPath = this.rootPath + '/' + LocalMusic.STR_FAC_VIDEO
@@ -1255,12 +1272,13 @@ export struct LocalMusic {
     Logger.info('onecold cachedValue = ' + cachedValue)
     const cachedHash = this.findStringCache(curPath + '_hash');
     const cachedDirList = this.findCache(curPath + "_dirList");
-    if (cachedValue !== undefined) {
+    if (cachedValue !== undefined&&!this.isRefreshing) {
       // this.videoLocalList = cachedValue;
       Logger.info('onecold 有缓存 cachedValue = ' + cachedValue)
       if (this.currentPath != this.rootPath) { //第一个封面赋值给currentTitleCover
         this.currentTitleCover = Utility.getFirstCoverFromList(cachedValue)
       }
+      this.isRefreshing = false
       this.updateListData(cachedValue)
     }
 
@@ -1353,7 +1371,7 @@ export struct LocalMusic {
       // this.setButtonStatus()
       this.openMusic(isOpen, destPath)
 
-
+      this.isRefreshing = false
 
     });
 
@@ -1810,22 +1828,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'
+    //       });
+    //     }
+    //   }
+    // })
 
 
   }
@@ -2124,6 +2167,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,
@@ -2453,7 +2500,7 @@ export struct LocalMusic {
                   emptyView(this.themeColor)
 
                 }
-                .position({ top:this.isShowCoverHeader() ? 260 : 180 })
+                .position({ top:this.isShowCoverHeader() ? 300 : 180 })
                 .transition(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 1.2, y: 1.2 })
                   .animation({ duration: 500 }),
                   TransitionEffect.scale({ x: 0, y: 0 })))
@@ -2507,22 +2554,46 @@ export struct LocalMusic {
                 transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
               })
               .onClick(() => {
-                this.isOneKeyTags = !this.isOneKeyTags
+                // if(ArrayUtil.isNotEmpty(this.selectedFiles)&&Utility.isMerge(this.selectedFiles,this.packName)){
+                  this.isOneKeyTags = !this.isOneKeyTags
+                // }else{
+                //   ToastUtil.showToast('选中的文件包含了不是本应用的DownLoad文件下,其他路径音频无权限批量内嵌')
+                // }
+
               })
 
-            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)
@@ -2625,10 +2696,13 @@ export struct LocalMusic {
           {
             selectedFiles:this.selectedFiles,
             isOneKeyTags:this.isOneKeyTags,
+            table:this.table,
             onTagsResult:(result: boolean)=>{
               this.isMultiSelect = false
               if(result){//如果嵌入成功,更新数据
-                this.doUpdateData()
+                setTimeout(()=>{
+                  this.doUpdateData()
+                },500)
               }
 
             }
@@ -2651,7 +2725,382 @@ export struct LocalMusic {
             onFixResult:(result: boolean)=>{
               this.isMultiSelect = false
               if(result){//如果嵌入成功,更新数据
+                this.deleteCache(this.currentPath)
+                this.doUpdateData()
+              }
+
+            }
+          }
+        )
+      }
+    }
+    .width('100%')
+    .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.isShowFileName = true
+              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.isShowFileName = true
+              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.isShowFileName = true
+              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.isShowFileName = true
+              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.isShowFileName = true
+              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.isShowFileName = true
+              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('合并失败')
+                }
+
               }
 
             }
@@ -3088,8 +3537,13 @@ export struct LocalMusic {
 
               }))
             .onClick(()=>{
-              this.doPlay(item,index)
-              this.isShowPlay = true
+              if (this.isMultiSelect) {
+                const isChecked = this.selectedFiles.some(x => x.filePath == item.filePath);
+                this.handleFileSelection(item, !isChecked);
+              } else {
+                this.doPlay(item,index)
+                this.isShowPlay = true
+              }
             })
         }
 
@@ -4869,6 +5323,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')
@@ -5005,13 +5530,16 @@ export struct LocalMusic {
         this.lockPath,PreferencesUtil.getStringSync('COVER_API',''));
       taskpool.execute(task, taskpool.Priority.HIGH).then(()=>{
         if(this.modeType==0){
-          this.getSortedFiles(this.currentPath)
+          setTimeout(()=>{
+            this.isRefreshing =false
+            this.deleteCache(this.currentPath)
+            this.getSortedFiles(this.currentPath)
+          },500)
+
         }
-        setTimeout(() => {
-          this.isRefreshing = false;
-        }, 100)
       }).catch((e:object)=>{
-        console.info("task1 catch e: " + e);
+        this.isRefreshing =false
+        console.info("onecold task1 catch e: " + e);
       })
     }
   }
@@ -6034,11 +6562,18 @@ export struct LocalMusic {
       ToastUtil.showToast('标题不能为空')
       return
     }
-
+    DialogHelper.showLoadingDialog()
+    await PermissionUtil.activatePermission(item.filePath)
+    let tempOutPath = ''
+    // 如果不是 packName 包下的文件,则直接路径用this.currentPath
+    if(!item.filePath.toLowerCase().includes(this.packName)){
+      tempOutPath  = this.currentPath + '/' + item.fileName
+    }
+    console.info('onecold tempOutPath ='+tempOutPath)
     //用户更改了封面,那就内嵌下封面
     if(StrUtil.isNotEmpty(this.imagePathStr)){
       const resultCover:boolean = await changeMusicCover(this.context,
-        item.filePath,this.imagePathStr,true);
+        item.filePath,this.imagePathStr,true,tempOutPath);
       this.imagePathStr = ''
       if(resultCover){
         ToastUtil.showToast('内嵌封面成功')
@@ -6061,16 +6596,34 @@ export struct LocalMusic {
       comment:this.commentStr,
       disc:this.discStr,
     };
-
+    let newInput = item.filePath
+    //内嵌标签歌路径如果不是包含包名,输入newInput要赋值tempOutPath,然后tempOutPath为空
+    if(!item.filePath.toLowerCase().includes(this.packName)&&FileUtil.accessSync(tempOutPath)){
+      newInput = tempOutPath
+      tempOutPath  = ''
+    }
     //内嵌下标签的值,设置overwrite为true会直接修改原文件
     const result:boolean = await repairAudioMetadata(
-      item.filePath,
+      newInput,
       this.lyricConStr,
       metadata,
-      true
+      true,
+      tempOutPath
     );
 
     if (result) {
+      if(!item.filePath.includes(this.packName)){
+        //内嵌成功的歌路径如果不是包含包名,则要入库
+        let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newInput,
+          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) => {
@@ -6104,12 +6657,15 @@ export struct LocalMusic {
             console.error(" onecold  编辑信息数据库失败原因: " + error);
           }
 
-
+          // 关闭进度条
+          DialogHelper.closeLoading();
         });
       console.log(' onecold 元数据标签更新成功,');
 
 
     } else {
+      // 关闭进度条
+      DialogHelper.closeLoading();
       ToastUtil.showToast('内嵌音乐标签失败')
       console.log('onecold 内嵌音乐标签失败');
     }
@@ -8104,7 +8660,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();
@@ -9567,7 +10123,7 @@ export struct LocalMusic {
   @Builder
   private musicNameInfo(isHidden: boolean) {
     Column({ space: 8 }) {
-      Text(this.name)
+      Text(this.isShowFileName&&this.currentSong?this.currentSong.fileName :this.name)
         .fontSize(this.name.length >= 28 || this.isCoverOpacity() ? 18 : 22)
         .fontWeight(FontWeight.Bold)
         .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
@@ -9580,7 +10136,7 @@ export struct LocalMusic {
         .textOverflow({ overflow: TextOverflow.MARQUEE })//超长滚动
         .fontColor(Color.White)
         .textAlign(TextAlign.Center)
-        .visibility(StrUtil.isEmpty(this.artist)?Visibility.None:Visibility.Visible)
+        .visibility(StrUtil.isEmpty(this.artist)||this.isShowFileName?Visibility.None:Visibility.Visible)
         .width('99%')
         .maxLines(1)
     }
@@ -11813,6 +12369,20 @@ export struct LocalMusic {
   }
 
   private async play(url: string,startOffset?:number) {
+    console.info('onecold this.url 1= '+url)
+    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();
     this.isOpenAB = false

+ 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(高质量,文件较小)';
+  }
+}

+ 58 - 9
entry/src/main/ets/view/TagsContentCover.ets

@@ -2,7 +2,7 @@ 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, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
+import { AppUtil, FileUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
 import fs from '@ohos.file.fs';
 import { changeMusicCover, findLocalCoverImage, getApiLyric, repairAudioMetadata,
   searchCover,
@@ -10,6 +10,8 @@ import { changeMusicCover, findLocalCoverImage, getApiLyric, repairAudioMetadata
 import { util } from '@kit.ArkTS';
 import { FFMpegTags, Utility } from '../common/util/Utility';
 import { DialogHelper } from '@pura/harmony-dialog';
+import PermissionUtil from '../common/util/PermissionUtil'
+import MediaTable from '../common/util/MediaTable';
 
 //批量编辑标签
 // 批量编辑标签
@@ -20,8 +22,10 @@ export struct TagsContentCover {
   }
   @State isLyric:boolean = true
   @State isCover:boolean = true
+  @State download_path:string = ''
   @Link isOneKeyTags: boolean;
-  @State bundleName: string = ''
+  @Prop table: MediaTable
+  @State packName: string = ''
   @State dataSource: LazyDataSource<VideoItem> = new LazyDataSource([])
   @Prop selectedFiles: Array<VideoItem>
   @StorageProp('topRectHeight') topRectHeight: number = 0;
@@ -48,7 +52,12 @@ export struct TagsContentCover {
   }
 
   async aboutToAppear() {
-    this.bundleName = AppUtil.getBundleName()
+    this.download_path = PreferencesUtil.getStringSync('download_path')+'批量内嵌'+'/'
+    if (!FileUtil.accessSync(this.download_path)) {
+      FileUtil.mkdirSync(this.download_path)
+    }
+    console.info('onecold download_path:', this.download_path)
+    this.packName = AppUtil.getBundleName()
     // 确保 selectedFiles 有默认值后再初始化 dataSource
     if (this.selectedFiles && this.selectedFiles.length > 0) {
       this.dataSource = new LazyDataSource(this.selectedFiles)
@@ -128,7 +137,24 @@ export struct TagsContentCover {
   @Builder
   TagSetting() {
     Column(){
+      Row() {
+        SymbolGlyph($r('sys.symbol.night_badge_clock'))
+          .fontSize(20)
+          .fontColor([this.themeColor])
+          .alignSelf(ItemAlign.Center)
+          .margin({ left: 15 })
+        Text('说明:不是本app路径下的歌曲将在DownLoad/天天静听/内嵌标签文件夹中生成新的内嵌歌曲。')
+          .margin({ left: 5 ,right: 5})
+          .fontSize(14)
+          .padding({top:4,bottom:4})
+          .fontColor($r('app.color.text_color'))
+          .fontWeight(480)
+          .layoutWeight(1)
 
+      }
+      .height(55)
+      .margin({top:15,bottom:8})
+      .clickEffect({level:ClickEffectLevel.HEAVY,scale:0.6})
       Row() {
         SymbolGlyph($r('sys.symbol.doc_text'))
           .fontSize(20)
@@ -262,7 +288,7 @@ export struct TagsContentCover {
           .margin({ left: 6 })
           .onClick(() => {
             DialogHelper.closeDialog('vip');
-            Utility.gotoMarket(getContext(this) as common.UIAbilityContext, this.bundleName)
+            Utility.gotoMarket(getContext(this) as common.UIAbilityContext, this.packName)
             setTimeout(()=>{
               PreferencesUtil.putSync('IS_HAO_PING',true)
             },15000)
@@ -318,10 +344,10 @@ export struct TagsContentCover {
       try {
         this.currentFileIndex = index + 1;
         this.currentFileName = item.name;
-
+        await PermissionUtil.activatePermission(item.filePath)
 
         let success = true;
-
+        let tempOutPath = ''
         // 1. 处理封面 (如果启用且需要处理)
         if(this.isCover){
           let  defaultCover = ''
@@ -345,11 +371,18 @@ export struct TagsContentCover {
           if (StrUtil.isNotEmpty(defaultCover)) {
             // 确保 filePath 存在
             if (item.filePath) {
+              // 如果不是 packName 包下的文件,则直接路径用this.download_path
+              if(!item.filePath.toLowerCase().includes(this.packName)){
+                tempOutPath  = this.download_path+ item.fileName
+              }
+              console.info('onecold tempOutPath: ' + tempOutPath)
+
+
               const result = await changeMusicCover(
                 this.context,
                 item.filePath,
                 defaultCover,
-                true // 覆盖原文件
+                true, tempOutPath
               );
               if (!result) {
                 success = false;
@@ -404,16 +437,32 @@ export struct TagsContentCover {
           // 如果有歌词内容,嵌入到音频文件
           if (StrUtil.isNotEmpty(lyricContent) && item.filePath) {
             const metadata: FFMpegTags = {};
+            let newInput = item.filePath
+            //内嵌标签歌路径如果不是包含包名,输入newInput要赋值tempOutPath,然后tempOutPath为空
+            if(!item.filePath.toLowerCase().includes(this.packName)&&FileUtil.accessSync(tempOutPath)){
+              newInput = tempOutPath
+              tempOutPath  = ''
+            }
             // 修复错误2: 添加非空检查
             const result = await repairAudioMetadata(
-              item.filePath,
+              newInput,
               lyricContent,
               metadata,
-              true // 覆盖原文件
+              true ,
+              tempOutPath
             );
             if (!result) {
               success = false;
             }else{
+              if(!item.filePath.includes(this.packName)){
+                //内嵌成功的歌路径如果不是包含包名,则要入库
+                let newItem: VideoItem = await Utility.uriGetMusicAssetsFromFile(this.context, newInput,
+                  CommonConstants.TYPE_LOCAL, true);
+                this.table.insert(newItem, (id: number) => {
+
+
+                });
+              }
               //内嵌成功后同步歌词到数据库
               syncLyricToDB(this.context,item.filePath,lyricContent)
             }

+ 1 - 0
entry/src/main/ets/viewmodel/WebDavAccount.ets

@@ -23,6 +23,7 @@ export class WebDavAccount{
   public imageFilePaths: string[] = []
   // 自定义封面路径
   public coverPath?: string
+  public webType : number = 0
 
   public setIsUseLocalHost(isuse: boolean){
     this.isUseLocalHost = isuse

+ 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": {}
 }