Browse Source

实现了长按音乐可以变速变调的功能

onecold 9 tháng trước cách đây
mục cha
commit
d3fb1ad641

+ 628 - 0
entry/src/main/ets/view/ChangeTone.ets

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

+ 92 - 16
entry/src/main/ets/view/LocalMusic.ets

@@ -48,7 +48,7 @@ import { repairAudioMetadata, convertDsfToWav, getApiLyric,changeMusicCover} fro
 import { FFMpegTags,Utility } from '../common/util/Utility';
 import { TagsContentCover } from '../view/TagsContentCover';
 import {
-  DeviceChangeReason,
+  // DeviceChangeReason,
   IjkMediaPlayer,
   InterruptEvent,
   InterruptHintType,
@@ -112,6 +112,7 @@ import { AudioFormatConverter } from './AudioFormatConverter';
 import { MergeAudio } from './MergeAudio';
 import { AIVoiceSeparation } from './AIVoiceSeparation';
 import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
+import { ChangeTone } from './ChangeTone';
 import {
   hdsEffect,
   HdsVisualComponent,
@@ -569,6 +570,7 @@ export struct LocalMusic {
   @State isExtractAudio:boolean = false
   @State isMergeAudio:boolean = false
   @State isCopyFileToDownLoad: boolean = false
+  @State isChangeTone:boolean = false
   @State offHeight: number = 75
   @State currentSongList: Array<VideoItem> = []//当前歌单
   @Consume currentSongListName:string //当前歌单名称
@@ -1081,16 +1083,16 @@ export struct LocalMusic {
     }
     this.mIjkMediaPlayer.on('audioInterrupt', event);
     // 音频设备断连回调处理
-    let deviceChangeEvent: Callback<InterruptEvent> = (event) => {
-      LogUtils.getInstance().LOGI(`Heanup deviceChange event: ${JSON.stringify(event)}`);
-      if (event.reason === DeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE) { // 音频设备连接
-
-      } else if (event.reason === DeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) { // 音频设备断开连接
-        this.pause();
-      }
-    }
-    // 订阅音频设备断开和连接事件
-    this.mIjkMediaPlayer.on('deviceChange', deviceChangeEvent);
+    // let deviceChangeEvent: Callback<InterruptEvent> = (event) => {
+    //   LogUtils.getInstance().LOGI(`Heanup deviceChange event: ${JSON.stringify(event)}`);
+    //   if (event.reason === DeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE) { // 音频设备连接
+    //
+    //   } else if (event.reason === DeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) { // 音频设备断开连接
+    //     this.pause();
+    //   }
+    // }
+    // // 订阅音频设备断开和连接事件
+    // this.mIjkMediaPlayer.on('deviceChange', deviceChangeEvent);
 
     const eventPause: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_PAUSE }
     // 监听广播事件(hicar断开暂停播放)
@@ -2349,6 +2351,9 @@ export struct LocalMusic {
       let documentSelectOptions = new picker.DocumentSelectOptions();
       documentSelectOptions.authMode = true
       documentSelectOptions.multiAuthMode = true
+      if( deviceInfo.sdkApiVersion>=21){//如果api大于21 ,可以支持1w首歌以上
+        documentSelectOptions.maxSelectNumber = 20000
+      }
       documentSelectOptions.mergeMode = picker.MergeTypeMode.AUDIO;
       let documentPicker = new picker.DocumentViewPicker(this.context);
       documentPicker.select(documentSelectOptions).then((documentSelectResult: Array<string>) => {
@@ -3842,6 +3847,68 @@ export struct LocalMusic {
     .height('100%')
   }
 
+  //变速变调
+  @Builder
+  ChangeToneBuilder(item:VideoItem) {
+    Scroll() {
+      Column() {
+        ChangeTone(
+          {
+            mVideoItem:item,
+            currentPath:this.currentPath,
+            videoUrl:this.videoUrl,
+            CONTROL_PlayStatus:this.CONTROL_PlayStatus,
+            currentTime:this.currentTime,
+            progressValue:this.progressValue,
+            onBack:()=>{
+              this.isChangeTone = !this.isChangeTone
+              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.isChangeTone = !this.isChangeTone
+              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
@@ -5766,7 +5833,16 @@ export struct LocalMusic {
               .onClick(() => {
                 this.isAIVoiceSeparation = !this.isAIVoiceSeparation
               })
-
+            MenuItem({
+              symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_counterclockwise_clock')),
+              content: $r('app.string.change_tone')
+            })
+              .bindContentCover($$this.isChangeTone, this.ChangeToneBuilder(item), {
+                transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
+              })
+              .onClick(() => {
+                this.isChangeTone = !this.isChangeTone
+              })
 
             if(Utility.isVideoByExtension(item.filePath)){
               MenuItem({
@@ -5853,7 +5929,7 @@ export struct LocalMusic {
               })
 
           }
-          .font({ size: 15, weight: FontWeight.Normal })
+          .font({ size: 14, weight: FontWeight.Normal })
         }
         .width(180)
       }
@@ -8865,10 +8941,10 @@ export struct LocalMusic {
               duration: 500,
               curve: Curve.Sharp
             }, () => {
-              this.scaleValueImage = Math.min(1, Math.max(0.3, 1 - event.offsetY / 260));
+              this.scaleValueImage = Math.min(1, Math.max(0.3, 1 - event.offsetY / 350));
               console.info('onecold scaleValueImage:', this.scaleValueImage)
 
-              this.scaleValueText =Math.min(1, Math.max(0.6, 1 - event.offsetY / 300));
+              this.scaleValueText =Math.min(1, Math.max(0.6, 1 - event.offsetY / 400));
             })
           }
         })
@@ -12771,7 +12847,7 @@ export struct LocalMusic {
 
     //针对dsf文件高采样率,统一转wav播放
     const sampleRate = this.currentSong?.sampleRate||0
-    if(this.videoUrl.toLowerCase().endsWith('.dsf')){
+    if(this.videoUrl.toLowerCase().endsWith('.dsf')&&sampleRate>=441000){
       console.info('onecold 采样率 ='+sampleRate)
       let newDsfPath = this.context.filesDir + '/'+FileUtil.getFileName(this.videoUrl)+'.wav'
       console.info('onecold newDsfPath ='+newDsfPath)

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

@@ -651,6 +651,10 @@
       "name": "vocal_separation",
       "value": "人声分离"
     },
+    {
+      "name": "change_tone",
+      "value": "变速变调"
+    },
     {
       "name": "extract_audio",
       "value": "提取音频"