Ver código fonte

Merge branch 'hqyNewVersion' of http://git.dayaedu.com/lex/resource-admin into develop

黄琪勇 1 ano atrás
pai
commit
a9485d7dc4

+ 0 - 2
components.d.ts

@@ -15,7 +15,6 @@ declare module '@vue/runtime-core' {
     NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
     NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
     NButton: typeof import('naive-ui')['NButton']
-    NCheckbox: typeof import('naive-ui')['NCheckbox']
     NConfigProvider: typeof import('naive-ui')['NConfigProvider']
     NCountdown: typeof import('naive-ui')['NCountdown']
     NDialogProvider: typeof import('naive-ui')['NDialogProvider']
@@ -37,7 +36,6 @@ declare module '@vue/runtime-core' {
     NRadio: typeof import('naive-ui')['NRadio']
     NRadioGroup: typeof import('naive-ui')['NRadioGroup']
     NSpace: typeof import('naive-ui')['NSpace']
-    NSpin: typeof import('naive-ui')['NSpin']
     NTooltip: typeof import('naive-ui')['NTooltip']
     Recharge: typeof import('./src/components/Lockscreen/Recharge.vue')['default']
     RouterError: typeof import('./src/components/RouterError/RouterError.vue')['default']

+ 11 - 0
src/views/music-library/api.ts

@@ -90,6 +90,17 @@ export const musicSheetImg = (params: object) => {
 }
 
 /**
+ * @description: 乐谱生成带节拍器的音频
+ */
+export const musicSheetAddMix = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheet/addMix',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
  * @description: 乐谱删除
  */
 export const musicSheetRemove = (params: object) => {

+ 16 - 0
src/views/music-library/music-sheet/component/music-list.tsx

@@ -42,6 +42,7 @@ import styles from './music-list.module.less'
 import MusicCreateImg from '../modal/music-create-img'
 import TheTooltip from '@components/TheTooltip'
 import { HelpCircleOutline } from '@vicons/ionicons5'
+import MusiceBeatTime from "../modal/musiceBeatTime"
 
 export default defineComponent({
   name: 'music-list',
@@ -87,6 +88,7 @@ export default defineComponent({
       userIdDisable: true, // 所属人
       userIdData: [] as any, // 所属人数据列表
       productOpen: false,
+      beatTimeOpen: false,
       productItem: {} as any
     })
 
@@ -304,6 +306,17 @@ export default defineComponent({
                   type="primary"
                   size="small"
                   text
+                  onClick={() => {
+                    state.productItem = row
+                    state.beatTimeOpen = true
+                  }}
+                >
+                  生成节拍器音频
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
                   disabled={!!row.status}
                   onClick={() => onRmove(row)}
                   v-auth="musicSheet/remove1753457445635645442"
@@ -844,6 +857,9 @@ export default defineComponent({
             }}
           />
         </NModal>
+        {
+          state.beatTimeOpen && <MusiceBeatTime id={ state.productItem.id } onClose={()=>{state.beatTimeOpen = false}}></MusiceBeatTime>
+        }
       </div>
     )
   }

+ 204 - 0
src/views/music-library/music-sheet/crunker/crunker.ts

@@ -0,0 +1,204 @@
+interface CrunkerConstructorOptions {
+   sampleRate: number
+   concurrentNetworkRequests: number
+}
+
+type CrunkerInputTypes = string | File | Blob
+
+export default class Crunker {
+   private readonly _sampleRate: number
+   private readonly _concurrentNetworkRequests: number
+   private readonly _context: AudioContext
+
+   constructor({ sampleRate, concurrentNetworkRequests = 200 }: Partial<CrunkerConstructorOptions> = {}) {
+      this._context = this._createContext(sampleRate)
+      sampleRate ||= this._context.sampleRate
+      this._sampleRate = sampleRate
+      this._concurrentNetworkRequests = concurrentNetworkRequests
+   }
+   private _createContext(sampleRate = 44_100): AudioContext {
+      window.AudioContext = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext
+      return new AudioContext({ sampleRate })
+   }
+   /**
+    *转换url等类型为buffer
+    */
+   async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
+      const buffers: AudioBuffer[] = []
+      const groups = Math.ceil(filepaths.length / this._concurrentNetworkRequests)
+      for (let i = 0; i < groups; i++) {
+         const group = filepaths.slice(i * this._concurrentNetworkRequests, (i + 1) * this._concurrentNetworkRequests)
+         buffers.push(...(await this._fetchAudio(...group)))
+      }
+      return buffers
+   }
+   private async _fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
+      return await Promise.all(
+         filepaths.map(async filepath => {
+            let buffer: ArrayBuffer
+            if (filepath instanceof File || filepath instanceof Blob) {
+               buffer = await filepath.arrayBuffer()
+            } else {
+               buffer = await fetch(filepath).then(response => {
+                  if (response.headers.has("Content-Type") && !response.headers.get("Content-Type")!.includes("audio/")) {
+                     console.warn(
+                        `Crunker: Attempted to fetch an audio file, but its MIME type is \`${
+                           response.headers.get("Content-Type")!.split(";")[0]
+                        }\`. We'll try and continue anyway. (file: "${filepath}")`
+                     )
+                  }
+                  return response.arrayBuffer()
+               })
+            }
+            /* 这里有个坑 safa浏览器老一点的版本不支持decodeAudioData返回promise 所以用这种老式写法 */
+            return await new Promise((res, rej) => {
+               this._context.decodeAudioData(
+                  buffer,
+                  buffer => {
+                     res(buffer)
+                  },
+                  err => {
+                     rej(err)
+                  }
+               )
+            })
+         })
+      )
+   }
+   /**
+    * 根据时间合并音频
+    */
+   mergeAudioBuffers(buffers: AudioBuffer[], times: number[]): AudioBuffer {
+      if (buffers.length !== times.length) {
+         throw new Error("buffer数量和times数量必须一致")
+      }
+      const output = this._context.createBuffer(this._maxNumberOfChannels(buffers), this._sampleRate * this._maxDuration(buffers), this._sampleRate)
+      buffers.forEach((buffer, index) => {
+         for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {
+            const outputData = output.getChannelData(channelNumber)
+            const bufferData = buffer.getChannelData(channelNumber)
+            const offsetNum = Math.round(times[index] * this._sampleRate) //时间偏差
+            for (let i = buffer.getChannelData(channelNumber).length - 1; i >= 0; i--) {
+               outputData[i + offsetNum] += bufferData[i]
+               // 当合并大于1或者小于-1的时候可能会爆音  所以这里取最大值和最小值
+               if (outputData[i + offsetNum] > 1) {
+                  outputData[i + offsetNum] = 1
+               }
+               if (outputData[i + offsetNum] < -1) {
+                  outputData[i + offsetNum] = -1
+               }
+            }
+            output.getChannelData(channelNumber).set(outputData)
+         }
+      })
+
+      return output
+   }
+   /**
+    * 根据buffer导出audio标签
+    */
+   exportAudioElement(buffer: AudioBuffer, type = "audio/mp3"): HTMLAudioElement {
+      const recorded = this._interleave(buffer)
+      const dataview = this._writeHeaders(recorded, buffer.numberOfChannels, buffer.sampleRate)
+      const audioBlob = new Blob([dataview], { type })
+      return this._renderAudioElement(audioBlob)
+   }
+   /**
+    * 计算音频前面的空白
+    */
+   calculateSilenceDuration(buffer: AudioBuffer) {
+      const threshold = 0.01 // 静音阈值,低于此值的部分认为是静音
+      const sampleRate = buffer.sampleRate
+      const channelData = buffer.getChannelData(0) // 只处理单声道数据
+      let silenceDuration = 0
+      for (let i = 0; i < channelData.length; i++) {
+         if (Math.abs(channelData[i]) > threshold) {
+            break
+         }
+         silenceDuration++
+      }
+      // 将样本数转换为秒
+      silenceDuration = silenceDuration / sampleRate
+      return silenceDuration
+   }
+   /**
+    * buffer 转为 blob
+    */
+   audioBuffToBlob(buffer: AudioBuffer, type = "audio/mp3") {
+      const recorded = this._interleave(buffer)
+      const dataview = this._writeHeaders(recorded, buffer.numberOfChannels, buffer.sampleRate)
+      return new Blob([dataview], { type })
+   }
+   private _maxNumberOfChannels(buffers: AudioBuffer[]): number {
+      return Math.max(...buffers.map(buffer => buffer.numberOfChannels))
+   }
+   private _maxDuration(buffers: AudioBuffer[]): number {
+      return Math.max(...buffers.map(buffer => buffer.duration))
+   }
+   private _interleave(input: AudioBuffer): Float32Array {
+      if (input.numberOfChannels === 1) {
+         return input.getChannelData(0)
+      }
+      const channels = []
+      for (let i = 0; i < input.numberOfChannels; i++) {
+         channels.push(input.getChannelData(i))
+      }
+      const length = channels.reduce((prev, channelData) => prev + channelData.length, 0)
+      const result = new Float32Array(length)
+      let index = 0
+      let inputIndex = 0
+      while (index < length) {
+         channels.forEach(channelData => {
+            result[index++] = channelData[inputIndex]
+         })
+         inputIndex++
+      }
+      return result
+   }
+   private _renderAudioElement(blob: Blob): HTMLAudioElement {
+      const audio = document.createElement("audio")
+      audio.src = this._renderURL(blob)
+      audio.load()
+      return audio
+   }
+   private _renderURL(blob: Blob): string {
+      return (window.URL || window.webkitURL).createObjectURL(blob)
+   }
+   private _writeHeaders(buffer: Float32Array, numOfChannels: number, sampleRate: number): DataView {
+      const bitDepth = 16
+      const bytesPerSample = bitDepth / 8
+      const sampleSize = numOfChannels * bytesPerSample
+      const fileHeaderSize = 8
+      const chunkHeaderSize = 36
+      const chunkDataSize = buffer.length * bytesPerSample
+      const chunkTotalSize = chunkHeaderSize + chunkDataSize
+      const arrayBuffer = new ArrayBuffer(fileHeaderSize + chunkTotalSize)
+      const view = new DataView(arrayBuffer)
+      this._writeString(view, 0, "RIFF")
+      view.setUint32(4, chunkTotalSize, true)
+      this._writeString(view, 8, "WAVE")
+      this._writeString(view, 12, "fmt ")
+      view.setUint32(16, 16, true)
+      view.setUint16(20, 1, true)
+      view.setUint16(22, numOfChannels, true)
+      view.setUint32(24, sampleRate, true)
+      view.setUint32(28, sampleRate * sampleSize, true)
+      view.setUint16(32, sampleSize, true)
+      view.setUint16(34, bitDepth, true)
+      this._writeString(view, 36, "data")
+      view.setUint32(40, chunkDataSize, true)
+      return this._floatTo16BitPCM(view, buffer, fileHeaderSize + chunkHeaderSize)
+   }
+   private _floatTo16BitPCM(dataview: DataView, buffer: Float32Array, offset: number): DataView {
+      for (let i = 0; i < buffer.length; i++, offset += 2) {
+         const tmp = Math.max(-1, Math.min(1, buffer[i]))
+         dataview.setInt16(offset, tmp < 0 ? tmp * 0x8000 : tmp * 0x7fff, true)
+      }
+      return dataview
+   }
+   private _writeString(dataview: DataView, offset: number, header: string): void {
+      for (let i = 0; i < header.length; i++) {
+         dataview.setUint8(offset + i, header.charCodeAt(i))
+      }
+   }
+}

+ 139 - 0
src/views/music-library/music-sheet/crunker/index.ts

@@ -0,0 +1,139 @@
+/**
+ * 音频合成节拍器
+ */
+import Crunker from './crunker'
+import tickMp3 from './tick.mp3'
+import tockMp3 from './tock.mp3'
+import { getUploadSign, onOnlyFileUpload } from '@/utils/oss-file-upload'
+import { ref } from 'vue'
+
+const crunker = new Crunker()
+
+type musicSheetType = {
+  audioFileUrl: string
+  audioBeatMixUrl: null | string
+  solmizationFileUrl: null | string
+  solmizationBeatUrl: null | string
+}
+
+type taskAudioType = {
+  obj: musicSheetType
+  type: 'audioFileUrl' | 'solmizationFileUrl'
+  audioBuff?: AudioBuffer
+}[]
+
+// 节拍器数据
+export const beatState = {
+  times: [] as number[][],
+  totalIndex: ref(0), // 总共需要处理的音频个数
+  currentIndex: ref(0) // 当前处理了多少条数据
+}
+
+// 节拍器音源
+let tickMp3Buff: null | AudioBuffer = null
+let tockMp3Buff: null | AudioBuffer = null
+
+export default async function audioMergeBeats({
+  musicSheetAccompanimentList,
+  musicSheetSoundList
+}: {
+  musicSheetAccompanimentList: musicSheetType[]
+  musicSheetSoundList: musicSheetType[]
+}) {
+  if (!beatState.times.length) return
+  try {
+    if (musicSheetSoundList.length + musicSheetAccompanimentList.length > 0) {
+      // 扁平化数据 生成任务队列
+      const taskAudio: taskAudioType = []
+      ;[...musicSheetSoundList, ...musicSheetAccompanimentList].map((item) => {
+        taskAudio.push({
+          obj: item,
+          type: 'audioFileUrl'
+        })
+        item.solmizationFileUrl && // 有唱名加上唱名
+          taskAudio.push({
+            obj: item,
+            type: 'solmizationFileUrl'
+          })
+      })
+      beatState.totalIndex.value = taskAudio.length
+      /* 加载节拍器 */
+      if (!tickMp3Buff || !tockMp3Buff) {
+        const [tickMp3Bf, tockMp3Bf] = await crunker.fetchAudio(tickMp3, tockMp3)
+        tickMp3Buff = tickMp3Bf
+        tockMp3Buff = tockMp3Bf
+      }
+      /* 加上所有的音频文件 */
+      await Promise.all(
+        taskAudio.map(async (item) => {
+          const [audioBuff] = await crunker.fetchAudio(item.obj[item.type]!)
+          item.audioBuff = audioBuff
+        })
+      )
+      /* 异步上传 */
+      await new Promise((res) => {
+        /* 合成音源 */
+        taskAudio.map(async (item) => {
+          const audioBlob = mergeBeats(item.audioBuff!)
+          const url = await uploadFile(audioBlob)
+          item.obj[item.type == 'audioFileUrl' ? 'audioBeatMixUrl' : 'solmizationBeatUrl'] = url
+          beatState.currentIndex.value++
+          if (beatState.currentIndex.value >= beatState.totalIndex.value) {
+            res(null)
+          }
+        })
+      })
+    }
+  } catch (err) {
+    console.log('处理音频合成上传失败', err)
+  }
+  // 清空数据
+  beatState.currentIndex.value = 0
+  beatState.totalIndex.value = 0
+  beatState.times = []
+}
+
+// 根据buffer合成音源返回blob
+function mergeBeats(audioBuff: AudioBuffer) {
+  // 计算音频空白时间
+  const silenceDuration = crunker.calculateSilenceDuration(audioBuff)
+  const beats: AudioBuffer[] = []
+  const currentTimes: number[] = []
+  beatState.times.map((items) => {
+    items.map((time, index) => {
+      beats.push(index === 0 ? tickMp3Buff! : tockMp3Buff!)
+      currentTimes.push(time + silenceDuration)
+    })
+  })
+  //合并
+  const mergeAudioBuff = crunker.mergeAudioBuffers([audioBuff, ...beats], [0, ...currentTimes])
+  //转为 blob
+  return crunker.audioBuffToBlob(mergeAudioBuff)
+}
+
+/**
+ * 上传文件
+ */
+async function uploadFile(audioBlob: Blob) {
+  const filename = `${new Date().getTime()}.mp3`
+  const { data } = await getUploadSign({
+    filename,
+    bucketName: 'cloud-coach',
+    postData: {
+      filename,
+      acl: 'public-read',
+      key: filename,
+      unknowValueField: []
+    }
+  })
+  const url = await onOnlyFileUpload('', {
+    KSSAccessKeyId: data.KSSAccessKeyId,
+    acl: 'public-read',
+    file: audioBlob,
+    key: filename,
+    name: filename,
+    policy: data.policy,
+    signature: data.signature
+  })
+  return url
+}

BIN
src/views/music-library/music-sheet/crunker/tick.mp3


BIN
src/views/music-library/music-sheet/crunker/tock.mp3


+ 185 - 203
src/views/music-library/music-sheet/modal/music-operationV2.tsx

@@ -400,18 +400,6 @@ export default defineComponent({
                 audioPlayType: 'PLAY'
               })
             }
-
-            if (musicSheetType == 'CONCERT' && forms.musicSheetSoundList_YY.length > 0) {
-              audioPlayTypes.push("PLAY")
-              forms.musicSheetSoundList_YY.forEach((musicSheetSound: any) => {
-                if (musicSheetSound.track && musicSheetSound.audioFileUrl) {
-                  musicSheetSoundList.push({
-                    ...musicSheetSound,
-                    musicSheetId: props.data.id,
-                  })
-                }
-              })
-            }
           } else {
             if (musicSheetType == 'SINGLE' && forms.musicSheetSoundList_YZ.length > 0) {
               audioPlayTypes.push("PLAY")
@@ -424,6 +412,7 @@ export default defineComponent({
                 }
               })
             }
+          }
 
             if (musicSheetType == 'CONCERT' && forms.musicSheetSoundList_YY.length > 0) {
               audioPlayTypes.push("PLAY")
@@ -436,7 +425,6 @@ export default defineComponent({
                 }
               })
             }
-          }
 
           if (state.bSongFile) {
             forms.musicSheetAccompanimentList.push({
@@ -1366,142 +1354,138 @@ export default defineComponent({
                 </NFormItemGi>
               </NGrid>
 
-              {forms.musicSheetType == 'SINGLE' && (
-                  <>
-                    <NAlert showIcon={false} style={{marginBottom: '12px'}}>
-                      演唱文件
-                    </NAlert>
-                    <NGrid cols={2}>
-                      <NFormItemGi
-                          label="是否播放节拍器"
-                          path="isPlaySingBeat"
-                          rule={[
-                            {
-                              required: true,
-                              message: '请选择是否播放节拍器'
-                            }
-                          ]}
-                      >
-                        <NRadioGroup v-model:value={forms.isPlaySingBeat}>
-                          <NRadio value={true}>是</NRadio>
-                          <NRadio value={false}>否</NRadio>
-                        </NRadioGroup>
-                      </NFormItemGi>
-                      {forms.isPlaySingBeat && (
-                          <NFormItemGi
-                              label="播放方式"
-                              path="isUseSingSystemBeat"
-                              rule={[
-                                {
-                                  required: true,
-                                  message: '请选择播放方式'
-                                }
-                              ]}
-                          >
-                            <NRadioGroup v-model:value={forms.isUseSingSystemBeat}>
-                              <NRadio value={true}>系统节拍器</NRadio>
-                              <NRadio value={false}>MP3节拍器</NRadio>
-                            </NRadioGroup>
-                          </NFormItemGi>
-                      )}
-                    </NGrid>
-                    <NGrid cols={2}>
-                      <NFormItemGi
-                          label="重复节拍时长"
-                          path="repeatedBeatsToSing"
-                          rule={[
-                            {
-                              required: false,
-                              message: '请选择是否重复节拍时长'
-                            }
-                          ]}
-                      >
-                        <NRadioGroup v-model:value={forms.repeatedBeatsToSing}>
-                          <NRadio value={true}>是</NRadio>
-                          <NRadio value={false}>否</NRadio>
-                        </NRadioGroup>
-                      </NFormItemGi>
-                    </NGrid>
-                    <NGrid cols={2}>
-                      <NFormItemGi
-                          label="上传范唱"
-                          path="fSongFile"
-                          rule={[
-                            {
-                              required: false,
-                              message: '请选择上传范唱',
-                              trigger: ['change', 'input']
-                            }
-                          ]}
-                      >
-                        <UploadFile
-                            desc={'上传范唱'}
-                            disabled={state.previewMode}
-                            size={10}
-                            key={'xmlFileUrl'}
-                            v-model:fileList={state.fSongFile}
-                            tips="仅支持上传.mp3格式文件"
-                            listType="image"
-                            accept=".mp3"
-                            bucketName="cloud-coach"
-                            text="点击上传范唱文件"
-                            onRemove={() => {
-                            }}
-                        />
-                      </NFormItemGi>
-                      <NFormItemGi
-                          label="上传伴唱"
-                          path="bSongFile"
-                          rule={[
-                            {
-                              required: false,
-                              message: '请选择上传.MID格式文件'
-                            }
-                          ]}
-                      >
-                        <UploadFile
-                            desc={'上传伴唱'}
-                            disabled={state.previewMode}
-                            size={10}
-                            v-model:fileList={state.bSongFile}
-                            tips="仅支持上传.mp3格式文件"
-                            listType="image"
-                            accept=".mp3"
-                            bucketName="cloud-coach"
-                            text="点击上传伴唱文件"
-                        />
-                      </NFormItemGi>
-                    </NGrid>
-                    <NGrid cols={2}>
-                      <NFormItemGi
-                          label="上传唱名"
-                          path="solmizationFileUrl"
-                          rule={[
-                            {
-                              required: false,
-                              message: '请选择上传唱名',
-                              trigger: ['change', 'input']
-                            }
-                          ]}
-                      >
-                        <UploadFile
-                            desc={'上传范唱'}
-                            disabled={state.previewMode}
-                            size={10}
-                            key={'xmlFileUrl'}
-                            v-model:fileList={forms.solmizationFileUrl}
-                            tips="仅支持上传.mp3格式文件"
-                            listType="image"
-                            accept=".mp3"
-                            bucketName="cloud-coach"
-                            text="点击上传范唱文件"
-                            onRemove={() => {
-                            }}
-                        />
-                      </NFormItemGi>
-                    </NGrid>
-                  </>
-              )}
+              <NAlert showIcon={false} style={{marginBottom: '12px'}}>
+                演唱文件
+              </NAlert>
+              <NGrid cols={2}>
+                <NFormItemGi
+                    label="是否播放节拍器"
+                    path="isPlaySingBeat"
+                    rule={[
+                      {
+                        required: true,
+                        message: '请选择是否播放节拍器'
+                      }
+                    ]}
+                >
+                  <NRadioGroup v-model:value={forms.isPlaySingBeat}>
+                    <NRadio value={true}>是</NRadio>
+                    <NRadio value={false}>否</NRadio>
+                  </NRadioGroup>
+                </NFormItemGi>
+                {forms.isPlaySingBeat && (
+                    <NFormItemGi
+                        label="播放方式"
+                        path="isUseSingSystemBeat"
+                        rule={[
+                          {
+                            required: true,
+                            message: '请选择播放方式'
+                          }
+                        ]}
+                    >
+                      <NRadioGroup v-model:value={forms.isUseSingSystemBeat}>
+                        <NRadio value={true}>系统节拍器</NRadio>
+                        <NRadio value={false}>MP3节拍器</NRadio>
+                      </NRadioGroup>
+                    </NFormItemGi>
+                )}
+              </NGrid>
+              <NGrid cols={2}>
+                <NFormItemGi
+                    label="重复节拍时长"
+                    path="repeatedBeatsToSing"
+                    rule={[
+                      {
+                        required: false,
+                        message: '请选择是否重复节拍时长'
+                      }
+                    ]}
+                >
+                  <NRadioGroup v-model:value={forms.repeatedBeatsToSing}>
+                    <NRadio value={true}>是</NRadio>
+                    <NRadio value={false}>否</NRadio>
+                  </NRadioGroup>
+                </NFormItemGi>
+              </NGrid>
+              <NGrid cols={2}>
+                <NFormItemGi
+                    label="上传范唱"
+                    path="fSongFile"
+                    rule={[
+                      {
+                        required: false,
+                        message: '请选择上传范唱',
+                        trigger: ['change', 'input']
+                      }
+                    ]}
+                >
+                  <UploadFile
+                      desc={'上传范唱'}
+                      disabled={state.previewMode}
+                      size={10}
+                      key={'xmlFileUrl'}
+                      v-model:fileList={state.fSongFile}
+                      tips="仅支持上传.mp3格式文件"
+                      listType="image"
+                      accept=".mp3"
+                      bucketName="cloud-coach"
+                      text="点击上传范唱文件"
+                      onRemove={() => {
+                      }}
+                  />
+                </NFormItemGi>
+                <NFormItemGi
+                    label="上传伴唱"
+                    path="bSongFile"
+                    rule={[
+                      {
+                        required: false,
+                        message: '请选择上传.MID格式文件'
+                      }
+                    ]}
+                >
+                  <UploadFile
+                      desc={'上传伴唱'}
+                      disabled={state.previewMode}
+                      size={10}
+                      v-model:fileList={state.bSongFile}
+                      tips="仅支持上传.mp3格式文件"
+                      listType="image"
+                      accept=".mp3"
+                      bucketName="cloud-coach"
+                      text="点击上传伴唱文件"
+                  />
+                </NFormItemGi>
+              </NGrid>
+              <NGrid cols={2}>
+                <NFormItemGi
+                    label="上传唱名"
+                    path="solmizationFileUrl"
+                    rule={[
+                      {
+                        required: false,
+                        message: '请选择上传唱名',
+                        trigger: ['change', 'input']
+                      }
+                    ]}
+                >
+                  <UploadFile
+                      desc={'上传范唱'}
+                      disabled={state.previewMode}
+                      size={10}
+                      key={'xmlFileUrl'}
+                      v-model:fileList={forms.solmizationFileUrl}
+                      tips="仅支持上传.mp3格式文件"
+                      listType="image"
+                      accept=".mp3"
+                      bucketName="cloud-coach"
+                      text="点击上传范唱文件"
+                      onRemove={() => {
+                      }}
+                  />
+                </NFormItemGi>
+              </NGrid>
               <NAlert showIcon={false} style={{marginBottom: '12px'}}>
                 演奏文件
               </NAlert>
@@ -1669,7 +1653,7 @@ export default defineComponent({
                       />
                     </NFormItemGi>
                 )}
-                {forms.isAllSubject && (
+                {forms.isAllSubject && forms.musicSheetType == 'SINGLE' && (
                     <NFormItemGi
                         label="上传原音"
                         path="musicSheetSoundList_all_subject"
@@ -1754,62 +1738,60 @@ export default defineComponent({
                     </NFormItem>
                   </>
               )}
-              {!forms.isAllSubject && (
-                  <NGrid cols={1}>
-                    <NFormItemGi
-                        label={`${forms.musicSheetType === 'SINGLE' ? '页面渲染声轨' : '用户可切换声轨'}`}
-                        path="multiTracksSelection"
-                        rule={[
-                          {
-                            required: true,
-                            message: `请选择${
-                                forms.musicSheetType === 'SINGLE' ? '页面渲染声轨' : '用户可切换声轨'
-                            }`,
-                            trigger: 'change',
-                            type: 'array'
-                          }
-                        ]}
-                    >
-                      <NGrid style="padding-top: 4px;">
-                        <NGi span={24}>
-                          <NRadioGroup
-                              v-model:value={state.multiTracks}
-                              onUpdateValue={(value) => {
-                                checkMultiTracks(value)
-                              }}
+              <NGrid cols={1}>
+                <NFormItemGi
+                    label={`${forms.musicSheetType === 'SINGLE' ? '页面渲染声轨' : '用户可切换声轨'}`}
+                    path="multiTracksSelection"
+                    rule={[
+                      {
+                        required: true,
+                        message: `请选择${
+                            forms.musicSheetType === 'SINGLE' ? '页面渲染声轨' : '用户可切换声轨'
+                        }`,
+                        trigger: 'change',
+                        type: 'array'
+                      }
+                    ]}
+                >
+                  <NGrid style="padding-top: 4px;">
+                    <NGi span={24}>
+                      <NRadioGroup
+                          v-model:value={state.multiTracks}
+                          onUpdateValue={(value) => {
+                            checkMultiTracks(value)
+                          }}
+                      >
+                        <NRadio value={'all'}>全选</NRadio>
+                        <NRadio value={'allUncheck'}>重置</NRadio>
+                        <NRadio value={'invert'}>反选</NRadio>
+                      </NRadioGroup>
+                    </NGi>
+                    {state.partListNames && state.partListNames.length > 0 && (
+                        <NGi span={24} style={'margin-top:5px'}>
+                          <NFormItemGi
+                              label=""
+                              path="multiTracksSelection"
+                              rule={[
+                                {
+                                  required: false
+                                }
+                              ]}
                           >
-                            <NRadio value={'all'}>全选</NRadio>
-                            <NRadio value={'allUncheck'}>重置</NRadio>
-                            <NRadio value={'invert'}>反选</NRadio>
-                          </NRadioGroup>
+                            <NCheckboxGroup v-model:value={forms.multiTracksSelection}>
+                              <NGrid yGap={2} cols={4}>
+                                {state.partListNames.map((item: any) => (
+                                    <NGi>
+                                      <NCheckbox value={item.value} label={item.label}/>
+                                    </NGi>
+                                ))}
+                              </NGrid>
+                            </NCheckboxGroup>
+                          </NFormItemGi>
                         </NGi>
-                        {state.partListNames && state.partListNames.length > 0 && (
-                            <NGi span={24} style={'margin-top:5px'}>
-                              <NFormItemGi
-                                  label=""
-                                  path="multiTracksSelection"
-                                  rule={[
-                                    {
-                                      required: false
-                                    }
-                                  ]}
-                              >
-                                <NCheckboxGroup v-model:value={forms.multiTracksSelection}>
-                                  <NGrid yGap={2} cols={4}>
-                                    {state.partListNames.map((item: any) => (
-                                        <NGi>
-                                          <NCheckbox value={item.value} label={item.label}/>
-                                        </NGi>
-                                    ))}
-                                  </NGrid>
-                                </NCheckboxGroup>
-                              </NFormItemGi>
-                            </NGi>
-                        )}
-                      </NGrid>
-                    </NFormItemGi>
+                    )}
                   </NGrid>
-              )}
+                </NFormItemGi>
+              </NGrid>
 
               {/*独奏*/}
               {forms.musicSheetType == 'SINGLE' && forms.playMode === 'MP3' && !forms.isAllSubject && forms.musicSheetSoundList_YZ.map((item: any, index: any) => {

+ 20 - 0
src/views/music-library/music-sheet/modal/musiceBeatTime/index.module.less

@@ -0,0 +1,20 @@
+.musiceBeatTime {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 10000;
+  background-color: rgba(255, 255, 255, 0.9);
+  .tit{
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 20px;
+    line-height: 20px;
+  }
+  .iframe{
+    display: none;
+  }
+}

+ 69 - 0
src/views/music-library/music-sheet/modal/musiceBeatTime/index.tsx

@@ -0,0 +1,69 @@
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+import styles from "./index.module.less"
+import { musicSheetAddMix } from '../../../api'
+import { useMessage } from 'naive-ui'
+
+export default defineComponent({
+  name: 'musiceBeatTime',
+  props: {
+    id: {
+      type: String
+    }
+  },
+  emits: ['close'],
+  setup(props, {emit}) {
+    const message = useMessage()
+    onMounted(() => {
+      window.addEventListener('message', handleBeatRes)
+    })
+
+    onUnmounted(() => {
+      window.removeEventListener('message', handleBeatRes)
+    })
+    function handleBeatRes(res: MessageEvent) {
+      const data = res.data
+      if (data?.api === 'webApi_beatTimes') {
+        iframeShow.value = false
+        handleBeatTimes(data)
+      }
+    }
+    async function handleBeatTimes(data:any){
+      try {
+        const { beatTime, singBeatTime} = JSON.parse(data.data)
+        await musicSheetAddMix({
+          id: props.id,
+          playTimeList: beatTime,
+          singTimeList: singBeatTime
+        })
+        message.success('生成成功')
+        emit("close")
+      }catch (err){
+        console.log('🚀 ~ 音频合成失败', err)
+        message.error('生成失败')
+        emit("close")
+      }
+    }
+    const iframeShow = ref(true)
+    const userStore = useUserStore()
+    const token = userStore.getToken
+    const apiUrls = {
+      'dev': 'https://dev.kt.colexiu.com',
+      'test': 'https://test.lexiaoya.cn',
+      'online': 'https://mec.colexiu.com'
+    }
+    const environment = location.origin.includes('//dev') ? 'dev' : location.origin.includes('//test') ? 'test' : location.origin.includes('//mec.colexiu') ? 'online' : 'dev'
+    const apiUrl = apiUrls[environment]
+    const prefix = /(localhost|192)/.test(location.host) ? 'https://dev.kt.colexiu.com/' : apiUrl
+    let src = prefix + `/instrument/?_t=${Date.now()}&id=${props.id}&Authorization=${token}&isCbs=true&isbeatTimes=true&musicRenderType=staff`
+    //let src = "http://192.168.3.122:3000/instrument.html" + `?_t=${Date.now()}&id=${props.id}&Authorization=${token}&isCbs=true&isbeatTimes=true&musicRenderType=staff`
+    return () => (
+      <div class={styles.musiceBeatTime}>
+        <div class={styles.tit}>节拍器音频生成中</div>
+        {
+          iframeShow.value && <iframe class={styles.iframe} width={'667px'} height={'375px'} frameborder="0" src={src}></iframe>
+        }
+      </div>
+    )
+  }
+})

+ 2 - 0
src/views/music-library/project-music-sheet/module/kt/addMusic.tsx

@@ -295,6 +295,7 @@ export default defineComponent({
                             labelField: 'name',
                             childrenField: 'children',
                             placeholderField: '请选择乐谱教材',
+                            checkStrategy:'child',
                             options: state.musicSheetCategories
                           })
                         ]
@@ -335,6 +336,7 @@ export default defineComponent({
               labelField="name"
               children-field="children"
               placeholder="请选择曲目分类"
+              checkStrategy={"child"}
               value={row.projectMusicCategoryId}
               options={state.musicSheetCategories}
               onUpdateValue={(value: any) => {

+ 1 - 0
src/views/music-library/project-music-sheet/module/kt/updateMusic.tsx

@@ -97,6 +97,7 @@ export default defineComponent({
                     placeholder="请选择乐谱教材"
                     value={forms.musicSheetCategoryId}
                     options={state.musicSheetCategories}
+                    checkStrategy={"child"}
                     onUpdateValue={(value: any) => {
                       forms.musicSheetCategoryId = value
                     }}

+ 1 - 1
vite.config.ts

@@ -19,7 +19,7 @@ function pathResolve(dir: string) {
 }
 
 // const proxyUrl = 'https://dev.lexiaoya.cn'
-// const proxyUrl = 'http://127.0.0.1:7293/'
+//const proxyUrl = 'http://127.0.0.1:7293/'
 // const proxyUrl = 'https://resource.colexiu.com/'
 const proxyUrl = 'https://dev.resource.colexiu.com'
 // https://test.resource.colexiu.com/