浏览代码

保存图片,和wav

liushengqiang 1 年之前
父节点
当前提交
0cf6123e0f

+ 57 - 0
src/helpers/parseABC.ts

@@ -21,3 +21,60 @@ export const getMinNoteUnit = (abcNotation: string) => {
 
 	return minNoteUnit;
 };
+
+// Convert an AudioBuffer to a Blob using WAVE representation
+export const bufferToWave = (audioBuffer: AudioBuffer) => {
+	var numOfChan = audioBuffer.numberOfChannels;
+	var length = audioBuffer.length * numOfChan * 2 + 44;
+	var buffer = new ArrayBuffer(length);
+	var view = new DataView(buffer);
+	var channels = [];
+	var i;
+	var sample;
+	var offset = 0;
+	var pos = 0;
+
+	// write WAVE header
+	setUint32(0x46464952);                         // "RIFF"
+	setUint32(length - 8);                         // file length - 8
+	setUint32(0x45564157);                         // "WAVE"
+
+	setUint32(0x20746d66);                         // "fmt " chunk
+	setUint32(16);                                 // length = 16
+	setUint16(1);                                  // PCM (uncompressed)
+	setUint16(numOfChan);
+	setUint32(audioBuffer.sampleRate);
+	setUint32(audioBuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
+	setUint16(numOfChan * 2);                      // block-align
+	setUint16(16);                                 // 16-bit (hardcoded in this demo)
+
+	setUint32(0x61746164);                         // "data" - chunk
+	setUint32(length - pos - 4);                   // chunk length
+
+	// write interleaved data
+	for(i = 0; i < numOfChan; i++)
+		channels.push(audioBuffer.getChannelData(i));
+
+	while(pos < length) {
+		for(i = 0; i < channels.length; i++) {             // interleave channels
+			sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp
+			sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // scale to 16-bit signed int
+			view.setInt16(pos, sample, true);          // write 16-bit sample
+			pos += 2;
+		}
+		offset++; // next source sample
+	}
+
+	// create Blob
+	return new Blob([buffer], {type: "audio/wav"});
+
+	function setUint16(data: any) {
+		view.setUint16(pos, data, true);
+		pos += 2;
+	}
+
+	function setUint32(data: any) {
+		view.setUint32(pos, data, true);
+		pos += 4;
+	}
+}

+ 5 - 0
src/pc/create/component/the-create/index.tsx

@@ -30,6 +30,7 @@ import TheSpeed from "/src/pc/home/component/the-speed";
 import { api_musicSheetCreationSave, api_subjectList } from "/src/pc/api";
 import { initMusic } from "/src/pc/home";
 import { encodeUrl } from "/src/utils";
+import { getQuery } from "/src/utils/queryString";
 const instruments = [
 	{
 		label: "竖笛",
@@ -104,6 +105,7 @@ export default defineComponent({
 			speed: 80,
 			measure: 30,
 			subjectCode: "recorder",
+			subjectId: 4,
 		});
 
 		const handleCreate = async () => {
@@ -122,6 +124,7 @@ export default defineComponent({
 				key: froms.key.value,
 				subjectCode: froms.subjectCode,
 				measure: froms.measure,
+				subjectId: froms.subjectId
 			});
 			emit("create");
 			formsOptions.loading = false;
@@ -129,6 +132,7 @@ export default defineComponent({
 
 		const handleOpenNotaion = (data: any) => {
 			const url = `${location.origin}/notation/#/?v=1.0.3&config=${encodeUrl(data)}`;
+			// console.log("🚀 ~ url:", url);
 			window.parent.postMessage(
 				{
 					api: "notation_open",
@@ -167,6 +171,7 @@ export default defineComponent({
 									class={[styles.item, froms.subjectCode === item.key && styles.itemActive]}
 									onClick={() => {
 										froms.subjectCode = item.key;
+										froms.subjectId = item.id;
 									}}
 								>
 									<div class={styles.itemImg}>

+ 97 - 87
src/pc/home/component/file-btn/index.tsx

@@ -1,5 +1,5 @@
-import { NDropdown } from "naive-ui";
-import { defineComponent } from "vue";
+import { NDropdown, NSpin } from "naive-ui";
+import { computed, defineComponent, ref, watch } from "vue";
 import styles from "./index.module.less";
 import { getImage } from "../../images";
 import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";
@@ -20,95 +20,105 @@ export type IFileBtnType =
 export default defineComponent({
 	name: "FileBtn",
 	emits: ["select"],
+	props: {
+		saveLoading: {
+			type: Boolean,
+			default: false,
+		},
+	},
 	setup(props, { emit }) {
-		const options: DropdownMixedOption[] = [
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_4.png")} />
-						<span>新建曲谱</span>
-					</div>
-				),
-				key: "newMusic",
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
-						<span>保存</span>
-					</div>
-				),
-				key: "save",
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
-						<span>导入</span>
-					</div>
-				),
-				key: "import",
-				// disabled: true,
-				children: [
-					{
-						label: "XML",
-						key: "xml",
-						// disabled: true,
-					},
-				],
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_1.png")} />
-						<span>上传到我的资源</span>
-					</div>
-				),
-				key: "upload",
-				disabled: true,
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_2.png")} />
-						<span>导出</span>
-					</div>
-				),
-				key: "export",
-				children: [
-					{
-						label: "XML",
-						key: "down-xml",
-					},
-					{
-						label: "PNG",
-						key: "png",
-					},
-					{
-						label: "WAV",
-						key: "wav",
-					},
-					{
-						label: "MIDI",
-						key: "midi",
-					},
-				],
-			},
-			{
-				label: () => (
-					<div class={styles.dropItem}>
-						<img class={styles.dropIcon} src={getImage("icon_26_3.png")} />
-						<span>打印</span>
-					</div>
-				),
-				key: "print",
-				disabled: true,
-			},
-		];
+		const options = computed(() => {
+			return [
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_4.png")} />
+							<span>新建曲谱</span>
+						</div>
+					),
+					key: "newMusic",
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
+							<span>保存</span>
+							{props.saveLoading && <NSpin style={{ marginLeft: "auto" }} size={14}></NSpin>}
+						</div>
+					),
+					key: "save",
+					disabled: props.saveLoading,
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_0.png")} />
+							<span>导入</span>
+						</div>
+					),
+					key: "import",
+					// disabled: true,
+					children: [
+						{
+							label: "XML",
+							key: "xml",
+							// disabled: true,
+						},
+					],
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_1.png")} />
+							<span>上传到我的资源</span>
+						</div>
+					),
+					key: "upload",
+					disabled: true,
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_2.png")} />
+							<span>导出</span>
+						</div>
+					),
+					key: "export",
+					children: [
+						{
+							label: "XML",
+							key: "down-xml",
+						},
+						{
+							label: "PNG",
+							key: "png",
+						},
+						{
+							label: "WAV",
+							key: "wav",
+						},
+						{
+							label: "MIDI",
+							key: "midi",
+						},
+					],
+				},
+				{
+					label: () => (
+						<div class={styles.dropItem}>
+							<img class={styles.dropIcon} src={getImage("icon_26_3.png")} />
+							<span>打印</span>
+						</div>
+					),
+					key: "print",
+					disabled: true,
+				},
+			];
+		});
 		return () => (
 			<NDropdown
 				class={styles.dropWrap}
-				options={options}
+				options={options.value}
 				trigger="click"
 				onSelect={(val) => {
 					console.log("🚀 ~ val:", val);

+ 125 - 71
src/pc/home/index.tsx

@@ -65,6 +65,8 @@ import { saveAs } from "file-saver";
 import qs from "query-string";
 import { useDocumentVisibility } from "@vueuse/core";
 import request from "/src/utils/request";
+import { api_uploadFile } from "/src/utils/uploadFile";
+import { bufferToWave } from "/src/helpers/parseABC";
 
 export const initMusic = (total: number): IMeasure[] => {
 	return new Array(total).fill(0).map((item, index) => {
@@ -132,10 +134,11 @@ export default defineComponent({
 			selectMearesShow: false, // 选择小节弹窗
 		});
 		const data = reactive({
+			saveLoading: false,
 			loading: true,
 			drawCount: 0,
 			isSave: true,
-			musicId: "",
+			musicId: Date.now().toString(),
 			musicName: "", // 曲谱名称
 			creator: "", // 创建者
 			subjectId: "", // 声部
@@ -1321,6 +1324,7 @@ export default defineComponent({
 				data.musicId = res.data.id || "";
 				data.musicName = res.data.name || "";
 				data.creator = res.data.creator || "";
+				data.subjectId = res.data.subjectId || "";
 				let abc = "" as any;
 				try {
 					abc = JSON.parse(res.data.creationData);
@@ -1346,56 +1350,78 @@ export default defineComponent({
 			data.loading = false;
 			return res;
 		};
+		const setSaveLoading = (tips: boolean) => {
+			data.saveLoading = true;
+			if (tips) {
+				message.loading("保存中...", { duration: 0 });
+			}
+		};
 		const handleSaveMusic = async (tips = true) => {
 			const query = getQuery();
 			abcData.abc.title = data.musicName;
 			abcData.abc.creator = data.creator;
-			if (query.id) {
-				await api_musicSheetCreationUpdate({
-					name: data.musicName,
-					creator: data.creator,
-					creationConfig: renderMeasures(abcData.abc, {
-						hiddenIndex: true,
-						showTitle: true,
-						showCreator: true,
-					}),
-					creationData: JSON.stringify(cleanDeep(abcData.abc)),
-					id: query.id,
-					subjectId: "",
-				});
-			} else {
-				const res = await api_musicSheetCreationSave({
-					name: data.musicName,
-					creator: data.creator,
-					creationConfig: renderMeasures(abcData.abc, {
-						hiddenIndex: true,
-						showTitle: true,
-						showCreator: true,
-					}),
-					creationData: JSON.stringify(cleanDeep(abcData.abc)),
-					subjectId: "",
-				});
-				if (res?.data) {
-					const hash = location.hash.split("?");
-					const qs_data = qs.parse(hash[1]);
-					qs_data.id = res.data;
-					try {
-						delete qs_data.config;
-					} catch (error) {
-						console.log("🚀 ~ error:", error);
+			setSaveLoading(tips);
+			const wavUrl = await productWav(false);
+			const pngUrl = await productPng(false);
+			console.log("🚀 ~ pngUrl:", pngUrl);
+			try {
+				if (query.id) {
+					await api_musicSheetCreationUpdate({
+						name: data.musicName,
+						creator: data.creator,
+						creationConfig: renderMeasures(abcData.abc, {
+							hiddenIndex: true,
+							showTitle: true,
+							showCreator: true,
+						}),
+						creationData: JSON.stringify(cleanDeep(abcData.abc)),
+						id: query.id,
+						subjectId: data.subjectId,
+						filePath: wavUrl,
+						coverImg: pngUrl
+					});
+				} else {
+					const res = await api_musicSheetCreationSave({
+						name: data.musicName,
+						creator: data.creator,
+						creationConfig: renderMeasures(abcData.abc, {
+							hiddenIndex: true,
+							showTitle: true,
+							showCreator: true,
+						}),
+						creationData: JSON.stringify(cleanDeep(abcData.abc)),
+						subjectId: data.subjectId,
+						filePath: wavUrl,
+						coverImg: pngUrl
+					});
+					if (res?.data) {
+						const hash = location.hash.split("?");
+						const qs_data = qs.parse(hash[1]);
+						qs_data.id = res.data;
+						try {
+							delete qs_data.config;
+						} catch (error) {
+							console.log("🚀 ~ error:", error);
+						}
+						location.hash = hash[0] + "?" + qs.stringify(qs_data);
 					}
-					location.hash = hash[0] + "?" + qs.stringify(qs_data);
 				}
+			} catch (error) {
+				console.log(error);
 			}
+
 			if (tips) {
+				message.destroyAll();
 				message.success("保存成功");
 			}
 			data.isSave = true;
+			data.saveLoading = false;
 		};
 		const hanldeInitCreate = () => {
 			const query = getQuery();
 			const abc = decodeUrl(query.config);
 			console.log("🚀 ~ abc:", abc);
+			data.subjectId = abc.subjectId || "";
 			abcData.abc.celf = abc.celf ?? "K:treble";
 			abcData.abc.key = abc.key ?? "K:C";
 			abcData.abc.meter = abc.meter ?? "M:4/4";
@@ -1533,36 +1559,53 @@ export default defineComponent({
 			handleResetRender();
 		};
 
+		const productPng = (isUrl = true) => {
+			return new Promise((resolve) => {
+				const paper = document.getElementById("exportPng");
+				if (!paper) return;
+				const abc = renderMeasures(abcData.abc, {
+					hiddenIndex: true,
+					showTitle: true,
+					showCreator: true,
+				});
+				ABCJS.renderAbc(paper, abc, abcData.abcOptions);
+				const svg: any = paper.children[0]?.cloneNode(true);
+				const svgBox = paper.getBoundingClientRect();
+				svg.setAttribute("width", `${svgBox.width * 3}`);
+				svg.setAttribute("height", `${svgBox.height * 3}`);
+				const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+				rect.setAttribute("x", "0");
+				rect.setAttribute("y", "0");
+				rect.setAttribute("width", `${svgBox.width * 10}`);
+				rect.setAttribute("height", `${svgBox.height * 10}`);
+				rect.setAttribute("fill", "#fff");
+				svg.prepend(rect);
+				if (svg) {
+					const _canvas = svg2canvas(svg.outerHTML);
+					if (isUrl) {
+						// document.body.appendChild(_canvas);
+						let el: any = document.createElement("a");
+						// 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
+						el.href = _canvas.toDataURL();
+						el.download = data.musicName + ".png";
+
+						// 创建一个点击事件并对 a 标签进行触发
+						const event = new MouseEvent("click");
+						el.dispatchEvent(event);
+					} else {
+						_canvas.toBlob(async (blob) => {
+							const pngUrl = await api_uploadFile(blob, data.musicId + ".png");
+							resolve(pngUrl);
+						}, "image/png");
+					}
+				}
+			});
+		};
+
 		const downPng = async () => {
 			abcData.abc.title = `T:${data.musicName}`;
 			abcData.abc.creator = `R:${data.creator}`;
-			const paper = document.getElementById("exportPng");
-			if (!paper) return;
-			const abc = renderMeasures(abcData.abc, { hiddenIndex: true, showTitle: true, showCreator: true });
-			ABCJS.renderAbc(paper, abc, abcData.abcOptions);
-			const svg: any = paper.children[0]?.cloneNode(true);
-			const svgBox = paper.getBoundingClientRect();
-			svg.setAttribute("width", `${svgBox.width * 3}`);
-			svg.setAttribute("height", `${svgBox.height * 3}`);
-			const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
-			rect.setAttribute("x", "0");
-			rect.setAttribute("y", "0");
-			rect.setAttribute("width", `${svgBox.width * 10}`);
-			rect.setAttribute("height", `${svgBox.height * 10}`);
-			rect.setAttribute("fill", "#fff");
-			svg.prepend(rect);
-			if (svg) {
-				const _canvas = svg2canvas(svg.outerHTML);
-				// document.body.appendChild(_canvas);
-				let el: any = document.createElement("a");
-				// 设置 href 为图片经过 base64 编码后的字符串,默认为 png 格式
-				el.href = _canvas.toDataURL();
-				el.download = data.musicName + ".png";
-
-				// 创建一个点击事件并对 a 标签进行触发
-				const event = new MouseEvent("click");
-				el.dispatchEvent(event);
-			}
+			productPng();
 		};
 
 		const downRef = ref();
@@ -1576,12 +1619,8 @@ export default defineComponent({
 			downRef.value.innerHTML = midi;
 			downRef.value.querySelector("a").click();
 		};
-		const downWav = () => {
-			try {
-				if (abcData.synthControl) {
-					abcData.synthControl.download("曲谱.wav");
-				}
-			} catch (error) {
+		const productWav = async (isUrl = true) => {
+			return new Promise((resolve) => {
 				const midiBuffer = new ABCJS.synth.CreateSynth();
 				midiBuffer
 					.init({
@@ -1589,11 +1628,25 @@ export default defineComponent({
 						options: abcData.synthOptions,
 					})
 					.then(() => {
-						midiBuffer.prime().then(() => {
-							// console.log(midiBuffer.download());
-							downloadFile(midiBuffer.download(), "曲谱.wav");
+						midiBuffer.prime().then(async () => {
+							if (isUrl) {
+								downloadFile(midiBuffer.download(), (data.musicName || "曲谱") + ".wav");
+							} else {
+								const blob = bufferToWave((midiBuffer as any).getAudioBuffer());
+								const wavurl = await api_uploadFile(blob, data.musicId + ".wav");
+								resolve(wavurl);
+							}
 						});
 					});
+			});
+		};
+		const downWav = () => {
+			try {
+				if (abcData.synthControl) {
+					abcData.synthControl.download((data.musicName || "曲谱") + ".wav");
+				}
+			} catch (error) {
+				productWav();
 			}
 		};
 
@@ -1707,6 +1760,7 @@ export default defineComponent({
 							</div>
 							<div class={styles.topBtn}>
 								<FileBtn
+									saveLoading={data.saveLoading}
 									onSelect={(val: IFileBtnType) => {
 										if (val === "newMusic") {
 											handleCreateMusic();

+ 59 - 0
src/utils/uploadFile.ts

@@ -0,0 +1,59 @@
+import axios from "axios";
+import request from "./request";
+const bucketName = "gyt";
+const ACL = "public-read";
+const ossUploadUrl = `https://${bucketName}.ks3-cn-beijing.ksyuncs.com/`;
+
+const policy = (params: object) => {
+	return request.post('/open/getUploadSign', {
+		data: params,
+    requestType: 'json',
+	});
+};
+
+const getPolicy = async (fileName: string) => {
+	const obj = {
+		filename: fileName,
+		bucketName: bucketName,
+		postData: {
+			filename: fileName,
+			acl: ACL,
+			key: fileName,
+		},
+	};
+	// console.log("🚀 ~ obj:", obj)
+	const { data } = await policy(obj);
+	// console.log(data);
+	return data;
+};
+const upload = async (policy: any, fileName: string, file: any, progress?: Function) => {
+	const url = ossUploadUrl + fileName;
+	const formData = new FormData();
+	formData.append("policy", policy.policy);
+	formData.append("signature", policy.signature);
+	formData.append("key", fileName);
+	formData.append("KSSAccessKeyId", policy.kssAccessKeyId);
+	formData.append("acl", ACL);
+	formData.append("name", fileName);
+	formData.append("file", file);
+	const res = await axios({
+		url: ossUploadUrl,
+		method: "post",
+		headers: {
+			"Content-Type": "multipart/form-data",
+		},
+		data: formData,
+		onUploadProgress: (progressEvent: any) => {
+			const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
+			progress && progress(complete);
+		},
+	} as any);
+	return url;
+};
+
+export const api_uploadFile = async (file: any, fileName: string, progress?: Function) => {
+	const policy = await getPolicy(fileName);
+	const url = await upload(policy, fileName, file, progress && progress);
+	console.log("🚀 ~ url:", url);
+	return url;
+};