Spaces:
Runtime error
Runtime error
xribene
commited on
Commit
·
b8caa91
1
Parent(s):
a9fcdaa
alphatab + drawAndListen
Browse files- frontend/public/Fantasia_no.2_importedFromGpByMusescore3.musicxml +0 -0
- frontend/src/App.vue +13 -11
- frontend/src/components/AlphaTabWidget.vue +58 -120
- frontend/src/components/AlphaTexDL.vue +610 -0
- frontend/src/components/AlphaTexWidget.vue +554 -0
- frontend/src/components/MusicXmlWidget2.vue +559 -0
- frontend/src/components/ScoreWidgetBase.vue +347 -295
- frontend/src/plugins/router.js +5 -0
- frontend/src/style.css +4 -3
- frontend/src/utils/timeUtils.js +11 -0
- frontend/src/views/Analyzer.vue +26 -23
- frontend/src/views/DrawAndListen.vue +139 -0
- frontend/uno.config.js +11 -4
frontend/public/Fantasia_no.2_importedFromGpByMusescore3.musicxml
ADDED
The diff for this file is too large to render.
See raw diff
|
|
frontend/src/App.vue
CHANGED
@@ -31,17 +31,15 @@ const state = ref({
|
|
31 |
Analyzer
|
32 |
</RouterLink>
|
33 |
</li>
|
|
|
|
|
|
|
|
|
|
|
34 |
</ul>
|
35 |
</nav>
|
36 |
|
37 |
<div class="content">
|
38 |
-
<!-- <a href="https://vitejs.dev" target="_blank">
|
39 |
-
<img src="/guitarDiffLogo2.svg" class="logo" alt="Vite logo" />
|
40 |
-
</a> -->
|
41 |
-
|
42 |
-
<!-- <keep-alive>
|
43 |
-
<router-view />
|
44 |
-
</keep-alive> -->
|
45 |
<RouterView v-slot="{ Component }">
|
46 |
<keep-alive>
|
47 |
<component :is="Component" :key="$route.fullPath" />
|
@@ -60,9 +58,9 @@ const state = ref({
|
|
60 |
}
|
61 |
|
62 |
.navbar {
|
63 |
-
background-color:
|
64 |
-
margin:
|
65 |
-
padding:
|
66 |
/* padding: 1rem; */
|
67 |
/* width: 100%; */
|
68 |
/* margin-left: 10px; */
|
@@ -83,7 +81,11 @@ const state = ref({
|
|
83 |
|
84 |
.nav-list li a {
|
85 |
text-decoration: none;
|
86 |
-
color:
|
|
|
|
|
|
|
|
|
87 |
}
|
88 |
|
89 |
.content {
|
|
|
31 |
Analyzer
|
32 |
</RouterLink>
|
33 |
</li>
|
34 |
+
<li>
|
35 |
+
<RouterLink :to="{ name: 'DrawAndListen' }">
|
36 |
+
Draw&Listen
|
37 |
+
</RouterLink>
|
38 |
+
</li>
|
39 |
</ul>
|
40 |
</nav>
|
41 |
|
42 |
<div class="content">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
<RouterView v-slot="{ Component }">
|
44 |
<keep-alive>
|
45 |
<component :is="Component" :key="$route.fullPath" />
|
|
|
58 |
}
|
59 |
|
60 |
.navbar {
|
61 |
+
background-color: var(--background-light2);
|
62 |
+
/* margin: 10px; */
|
63 |
+
padding: 5px;
|
64 |
/* padding: 1rem; */
|
65 |
/* width: 100%; */
|
66 |
/* margin-left: 10px; */
|
|
|
81 |
|
82 |
.nav-list li a {
|
83 |
text-decoration: none;
|
84 |
+
color: var(--text-color-dark);
|
85 |
+
background-color: var(--accent-color2);
|
86 |
+
padding: 0.8rem;
|
87 |
+
font-size: 1.2rem;
|
88 |
+
font-weight: 600;
|
89 |
}
|
90 |
|
91 |
.content {
|
frontend/src/components/AlphaTabWidget.vue
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
<script setup>
|
2 |
-
import { computed, onMounted, onUpdated, ref, watch } from 'vue'
|
3 |
import { Pane } from 'tweakpane'
|
4 |
|
5 |
const props = defineProps({
|
@@ -22,9 +22,9 @@ const props = defineProps({
|
|
22 |
|
23 |
const emit = defineEmits(['updateScoreCode'])
|
24 |
|
25 |
-
const openSheetMusicDisplay = null
|
26 |
-
const timeoutID = null
|
27 |
-
let pane = null
|
28 |
let api = null
|
29 |
let main = null
|
30 |
let wrapper = null
|
@@ -37,48 +37,61 @@ const parsingError = ref('')
|
|
37 |
// const count = ref(0)
|
38 |
const settingsStatus = ref(false)
|
39 |
const isActive = ref(false)
|
40 |
-
|
41 |
-
const
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
51 |
|
52 |
const divId = computed(() => {
|
53 |
return `xmlScoreContainer_${props.id}`
|
54 |
})
|
55 |
|
56 |
-
function initializePane() {
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
-
onMounted(
|
74 |
// console.log($(`#${divId.value}`)[0])
|
75 |
-
initializePane()
|
76 |
-
wrapper = document.querySelector('.at-wrap')
|
77 |
-
main = wrapper.querySelector('.at-main')
|
|
|
|
|
78 |
// initialize alphatab
|
79 |
settings = {
|
80 |
// file: 'https://www.alphatab.net/files/canon.gp',
|
81 |
-
|
82 |
player: {
|
83 |
enablePlayer: true,
|
84 |
soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2',
|
@@ -89,12 +102,12 @@ onMounted(async () => {
|
|
89 |
})
|
90 |
|
91 |
function initializeOSMD() {
|
|
|
92 |
api = new alphaTab.AlphaTabApi(main, settings)
|
93 |
-
api.tex(`
|
94 |
-
\\title 'Test' . 3.3.4
|
95 |
-
:4 2.3 3.3 :8 3.3 4.3 3.3 4.3 |
|
96 |
-
|
97 |
-
`)
|
98 |
|
99 |
// overlay logic
|
100 |
const overlay = wrapper.querySelector('.at-overlay')
|
@@ -271,77 +284,7 @@ function initializeOSMD() {
|
|
271 |
songPosition.innerText
|
272 |
= `${formatDuration(e.currentTime)} / ${formatDuration(e.endTime)}`
|
273 |
})
|
274 |
-
|
275 |
-
// await fetch('/guitar-diff-example.musicxml')
|
276 |
-
// .then((response) => {
|
277 |
-
// return response.text()
|
278 |
-
// })
|
279 |
-
// .then((data) => {
|
280 |
-
// console.log('in data')
|
281 |
-
// openSheetMusicDisplay.load(data)
|
282 |
-
// .then(
|
283 |
-
// () => {
|
284 |
-
// window.osmd = openSheetMusicDisplay
|
285 |
-
// openSheetMusicDisplay.render()
|
286 |
-
// // openSheetMusicDisplay.cursor.show();
|
287 |
-
// console.log('in load')
|
288 |
-
// showParsingError.value = false
|
289 |
-
// currentScoreCode.value = data
|
290 |
-
// },
|
291 |
-
// )
|
292 |
-
// .catch((e) => {
|
293 |
-
// showParsingError.value = true
|
294 |
-
// console.log(e.message.replace(/(?:\r\n|\r|\n)/g, '<br>'))
|
295 |
-
// parsingError.value = e.message.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
296 |
-
// })
|
297 |
-
// })
|
298 |
-
}
|
299 |
-
|
300 |
-
// function showScore() {
|
301 |
-
// openSheetMusicDisplay.clear()
|
302 |
-
// console.log(currentScoreCode.value)
|
303 |
-
// openSheetMusicDisplay.load(currentScoreCode.value)
|
304 |
-
// .then(
|
305 |
-
// () => {
|
306 |
-
// window.osmd = openSheetMusicDisplay
|
307 |
-
// openSheetMusicDisplay.render()
|
308 |
-
// // openSheetMusicDisplay.cursor.show();
|
309 |
-
// console.log('in load')
|
310 |
-
// showParsingError.value = false
|
311 |
-
// },
|
312 |
-
// )
|
313 |
-
// .catch((e) => {
|
314 |
-
// showParsingError.value = true
|
315 |
-
// console.log(e.message.replace(/(?:\r\n|\r|\n)/g, '<br>'))
|
316 |
-
// parsingError.value = e.message.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
317 |
-
// })
|
318 |
-
// }
|
319 |
-
|
320 |
-
// function handleKeyUp(_event) {
|
321 |
-
// if (timeoutID)
|
322 |
-
// clearTimeout(timeoutID)
|
323 |
-
// timeoutID = setTimeout(showScore, 500)
|
324 |
-
// }
|
325 |
-
|
326 |
-
// function handleDrop(event) {
|
327 |
-
// event.preventDefault()
|
328 |
-
// const textData = event.dataTransfer.getData('text')
|
329 |
-
// const file = event.dataTransfer.files[0]
|
330 |
-
// console.log(textData)
|
331 |
-
// console.log(file)
|
332 |
-
// if (file) {
|
333 |
-
// const reader = new FileReader()
|
334 |
-
// reader.onload = function (e) {
|
335 |
-
// currentScoreCode.value = e.target.result
|
336 |
-
// showScore()
|
337 |
-
// }
|
338 |
-
// reader.readAsText(file)
|
339 |
-
// }
|
340 |
-
// else if (textData) {
|
341 |
-
// currentScoreCode.value = textData
|
342 |
-
// showScore()
|
343 |
-
// }
|
344 |
-
// }
|
345 |
|
346 |
function showHideSettings() {
|
347 |
console.log('showHideSettings')
|
@@ -391,7 +334,7 @@ watch(currentScoreCode, () => {
|
|
391 |
-->
|
392 |
<div>
|
393 |
<div>
|
394 |
-
<div class="at-wrap">
|
395 |
<div class="at-overlay">
|
396 |
<div class="at-overlay-content">
|
397 |
Music sheet is loading
|
@@ -404,7 +347,7 @@ watch(currentScoreCode, () => {
|
|
404 |
</div>
|
405 |
</div>
|
406 |
<div class="at-viewport">
|
407 |
-
<div class="at-main" />
|
408 |
</div>
|
409 |
</div>
|
410 |
<div class="at-controls">
|
@@ -485,7 +428,7 @@ watch(currentScoreCode, () => {
|
|
485 |
<button class="left-align" @click="showHideSettings">
|
486 |
Settings
|
487 |
</button>
|
488 |
-
<div
|
489 |
|
490 |
<div v-show="settingsStatus" id="tabSettingsArea22">
|
491 |
<div id="paneContainerXml22" />
|
@@ -515,11 +458,6 @@ watch(currentScoreCode, () => {
|
|
515 |
</template>
|
516 |
|
517 |
<style scoped>
|
518 |
-
body {
|
519 |
-
font-family: Arial, Helvetica, sans-serif;
|
520 |
-
font-size: 12px;
|
521 |
-
}
|
522 |
-
|
523 |
.at-wrap {
|
524 |
width: 80vw;
|
525 |
height: 80vh;
|
|
|
1 |
<script setup>
|
2 |
+
import { computed, onMounted, onUnmounted, onUpdated, ref, watch } from 'vue'
|
3 |
import { Pane } from 'tweakpane'
|
4 |
|
5 |
const props = defineProps({
|
|
|
22 |
|
23 |
const emit = defineEmits(['updateScoreCode'])
|
24 |
|
25 |
+
// const openSheetMusicDisplay = null
|
26 |
+
// const timeoutID = null
|
27 |
+
// let pane = null
|
28 |
let api = null
|
29 |
let main = null
|
30 |
let wrapper = null
|
|
|
37 |
// const count = ref(0)
|
38 |
const settingsStatus = ref(false)
|
39 |
const isActive = ref(false)
|
40 |
+
const wrapRef = ref(null)
|
41 |
+
const mainRef = ref(null)
|
42 |
+
|
43 |
+
// const SETTINGS_PARAMS = {
|
44 |
+
// scale: 1.0,
|
45 |
+
// // width: 800,
|
46 |
+
// // factor: 123,
|
47 |
+
// // title: 'hello',
|
48 |
+
// // color: '#ff0055',
|
49 |
+
// // percentage: 50,
|
50 |
+
// // theme: 'dark',
|
51 |
+
// // prop: 'Put your\nmultiline\ntext here!'
|
52 |
+
// }
|
53 |
|
54 |
const divId = computed(() => {
|
55 |
return `xmlScoreContainer_${props.id}`
|
56 |
})
|
57 |
|
58 |
+
// function initializePane() {
|
59 |
+
// pane = new Pane({
|
60 |
+
// container: document.getElementById('paneContainerXml'),
|
61 |
+
// title: 'MusicXML Settings',
|
62 |
+
// },
|
63 |
+
// )
|
64 |
+
// pane.addBinding(SETTINGS_PARAMS, 'scale', {
|
65 |
+
// min: 0.5,
|
66 |
+
// max: 2.0,
|
67 |
+
// step: 0.1,
|
68 |
+
// }).on('change', (ev) => {
|
69 |
+
// console.log(ev.value)
|
70 |
+
// openSheetMusicDisplay.zoom = ev.value
|
71 |
+
// openSheetMusicDisplay.render()
|
72 |
+
// })
|
73 |
+
// }
|
74 |
+
|
75 |
+
onUnmounted(() => {
|
76 |
+
console.log('onUnmounted')
|
77 |
+
// pane = null
|
78 |
+
api = null
|
79 |
+
main = null
|
80 |
+
wrapper = null
|
81 |
+
settings = null
|
82 |
+
})
|
83 |
|
84 |
+
onMounted(() => {
|
85 |
// console.log($(`#${divId.value}`)[0])
|
86 |
+
// initializePane()
|
87 |
+
// wrapper = document.querySelector('.at-wrap')
|
88 |
+
// main = wrapper.querySelector('.at-main')
|
89 |
+
wrapper = wrapRef.value
|
90 |
+
main = mainRef.value
|
91 |
// initialize alphatab
|
92 |
settings = {
|
93 |
// file: 'https://www.alphatab.net/files/canon.gp',
|
94 |
+
file: '/Fantasia_no.2.musicxml.gpx',
|
95 |
player: {
|
96 |
enablePlayer: true,
|
97 |
soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2',
|
|
|
102 |
})
|
103 |
|
104 |
function initializeOSMD() {
|
105 |
+
console.log('initializeOSMD')
|
106 |
api = new alphaTab.AlphaTabApi(main, settings)
|
107 |
+
// api.tex(`
|
108 |
+
// \\title 'Test' . 3.3.4
|
109 |
+
// :4 2.3 3.3 :8 3.3 4.3 3.3 4.3 |
|
110 |
+
// `)
|
|
|
111 |
|
112 |
// overlay logic
|
113 |
const overlay = wrapper.querySelector('.at-overlay')
|
|
|
284 |
songPosition.innerText
|
285 |
= `${formatDuration(e.currentTime)} / ${formatDuration(e.endTime)}`
|
286 |
})
|
287 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
|
289 |
function showHideSettings() {
|
290 |
console.log('showHideSettings')
|
|
|
334 |
-->
|
335 |
<div>
|
336 |
<div>
|
337 |
+
<div ref="wrapRef" class="at-wrap">
|
338 |
<div class="at-overlay">
|
339 |
<div class="at-overlay-content">
|
340 |
Music sheet is loading
|
|
|
347 |
</div>
|
348 |
</div>
|
349 |
<div class="at-viewport">
|
350 |
+
<div ref="mainRef" class="at-main" />
|
351 |
</div>
|
352 |
</div>
|
353 |
<div class="at-controls">
|
|
|
428 |
<button class="left-align" @click="showHideSettings">
|
429 |
Settings
|
430 |
</button>
|
431 |
+
<div class="scoreContainer22" />
|
432 |
|
433 |
<div v-show="settingsStatus" id="tabSettingsArea22">
|
434 |
<div id="paneContainerXml22" />
|
|
|
458 |
</template>
|
459 |
|
460 |
<style scoped>
|
|
|
|
|
|
|
|
|
|
|
461 |
.at-wrap {
|
462 |
width: 80vw;
|
463 |
height: 80vh;
|
frontend/src/components/AlphaTexDL.vue
ADDED
@@ -0,0 +1,610 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script setup>
|
2 |
+
import { computed, onMounted, onUnmounted, onUpdated, ref, toRaw, watch } from 'vue'
|
3 |
+
import { Pane } from 'tweakpane'
|
4 |
+
import formatDuration from '@/utils/timeUtils.js'
|
5 |
+
|
6 |
+
const props = defineProps({
|
7 |
+
alphaTex: {
|
8 |
+
type: String,
|
9 |
+
required: false,
|
10 |
+
},
|
11 |
+
})
|
12 |
+
|
13 |
+
const emit = defineEmits(['updateScoreCode'])
|
14 |
+
|
15 |
+
// const openSheetMusicDisplay = null
|
16 |
+
// const timeoutID = null
|
17 |
+
let pane = null
|
18 |
+
let api = null
|
19 |
+
let wrapper = null
|
20 |
+
let settings = null
|
21 |
+
let timeoutID = null
|
22 |
+
|
23 |
+
// const message = ref('hello')
|
24 |
+
const currentScoreCode = ref(props.alphaTex)
|
25 |
+
const showParsingError = ref(false)
|
26 |
+
const parsingError = ref('')
|
27 |
+
// const count = ref(0)
|
28 |
+
const settingsStatus = ref(false)
|
29 |
+
const isActive = ref(false)
|
30 |
+
const wrapRef = ref(null)
|
31 |
+
const mainRef = ref(null)
|
32 |
+
const viewPortRef = ref(null)
|
33 |
+
const paneContainer = ref(null)
|
34 |
+
const canvasRef = ref(null)
|
35 |
+
|
36 |
+
const tracks = ref([])
|
37 |
+
const songPosition = ref('00:00 / 00:00')
|
38 |
+
const previousTime = -1
|
39 |
+
const title = ref('')
|
40 |
+
const artist = ref('')
|
41 |
+
const isPlaying = ref(false)
|
42 |
+
const selectedLayout = ref('page')
|
43 |
+
const isZoomMenuVisible = ref(false)
|
44 |
+
const selectedZoom = ref(100)
|
45 |
+
const zoomOptions = [
|
46 |
+
{ value: '25', label: '25%' },
|
47 |
+
{ value: '50', label: '50%' },
|
48 |
+
{ value: '75', label: '75%' },
|
49 |
+
{ value: '90', label: '90%' },
|
50 |
+
{ value: '100', label: '100%' },
|
51 |
+
{ value: '110', label: '110%' },
|
52 |
+
{ value: '125', label: '125%' },
|
53 |
+
{ value: '150', label: '150%' },
|
54 |
+
{ value: '200', label: '200%' },
|
55 |
+
]
|
56 |
+
const metronomeIsActive = ref(false)
|
57 |
+
// const loopIsActive = ref(false)
|
58 |
+
|
59 |
+
const CANVAS_SIZE_X = 600
|
60 |
+
const CANVAS_SIZE_Y = 300
|
61 |
+
const CANVAS_SCALE = 1
|
62 |
+
const isMouseDown = false
|
63 |
+
let mousePressed = false
|
64 |
+
const hasIntroText = true
|
65 |
+
let lastX = 0
|
66 |
+
let lastY = 0
|
67 |
+
let isFirstCurve = true
|
68 |
+
let ctx = null
|
69 |
+
|
70 |
+
const SETTINGS_PARAMS = {
|
71 |
+
scale: 1.0,
|
72 |
+
stretchForce: 1.0,
|
73 |
+
// width: 800,
|
74 |
+
// factor: 123,
|
75 |
+
// title: 'hello',
|
76 |
+
// color: '#ff0055',
|
77 |
+
// percentage: 50,
|
78 |
+
// theme: 'dark',
|
79 |
+
// prop: 'Put your\nmultiline\ntext here!'
|
80 |
+
}
|
81 |
+
|
82 |
+
// const divId = computed(() => {
|
83 |
+
// return `xmlScoreContainer_${props.id}`
|
84 |
+
// })
|
85 |
+
|
86 |
+
function initializePane() {
|
87 |
+
pane = new Pane({
|
88 |
+
container: paneContainer.value,
|
89 |
+
title: 'Notation Settings',
|
90 |
+
},
|
91 |
+
)
|
92 |
+
pane.addBinding(SETTINGS_PARAMS, 'scale', {
|
93 |
+
min: 0.1,
|
94 |
+
max: 2.0,
|
95 |
+
step: 0.1,
|
96 |
+
}).on('change', (ev) => {
|
97 |
+
console.log(ev.value)
|
98 |
+
api.settings.display.scale = ev.value
|
99 |
+
api.updateSettings()
|
100 |
+
api.render()
|
101 |
+
})
|
102 |
+
pane.addBinding(SETTINGS_PARAMS, 'stretchForce', {
|
103 |
+
min: 0.1,
|
104 |
+
max: 2.0,
|
105 |
+
step: 0.1,
|
106 |
+
}).on('change', (ev) => {
|
107 |
+
console.log(ev.value)
|
108 |
+
api.settings.display.stretchForce = ev.value
|
109 |
+
api.updateSettings()
|
110 |
+
api.render()
|
111 |
+
})
|
112 |
+
}
|
113 |
+
|
114 |
+
onUnmounted(() => {
|
115 |
+
console.log('onUnmounted')
|
116 |
+
// pane = null
|
117 |
+
api = null
|
118 |
+
wrapper = null
|
119 |
+
settings = null
|
120 |
+
})
|
121 |
+
|
122 |
+
onMounted(() => {
|
123 |
+
// console.log($(`#${divId.value}`)[0])
|
124 |
+
initializePane()
|
125 |
+
// wrapper = document.querySelector('.at-wrap')
|
126 |
+
// main = wrapper.querySelector('.at-main')
|
127 |
+
wrapper = wrapRef.value
|
128 |
+
// initialize alphatab
|
129 |
+
settings = {
|
130 |
+
// file: 'https://www.alphatab.net/files/canon.gp',
|
131 |
+
// file: '/Fantasia_no.2.musicxml.gpx',
|
132 |
+
player: {
|
133 |
+
enablePlayer: true,
|
134 |
+
soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2',
|
135 |
+
scrollElement: viewPortRef.value,
|
136 |
+
|
137 |
+
},
|
138 |
+
display: {
|
139 |
+
// barsPerRow: 3,
|
140 |
+
layoutMode: alphaTab.LayoutMode.Horizontal,
|
141 |
+
padding: [0, 0, 0, 0],
|
142 |
+
stretchForce: 1,
|
143 |
+
},
|
144 |
+
notation: {
|
145 |
+
elements: {
|
146 |
+
scoreTitle: false,
|
147 |
+
trackNames: false,
|
148 |
+
guitarTuning: false,
|
149 |
+
},
|
150 |
+
},
|
151 |
+
staveProfile: alphaTab.StaveProfile.Score,
|
152 |
+
}
|
153 |
+
initAlphaTab()
|
154 |
+
|
155 |
+
ctx = canvasRef.value.getContext('2d')
|
156 |
+
ctx.lineWidth = 2
|
157 |
+
ctx.lineJoin = 'round'
|
158 |
+
ctx.lineCap = 'round'
|
159 |
+
ctx.font = '20px sans-serif'
|
160 |
+
ctx.textAlign = 'center'
|
161 |
+
ctx.textBaseline = 'middle'
|
162 |
+
ctx.fillStyle = '#212121'
|
163 |
+
ctx.fillText('Draw a curve!', 600 / 2, 300 / 2)
|
164 |
+
|
165 |
+
// Set the line color for the canvas.
|
166 |
+
ctx.strokeStyle = '#212121'
|
167 |
+
|
168 |
+
// function drawLine(fromX, fromY, toX, toY) {
|
169 |
+
// // Draws a line from (fromX, fromY) to (toX, toY).
|
170 |
+
// ctx.beginPath()
|
171 |
+
// ctx.moveTo(fromX, fromY)
|
172 |
+
// ctx.lineCap = 'round'
|
173 |
+
// ctx.lineTo(toX, toY)
|
174 |
+
// ctx.closePath()
|
175 |
+
// ctx.stroke()
|
176 |
+
})
|
177 |
+
|
178 |
+
function Draw(x, y, isDown) {
|
179 |
+
if (isFirstCurve) {
|
180 |
+
ctx.clearRect(0, 0, CANVAS_SIZE_X, CANVAS_SIZE_Y)
|
181 |
+
isFirstCurve = false
|
182 |
+
}
|
183 |
+
if (isDown) {
|
184 |
+
ctx.beginPath()
|
185 |
+
// ctx.strokeStyle = $('#selColor').val();
|
186 |
+
// ctx.lineWidth = $('#selWidth').val();
|
187 |
+
// ctx.lineJoin = "round";
|
188 |
+
ctx.moveTo(lastX, lastY)
|
189 |
+
ctx.lineTo(x, y)
|
190 |
+
ctx.closePath()
|
191 |
+
ctx.stroke()
|
192 |
+
}
|
193 |
+
lastX = x
|
194 |
+
lastY = y
|
195 |
+
}
|
196 |
+
// function canvasMouseDown(e) {
|
197 |
+
// mousePressed = true
|
198 |
+
// Draw(e.pageX - $(this).offset().left, e.pageY - $(this).offset().top, false)
|
199 |
+
// }
|
200 |
+
function canvasMouseDown(e) {
|
201 |
+
const rect = e.target.getBoundingClientRect()
|
202 |
+
const mouseX = e.clientX - rect.left
|
203 |
+
const mouseY = e.clientY - rect.top
|
204 |
+
|
205 |
+
mousePressed = true
|
206 |
+
Draw(mouseX, mouseY, false)
|
207 |
+
}
|
208 |
+
function canvasMouseMove(e) {
|
209 |
+
if (mousePressed) {
|
210 |
+
const rect = e.target.getBoundingClientRect()
|
211 |
+
const mouseX = e.clientX - rect.left
|
212 |
+
const mouseY = e.clientY - rect.top
|
213 |
+
|
214 |
+
Draw(mouseX, mouseY, true)
|
215 |
+
}
|
216 |
+
}
|
217 |
+
function canvasMouseUp(e) {
|
218 |
+
mousePressed = false
|
219 |
+
const imgData = ctx.getImageData(0, 0, CANVAS_SIZE_X, CANVAS_SIZE_Y)
|
220 |
+
console.log(imgData)
|
221 |
+
// curveList.push(imgData)
|
222 |
+
}
|
223 |
+
function canvasMouseOut(e) {
|
224 |
+
mousePressed = false
|
225 |
+
}
|
226 |
+
function updateZoom() {
|
227 |
+
// Handle zoom update logic here
|
228 |
+
// const zoomLevel = Number.parseInt(zoom.value) / 100
|
229 |
+
api.settings.display.scale = selectedZoom.value / 100
|
230 |
+
api.updateSettings()
|
231 |
+
api.render()
|
232 |
+
}
|
233 |
+
|
234 |
+
function scoreExport() {
|
235 |
+
const exporter = new alphaTab.exporter.Gp7Exporter()
|
236 |
+
const data = exporter.export(api.score, api.settings)
|
237 |
+
|
238 |
+
// trigger download
|
239 |
+
const a = document.createElement('a')
|
240 |
+
a.download = api.score.title.length > 0 ? `${api.score.title}.gp` : 'Untitled.gp'
|
241 |
+
a.href = URL.createObjectURL(new Blob([data]))
|
242 |
+
document.body.appendChild(a)
|
243 |
+
a.click()
|
244 |
+
document.body.removeChild(a)
|
245 |
+
}
|
246 |
+
|
247 |
+
function handleKeyUp(event) {
|
248 |
+
if (timeoutID)
|
249 |
+
clearTimeout(timeoutID)
|
250 |
+
timeoutID = setTimeout(showScore, 500)
|
251 |
+
}
|
252 |
+
|
253 |
+
// function handleDrop(event) {
|
254 |
+
// event.preventDefault()
|
255 |
+
// const textData = event.dataTransfer.getData('text')
|
256 |
+
// const file = event.dataTransfer.files[0]
|
257 |
+
// console.log(textData)
|
258 |
+
// console.log(file)
|
259 |
+
// if (file) {
|
260 |
+
// const reader = new FileReader()
|
261 |
+
// reader.onload = function (e) {
|
262 |
+
// currentScoreCode.value = e.target.result
|
263 |
+
// showScore()
|
264 |
+
// }
|
265 |
+
// reader.readAsText(file)
|
266 |
+
// }
|
267 |
+
// else if (textData) {
|
268 |
+
// currentScoreCode.value = textData
|
269 |
+
// showScore()
|
270 |
+
// }
|
271 |
+
// }
|
272 |
+
|
273 |
+
function showScore() {
|
274 |
+
try {
|
275 |
+
api.tex(currentScoreCode.value)
|
276 |
+
showParsingError.value = false
|
277 |
+
}
|
278 |
+
catch (e) {
|
279 |
+
showParsingError.value = true
|
280 |
+
console.log(e.message.replace(/(?:\r\n|\r|\n)/g, '<br>'))
|
281 |
+
parsingError.value = e.message.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
282 |
+
}
|
283 |
+
}
|
284 |
+
|
285 |
+
function initAlphaTab() {
|
286 |
+
console.log('initAlphaTab')
|
287 |
+
api = new alphaTab.AlphaTabApi(mainRef.value, settings)
|
288 |
+
// api.tex(`
|
289 |
+
// \\title 'Test' . 3.3.4
|
290 |
+
// :4 2.3 3.3 :8 3.3 4.3 3.3 4.3 |
|
291 |
+
// `)
|
292 |
+
api.tex(currentScoreCode.value)
|
293 |
+
|
294 |
+
// overlay logic
|
295 |
+
const overlay = wrapper.querySelector('.at-overlay')
|
296 |
+
api.renderStarted.on(() => {
|
297 |
+
overlay.style.display = 'flex'
|
298 |
+
})
|
299 |
+
api.renderFinished.on(() => {
|
300 |
+
overlay.style.display = 'none'
|
301 |
+
})
|
302 |
+
|
303 |
+
/** Controls */
|
304 |
+
api.scoreLoaded.on((score) => {
|
305 |
+
title.value = score.title
|
306 |
+
artist.value = score.artist
|
307 |
+
})
|
308 |
+
|
309 |
+
api.playerStateChanged.on((e) => {
|
310 |
+
isPlaying.value = e.state
|
311 |
+
})
|
312 |
+
api.playerPositionChanged.on((e) => {
|
313 |
+
// reduce number of UI updates to second changes.
|
314 |
+
const currentSeconds = (e.currentTime / 1000) | 0
|
315 |
+
if (currentSeconds > previousTime)
|
316 |
+
songPosition.value = `${formatDuration(e.currentTime)} / ${formatDuration(e.endTime)}`
|
317 |
+
})
|
318 |
+
}
|
319 |
+
|
320 |
+
function showHideSettings() {
|
321 |
+
console.log('showHideSettings')
|
322 |
+
settingsStatus.value = !settingsStatus.value
|
323 |
+
}
|
324 |
+
|
325 |
+
// invoked when there is any type of change in the component
|
326 |
+
onUpdated(() => {
|
327 |
+
if (props.isActive !== isActive.value) {
|
328 |
+
// Prop has changed, update the ref and emit an event
|
329 |
+
if (props.isActive) {
|
330 |
+
isActive.value = props.isActive
|
331 |
+
emit('updateScoreCode', currentScoreCode.value)
|
332 |
+
// console.log("emitted updateScoreCode");
|
333 |
+
// console.log(currentScoreCode.value);
|
334 |
+
}
|
335 |
+
isActive.value = props.isActive
|
336 |
+
}
|
337 |
+
})
|
338 |
+
|
339 |
+
// watch(currentScoreCode, () => {
|
340 |
+
// // Emit the event so that the parent component can update the tabText
|
341 |
+
// // prop
|
342 |
+
// emit('updateScoreCode', currentScoreCode.value)
|
343 |
+
// // console.log("emitted updateScoreCode");
|
344 |
+
// // console.log(currentScoreCode.value);
|
345 |
+
// })
|
346 |
+
watch(currentScoreCode, () => {
|
347 |
+
// Emit the event so that the parent component can update the tabText
|
348 |
+
// prop
|
349 |
+
emit('updateScoreCode', currentScoreCode.value)
|
350 |
+
console.log('emitted updateScoreCode')
|
351 |
+
// this should go to the first tab that is shown (for now vextab)
|
352 |
+
}, { immediate: true })
|
353 |
+
|
354 |
+
// const moveCursor = () => {
|
355 |
+
// openSheetMusicDisplay.cursor.next();
|
356 |
+
// };
|
357 |
+
|
358 |
+
// const fillScore = () => {
|
359 |
+
// // console.log(vt);
|
360 |
+
// // Perform any logic to fill the score
|
361 |
+
// response.value = 'Button pressed!'; // Update response if needed
|
362 |
+
|
363 |
+
// };
|
364 |
+
</script>
|
365 |
+
|
366 |
+
<template>
|
367 |
+
<div>
|
368 |
+
<div
|
369 |
+
ref="wrapRef"
|
370 |
+
class="at-wrap"
|
371 |
+
>
|
372 |
+
<div class="canvasWrapper">
|
373 |
+
<canvas
|
374 |
+
ref="canvasRef" class="canvas"
|
375 |
+
width="600" height="300"
|
376 |
+
@mousedown="canvasMouseDown"
|
377 |
+
@mousemove="canvasMouseMove"
|
378 |
+
@mouseup="canvasMouseUp"
|
379 |
+
@mouseout="canvasMouseOut"
|
380 |
+
/>
|
381 |
+
</div>
|
382 |
+
|
383 |
+
<div class="at-overlay">
|
384 |
+
<div class="at-overlay-content">
|
385 |
+
Music sheet is loading
|
386 |
+
</div>
|
387 |
+
</div>
|
388 |
+
<div class="at-content">
|
389 |
+
<div ref="viewPortRef" class="at-viewport">
|
390 |
+
<div ref="mainRef" class="at-main" />
|
391 |
+
</div>
|
392 |
+
</div>
|
393 |
+
<div class="at-controls">
|
394 |
+
<div class="at-controls-left">
|
395 |
+
<button class="btn i-fad:rew" @click="api.stop()" />
|
396 |
+
<button class="btn" :class="{ 'i-fad:play': !isPlaying, 'i-fad:pause': isPlaying }" @click="api.playPause()" />
|
397 |
+
|
398 |
+
<div class="at-song-info">
|
399 |
+
{{ title }} - {{ artist }}
|
400 |
+
</div>
|
401 |
+
<div class="at-song-position">
|
402 |
+
{{ songPosition }}
|
403 |
+
</div>
|
404 |
+
</div>
|
405 |
+
<div class="at-controls-right">
|
406 |
+
<!-- <div :class="{ active: metronomeIsActive }">
|
407 |
+
<button class="btn i-fad:metronome" @click="toggleMetronome" />
|
408 |
+
</div> -->
|
409 |
+
<!-- <div>
|
410 |
+
<button class="btn i-fad:repeat" :class="{ active: loopingIsActive }" @click="toggleLooping" />
|
411 |
+
</div> -->
|
412 |
+
<div :class="{ active: settingsStatus }">
|
413 |
+
<button class="btn i-fa:edit" @click="showHideSettings" />
|
414 |
+
</div>
|
415 |
+
|
416 |
+
<div>
|
417 |
+
<button class="btn i-foundation:print" @click="api.print()" />
|
418 |
+
</div>
|
419 |
+
|
420 |
+
<div>
|
421 |
+
<button class="btn i-foundation:page-export" @click="scoreExport" />
|
422 |
+
</div>
|
423 |
+
|
424 |
+
<div class="at-zoom">
|
425 |
+
<!-- <button class="btn i-fad:zoomin" @click="toggleZoomMenu" /> -->
|
426 |
+
<select v-model="selectedZoom" @change="updateZoom">
|
427 |
+
<option v-for="zoomOption in zoomOptions" :key="zoomOption.value" :value="zoomOption.value">
|
428 |
+
{{ zoomOption.label }}
|
429 |
+
</option>
|
430 |
+
</select>
|
431 |
+
</div>
|
432 |
+
</div>
|
433 |
+
</div>
|
434 |
+
</div>
|
435 |
+
<div v-show="settingsStatus" id="tabSettingsArea">
|
436 |
+
<div ref="paneContainer" />
|
437 |
+
<textarea
|
438 |
+
v-model="currentScoreCode"
|
439 |
+
class="editorBox"
|
440 |
+
placeholder="add your musicxml code here or drag a file"
|
441 |
+
@keyup="handleKeyUp"
|
442 |
+
@drop="handleDrop"
|
443 |
+
@dragover.prevent
|
444 |
+
/>
|
445 |
+
</div>
|
446 |
+
<div v-if="showParsingError" class="parsing-error" v-html="parsingError" />
|
447 |
+
</div>
|
448 |
+
|
449 |
+
<!-- </div> -->
|
450 |
+
</template>
|
451 |
+
|
452 |
+
<style scoped>
|
453 |
+
#tabSettingsArea {
|
454 |
+
display: flex;
|
455 |
+
flex-direction: row;
|
456 |
+
align-items: center;
|
457 |
+
gap: 20px;
|
458 |
+
margin-left: 10px;
|
459 |
+
}
|
460 |
+
.at-wrap {
|
461 |
+
width: 60vw;
|
462 |
+
/* width:90%; */
|
463 |
+
/* height: 50vh; */
|
464 |
+
margin: 0 auto;
|
465 |
+
/* border: 1px solid rgba(0, 0, 0, 0.12); */
|
466 |
+
display: flex;
|
467 |
+
flex-direction: column;
|
468 |
+
overflow: hidden;
|
469 |
+
position: relative;
|
470 |
+
justify-content: center;
|
471 |
+
}
|
472 |
+
.at-content {
|
473 |
+
position: relative;
|
474 |
+
overflow: hidden;
|
475 |
+
flex: 1 1 auto;
|
476 |
+
display: flex;
|
477 |
+
flex-direction: row;
|
478 |
+
}
|
479 |
+
|
480 |
+
.canvas-wrapper {
|
481 |
+
position: relative;
|
482 |
+
overflow: hidden;
|
483 |
+
flex: 1 1 auto;
|
484 |
+
display: flex;
|
485 |
+
flex-direction: row;
|
486 |
+
background-color: green;
|
487 |
+
}
|
488 |
+
.canvas {
|
489 |
+
background-color: var(--accent-color2);
|
490 |
+
width: 600px;
|
491 |
+
height: 300px;
|
492 |
+
}
|
493 |
+
.at-viewport {
|
494 |
+
flex: 100%;
|
495 |
+
overflow-y: scroll;
|
496 |
+
/* position: absolute;
|
497 |
+
top: 0;
|
498 |
+
left: 70px;
|
499 |
+
right: 0;
|
500 |
+
bottom: 0;
|
501 |
+
padding-right: 20px; */
|
502 |
+
}
|
503 |
+
|
504 |
+
.at-sidebar {
|
505 |
+
/* position: absolute;
|
506 |
+
top: 0;
|
507 |
+
left: 0;
|
508 |
+
bottom: 0;
|
509 |
+
max-width: 70px;
|
510 |
+
width: auto; */
|
511 |
+
flex: 0%;
|
512 |
+
display: flex;
|
513 |
+
align-content: stretch;
|
514 |
+
justify-content: center;
|
515 |
+
max-width: 70px;
|
516 |
+
z-index: 1001;
|
517 |
+
overflow: hidden;
|
518 |
+
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
519 |
+
background: #862b2b;
|
520 |
+
|
521 |
+
}
|
522 |
+
|
523 |
+
.at-track-list {
|
524 |
+
display: flex;
|
525 |
+
flex-direction: column;
|
526 |
+
}
|
527 |
+
|
528 |
+
.at-controls {
|
529 |
+
/* flex: 0 0 auto; */
|
530 |
+
display: flex;
|
531 |
+
justify-content: space-between;
|
532 |
+
background: #7b8379;
|
533 |
+
color: #e2e2c3;
|
534 |
+
}
|
535 |
+
|
536 |
+
.at-controls > div {
|
537 |
+
display: flex;
|
538 |
+
justify-content: flex-start;
|
539 |
+
align-content: center;
|
540 |
+
align-items: center;
|
541 |
+
}
|
542 |
+
|
543 |
+
.at-controls > div > * {
|
544 |
+
display: flex;
|
545 |
+
text-align: center;
|
546 |
+
align-items: center;
|
547 |
+
justify-content: center;
|
548 |
+
cursor: pointer;
|
549 |
+
padding: 4px;
|
550 |
+
margin: 0 3px;
|
551 |
+
}
|
552 |
+
|
553 |
+
.at-controls .btn {
|
554 |
+
color: #e2e2c3;
|
555 |
+
/* border-radius: 0; */
|
556 |
+
height: 40px;
|
557 |
+
width: 40px;
|
558 |
+
font-size: 16px;
|
559 |
+
}
|
560 |
+
|
561 |
+
.at-controls .active {
|
562 |
+
background-color: #85b379;
|
563 |
+
/* text-decoration: none; */
|
564 |
+
}
|
565 |
+
|
566 |
+
.at-controls select {
|
567 |
+
-moz-appearance: none;
|
568 |
+
-webkit-appearance: none;
|
569 |
+
appearance: none;
|
570 |
+
border: none;
|
571 |
+
width: 100%;
|
572 |
+
height: 40px;
|
573 |
+
background: #85b379;
|
574 |
+
padding: 4px 10px;
|
575 |
+
color: #e2e2c3;
|
576 |
+
font-size: 16px;
|
577 |
+
text-align-last: center;
|
578 |
+
text-align: center;
|
579 |
+
-ms-text-align-last: center;
|
580 |
+
-moz-text-align-last: center;
|
581 |
+
cursor: pointer;
|
582 |
+
}
|
583 |
+
|
584 |
+
/* .at-song-title {
|
585 |
+
font-weight: bold;
|
586 |
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
587 |
+
} */
|
588 |
+
|
589 |
+
.at-cursor-bar {
|
590 |
+
/* Defines the color of the bar background when a bar is played */
|
591 |
+
background: rgba(255, 242, 0, 0.25);
|
592 |
+
}
|
593 |
+
|
594 |
+
.at-selection div {
|
595 |
+
/* Defines the color of the selection background */
|
596 |
+
background: rgba(64, 64, 255, 0.1);
|
597 |
+
}
|
598 |
+
|
599 |
+
.at-cursor-beat {
|
600 |
+
/* Defines the beat cursor */
|
601 |
+
background: rgba(64, 64, 255, 0.75);
|
602 |
+
width: 3px;
|
603 |
+
}
|
604 |
+
|
605 |
+
.at-highlight * {
|
606 |
+
/* Defines the color of the music symbols when they are being played (svg) */
|
607 |
+
fill: #0078ff;
|
608 |
+
stroke: #0078ff;
|
609 |
+
}
|
610 |
+
</style>
|
frontend/src/components/AlphaTexWidget.vue
ADDED
@@ -0,0 +1,554 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script setup>
|
2 |
+
import { computed, onMounted, onUnmounted, onUpdated, ref, toRaw, watch } from 'vue'
|
3 |
+
import { Pane } from 'tweakpane'
|
4 |
+
import formatDuration from '@/utils/timeUtils.js'
|
5 |
+
|
6 |
+
const props = defineProps({
|
7 |
+
// int id
|
8 |
+
id: {
|
9 |
+
type: String,
|
10 |
+
required: true,
|
11 |
+
},
|
12 |
+
alphaTex: {
|
13 |
+
type: String,
|
14 |
+
required: false,
|
15 |
+
},
|
16 |
+
isActive: {
|
17 |
+
type: Boolean,
|
18 |
+
required: false,
|
19 |
+
default: false,
|
20 |
+
},
|
21 |
+
|
22 |
+
})
|
23 |
+
|
24 |
+
const emit = defineEmits(['updateScoreCode'])
|
25 |
+
|
26 |
+
// const openSheetMusicDisplay = null
|
27 |
+
// const timeoutID = null
|
28 |
+
let pane = null
|
29 |
+
let api = null
|
30 |
+
let main = null
|
31 |
+
let wrapper = null
|
32 |
+
let settings = null
|
33 |
+
let timeoutID = null
|
34 |
+
|
35 |
+
// const message = ref('hello')
|
36 |
+
const currentScoreCode = ref(props.alphaTex)
|
37 |
+
const showParsingError = ref(false)
|
38 |
+
const parsingError = ref('')
|
39 |
+
// const count = ref(0)
|
40 |
+
const settingsStatus = ref(false)
|
41 |
+
const isActive = ref(false)
|
42 |
+
const wrapRef = ref(null)
|
43 |
+
const mainRef = ref(null)
|
44 |
+
const viewPortRef = ref(null)
|
45 |
+
const paneContainer = ref(null)
|
46 |
+
|
47 |
+
const tracks = ref([])
|
48 |
+
const songPosition = ref('00:00 / 00:00')
|
49 |
+
const previousTime = -1
|
50 |
+
const title = ref('')
|
51 |
+
const artist = ref('')
|
52 |
+
const isPlaying = ref(false)
|
53 |
+
const selectedLayout = ref('page')
|
54 |
+
const isZoomMenuVisible = ref(false)
|
55 |
+
const selectedZoom = ref(100)
|
56 |
+
const zoomOptions = [
|
57 |
+
{ value: '25', label: '25%' },
|
58 |
+
{ value: '50', label: '50%' },
|
59 |
+
{ value: '75', label: '75%' },
|
60 |
+
{ value: '90', label: '90%' },
|
61 |
+
{ value: '100', label: '100%' },
|
62 |
+
{ value: '110', label: '110%' },
|
63 |
+
{ value: '125', label: '125%' },
|
64 |
+
{ value: '150', label: '150%' },
|
65 |
+
{ value: '200', label: '200%' },
|
66 |
+
]
|
67 |
+
const metronomeIsActive = ref(false)
|
68 |
+
// const loopIsActive = ref(false)
|
69 |
+
|
70 |
+
const SETTINGS_PARAMS = {
|
71 |
+
scale: 1.0,
|
72 |
+
// width: 800,
|
73 |
+
// factor: 123,
|
74 |
+
// title: 'hello',
|
75 |
+
// color: '#ff0055',
|
76 |
+
// percentage: 50,
|
77 |
+
// theme: 'dark',
|
78 |
+
// prop: 'Put your\nmultiline\ntext here!'
|
79 |
+
}
|
80 |
+
|
81 |
+
// const divId = computed(() => {
|
82 |
+
// return `xmlScoreContainer_${props.id}`
|
83 |
+
// })
|
84 |
+
|
85 |
+
function initializePane() {
|
86 |
+
pane = new Pane({
|
87 |
+
container: paneContainer.value,
|
88 |
+
title: 'MusicXML Settings',
|
89 |
+
},
|
90 |
+
)
|
91 |
+
pane.addBinding(SETTINGS_PARAMS, 'scale', {
|
92 |
+
min: 0.1,
|
93 |
+
max: 2.0,
|
94 |
+
step: 0.1,
|
95 |
+
}).on('change', (ev) => {
|
96 |
+
console.log(ev.value)
|
97 |
+
api.settings.display.scale = ev.value
|
98 |
+
api.updateSettings()
|
99 |
+
api.render()
|
100 |
+
})
|
101 |
+
}
|
102 |
+
|
103 |
+
onUnmounted(() => {
|
104 |
+
console.log('onUnmounted')
|
105 |
+
// pane = null
|
106 |
+
api = null
|
107 |
+
main = null
|
108 |
+
wrapper = null
|
109 |
+
settings = null
|
110 |
+
})
|
111 |
+
|
112 |
+
onMounted(() => {
|
113 |
+
// console.log($(`#${divId.value}`)[0])
|
114 |
+
initializePane()
|
115 |
+
// wrapper = document.querySelector('.at-wrap')
|
116 |
+
// main = wrapper.querySelector('.at-main')
|
117 |
+
wrapper = wrapRef.value
|
118 |
+
main = mainRef.value
|
119 |
+
// initialize alphatab
|
120 |
+
settings = {
|
121 |
+
// file: 'https://www.alphatab.net/files/canon.gp',
|
122 |
+
// file: '/Fantasia_no.2.musicxml.gpx',
|
123 |
+
player: {
|
124 |
+
enablePlayer: true,
|
125 |
+
soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2',
|
126 |
+
scrollElement: viewPortRef.value,
|
127 |
+
|
128 |
+
},
|
129 |
+
}
|
130 |
+
initAlphaTab()
|
131 |
+
})
|
132 |
+
|
133 |
+
function renderTrack(track) {
|
134 |
+
// Assuming you have an API method to render tracks
|
135 |
+
// Example: api.renderTracks([track]);
|
136 |
+
api.renderTracks([toRaw(track)])
|
137 |
+
}
|
138 |
+
|
139 |
+
function updateLayout() {
|
140 |
+
switch (selectedLayout.value) {
|
141 |
+
case 'horizontal':
|
142 |
+
api.settings.display.layoutMode = alphaTab.LayoutMode.Horizontal
|
143 |
+
break
|
144 |
+
case 'page':
|
145 |
+
api.settings.display.layoutMode = alphaTab.LayoutMode.Page
|
146 |
+
break
|
147 |
+
}
|
148 |
+
api.updateSettings()
|
149 |
+
api.render()
|
150 |
+
}
|
151 |
+
|
152 |
+
// function toggleZoomMenu() {
|
153 |
+
// isZoomMenuVisible.value = !isZoomMenuVisible.value
|
154 |
+
// }
|
155 |
+
|
156 |
+
function updateZoom() {
|
157 |
+
// Handle zoom update logic here
|
158 |
+
// const zoomLevel = Number.parseInt(zoom.value) / 100
|
159 |
+
api.settings.display.scale = selectedZoom.value / 100
|
160 |
+
api.updateSettings()
|
161 |
+
api.render()
|
162 |
+
}
|
163 |
+
|
164 |
+
function scoreExport() {
|
165 |
+
const exporter = new alphaTab.exporter.Gp7Exporter()
|
166 |
+
const data = exporter.export(api.score, api.settings)
|
167 |
+
|
168 |
+
// trigger download
|
169 |
+
const a = document.createElement('a')
|
170 |
+
a.download = api.score.title.length > 0 ? `${api.score.title}.gp` : 'Untitled.gp'
|
171 |
+
a.href = URL.createObjectURL(new Blob([data]))
|
172 |
+
document.body.appendChild(a)
|
173 |
+
a.click()
|
174 |
+
document.body.removeChild(a)
|
175 |
+
}
|
176 |
+
|
177 |
+
function toggleMetronome() {
|
178 |
+
metronomeIsActive.value = !metronomeIsActive.value
|
179 |
+
if (metronomeIsActive.value)
|
180 |
+
api.metronomeVolume = 1
|
181 |
+
|
182 |
+
else
|
183 |
+
api.metronomeVolume = 0
|
184 |
+
}
|
185 |
+
|
186 |
+
function handleKeyUp(event) {
|
187 |
+
if (timeoutID)
|
188 |
+
clearTimeout(timeoutID)
|
189 |
+
timeoutID = setTimeout(showScore, 500)
|
190 |
+
}
|
191 |
+
|
192 |
+
// function handleDrop(event) {
|
193 |
+
// event.preventDefault()
|
194 |
+
// const textData = event.dataTransfer.getData('text')
|
195 |
+
// const file = event.dataTransfer.files[0]
|
196 |
+
// console.log(textData)
|
197 |
+
// console.log(file)
|
198 |
+
// if (file) {
|
199 |
+
// const reader = new FileReader()
|
200 |
+
// reader.onload = function (e) {
|
201 |
+
// currentScoreCode.value = e.target.result
|
202 |
+
// showScore()
|
203 |
+
// }
|
204 |
+
// reader.readAsText(file)
|
205 |
+
// }
|
206 |
+
// else if (textData) {
|
207 |
+
// currentScoreCode.value = textData
|
208 |
+
// showScore()
|
209 |
+
// }
|
210 |
+
// }
|
211 |
+
|
212 |
+
function showScore() {
|
213 |
+
try {
|
214 |
+
api.tex(currentScoreCode.value)
|
215 |
+
showParsingError.value = false
|
216 |
+
}
|
217 |
+
catch (e) {
|
218 |
+
showParsingError.value = true
|
219 |
+
console.log(e.message.replace(/(?:\r\n|\r|\n)/g, '<br>'))
|
220 |
+
parsingError.value = e.message.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
221 |
+
}
|
222 |
+
}
|
223 |
+
|
224 |
+
function initAlphaTab() {
|
225 |
+
console.log('initAlphaTab')
|
226 |
+
api = new alphaTab.AlphaTabApi(main, settings)
|
227 |
+
// api.tex(`
|
228 |
+
// \\title 'Test' . 3.3.4
|
229 |
+
// :4 2.3 3.3 :8 3.3 4.3 3.3 4.3 |
|
230 |
+
// `)
|
231 |
+
api.tex(currentScoreCode.value)
|
232 |
+
|
233 |
+
// overlay logic
|
234 |
+
const overlay = wrapper.querySelector('.at-overlay')
|
235 |
+
api.renderStarted.on(() => {
|
236 |
+
overlay.style.display = 'flex'
|
237 |
+
})
|
238 |
+
api.renderFinished.on(() => {
|
239 |
+
overlay.style.display = 'none'
|
240 |
+
})
|
241 |
+
|
242 |
+
// track selector
|
243 |
+
api.scoreLoaded.on((score) => {
|
244 |
+
tracks.value = score.tracks
|
245 |
+
})
|
246 |
+
|
247 |
+
api.renderStarted.on(() => {
|
248 |
+
})
|
249 |
+
|
250 |
+
/** Controls */
|
251 |
+
api.scoreLoaded.on((score) => {
|
252 |
+
title.value = score.title
|
253 |
+
artist.value = score.artist
|
254 |
+
})
|
255 |
+
|
256 |
+
api.playerStateChanged.on((e) => {
|
257 |
+
isPlaying.value = e.state
|
258 |
+
})
|
259 |
+
api.playerPositionChanged.on((e) => {
|
260 |
+
// reduce number of UI updates to second changes.
|
261 |
+
const currentSeconds = (e.currentTime / 1000) | 0
|
262 |
+
if (currentSeconds > previousTime)
|
263 |
+
songPosition.value = `${formatDuration(e.currentTime)} / ${formatDuration(e.endTime)}`
|
264 |
+
})
|
265 |
+
}
|
266 |
+
|
267 |
+
function showHideSettings() {
|
268 |
+
console.log('showHideSettings')
|
269 |
+
settingsStatus.value = !settingsStatus.value
|
270 |
+
}
|
271 |
+
|
272 |
+
// invoked when there is any type of change in the component
|
273 |
+
onUpdated(() => {
|
274 |
+
if (props.isActive !== isActive.value) {
|
275 |
+
// Prop has changed, update the ref and emit an event
|
276 |
+
if (props.isActive) {
|
277 |
+
isActive.value = props.isActive
|
278 |
+
emit('updateScoreCode', currentScoreCode.value)
|
279 |
+
// console.log("emitted updateScoreCode");
|
280 |
+
// console.log(currentScoreCode.value);
|
281 |
+
}
|
282 |
+
isActive.value = props.isActive
|
283 |
+
}
|
284 |
+
})
|
285 |
+
|
286 |
+
// watch(currentScoreCode, () => {
|
287 |
+
// // Emit the event so that the parent component can update the tabText
|
288 |
+
// // prop
|
289 |
+
// emit('updateScoreCode', currentScoreCode.value)
|
290 |
+
// // console.log("emitted updateScoreCode");
|
291 |
+
// // console.log(currentScoreCode.value);
|
292 |
+
// })
|
293 |
+
watch(currentScoreCode, () => {
|
294 |
+
// Emit the event so that the parent component can update the tabText
|
295 |
+
// prop
|
296 |
+
emit('updateScoreCode', currentScoreCode.value)
|
297 |
+
console.log('emitted updateScoreCode')
|
298 |
+
// this should go to the first tab that is shown (for now vextab)
|
299 |
+
}, { immediate: true })
|
300 |
+
|
301 |
+
// const moveCursor = () => {
|
302 |
+
// openSheetMusicDisplay.cursor.next();
|
303 |
+
// };
|
304 |
+
|
305 |
+
// const fillScore = () => {
|
306 |
+
// // console.log(vt);
|
307 |
+
// // Perform any logic to fill the score
|
308 |
+
// response.value = 'Button pressed!'; // Update response if needed
|
309 |
+
|
310 |
+
// };
|
311 |
+
</script>
|
312 |
+
|
313 |
+
<template>
|
314 |
+
<div>
|
315 |
+
<div
|
316 |
+
ref="wrapRef"
|
317 |
+
class="at-wrap"
|
318 |
+
>
|
319 |
+
<div class="at-overlay">
|
320 |
+
<div class="at-overlay-content">
|
321 |
+
Music sheet is loading
|
322 |
+
</div>
|
323 |
+
</div>
|
324 |
+
<div class="at-content">
|
325 |
+
<div class="at-sidebar">
|
326 |
+
<div class="at-track-list">
|
327 |
+
<div v-for="track in tracks" :key="track.index" class="at-track" @click="renderTrack(track)">
|
328 |
+
<div class="at-track-icon">
|
329 |
+
<i class="i-fa:music" />
|
330 |
+
</div>
|
331 |
+
<div class="at-track-details">
|
332 |
+
<div class="at-track-name">
|
333 |
+
{{ track.name }}
|
334 |
+
</div>
|
335 |
+
</div>
|
336 |
+
</div>
|
337 |
+
</div>
|
338 |
+
</div>
|
339 |
+
<div ref="viewPortRef" class="at-viewport">
|
340 |
+
<div ref="mainRef" class="at-main" />
|
341 |
+
</div>
|
342 |
+
</div>
|
343 |
+
<div class="at-controls">
|
344 |
+
<div class="at-controls-left">
|
345 |
+
<button class="btn i-fad:rew" @click="api.stop()" />
|
346 |
+
<button class="btn" :class="{ 'i-fad:play': !isPlaying, 'i-fad:pause': isPlaying }" @click="api.playPause()" />
|
347 |
+
|
348 |
+
<div class="at-song-info">
|
349 |
+
{{ title }} - {{ artist }}
|
350 |
+
</div>
|
351 |
+
<div class="at-song-position">
|
352 |
+
{{ songPosition }}
|
353 |
+
</div>
|
354 |
+
</div>
|
355 |
+
<div class="at-controls-right">
|
356 |
+
<div :class="{ active: metronomeIsActive }">
|
357 |
+
<button class="btn i-fad:metronome" @click="toggleMetronome" />
|
358 |
+
</div>
|
359 |
+
<!-- <div>
|
360 |
+
<button class="btn i-fad:repeat" :class="{ active: loopingIsActive }" @click="toggleLooping" />
|
361 |
+
</div> -->
|
362 |
+
<div :class="{ active: settingsStatus }">
|
363 |
+
<button class="btn i-fa:edit" @click="showHideSettings" />
|
364 |
+
</div>
|
365 |
+
|
366 |
+
<div>
|
367 |
+
<button class="btn i-foundation:print" @click="api.print()" />
|
368 |
+
</div>
|
369 |
+
|
370 |
+
<div>
|
371 |
+
<button class="btn i-foundation:page-export" @click="scoreExport" />
|
372 |
+
</div>
|
373 |
+
|
374 |
+
<div class="at-zoom">
|
375 |
+
<!-- <button class="btn i-fad:zoomin" @click="toggleZoomMenu" /> -->
|
376 |
+
<select v-model="selectedZoom" @change="updateZoom">
|
377 |
+
<option v-for="zoomOption in zoomOptions" :key="zoomOption.value" :value="zoomOption.value">
|
378 |
+
{{ zoomOption.label }}
|
379 |
+
</option>
|
380 |
+
</select>
|
381 |
+
</div>
|
382 |
+
<div class="at-layout">
|
383 |
+
<select v-model="selectedLayout" @change="updateLayout">
|
384 |
+
<option value="horizontal">
|
385 |
+
Horizontal
|
386 |
+
</option>
|
387 |
+
<option value="page">
|
388 |
+
Page
|
389 |
+
</option>
|
390 |
+
</select>
|
391 |
+
</div>
|
392 |
+
</div>
|
393 |
+
</div>
|
394 |
+
</div>
|
395 |
+
<div v-show="settingsStatus" id="tabSettingsArea">
|
396 |
+
<div ref="paneContainer" />
|
397 |
+
<textarea
|
398 |
+
v-model="currentScoreCode"
|
399 |
+
class="editorBox"
|
400 |
+
placeholder="add your musicxml code here or drag a file"
|
401 |
+
@keyup="handleKeyUp"
|
402 |
+
@drop="handleDrop"
|
403 |
+
@dragover.prevent
|
404 |
+
/>
|
405 |
+
</div>
|
406 |
+
<div v-if="showParsingError" class="parsing-error" v-html="parsingError" />
|
407 |
+
</div>
|
408 |
+
|
409 |
+
<!-- </div> -->
|
410 |
+
</template>
|
411 |
+
|
412 |
+
<style scoped>
|
413 |
+
#tabSettingsArea {
|
414 |
+
display: flex;
|
415 |
+
flex-direction: row;
|
416 |
+
align-items: center;
|
417 |
+
gap: 20px;
|
418 |
+
margin-left: 10px;
|
419 |
+
}
|
420 |
+
.at-wrap {
|
421 |
+
width: 50vw;
|
422 |
+
height: 50vh;
|
423 |
+
margin: 0 auto;
|
424 |
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
425 |
+
display: flex;
|
426 |
+
flex-direction: column;
|
427 |
+
overflow: hidden;
|
428 |
+
position: relative;
|
429 |
+
}
|
430 |
+
.at-content {
|
431 |
+
position: relative;
|
432 |
+
overflow: hidden;
|
433 |
+
flex: 1 1 auto;
|
434 |
+
display: flex;
|
435 |
+
flex-direction: row;
|
436 |
+
}
|
437 |
+
.at-viewport {
|
438 |
+
flex: 100%;
|
439 |
+
overflow-y: scroll;
|
440 |
+
/* position: absolute;
|
441 |
+
top: 0;
|
442 |
+
left: 70px;
|
443 |
+
right: 0;
|
444 |
+
bottom: 0;
|
445 |
+
padding-right: 20px; */
|
446 |
+
}
|
447 |
+
|
448 |
+
.at-sidebar {
|
449 |
+
/* position: absolute;
|
450 |
+
top: 0;
|
451 |
+
left: 0;
|
452 |
+
bottom: 0;
|
453 |
+
max-width: 70px;
|
454 |
+
width: auto; */
|
455 |
+
flex: 0%;
|
456 |
+
display: flex;
|
457 |
+
align-content: stretch;
|
458 |
+
justify-content: center;
|
459 |
+
max-width: 70px;
|
460 |
+
z-index: 1001;
|
461 |
+
overflow: hidden;
|
462 |
+
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
463 |
+
background: #862b2b;
|
464 |
+
|
465 |
+
}
|
466 |
+
|
467 |
+
.at-track-list {
|
468 |
+
display: flex;
|
469 |
+
flex-direction: column;
|
470 |
+
}
|
471 |
+
|
472 |
+
.at-controls {
|
473 |
+
/* flex: 0 0 auto; */
|
474 |
+
display: flex;
|
475 |
+
justify-content: space-between;
|
476 |
+
background: #329e17;
|
477 |
+
color: #e2e2c3;
|
478 |
+
}
|
479 |
+
|
480 |
+
.at-controls > div {
|
481 |
+
display: flex;
|
482 |
+
justify-content: flex-start;
|
483 |
+
align-content: center;
|
484 |
+
align-items: center;
|
485 |
+
}
|
486 |
+
|
487 |
+
.at-controls > div > * {
|
488 |
+
display: flex;
|
489 |
+
text-align: center;
|
490 |
+
align-items: center;
|
491 |
+
justify-content: center;
|
492 |
+
cursor: pointer;
|
493 |
+
padding: 4px;
|
494 |
+
margin: 0 3px;
|
495 |
+
}
|
496 |
+
|
497 |
+
.at-controls .btn {
|
498 |
+
color: #e2e2c3;
|
499 |
+
/* border-radius: 0; */
|
500 |
+
height: 40px;
|
501 |
+
width: 40px;
|
502 |
+
font-size: 16px;
|
503 |
+
}
|
504 |
+
|
505 |
+
.at-controls .active {
|
506 |
+
background-color: #5bbb43;
|
507 |
+
/* text-decoration: none; */
|
508 |
+
}
|
509 |
+
|
510 |
+
.at-controls select {
|
511 |
+
-moz-appearance: none;
|
512 |
+
-webkit-appearance: none;
|
513 |
+
appearance: none;
|
514 |
+
border: none;
|
515 |
+
width: 100%;
|
516 |
+
height: 40px;
|
517 |
+
background: #5bbb43;
|
518 |
+
padding: 4px 10px;
|
519 |
+
color: #e2e2c3;
|
520 |
+
font-size: 16px;
|
521 |
+
text-align-last: center;
|
522 |
+
text-align: center;
|
523 |
+
-ms-text-align-last: center;
|
524 |
+
-moz-text-align-last: center;
|
525 |
+
cursor: pointer;
|
526 |
+
}
|
527 |
+
|
528 |
+
/* .at-song-title {
|
529 |
+
font-weight: bold;
|
530 |
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
531 |
+
} */
|
532 |
+
|
533 |
+
.at-cursor-bar {
|
534 |
+
/* Defines the color of the bar background when a bar is played */
|
535 |
+
background: rgba(255, 242, 0, 0.25);
|
536 |
+
}
|
537 |
+
|
538 |
+
.at-selection div {
|
539 |
+
/* Defines the color of the selection background */
|
540 |
+
background: rgba(64, 64, 255, 0.1);
|
541 |
+
}
|
542 |
+
|
543 |
+
.at-cursor-beat {
|
544 |
+
/* Defines the beat cursor */
|
545 |
+
background: rgba(64, 64, 255, 0.75);
|
546 |
+
width: 3px;
|
547 |
+
}
|
548 |
+
|
549 |
+
.at-highlight * {
|
550 |
+
/* Defines the color of the music symbols when they are being played (svg) */
|
551 |
+
fill: #0078ff;
|
552 |
+
stroke: #0078ff;
|
553 |
+
}
|
554 |
+
</style>
|
frontend/src/components/MusicXmlWidget2.vue
ADDED
@@ -0,0 +1,559 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script setup>
|
2 |
+
import { computed, onMounted, onUnmounted, onUpdated, ref, toRaw, watch } from 'vue'
|
3 |
+
import { Pane } from 'tweakpane'
|
4 |
+
import { OpenSheetMusicDisplay } from 'opensheetmusicdisplay'
|
5 |
+
import formatDuration from '@/utils/timeUtils.js'
|
6 |
+
|
7 |
+
const props = defineProps({
|
8 |
+
// int id
|
9 |
+
id: {
|
10 |
+
type: String,
|
11 |
+
required: true,
|
12 |
+
},
|
13 |
+
// xmlText: {
|
14 |
+
// type: String,
|
15 |
+
// required: false,
|
16 |
+
// },
|
17 |
+
xmlText: {
|
18 |
+
type: String,
|
19 |
+
required: false,
|
20 |
+
},
|
21 |
+
isActive: {
|
22 |
+
type: Boolean,
|
23 |
+
required: false,
|
24 |
+
default: false,
|
25 |
+
},
|
26 |
+
|
27 |
+
})
|
28 |
+
|
29 |
+
const emit = defineEmits(['updateScoreCode'])
|
30 |
+
|
31 |
+
let pane = null
|
32 |
+
let api = null
|
33 |
+
let main = null
|
34 |
+
let wrapper = null
|
35 |
+
let settings = null
|
36 |
+
let timeoutID = null
|
37 |
+
let openSheetMusicDisplay = null
|
38 |
+
|
39 |
+
// const message = ref('hello')
|
40 |
+
const currentScoreCode = ref(props.xmlText)
|
41 |
+
const showParsingError = ref(false)
|
42 |
+
const parsingError = ref('')
|
43 |
+
// const count = ref(0)
|
44 |
+
const settingsStatus = ref(false)
|
45 |
+
const isActive = ref(false)
|
46 |
+
const wrapRef = ref(null)
|
47 |
+
const mainRef = ref(null)
|
48 |
+
const viewPortRef = ref(null)
|
49 |
+
const paneContainer = ref(null)
|
50 |
+
|
51 |
+
const tracks = ref([])
|
52 |
+
const songPosition = ref('00:00 / 00:00')
|
53 |
+
const previousTime = -1
|
54 |
+
const title = ref('')
|
55 |
+
const artist = ref('')
|
56 |
+
const isPlaying = ref(false)
|
57 |
+
const selectedLayout = ref('page')
|
58 |
+
const isZoomMenuVisible = ref(false)
|
59 |
+
const selectedZoom = ref(100)
|
60 |
+
const zoomOptions = [
|
61 |
+
{ value: '25', label: '25%' },
|
62 |
+
{ value: '50', label: '50%' },
|
63 |
+
{ value: '75', label: '75%' },
|
64 |
+
{ value: '90', label: '90%' },
|
65 |
+
{ value: '100', label: '100%' },
|
66 |
+
{ value: '110', label: '110%' },
|
67 |
+
{ value: '125', label: '125%' },
|
68 |
+
{ value: '150', label: '150%' },
|
69 |
+
{ value: '200', label: '200%' },
|
70 |
+
]
|
71 |
+
const metronomeIsActive = ref(false)
|
72 |
+
// const loopIsActive = ref(false)
|
73 |
+
|
74 |
+
const SETTINGS_PARAMS = {
|
75 |
+
scale: 1.0,
|
76 |
+
// width: 800,
|
77 |
+
// factor: 123,
|
78 |
+
// title: 'hello',
|
79 |
+
// color: '#ff0055',
|
80 |
+
// percentage: 50,
|
81 |
+
// theme: 'dark',
|
82 |
+
// prop: 'Put your\nmultiline\ntext here!'
|
83 |
+
}
|
84 |
+
|
85 |
+
// const divId = computed(() => {
|
86 |
+
// return `xmlScoreContainer_${props.id}`
|
87 |
+
// })
|
88 |
+
|
89 |
+
function initializePane() {
|
90 |
+
pane = new Pane({
|
91 |
+
container: paneContainer.value,
|
92 |
+
title: 'MusicXML Settings',
|
93 |
+
},
|
94 |
+
)
|
95 |
+
pane.addBinding(SETTINGS_PARAMS, 'scale', {
|
96 |
+
min: 0.1,
|
97 |
+
max: 2.0,
|
98 |
+
step: 0.1,
|
99 |
+
}).on('change', (ev) => {
|
100 |
+
console.log(ev.value)
|
101 |
+
openSheetMusicDisplay.zoom = ev.value
|
102 |
+
openSheetMusicDisplay.render()
|
103 |
+
})
|
104 |
+
}
|
105 |
+
|
106 |
+
onUnmounted(() => {
|
107 |
+
console.log('onUnmounted')
|
108 |
+
// pane = null
|
109 |
+
api = null
|
110 |
+
main = null
|
111 |
+
wrapper = null
|
112 |
+
settings = null
|
113 |
+
})
|
114 |
+
|
115 |
+
onMounted(async () => {
|
116 |
+
// console.log($(`#${divId.value}`)[0])
|
117 |
+
initializePane()
|
118 |
+
// wrapper = document.querySelector('.at-wrap')
|
119 |
+
// main = wrapper.querySelector('.at-main')
|
120 |
+
wrapper = wrapRef.value
|
121 |
+
main = mainRef.value
|
122 |
+
// initialize alphatab
|
123 |
+
// settings = {
|
124 |
+
// // file: 'https://www.alphatab.net/files/canon.gp',
|
125 |
+
// // file: '/Fantasia_no.2.musicxml.gpx',
|
126 |
+
// player: {
|
127 |
+
// enablePlayer: true,
|
128 |
+
// soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2',
|
129 |
+
// scrollElement: viewPortRef.value,
|
130 |
+
|
131 |
+
// },
|
132 |
+
// }
|
133 |
+
await initOSMD()
|
134 |
+
})
|
135 |
+
|
136 |
+
function updateLayout() {
|
137 |
+
switch (selectedLayout.value) {
|
138 |
+
case 'horizontal':
|
139 |
+
openSheetMusicDisplay.renderSingleHorizontalStaffline = true
|
140 |
+
break
|
141 |
+
case 'page':
|
142 |
+
openSheetMusicDisplay.renderSingleHorizontalStaffline = false
|
143 |
+
break
|
144 |
+
}
|
145 |
+
openSheetMusicDisplay.render()
|
146 |
+
}
|
147 |
+
|
148 |
+
function updateZoom() {
|
149 |
+
// Handle zoom update logic here
|
150 |
+
// const zoomLevel = Number.parseInt(zoom.value) / 100
|
151 |
+
openSheetMusicDisplay.zoom = selectedZoom.value / 100
|
152 |
+
openSheetMusicDisplay.render()
|
153 |
+
}
|
154 |
+
|
155 |
+
function handleKeyUp(event) {
|
156 |
+
if (timeoutID)
|
157 |
+
clearTimeout(timeoutID)
|
158 |
+
timeoutID = setTimeout(showScore, 500)
|
159 |
+
}
|
160 |
+
|
161 |
+
function handleDrop(event) {
|
162 |
+
event.preventDefault()
|
163 |
+
const textData = event.dataTransfer.getData('text')
|
164 |
+
const file = event.dataTransfer.files[0]
|
165 |
+
console.log(textData)
|
166 |
+
console.log(file)
|
167 |
+
if (file) {
|
168 |
+
const reader = new FileReader()
|
169 |
+
reader.onload = function (e) {
|
170 |
+
currentScoreCode.value = e.target.result
|
171 |
+
showScore()
|
172 |
+
}
|
173 |
+
reader.readAsText(file)
|
174 |
+
}
|
175 |
+
else if (textData) {
|
176 |
+
currentScoreCode.value = textData
|
177 |
+
showScore()
|
178 |
+
}
|
179 |
+
}
|
180 |
+
|
181 |
+
function showScore() {
|
182 |
+
openSheetMusicDisplay.clear()
|
183 |
+
console.log(currentScoreCode.value)
|
184 |
+
openSheetMusicDisplay.load(currentScoreCode.value)
|
185 |
+
.then(
|
186 |
+
() => {
|
187 |
+
window.osmd = openSheetMusicDisplay
|
188 |
+
openSheetMusicDisplay.render()
|
189 |
+
// openSheetMusicDisplay.cursor.show();
|
190 |
+
console.log('in load')
|
191 |
+
showParsingError.value = false
|
192 |
+
},
|
193 |
+
)
|
194 |
+
.catch((e) => {
|
195 |
+
showParsingError.value = true
|
196 |
+
console.log(e.message.replace(/(?:\r\n|\r|\n)/g, '<br>'))
|
197 |
+
parsingError.value = e.message.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
198 |
+
})
|
199 |
+
}
|
200 |
+
|
201 |
+
async function initOSMD() {
|
202 |
+
// renderer = new Flow.Renderer($('#' + divId.value)[0], Flow.Renderer.Backends.SVG);
|
203 |
+
// artist = new Artist(10, 10, 750, { scale: 0.8 });
|
204 |
+
// tab = new VexTab(artist);
|
205 |
+
openSheetMusicDisplay = new OpenSheetMusicDisplay(mainRef.value, {
|
206 |
+
autoResize: true,
|
207 |
+
backend: 'svg',
|
208 |
+
disableCursor: false,
|
209 |
+
drawingParameters: 'full', // : "default", // try compact (instead of default)
|
210 |
+
drawPartNames: false, // try false
|
211 |
+
drawTitle: false,
|
212 |
+
drawSubtitle: false,
|
213 |
+
drawComposer: false,
|
214 |
+
// darkMode: true,
|
215 |
+
// cursor
|
216 |
+
followCursor: true,
|
217 |
+
drawFingerings: true,
|
218 |
+
// fingeringPosition: "left", // Above/Below is default. try left or right. experimental: above, below.
|
219 |
+
// fingeringPositionFromXML: false, // do this if you want them always left, for example.
|
220 |
+
// fingeringInsideStafflines: "true", // default: false. true draws fingerings directly above/below notes
|
221 |
+
setWantedStemDirectionByXml: true, // try false, which was previously the default behavior
|
222 |
+
// drawUpToMeasureNumber: 20, // draws only up to measure 3, meaning it draws measure 1 to 3 of the piece.
|
223 |
+
// drawFromMeasureNumber : 1,
|
224 |
+
// drawUpToMeasureNumber : 3,
|
225 |
+
drawMeasureNumbers: true,
|
226 |
+
|
227 |
+
// drawMeasureNumbers: false, // disable drawing measure numbers
|
228 |
+
// measureNumberInterval: 4, // draw measure numbers only every 4 bars (and at the beginning of a new system)
|
229 |
+
useXMLMeasureNumbers: true, // read measure numbers from xml
|
230 |
+
|
231 |
+
// coloring options
|
232 |
+
coloringEnabled: true,
|
233 |
+
// defaultColorNotehead: "#CC0055", // try setting a default color. default is black (undefined)
|
234 |
+
// defaultColorStem: "#BB0099",
|
235 |
+
|
236 |
+
autoBeam: false, // try true, OSMD Function Test AutoBeam sample
|
237 |
+
autoBeamOptions: {
|
238 |
+
beam_rests: false,
|
239 |
+
beam_middle_rests_only: false,
|
240 |
+
// groups: [[3,4], [1,1]],
|
241 |
+
maintain_stem_directions: false,
|
242 |
+
},
|
243 |
+
// pageFormat: "A4_P",//"Endless",//pageFormat,
|
244 |
+
// pageBackgroundColor: "red",
|
245 |
+
renderSingleHorizontalStaffline: true, // true
|
246 |
+
|
247 |
+
// tupletsBracketed: true, // creates brackets for all tuplets except triplets, even when not set by xml
|
248 |
+
// tripletsBracketed: true,
|
249 |
+
// tupletsRatioed: true, // unconventional; renders ratios for tuplets (3:2 instead of 3 for triplets)
|
250 |
+
})
|
251 |
+
// window.osmd = openSheetMusicDisplay;
|
252 |
+
// artist.reset();
|
253 |
+
// tab.reset();
|
254 |
+
// tab.parse(currentTabText.value);
|
255 |
+
// artist.render(renderer);
|
256 |
+
await fetch('/Fantasia_no.2.musicxml')
|
257 |
+
.then((response) => {
|
258 |
+
return response.text()
|
259 |
+
})
|
260 |
+
.then((data) => {
|
261 |
+
console.log('in data')
|
262 |
+
openSheetMusicDisplay.load(data)
|
263 |
+
.then(
|
264 |
+
() => {
|
265 |
+
window.osmd = openSheetMusicDisplay
|
266 |
+
openSheetMusicDisplay.render()
|
267 |
+
// openSheetMusicDisplay.cursor.show();
|
268 |
+
console.log('in load')
|
269 |
+
showParsingError.value = false
|
270 |
+
currentScoreCode.value = data
|
271 |
+
},
|
272 |
+
)
|
273 |
+
.catch((e) => {
|
274 |
+
showParsingError.value = true
|
275 |
+
console.log(e.message.replace(/(?:\r\n|\r|\n)/g, '<br>'))
|
276 |
+
parsingError.value = e.message.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
277 |
+
})
|
278 |
+
|
279 |
+
// .catch((e) => {
|
280 |
+
// showParsingError.value = true;
|
281 |
+
// console.log(e.message.replace(/(?:\r\n|\r|\n)/g, '<br>'));
|
282 |
+
// parsingError.value = e.message.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
283 |
+
// });
|
284 |
+
})
|
285 |
+
|
286 |
+
// title.value = score.title
|
287 |
+
// artist.value = score.artist
|
288 |
+
}
|
289 |
+
|
290 |
+
function showHideSettings() {
|
291 |
+
console.log('showHideSettings')
|
292 |
+
settingsStatus.value = !settingsStatus.value
|
293 |
+
}
|
294 |
+
|
295 |
+
// invoked when there is any type of change in the component
|
296 |
+
onUpdated(() => {
|
297 |
+
if (props.isActive !== isActive.value) {
|
298 |
+
// Prop has changed, update the ref and emit an event
|
299 |
+
if (props.isActive) {
|
300 |
+
isActive.value = props.isActive
|
301 |
+
emit('updateScoreCode', currentScoreCode.value)
|
302 |
+
// console.log("emitted updateScoreCode");
|
303 |
+
// console.log(currentScoreCode.value);
|
304 |
+
}
|
305 |
+
isActive.value = props.isActive
|
306 |
+
}
|
307 |
+
})
|
308 |
+
|
309 |
+
// watch(currentScoreCode, () => {
|
310 |
+
// // Emit the event so that the parent component can update the tabText
|
311 |
+
// // prop
|
312 |
+
// emit('updateScoreCode', currentScoreCode.value)
|
313 |
+
// // console.log("emitted updateScoreCode");
|
314 |
+
// // console.log(currentScoreCode.value);
|
315 |
+
// })
|
316 |
+
watch(currentScoreCode, () => {
|
317 |
+
// Emit the event so that the parent component can update the tabText
|
318 |
+
// prop
|
319 |
+
emit('updateScoreCode', currentScoreCode.value)
|
320 |
+
console.log('emitted updateScoreCode')
|
321 |
+
// this should go to the first tab that is shown (for now vextab)
|
322 |
+
}, { immediate: true })
|
323 |
+
|
324 |
+
// const moveCursor = () => {
|
325 |
+
// openSheetMusicDisplay.cursor.next();
|
326 |
+
// };
|
327 |
+
|
328 |
+
// const fillScore = () => {
|
329 |
+
// // console.log(vt);
|
330 |
+
// // Perform any logic to fill the score
|
331 |
+
// response.value = 'Button pressed!'; // Update response if needed
|
332 |
+
|
333 |
+
// };
|
334 |
+
</script>
|
335 |
+
|
336 |
+
<template>
|
337 |
+
<div>
|
338 |
+
<div
|
339 |
+
ref="wrapRef"
|
340 |
+
class="at-wrap"
|
341 |
+
>
|
342 |
+
<!-- <div class="at-overlay">
|
343 |
+
<div class="at-overlay-content">
|
344 |
+
Music sheet is loading
|
345 |
+
</div>
|
346 |
+
</div> -->
|
347 |
+
<div class="at-content">
|
348 |
+
<!-- <div class="at-sidebar">
|
349 |
+
<div class="at-track-list">
|
350 |
+
<div v-for="track in tracks" :key="track.index" class="at-track" @click="renderTrack(track)">
|
351 |
+
<div class="at-track-icon">
|
352 |
+
<i class="i-fa:music" />
|
353 |
+
</div>
|
354 |
+
<div class="at-track-details">
|
355 |
+
<div class="at-track-name">
|
356 |
+
{{ track.name }}
|
357 |
+
</div>
|
358 |
+
</div>
|
359 |
+
</div>
|
360 |
+
</div>
|
361 |
+
</div> -->
|
362 |
+
<div ref="viewPortRef" class="at-viewport">
|
363 |
+
<div ref="mainRef" class="at-main" />
|
364 |
+
</div>
|
365 |
+
</div>
|
366 |
+
<div class="at-controls">
|
367 |
+
<div class="at-controls-left">
|
368 |
+
<div class="at-song-info">
|
369 |
+
{{ title }} - {{ artist }}
|
370 |
+
</div>
|
371 |
+
</div>
|
372 |
+
<div class="at-controls-right">
|
373 |
+
<div :class="{ active: settingsStatus }">
|
374 |
+
<button class="btn i-fa:edit" @click="showHideSettings" />
|
375 |
+
</div>
|
376 |
+
|
377 |
+
<!-- <div>
|
378 |
+
<button class="btn i-foundation:print" @click="api.print()" />
|
379 |
+
</div>
|
380 |
+
|
381 |
+
<div>
|
382 |
+
<button class="btn i-foundation:page-export" @click="scoreExport" />
|
383 |
+
</div> -->
|
384 |
+
|
385 |
+
<div class="at-zoom">
|
386 |
+
<!-- <button class="btn i-fad:zoomin" @click="toggleZoomMenu" /> -->
|
387 |
+
<select v-model="selectedZoom" @change="updateZoom">
|
388 |
+
<option v-for="zoomOption in zoomOptions" :key="zoomOption.value" :value="zoomOption.value">
|
389 |
+
{{ zoomOption.label }}
|
390 |
+
</option>
|
391 |
+
</select>
|
392 |
+
</div>
|
393 |
+
<div class="at-layout">
|
394 |
+
<select v-model="selectedLayout" @change="updateLayout">
|
395 |
+
<option value="horizontal">
|
396 |
+
Horizontal
|
397 |
+
</option>
|
398 |
+
<option value="page">
|
399 |
+
Page
|
400 |
+
</option>
|
401 |
+
</select>
|
402 |
+
</div>
|
403 |
+
</div>
|
404 |
+
</div>
|
405 |
+
</div>
|
406 |
+
<div v-show="settingsStatus" id="tabSettingsArea">
|
407 |
+
<div ref="paneContainer" />
|
408 |
+
<textarea
|
409 |
+
v-model="currentScoreCode"
|
410 |
+
class="editorBox"
|
411 |
+
placeholder="add your musicxml code here or drag a file"
|
412 |
+
@keyup="handleKeyUp"
|
413 |
+
@drop="handleDrop"
|
414 |
+
@dragover.prevent
|
415 |
+
/>
|
416 |
+
</div>
|
417 |
+
<div v-if="showParsingError" class="parsing-error" v-html="parsingError" />
|
418 |
+
</div>
|
419 |
+
|
420 |
+
<!-- </div> -->
|
421 |
+
</template>
|
422 |
+
|
423 |
+
<style scoped>
|
424 |
+
#tabSettingsArea {
|
425 |
+
display: flex;
|
426 |
+
flex-direction: row;
|
427 |
+
align-items: center;
|
428 |
+
gap: 20px;
|
429 |
+
margin-left: 10px;
|
430 |
+
}
|
431 |
+
.at-wrap {
|
432 |
+
width: 50vw;
|
433 |
+
height: 50vh;
|
434 |
+
margin: 0 auto;
|
435 |
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
436 |
+
display: flex;
|
437 |
+
flex-direction: column;
|
438 |
+
overflow: hidden;
|
439 |
+
position: relative;
|
440 |
+
}
|
441 |
+
.at-content {
|
442 |
+
position: relative;
|
443 |
+
overflow: hidden;
|
444 |
+
flex: 1 1 auto;
|
445 |
+
display: flex;
|
446 |
+
flex-direction: row;
|
447 |
+
}
|
448 |
+
.at-viewport {
|
449 |
+
flex: 100%;
|
450 |
+
overflow-y: scroll;
|
451 |
+
/* position: absolute;
|
452 |
+
top: 0;
|
453 |
+
left: 70px;
|
454 |
+
right: 0;
|
455 |
+
bottom: 0;
|
456 |
+
padding-right: 20px; */
|
457 |
+
}
|
458 |
+
|
459 |
+
/* .at-sidebar {
|
460 |
+
|
461 |
+
flex: 15%;
|
462 |
+
display: flex;
|
463 |
+
align-content: stretch;
|
464 |
+
justify-content: center;
|
465 |
+
max-width: 70px;
|
466 |
+
z-index: 1001;
|
467 |
+
overflow: hidden;
|
468 |
+
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
469 |
+
background: #862b2b;
|
470 |
+
} */
|
471 |
+
|
472 |
+
.at-track-list {
|
473 |
+
display: flex;
|
474 |
+
flex-direction: column;
|
475 |
+
}
|
476 |
+
|
477 |
+
.at-controls {
|
478 |
+
/* flex: 0 0 auto; */
|
479 |
+
display: flex;
|
480 |
+
justify-content: space-between;
|
481 |
+
background: #329e17;
|
482 |
+
color: #e2e2c3;
|
483 |
+
}
|
484 |
+
|
485 |
+
.at-controls > div {
|
486 |
+
display: flex;
|
487 |
+
justify-content: flex-start;
|
488 |
+
align-content: center;
|
489 |
+
align-items: center;
|
490 |
+
}
|
491 |
+
|
492 |
+
.at-controls > div > * {
|
493 |
+
display: flex;
|
494 |
+
text-align: center;
|
495 |
+
align-items: center;
|
496 |
+
justify-content: center;
|
497 |
+
cursor: pointer;
|
498 |
+
padding: 4px;
|
499 |
+
margin: 0 3px;
|
500 |
+
}
|
501 |
+
|
502 |
+
.at-controls .btn {
|
503 |
+
color: #e2e2c3;
|
504 |
+
/* border-radius: 0; */
|
505 |
+
height: 40px;
|
506 |
+
width: 40px;
|
507 |
+
font-size: 16px;
|
508 |
+
}
|
509 |
+
|
510 |
+
.at-controls .active {
|
511 |
+
background-color: #5bbb43;
|
512 |
+
/* text-decoration: none; */
|
513 |
+
}
|
514 |
+
|
515 |
+
.at-controls select {
|
516 |
+
-moz-appearance: none;
|
517 |
+
-webkit-appearance: none;
|
518 |
+
appearance: none;
|
519 |
+
border: none;
|
520 |
+
width: 100%;
|
521 |
+
height: 40px;
|
522 |
+
background: #5bbb43;
|
523 |
+
padding: 4px 10px;
|
524 |
+
color: #e2e2c3;
|
525 |
+
font-size: 16px;
|
526 |
+
text-align-last: center;
|
527 |
+
text-align: center;
|
528 |
+
-ms-text-align-last: center;
|
529 |
+
-moz-text-align-last: center;
|
530 |
+
cursor: pointer;
|
531 |
+
}
|
532 |
+
|
533 |
+
/* .at-song-title {
|
534 |
+
font-weight: bold;
|
535 |
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
536 |
+
} */
|
537 |
+
|
538 |
+
.at-cursor-bar {
|
539 |
+
/* Defines the color of the bar background when a bar is played */
|
540 |
+
background: rgba(255, 242, 0, 0.25);
|
541 |
+
}
|
542 |
+
|
543 |
+
.at-selection div {
|
544 |
+
/* Defines the color of the selection background */
|
545 |
+
background: rgba(64, 64, 255, 0.1);
|
546 |
+
}
|
547 |
+
|
548 |
+
.at-cursor-beat {
|
549 |
+
/* Defines the beat cursor */
|
550 |
+
background: rgba(64, 64, 255, 0.75);
|
551 |
+
width: 3px;
|
552 |
+
}
|
553 |
+
|
554 |
+
.at-highlight * {
|
555 |
+
/* Defines the color of the music symbols when they are being played (svg) */
|
556 |
+
fill: #0078ff;
|
557 |
+
stroke: #0078ff;
|
558 |
+
}
|
559 |
+
</style>
|
frontend/src/components/ScoreWidgetBase.vue
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
<script setup>
|
2 |
-
import { computed, onMounted, onUpdated, ref, watch } from 'vue'
|
3 |
import { Pane } from 'tweakpane'
|
|
|
4 |
|
5 |
const props = defineProps({
|
6 |
// int id
|
@@ -8,10 +9,6 @@ const props = defineProps({
|
|
8 |
type: String,
|
9 |
required: true,
|
10 |
},
|
11 |
-
alphtaText: {
|
12 |
-
type: String,
|
13 |
-
required: false,
|
14 |
-
},
|
15 |
isActive: {
|
16 |
type: Boolean,
|
17 |
required: false,
|
@@ -22,8 +19,8 @@ const props = defineProps({
|
|
22 |
|
23 |
const emit = defineEmits(['updateScoreCode'])
|
24 |
|
25 |
-
const openSheetMusicDisplay = null
|
26 |
-
const timeoutID = null
|
27 |
let pane = null
|
28 |
let api = null
|
29 |
let main = null
|
@@ -37,6 +34,33 @@ const parsingError = ref('')
|
|
37 |
// const count = ref(0)
|
38 |
const settingsStatus = ref(false)
|
39 |
const isActive = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
|
41 |
const SETTINGS_PARAMS = {
|
42 |
scale: 1.0,
|
@@ -49,55 +73,121 @@ const SETTINGS_PARAMS = {
|
|
49 |
// prop: 'Put your\nmultiline\ntext here!'
|
50 |
}
|
51 |
|
52 |
-
const divId = computed(() => {
|
53 |
-
|
54 |
-
})
|
55 |
|
56 |
function initializePane() {
|
57 |
pane = new Pane({
|
58 |
-
container:
|
59 |
title: 'MusicXML Settings',
|
60 |
},
|
61 |
)
|
62 |
pane.addBinding(SETTINGS_PARAMS, 'scale', {
|
63 |
-
min: 0.
|
64 |
max: 2.0,
|
65 |
step: 0.1,
|
66 |
}).on('change', (ev) => {
|
67 |
console.log(ev.value)
|
68 |
-
|
69 |
-
|
|
|
70 |
})
|
71 |
}
|
72 |
|
73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
// console.log($(`#${divId.value}`)[0])
|
75 |
initializePane()
|
76 |
-
wrapper = document.querySelector('.
|
77 |
-
main = wrapper.querySelector('.
|
|
|
|
|
78 |
// initialize alphatab
|
79 |
settings = {
|
80 |
// file: 'https://www.alphatab.net/files/canon.gp',
|
81 |
-
|
82 |
player: {
|
83 |
enablePlayer: true,
|
84 |
soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2',
|
85 |
-
scrollElement:
|
|
|
86 |
},
|
87 |
}
|
88 |
-
|
89 |
})
|
90 |
|
91 |
-
function
|
92 |
-
|
93 |
-
api.
|
94 |
-
|
95 |
-
|
96 |
|
97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
99 |
// overlay logic
|
100 |
-
const overlay = wrapper.querySelector('.
|
101 |
api.renderStarted.on(() => {
|
102 |
overlay.style.display = 'flex'
|
103 |
})
|
@@ -106,170 +196,27 @@ function initializeOSMD() {
|
|
106 |
})
|
107 |
|
108 |
// track selector
|
109 |
-
function createTrackItem(track) {
|
110 |
-
const templateElement = document.querySelector('#at2-track-template')
|
111 |
-
const templateContent = templateElement.firstElementChild
|
112 |
-
const clonedNode = templateContent.cloneNode(true)
|
113 |
-
const trackItem = clonedNode.children[1]
|
114 |
-
trackItem.querySelector('.at2-track-name').textContent = track.name
|
115 |
-
trackItem.track = track
|
116 |
-
trackItem.onclick = (e) => {
|
117 |
-
e.stopPropagation()
|
118 |
-
api.renderTracks([track])
|
119 |
-
}
|
120 |
-
return trackItem
|
121 |
-
}
|
122 |
-
const trackList = wrapper.querySelector('.at2-track-list')
|
123 |
api.scoreLoaded.on((score) => {
|
124 |
-
|
125 |
-
trackList.innerHTML = ''
|
126 |
-
// generate a track item for all tracks of the score
|
127 |
-
score.tracks.forEach((track) => {
|
128 |
-
trackList.appendChild(createTrackItem(track))
|
129 |
-
})
|
130 |
})
|
|
|
131 |
api.renderStarted.on(() => {
|
132 |
-
// collect tracks being rendered
|
133 |
-
const tracks = new Map()
|
134 |
-
api.tracks.forEach((t) => {
|
135 |
-
tracks.set(t.index, t)
|
136 |
-
})
|
137 |
-
// mark the item as active or not
|
138 |
-
const trackItems = trackList.querySelectorAll('.at2-track')
|
139 |
-
trackItems.forEach((trackItem) => {
|
140 |
-
if (tracks.has(trackItem.track.index))
|
141 |
-
trackItem.classList.add('active')
|
142 |
-
|
143 |
-
else
|
144 |
-
trackItem.classList.remove('active')
|
145 |
-
})
|
146 |
})
|
147 |
|
148 |
/** Controls */
|
149 |
api.scoreLoaded.on((score) => {
|
150 |
-
|
151 |
-
|
152 |
})
|
153 |
|
154 |
-
const countIn = wrapper.querySelector('.at2-controls .at2-count-in')
|
155 |
-
countIn.onclick = () => {
|
156 |
-
countIn.classList.toggle('active')
|
157 |
-
if (countIn.classList.contains('active'))
|
158 |
-
api.countInVolume = 1
|
159 |
-
|
160 |
-
else
|
161 |
-
api.countInVolume = 0
|
162 |
-
}
|
163 |
-
|
164 |
-
const metronome = wrapper.querySelector('.at2-controls .at2-metronome')
|
165 |
-
metronome.onclick = () => {
|
166 |
-
metronome.classList.toggle('active')
|
167 |
-
if (metronome.classList.contains('active'))
|
168 |
-
api.metronomeVolume = 1
|
169 |
-
|
170 |
-
else
|
171 |
-
api.metronomeVolume = 0
|
172 |
-
}
|
173 |
-
|
174 |
-
const loop = wrapper.querySelector('.at2-controls .at2-loop')
|
175 |
-
loop.onclick = () => {
|
176 |
-
loop.classList.toggle('active')
|
177 |
-
api.isLooping = loop.classList.contains('active')
|
178 |
-
}
|
179 |
-
|
180 |
-
wrapper.querySelector('.at2-controls .at2-print').onclick = () => {
|
181 |
-
api.print()
|
182 |
-
}
|
183 |
-
|
184 |
-
const zoom = wrapper.querySelector('.at2-controls .at2-zoom select')
|
185 |
-
zoom.onchange = () => {
|
186 |
-
const zoomLevel = Number.parseInt(zoom.value) / 100
|
187 |
-
api.settings.display.scale = zoomLevel
|
188 |
-
api.updateSettings()
|
189 |
-
api.render()
|
190 |
-
}
|
191 |
-
|
192 |
-
const layout = wrapper.querySelector('.at2-controls .at2-layout select')
|
193 |
-
layout.onchange = () => {
|
194 |
-
switch (layout.value) {
|
195 |
-
case 'horizontal':
|
196 |
-
api.settings.display.layoutMode = alphaTab.LayoutMode.Horizontal
|
197 |
-
break
|
198 |
-
case 'page':
|
199 |
-
api.settings.display.layoutMode = alphaTab.LayoutMode.Page
|
200 |
-
break
|
201 |
-
}
|
202 |
-
api.updateSettings()
|
203 |
-
api.render()
|
204 |
-
}
|
205 |
-
|
206 |
-
// player loading indicator
|
207 |
-
const playerIndicator = wrapper.querySelector(
|
208 |
-
'.at2-controls .at2-player-progress',
|
209 |
-
)
|
210 |
-
api.soundFontLoad.on((e) => {
|
211 |
-
const percentage = Math.floor((e.loaded / e.total) * 100)
|
212 |
-
playerIndicator.innerText = `${percentage}%`
|
213 |
-
})
|
214 |
-
api.playerReady.on(() => {
|
215 |
-
playerIndicator.style.display = 'none'
|
216 |
-
})
|
217 |
-
|
218 |
-
// main player controls
|
219 |
-
const playPause = wrapper.querySelector(
|
220 |
-
'.at2-controls .at2-player-play-pause',
|
221 |
-
)
|
222 |
-
const stop = wrapper.querySelector('.at2-controls .at2-player-stop')
|
223 |
-
playPause.onclick = (e) => {
|
224 |
-
if (e.target.classList.contains('disabled'))
|
225 |
-
return
|
226 |
-
|
227 |
-
api.playPause()
|
228 |
-
}
|
229 |
-
stop.onclick = (e) => {
|
230 |
-
if (e.target.classList.contains('disabled'))
|
231 |
-
return
|
232 |
-
|
233 |
-
api.stop()
|
234 |
-
}
|
235 |
-
api.playerReady.on(() => {
|
236 |
-
playPause.classList.remove('disabled')
|
237 |
-
stop.classList.remove('disabled')
|
238 |
-
})
|
239 |
api.playerStateChanged.on((e) => {
|
240 |
-
|
241 |
-
if (e.state === alphaTab.synth.PlayerState.Playing) {
|
242 |
-
icon.classList.remove('i-fa:play')
|
243 |
-
icon.classList.add('i-fa:pause')
|
244 |
-
}
|
245 |
-
else {
|
246 |
-
icon.classList.remove('i-fa:pause')
|
247 |
-
icon.classList.add('i-fa:play')
|
248 |
-
}
|
249 |
})
|
250 |
-
|
251 |
-
// song position
|
252 |
-
function formatDuration(milliseconds) {
|
253 |
-
let seconds = milliseconds / 1000
|
254 |
-
const minutes = (seconds / 60) | 0
|
255 |
-
seconds = (seconds - minutes * 60) | 0
|
256 |
-
return (
|
257 |
-
`${String(minutes).padStart(2, '0')
|
258 |
-
}:${
|
259 |
-
String(seconds).padStart(2, '0')}`
|
260 |
-
)
|
261 |
-
}
|
262 |
-
|
263 |
-
const songPosition = wrapper.querySelector('.at2-song-position')
|
264 |
-
const previousTime = -1
|
265 |
api.playerPositionChanged.on((e) => {
|
266 |
// reduce number of UI updates to second changes.
|
267 |
const currentSeconds = (e.currentTime / 1000) | 0
|
268 |
-
if (currentSeconds
|
269 |
-
|
270 |
-
|
271 |
-
songPosition.innerText
|
272 |
-
= `${formatDuration(e.currentTime)} / ${formatDuration(e.endTime)}`
|
273 |
})
|
274 |
}
|
275 |
|
@@ -313,138 +260,243 @@ watch(currentScoreCode, () => {
|
|
313 |
</script>
|
314 |
|
315 |
<template>
|
316 |
-
<!--
|
317 |
-
Which ever of the following elements wants to use the class
|
318 |
-
attributes coming from the parent component needs to use
|
319 |
-
:class="$attrs.class"
|
320 |
-
UNLESS I have a single root div-element
|
321 |
-
-->
|
322 |
<div>
|
323 |
-
<div
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
</div>
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
</div>
|
336 |
</div>
|
337 |
-
<div class="at2-viewport">
|
338 |
-
<div class="at2-main" />
|
339 |
-
</div>
|
340 |
</div>
|
341 |
-
<div class="
|
342 |
-
<div class="
|
343 |
-
<a class="btn at2-player-stop disabled">
|
344 |
-
<i class="fas i-fa:step-backward"> Begining </i>
|
345 |
-
</a>
|
346 |
-
<a class="btn at2-player-play-pause disabled">
|
347 |
-
<i class="fas i-fa:play">SkAta</i>
|
348 |
-
</a>
|
349 |
-
<span class="at2-player-progress">0%</span>
|
350 |
-
<div class="at2-song-info">
|
351 |
-
<span class="at2-song-title" /> -
|
352 |
-
<span class="at2-song-artist" />
|
353 |
-
</div>
|
354 |
-
<div class="at2-song-position">
|
355 |
-
00:00 / 00:00
|
356 |
-
</div>
|
357 |
-
</div>
|
358 |
-
<div class="at2-controls-right">
|
359 |
-
<a class="btn toggle at2-count-in">
|
360 |
-
<i class="fas fa-hourglass-half" />
|
361 |
-
</a>
|
362 |
-
<a class="btn at2-metronome">
|
363 |
-
<i class="fas i-fa:edit" />
|
364 |
-
</a>
|
365 |
-
<a class="btn at2-loop">
|
366 |
-
<i class="fas i-fa:retweet" />
|
367 |
-
</a>
|
368 |
-
<a class="btn at2-print">
|
369 |
-
<i class="fas i-fa:print" />
|
370 |
-
</a>
|
371 |
-
<div class="at2-zoom">
|
372 |
-
<i class="fas i-fa:search" />
|
373 |
-
<select>
|
374 |
-
<option value="25">
|
375 |
-
25%
|
376 |
-
</option>
|
377 |
-
<option value="50">
|
378 |
-
50%
|
379 |
-
</option>
|
380 |
-
<option value="75">
|
381 |
-
75%
|
382 |
-
</option>
|
383 |
-
<option value="90">
|
384 |
-
90%
|
385 |
-
</option>
|
386 |
-
<option value="100" selected>
|
387 |
-
100%
|
388 |
-
</option>
|
389 |
-
<option value="110">
|
390 |
-
110%
|
391 |
-
</option>
|
392 |
-
<option value="125">
|
393 |
-
125%
|
394 |
-
</option>
|
395 |
-
<option value="150">
|
396 |
-
150%
|
397 |
-
</option>
|
398 |
-
<option value="200">
|
399 |
-
200%
|
400 |
-
</option>
|
401 |
-
</select>
|
402 |
-
</div>
|
403 |
-
<div class="at2-layout">
|
404 |
-
<select>
|
405 |
-
<option value="horizontal">
|
406 |
-
Horizontal
|
407 |
-
</option>
|
408 |
-
<option value="page" selected>
|
409 |
-
Page
|
410 |
-
</option>
|
411 |
-
</select>
|
412 |
-
</div>
|
413 |
-
</div>
|
414 |
</div>
|
415 |
</div>
|
416 |
-
<
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
<div v-show="settingsStatus" id="tabSettingsArea22">
|
422 |
-
<div id="paneContainerXml22" />
|
423 |
-
<textarea
|
424 |
-
v-model="currentScoreCode"
|
425 |
-
class="editorBox22"
|
426 |
-
placeholder="add your musicxml code here or drag a file"
|
427 |
-
@keyup="handleKeyUp"
|
428 |
-
@drop="handleDrop"
|
429 |
-
@dragover.prevent
|
430 |
-
/>
|
431 |
-
</div>
|
432 |
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
</div>
|
440 |
-
<div class="
|
441 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
442 |
</div>
|
443 |
</div>
|
444 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
445 |
</div>
|
|
|
|
|
446 |
</template>
|
447 |
|
448 |
<style scoped>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
450 |
</style>
|
|
|
1 |
<script setup>
|
2 |
+
import { computed, onMounted, onUnmounted, onUpdated, ref, toRaw, watch } from 'vue'
|
3 |
import { Pane } from 'tweakpane'
|
4 |
+
import formatDuration from '@/utils/timeUtils.js'
|
5 |
|
6 |
const props = defineProps({
|
7 |
// int id
|
|
|
9 |
type: String,
|
10 |
required: true,
|
11 |
},
|
|
|
|
|
|
|
|
|
12 |
isActive: {
|
13 |
type: Boolean,
|
14 |
required: false,
|
|
|
19 |
|
20 |
const emit = defineEmits(['updateScoreCode'])
|
21 |
|
22 |
+
// const openSheetMusicDisplay = null
|
23 |
+
// const timeoutID = null
|
24 |
let pane = null
|
25 |
let api = null
|
26 |
let main = null
|
|
|
34 |
// const count = ref(0)
|
35 |
const settingsStatus = ref(false)
|
36 |
const isActive = ref(false)
|
37 |
+
const wrapRef = ref(null)
|
38 |
+
const mainRef = ref(null)
|
39 |
+
const viewPortRef = ref(null)
|
40 |
+
const paneContainer = ref(null)
|
41 |
+
|
42 |
+
const tracks = ref([])
|
43 |
+
const songPosition = ref('00:00 / 00:00')
|
44 |
+
const previousTime = -1
|
45 |
+
const title = ref('')
|
46 |
+
const artist = ref('')
|
47 |
+
const isPlaying = ref(false)
|
48 |
+
const selectedLayout = ref('page')
|
49 |
+
const isZoomMenuVisible = ref(false)
|
50 |
+
const selectedZoom = ref(100)
|
51 |
+
const zoomOptions = [
|
52 |
+
{ value: '25', label: '25%' },
|
53 |
+
{ value: '50', label: '50%' },
|
54 |
+
{ value: '75', label: '75%' },
|
55 |
+
{ value: '90', label: '90%' },
|
56 |
+
{ value: '100', label: '100%' },
|
57 |
+
{ value: '110', label: '110%' },
|
58 |
+
{ value: '125', label: '125%' },
|
59 |
+
{ value: '150', label: '150%' },
|
60 |
+
{ value: '200', label: '200%' },
|
61 |
+
]
|
62 |
+
const metronomeIsActive = ref(false)
|
63 |
+
// const loopIsActive = ref(false)
|
64 |
|
65 |
const SETTINGS_PARAMS = {
|
66 |
scale: 1.0,
|
|
|
73 |
// prop: 'Put your\nmultiline\ntext here!'
|
74 |
}
|
75 |
|
76 |
+
// const divId = computed(() => {
|
77 |
+
// return `xmlScoreContainer_${props.id}`
|
78 |
+
// })
|
79 |
|
80 |
function initializePane() {
|
81 |
pane = new Pane({
|
82 |
+
container: paneContainer.value,
|
83 |
title: 'MusicXML Settings',
|
84 |
},
|
85 |
)
|
86 |
pane.addBinding(SETTINGS_PARAMS, 'scale', {
|
87 |
+
min: 0.1,
|
88 |
max: 2.0,
|
89 |
step: 0.1,
|
90 |
}).on('change', (ev) => {
|
91 |
console.log(ev.value)
|
92 |
+
api.settings.display.scale = ev.value
|
93 |
+
api.updateSettings()
|
94 |
+
api.render()
|
95 |
})
|
96 |
}
|
97 |
|
98 |
+
onUnmounted(() => {
|
99 |
+
console.log('onUnmounted')
|
100 |
+
// pane = null
|
101 |
+
api = null
|
102 |
+
main = null
|
103 |
+
wrapper = null
|
104 |
+
settings = null
|
105 |
+
})
|
106 |
+
|
107 |
+
onMounted(() => {
|
108 |
// console.log($(`#${divId.value}`)[0])
|
109 |
initializePane()
|
110 |
+
// wrapper = document.querySelector('.at-wrap')
|
111 |
+
// main = wrapper.querySelector('.at-main')
|
112 |
+
wrapper = wrapRef.value
|
113 |
+
main = mainRef.value
|
114 |
// initialize alphatab
|
115 |
settings = {
|
116 |
// file: 'https://www.alphatab.net/files/canon.gp',
|
117 |
+
file: '/Fantasia_no.2.musicxml.gpx',
|
118 |
player: {
|
119 |
enablePlayer: true,
|
120 |
soundFont: 'https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2',
|
121 |
+
scrollElement: viewPortRef.value,
|
122 |
+
|
123 |
},
|
124 |
}
|
125 |
+
initAlphaTab()
|
126 |
})
|
127 |
|
128 |
+
function renderTrack(track) {
|
129 |
+
// Assuming you have an API method to render tracks
|
130 |
+
// Example: api.renderTracks([track]);
|
131 |
+
api.renderTracks([toRaw(track)])
|
132 |
+
}
|
133 |
|
134 |
+
function updateLayout() {
|
135 |
+
switch (selectedLayout.value) {
|
136 |
+
case 'horizontal':
|
137 |
+
api.settings.display.layoutMode = alphaTab.LayoutMode.Horizontal
|
138 |
+
break
|
139 |
+
case 'page':
|
140 |
+
api.settings.display.layoutMode = alphaTab.LayoutMode.Page
|
141 |
+
break
|
142 |
+
}
|
143 |
+
api.updateSettings()
|
144 |
+
api.render()
|
145 |
+
}
|
146 |
+
|
147 |
+
// function toggleZoomMenu() {
|
148 |
+
// isZoomMenuVisible.value = !isZoomMenuVisible.value
|
149 |
+
// }
|
150 |
+
|
151 |
+
function updateZoom() {
|
152 |
+
// Handle zoom update logic here
|
153 |
+
// const zoomLevel = Number.parseInt(zoom.value) / 100
|
154 |
+
api.settings.display.scale = selectedZoom.value / 100
|
155 |
+
api.updateSettings()
|
156 |
+
api.render()
|
157 |
+
}
|
158 |
+
|
159 |
+
function scoreExport() {
|
160 |
+
const exporter = new alphaTab.exporter.Gp7Exporter()
|
161 |
+
const data = exporter.export(api.score, api.settings)
|
162 |
+
|
163 |
+
// trigger download
|
164 |
+
const a = document.createElement('a')
|
165 |
+
a.download = api.score.title.length > 0 ? `${api.score.title}.gp` : 'Untitled.gp'
|
166 |
+
a.href = URL.createObjectURL(new Blob([data]))
|
167 |
+
document.body.appendChild(a)
|
168 |
+
a.click()
|
169 |
+
document.body.removeChild(a)
|
170 |
+
}
|
171 |
+
|
172 |
+
function toggleMetronome() {
|
173 |
+
metronomeIsActive.value = !metronomeIsActive.value
|
174 |
+
if (metronomeIsActive.value)
|
175 |
+
api.metronomeVolume = 1
|
176 |
+
|
177 |
+
else
|
178 |
+
api.metronomeVolume = 0
|
179 |
+
}
|
180 |
+
|
181 |
+
function initAlphaTab() {
|
182 |
+
console.log('initAlphaTab')
|
183 |
+
api = new alphaTab.AlphaTabApi(main, settings)
|
184 |
+
// api.tex(`
|
185 |
+
// \\title 'Test' . 3.3.4
|
186 |
+
// :4 2.3 3.3 :8 3.3 4.3 3.3 4.3 |
|
187 |
+
// `)
|
188 |
|
189 |
// overlay logic
|
190 |
+
const overlay = wrapper.querySelector('.at-overlay')
|
191 |
api.renderStarted.on(() => {
|
192 |
overlay.style.display = 'flex'
|
193 |
})
|
|
|
196 |
})
|
197 |
|
198 |
// track selector
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
api.scoreLoaded.on((score) => {
|
200 |
+
tracks.value = score.tracks
|
|
|
|
|
|
|
|
|
|
|
201 |
})
|
202 |
+
|
203 |
api.renderStarted.on(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
})
|
205 |
|
206 |
/** Controls */
|
207 |
api.scoreLoaded.on((score) => {
|
208 |
+
title.value = score.title
|
209 |
+
artist.value = score.artist
|
210 |
})
|
211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
api.playerStateChanged.on((e) => {
|
213 |
+
isPlaying.value = e.state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
214 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
api.playerPositionChanged.on((e) => {
|
216 |
// reduce number of UI updates to second changes.
|
217 |
const currentSeconds = (e.currentTime / 1000) | 0
|
218 |
+
if (currentSeconds > previousTime)
|
219 |
+
songPosition.value = `${formatDuration(e.currentTime)} / ${formatDuration(e.endTime)}`
|
|
|
|
|
|
|
220 |
})
|
221 |
}
|
222 |
|
|
|
260 |
</script>
|
261 |
|
262 |
<template>
|
|
|
|
|
|
|
|
|
|
|
|
|
263 |
<div>
|
264 |
+
<div
|
265 |
+
ref="wrapRef"
|
266 |
+
class="at-wrap"
|
267 |
+
>
|
268 |
+
<div class="at-overlay">
|
269 |
+
<div class="at-overlay-content">
|
270 |
+
Music sheet is loading
|
271 |
</div>
|
272 |
+
</div>
|
273 |
+
<div class="at-content">
|
274 |
+
<div class="at-sidebar">
|
275 |
+
<div class="at-track-list">
|
276 |
+
<div v-for="track in tracks" :key="track.index" class="at-track" @click="renderTrack(track)">
|
277 |
+
<div class="at-track-icon">
|
278 |
+
<i class="i-fa:music" />
|
279 |
+
</div>
|
280 |
+
<div class="at-track-details">
|
281 |
+
<div class="at-track-name">
|
282 |
+
{{ track.name }}
|
283 |
+
</div>
|
284 |
+
</div>
|
285 |
</div>
|
286 |
</div>
|
|
|
|
|
|
|
287 |
</div>
|
288 |
+
<div ref="viewPortRef" class="at-viewport">
|
289 |
+
<div ref="mainRef" class="at-main" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
290 |
</div>
|
291 |
</div>
|
292 |
+
<div class="at-controls">
|
293 |
+
<div class="at-controls-left">
|
294 |
+
<button class="btn i-fad:rew" @click="api.stop()" />
|
295 |
+
<button class="btn" :class="{ 'i-fad:play': !isPlaying, 'i-fad:pause': isPlaying }" @click="api.playPause()" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
|
297 |
+
<div class="at-song-info">
|
298 |
+
{{ title }} - {{ artist }}
|
299 |
+
</div>
|
300 |
+
<div class="at-song-position">
|
301 |
+
{{ songPosition }}
|
302 |
+
</div>
|
303 |
</div>
|
304 |
+
<div class="at-controls-right">
|
305 |
+
<div :class="{ active: metronomeIsActive }">
|
306 |
+
<button class="btn i-fad:metronome" @click="toggleMetronome" />
|
307 |
+
</div>
|
308 |
+
<!-- <div>
|
309 |
+
<button class="btn i-fad:repeat" :class="{ active: loopingIsActive }" @click="toggleLooping" />
|
310 |
+
</div> -->
|
311 |
+
<div :class="{ active: settingsStatus }">
|
312 |
+
<button class="btn i-fa:edit" @click="showHideSettings" />
|
313 |
+
</div>
|
314 |
+
|
315 |
+
<div>
|
316 |
+
<button class="btn i-foundation:print" @click="api.print()" />
|
317 |
+
</div>
|
318 |
+
|
319 |
+
<div>
|
320 |
+
<button class="btn i-foundation:page-export" @click="scoreExport" />
|
321 |
+
</div>
|
322 |
+
|
323 |
+
<div class="at-zoom">
|
324 |
+
<!-- <button class="btn i-fad:zoomin" @click="toggleZoomMenu" /> -->
|
325 |
+
<select v-model="selectedZoom" @change="updateZoom">
|
326 |
+
<option v-for="zoomOption in zoomOptions" :key="zoomOption.value" :value="zoomOption.value">
|
327 |
+
{{ zoomOption.label }}
|
328 |
+
</option>
|
329 |
+
</select>
|
330 |
+
</div>
|
331 |
+
<div class="at-layout">
|
332 |
+
<select v-model="selectedLayout" @change="updateLayout">
|
333 |
+
<option value="horizontal">
|
334 |
+
Horizontal
|
335 |
+
</option>
|
336 |
+
<option value="page">
|
337 |
+
Page
|
338 |
+
</option>
|
339 |
+
</select>
|
340 |
+
</div>
|
341 |
</div>
|
342 |
</div>
|
343 |
+
</div>
|
344 |
+
<div v-show="settingsStatus" id="tabSettingsArea">
|
345 |
+
<div ref="paneContainer" />
|
346 |
+
<textarea
|
347 |
+
v-model="currentScoreCode"
|
348 |
+
class="editorBox"
|
349 |
+
placeholder="add your musicxml code here or drag a file"
|
350 |
+
@keyup="handleKeyUp"
|
351 |
+
@drop="handleDrop"
|
352 |
+
@dragover.prevent
|
353 |
+
/>
|
354 |
+
</div>
|
355 |
+
<div v-if="showParsingError" class="parsing-error" v-html="parsingError" />
|
356 |
</div>
|
357 |
+
|
358 |
+
<!-- </div> -->
|
359 |
</template>
|
360 |
|
361 |
<style scoped>
|
362 |
+
#tabSettingsArea {
|
363 |
+
display: flex;
|
364 |
+
flex-direction: row;
|
365 |
+
align-items: center;
|
366 |
+
gap: 20px;
|
367 |
+
margin-left: 10px;
|
368 |
+
}
|
369 |
+
.at-wrap {
|
370 |
+
width: 50vw;
|
371 |
+
height: 50vh;
|
372 |
+
/* margin: 0 auto; */
|
373 |
+
border: 2px solid rgba(0, 0, 0, 0.568);
|
374 |
+
display: flex;
|
375 |
+
flex-direction: column;
|
376 |
+
overflow: hidden;
|
377 |
+
position: relative;
|
378 |
+
}
|
379 |
+
.at-content {
|
380 |
+
position: relative;
|
381 |
+
overflow: hidden;
|
382 |
+
flex: 1 1 auto;
|
383 |
+
display: flex;
|
384 |
+
flex-direction: row;
|
385 |
+
}
|
386 |
+
.at-viewport {
|
387 |
+
flex: 80%;
|
388 |
+
overflow-y: scroll;
|
389 |
+
/* position: absolute;
|
390 |
+
top: 0;
|
391 |
+
left: 70px;
|
392 |
+
right: 0;
|
393 |
+
bottom: 0;
|
394 |
+
padding-right: 20px; */
|
395 |
+
}
|
396 |
+
|
397 |
+
.at-sidebar {
|
398 |
+
/* position: absolute;
|
399 |
+
top: 0;
|
400 |
+
left: 0;
|
401 |
+
bottom: 0;
|
402 |
+
max-width: 70px;
|
403 |
+
width: auto; */
|
404 |
+
flex: 15%;
|
405 |
+
display: flex;
|
406 |
+
align-content: stretch;
|
407 |
+
justify-content: center;
|
408 |
+
max-width: 70px;
|
409 |
+
z-index: 1001;
|
410 |
+
overflow: hidden;
|
411 |
+
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
412 |
+
background: #bec0b6;
|
413 |
+
}
|
414 |
+
|
415 |
+
.at-track-list {
|
416 |
+
display: flex;
|
417 |
+
flex-direction: column;
|
418 |
+
}
|
419 |
+
|
420 |
+
.at-controls {
|
421 |
+
/* flex: 0 0 auto; */
|
422 |
+
display: flex;
|
423 |
+
justify-content: space-between;
|
424 |
+
background: #329e17;
|
425 |
+
color: #e2e2c3;
|
426 |
+
}
|
427 |
+
|
428 |
+
.at-controls > div {
|
429 |
+
display: flex;
|
430 |
+
justify-content: flex-start;
|
431 |
+
align-content: center;
|
432 |
+
align-items: center;
|
433 |
+
}
|
434 |
+
|
435 |
+
.at-controls > div > * {
|
436 |
+
display: flex;
|
437 |
+
text-align: center;
|
438 |
+
align-items: center;
|
439 |
+
justify-content: center;
|
440 |
+
cursor: pointer;
|
441 |
+
padding: 4px;
|
442 |
+
margin: 0 3px;
|
443 |
+
}
|
444 |
|
445 |
+
.at-controls .btn {
|
446 |
+
color: #e2e2c3;
|
447 |
+
/* border-radius: 0; */
|
448 |
+
height: 40px;
|
449 |
+
width: 40px;
|
450 |
+
font-size: 16px;
|
451 |
+
}
|
452 |
+
|
453 |
+
.at-controls .active {
|
454 |
+
background-color: #5bbb43;
|
455 |
+
/* text-decoration: none; */
|
456 |
+
}
|
457 |
+
|
458 |
+
.at-controls select {
|
459 |
+
-moz-appearance: none;
|
460 |
+
-webkit-appearance: none;
|
461 |
+
appearance: none;
|
462 |
+
border: none;
|
463 |
+
width: 100%;
|
464 |
+
height: 40px;
|
465 |
+
background: #5bbb43;
|
466 |
+
padding: 4px 10px;
|
467 |
+
color: #e2e2c3;
|
468 |
+
font-size: 16px;
|
469 |
+
text-align-last: center;
|
470 |
+
text-align: center;
|
471 |
+
-ms-text-align-last: center;
|
472 |
+
-moz-text-align-last: center;
|
473 |
+
cursor: pointer;
|
474 |
+
}
|
475 |
+
|
476 |
+
/* .at-song-title {
|
477 |
+
font-weight: bold;
|
478 |
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
479 |
+
} */
|
480 |
+
|
481 |
+
.at-cursor-bar {
|
482 |
+
/* Defines the color of the bar background when a bar is played */
|
483 |
+
background: rgba(255, 242, 0, 0.25);
|
484 |
+
}
|
485 |
+
|
486 |
+
.at-selection div {
|
487 |
+
/* Defines the color of the selection background */
|
488 |
+
background: rgba(64, 64, 255, 0.1);
|
489 |
+
}
|
490 |
+
|
491 |
+
.at-cursor-beat {
|
492 |
+
/* Defines the beat cursor */
|
493 |
+
background: rgba(64, 64, 255, 0.75);
|
494 |
+
width: 3px;
|
495 |
+
}
|
496 |
+
|
497 |
+
.at-highlight * {
|
498 |
+
/* Defines the color of the music symbols when they are being played (svg) */
|
499 |
+
fill: #0078ff;
|
500 |
+
stroke: #0078ff;
|
501 |
+
}
|
502 |
</style>
|
frontend/src/plugins/router.js
CHANGED
@@ -6,6 +6,11 @@ const routes = [
|
|
6 |
name: 'Analyzer',
|
7 |
component: () => import('@/views/Analyzer.vue'),
|
8 |
},
|
|
|
|
|
|
|
|
|
|
|
9 |
{
|
10 |
path: '/',
|
11 |
name: 'Home',
|
|
|
6 |
name: 'Analyzer',
|
7 |
component: () => import('@/views/Analyzer.vue'),
|
8 |
},
|
9 |
+
{
|
10 |
+
path: '/drawlisten',
|
11 |
+
name: 'DrawAndListen',
|
12 |
+
component: () => import('@/views/DrawAndListen.vue'),
|
13 |
+
},
|
14 |
{
|
15 |
path: '/',
|
16 |
name: 'Home',
|
frontend/src/style.css
CHANGED
@@ -16,6 +16,7 @@
|
|
16 |
--background-light2: #dee8f8;
|
17 |
--background-dark: #3c3d3d;
|
18 |
--accent-color: #b9a329;
|
|
|
19 |
--text-color-dark: #333333;
|
20 |
--text-color-light: #e0e0e0;
|
21 |
color: var(--text-color-dark);
|
@@ -34,12 +35,12 @@ body {
|
|
34 |
/* margin: 100px; */
|
35 |
margin: 0px;
|
36 |
/* display: flex; */
|
37 |
-
|
38 |
/* min-width: 320px; */
|
39 |
/* min-height: 100vh; */
|
40 |
/* overflow: auto; */
|
41 |
-
|
42 |
-
background-color: rgb(17, 73, 241);
|
43 |
/* width: 100vw; */
|
44 |
}
|
45 |
|
|
|
16 |
--background-light2: #dee8f8;
|
17 |
--background-dark: #3c3d3d;
|
18 |
--accent-color: #b9a329;
|
19 |
+
--accent-color2: #e2d16f;
|
20 |
--text-color-dark: #333333;
|
21 |
--text-color-light: #e0e0e0;
|
22 |
color: var(--text-color-dark);
|
|
|
35 |
/* margin: 100px; */
|
36 |
margin: 0px;
|
37 |
/* display: flex; */
|
38 |
+
place-items: top center;
|
39 |
/* min-width: 320px; */
|
40 |
/* min-height: 100vh; */
|
41 |
/* overflow: auto; */
|
42 |
+
background-color: var(--background-dark);
|
43 |
+
/* background-color: rgb(17, 73, 241); */
|
44 |
/* width: 100vw; */
|
45 |
}
|
46 |
|
frontend/src/utils/timeUtils.js
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// song position
|
2 |
+
export default function formatDuration(milliseconds) {
|
3 |
+
let seconds = milliseconds / 1000
|
4 |
+
const minutes = (seconds / 60) | 0
|
5 |
+
seconds = (seconds - minutes * 60) | 0
|
6 |
+
return (
|
7 |
+
`${String(minutes).padStart(2, '0')
|
8 |
+
}:${
|
9 |
+
String(seconds).padStart(2, '0')}`
|
10 |
+
)
|
11 |
+
}
|
frontend/src/views/Analyzer.vue
CHANGED
@@ -1,10 +1,10 @@
|
|
1 |
<script setup>
|
2 |
import { client } from '@gradio/client'
|
3 |
import { onMounted, ref } from 'vue'
|
4 |
-
import AlphaTabWidget from '../components/AlphaTabWidget.vue'
|
5 |
import VextabWidget from '@/components/VextabWidget.vue'
|
6 |
-
import MusicXmlWidget from '@/components/
|
7 |
import ScoreBaseWidget from '@/components/ScoreWidgetBase.vue'
|
|
|
8 |
import '@/style.css'
|
9 |
|
10 |
let app = null
|
@@ -14,8 +14,8 @@ const currentScoreCode = ref('')
|
|
14 |
const tabs = [
|
15 |
{ name: 'VexTab', component: VextabWidget, id: 'first' },
|
16 |
{ name: 'MusicXml', component: MusicXmlWidget, id: 'second' },
|
17 |
-
|
18 |
-
|
19 |
]
|
20 |
const tabText_component1 = `
|
21 |
tabstave notation=true key=A time=4/4
|
@@ -24,10 +24,9 @@ notes :q =|: (5/2.5/3.7/4) :8 7-5h6/3 ^3^ 5h6-7/5 ^3^ :q 7V/4 |
|
|
24 |
notes :8 t12p7/4 s5s3/4 :8 3s:16:5-7/5 :q p5/4
|
25 |
text :w, |#segno, ,|, :hd, , #tr
|
26 |
`
|
27 |
-
const
|
28 |
-
|
29 |
-
|
30 |
-
notes :q =|: (5/2.5/3.7/4) :8 7-5h6/3 ^3^ 5h6-7/5 ^3^ :q 7V/4 |
|
31 |
`
|
32 |
|
33 |
async function connectToServer() {
|
@@ -68,9 +67,9 @@ function updateScoreCode(scoreCode) {
|
|
68 |
<h1 class="title">
|
69 |
Guitar Score Analyzer
|
70 |
</h1>
|
71 |
-
<p class="description">
|
72 |
You can load a guitar score either using Vextab or MusicXml
|
73 |
-
</p>
|
74 |
<br>
|
75 |
|
76 |
<!-- Tab Buttons -->
|
@@ -104,7 +103,7 @@ function updateScoreCode(scoreCode) {
|
|
104 |
</div>
|
105 |
|
106 |
<!-- MusicXml Widget -->
|
107 |
-
<div v-show="currentTab === 1" class="tab" :class="{ active: currentTab ===
|
108 |
<MusicXmlWidget
|
109 |
:id="tabs[currentTab].id"
|
110 |
class="tab-musicXml"
|
@@ -113,25 +112,22 @@ function updateScoreCode(scoreCode) {
|
|
113 |
/>
|
114 |
</div>
|
115 |
|
116 |
-
|
117 |
-
<
|
118 |
:id="tabs[currentTab].id"
|
119 |
-
|
120 |
-
:tab-text="tabText_component2"
|
121 |
:is-active="currentTab === 2"
|
122 |
@update-score-code="updateScoreCode"
|
123 |
/>
|
124 |
-
</div>
|
125 |
|
126 |
-
|
127 |
<ScoreBaseWidget
|
128 |
:id="tabs[currentTab].id"
|
129 |
-
class="tab-vexTab"
|
130 |
-
:tab-text="tabText_component2"
|
131 |
:is-active="currentTab === 3"
|
132 |
@update-score-code="updateScoreCode"
|
133 |
/>
|
134 |
-
</div>
|
135 |
<!-- </div> -->
|
136 |
|
137 |
<!-- Report Container -->
|
@@ -151,6 +147,11 @@ function updateScoreCode(scoreCode) {
|
|
151 |
max-width: 1280px; /* Adjust the max width as needed */
|
152 |
margin: auto;
|
153 |
background-color: rgb(238, 235, 212);
|
|
|
|
|
|
|
|
|
|
|
154 |
}
|
155 |
|
156 |
.title {
|
@@ -171,10 +172,11 @@ function updateScoreCode(scoreCode) {
|
|
171 |
}
|
172 |
|
173 |
.tab-button {
|
174 |
-
background-color:
|
175 |
padding: 10px;
|
176 |
margin-right: 5px;
|
177 |
cursor: pointer;
|
|
|
178 |
}
|
179 |
|
180 |
.tab-button.active {
|
@@ -187,12 +189,13 @@ function updateScoreCode(scoreCode) {
|
|
187 |
|
188 |
.tab.active {
|
189 |
display: flex;
|
190 |
-
/* background-color: #
|
191 |
width: 800px;
|
192 |
max-width: 1280px;
|
193 |
min-width:200px;
|
194 |
min-height:300px;
|
195 |
-
|
|
|
196 |
}
|
197 |
|
198 |
.analyze-button {
|
|
|
1 |
<script setup>
|
2 |
import { client } from '@gradio/client'
|
3 |
import { onMounted, ref } from 'vue'
|
|
|
4 |
import VextabWidget from '@/components/VextabWidget.vue'
|
5 |
+
import MusicXmlWidget from '@/components/MusicXmlWidget2.vue'
|
6 |
import ScoreBaseWidget from '@/components/ScoreWidgetBase.vue'
|
7 |
+
import AlphaTexWidget from '@/components/AlphaTexWidget.vue'
|
8 |
import '@/style.css'
|
9 |
|
10 |
let app = null
|
|
|
14 |
const tabs = [
|
15 |
{ name: 'VexTab', component: VextabWidget, id: 'first' },
|
16 |
{ name: 'MusicXml', component: MusicXmlWidget, id: 'second' },
|
17 |
+
{ name: 'AlphaTex', component: AlphaTexWidget, id: 'third' },
|
18 |
+
{ name: 'AlphaTab', component: ScoreBaseWidget, id: 'fourth' },
|
19 |
]
|
20 |
const tabText_component1 = `
|
21 |
tabstave notation=true key=A time=4/4
|
|
|
24 |
notes :8 t12p7/4 s5s3/4 :8 3s:16:5-7/5 :q p5/4
|
25 |
text :w, |#segno, ,|, :hd, , #tr
|
26 |
`
|
27 |
+
const alphaTex = `
|
28 |
+
\\title 'Test' . 3.3.4
|
29 |
+
:4 2.3 3.3 :8 3.3 4.3 3.3 4.3 |
|
|
|
30 |
`
|
31 |
|
32 |
async function connectToServer() {
|
|
|
67 |
<h1 class="title">
|
68 |
Guitar Score Analyzer
|
69 |
</h1>
|
70 |
+
<!-- <p class="description">
|
71 |
You can load a guitar score either using Vextab or MusicXml
|
72 |
+
</p> -->
|
73 |
<br>
|
74 |
|
75 |
<!-- Tab Buttons -->
|
|
|
103 |
</div>
|
104 |
|
105 |
<!-- MusicXml Widget -->
|
106 |
+
<div v-show="currentTab === 1" class="tab" :class="{ active: currentTab === 1 }">
|
107 |
<MusicXmlWidget
|
108 |
:id="tabs[currentTab].id"
|
109 |
class="tab-musicXml"
|
|
|
112 |
/>
|
113 |
</div>
|
114 |
|
115 |
+
<div v-if="currentTab === 2" class="tab" :class="{ active: currentTab === 2 }">
|
116 |
+
<AlphaTexWidget
|
117 |
:id="tabs[currentTab].id"
|
118 |
+
:alpha-tex="alphaTex"
|
|
|
119 |
:is-active="currentTab === 2"
|
120 |
@update-score-code="updateScoreCode"
|
121 |
/>
|
122 |
+
</div>
|
123 |
|
124 |
+
<div v-if="currentTab === 3" class="tab" :class="{ active: currentTab === 3 }">
|
125 |
<ScoreBaseWidget
|
126 |
:id="tabs[currentTab].id"
|
|
|
|
|
127 |
:is-active="currentTab === 3"
|
128 |
@update-score-code="updateScoreCode"
|
129 |
/>
|
130 |
+
</div>
|
131 |
<!-- </div> -->
|
132 |
|
133 |
<!-- Report Container -->
|
|
|
147 |
max-width: 1280px; /* Adjust the max width as needed */
|
148 |
margin: auto;
|
149 |
background-color: rgb(238, 235, 212);
|
150 |
+
/* height: 100vh; */
|
151 |
+
display: flex;
|
152 |
+
flex-direction: column;
|
153 |
+
justify-content: center;
|
154 |
+
align-items: center;
|
155 |
}
|
156 |
|
157 |
.title {
|
|
|
172 |
}
|
173 |
|
174 |
.tab-button {
|
175 |
+
background-color: var(--accent-color2);
|
176 |
padding: 10px;
|
177 |
margin-right: 5px;
|
178 |
cursor: pointer;
|
179 |
+
color: var(--text-color-dark);
|
180 |
}
|
181 |
|
182 |
.tab-button.active {
|
|
|
189 |
|
190 |
.tab.active {
|
191 |
display: flex;
|
192 |
+
/* background-color: #f5f5f0; */
|
193 |
width: 800px;
|
194 |
max-width: 1280px;
|
195 |
min-width:200px;
|
196 |
min-height:300px;
|
197 |
+
justify-content: center;
|
198 |
+
align-items: center;
|
199 |
}
|
200 |
|
201 |
.analyze-button {
|
frontend/src/views/DrawAndListen.vue
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script setup>
|
2 |
+
import { client } from '@gradio/client'
|
3 |
+
import { onMounted, ref } from 'vue'
|
4 |
+
import VextabWidget from '@/components/VextabWidget.vue'
|
5 |
+
import MusicXmlWidget from '@/components/MusicXmlWidget2.vue'
|
6 |
+
import ScoreBaseWidget from '@/components/ScoreWidgetBase.vue'
|
7 |
+
import AlphaTexDL from '@/components/AlphaTexDL.vue'
|
8 |
+
import '@/style.css'
|
9 |
+
|
10 |
+
let app = null
|
11 |
+
const response = ref('')
|
12 |
+
const currentTab = ref(0)
|
13 |
+
const currentScoreCode = ref('')
|
14 |
+
|
15 |
+
const alphaTex = `
|
16 |
+
15.2.4 :16 17.2 15.2 :8 14.2 14.1 17.1.4 |
|
17 |
+
:4 r r r r |
|
18 |
+
:8 1.3 3.2 1.1 3.1 3.2 3.1 3.4 2.3 1.3 2.3
|
19 |
+
`
|
20 |
+
|
21 |
+
async function connectToServer() {
|
22 |
+
try {
|
23 |
+
// app = await client('xribene/guitar-diff')
|
24 |
+
app = await client('https://xribene-guitar-diff.hf.space/--replicas/qmsjt/')
|
25 |
+
}
|
26 |
+
catch (error) {
|
27 |
+
console.error('Error fetching data:', error)
|
28 |
+
}
|
29 |
+
}
|
30 |
+
|
31 |
+
onMounted(async () => {
|
32 |
+
// Call the async function when the component is mounted
|
33 |
+
await connectToServer()
|
34 |
+
})
|
35 |
+
|
36 |
+
async function processScore() {
|
37 |
+
try {
|
38 |
+
const result = await app.predict('/process', [currentScoreCode.value])
|
39 |
+
response.value = result.data
|
40 |
+
console.log(result.data)
|
41 |
+
}
|
42 |
+
catch (error) {
|
43 |
+
console.error('Error processing the score:', error)
|
44 |
+
}
|
45 |
+
}
|
46 |
+
|
47 |
+
function updateScoreCode(scoreCode) {
|
48 |
+
currentScoreCode.value = scoreCode
|
49 |
+
// console.log("updateScoreCode")
|
50 |
+
// console.log(currentScoreCode.value);
|
51 |
+
}
|
52 |
+
</script>
|
53 |
+
|
54 |
+
<template>
|
55 |
+
<div class="draw-and-listen">
|
56 |
+
<h1 class="title">
|
57 |
+
Draw & Listen
|
58 |
+
</h1>
|
59 |
+
<!-- <p class="description">
|
60 |
+
You can load a guitar score either using Vextab or MusicXml
|
61 |
+
</p> -->
|
62 |
+
<br>
|
63 |
+
|
64 |
+
<!-- Tab Buttons -->
|
65 |
+
<!-- <div class="tab-buttons">
|
66 |
+
<button
|
67 |
+
v-for="(tab, index) in tabs"
|
68 |
+
:key="index"
|
69 |
+
class="tab-button" :class="[{ active: currentTab === index }]"
|
70 |
+
@click="currentTab = index"
|
71 |
+
>
|
72 |
+
{{ tab.name }}
|
73 |
+
</button>
|
74 |
+
</div> -->
|
75 |
+
<br>
|
76 |
+
<br>
|
77 |
+
|
78 |
+
<div class="tab">
|
79 |
+
<AlphaTexDL
|
80 |
+
:alpha-tex="alphaTex"
|
81 |
+
@update-score-code="updateScoreCode"
|
82 |
+
/>
|
83 |
+
</div>
|
84 |
+
|
85 |
+
<!-- </div> -->
|
86 |
+
<br>
|
87 |
+
<br>
|
88 |
+
<!-- Report Container -->
|
89 |
+
<div class="debug-background-color report-container">
|
90 |
+
<button class="analyze-button" @click="processScore">
|
91 |
+
Generate
|
92 |
+
</button>
|
93 |
+
<!-- <p class="response">
|
94 |
+
{{ response }}
|
95 |
+
</p> -->
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
</template>
|
99 |
+
|
100 |
+
<style scoped>
|
101 |
+
.draw-and-listen {
|
102 |
+
/* max-width: 1280px; Adjust the max width as needed */
|
103 |
+
width: 60vw;
|
104 |
+
height: 100vh;
|
105 |
+
margin: auto;
|
106 |
+
background-color: rgb(238, 235, 212);
|
107 |
+
/* height: 100vh; */
|
108 |
+
display: flex;
|
109 |
+
flex-direction: column;
|
110 |
+
justify-content: center;
|
111 |
+
align-items: center;
|
112 |
+
}
|
113 |
+
|
114 |
+
.title {
|
115 |
+
font-size: 2em;
|
116 |
+
/* flex: 20%; */
|
117 |
+
/* margin-bottom: 10px; */
|
118 |
+
/* background-color: aqua; */
|
119 |
+
text-align: center;
|
120 |
+
}
|
121 |
+
|
122 |
+
.tab {
|
123 |
+
flex: 80%;
|
124 |
+
/* background-color: aquamarine; */
|
125 |
+
}
|
126 |
+
|
127 |
+
.description {
|
128 |
+
font-size: 1.2em;
|
129 |
+
margin-bottom: 20px;
|
130 |
+
/* background-color: aquamarine; */
|
131 |
+
}
|
132 |
+
|
133 |
+
.analyze-button {
|
134 |
+
padding: 10px;
|
135 |
+
background-color: #4caf50;
|
136 |
+
color: #fff;
|
137 |
+
cursor: pointer;
|
138 |
+
}
|
139 |
+
</style>
|
frontend/uno.config.js
CHANGED
@@ -10,6 +10,11 @@ import {
|
|
10 |
import presetIcons from '@unocss/preset-icons/browser'
|
11 |
|
12 |
export default defineConfig({
|
|
|
|
|
|
|
|
|
|
|
13 |
presets: [
|
14 |
presetUno(),
|
15 |
presetIcons({
|
@@ -26,11 +31,13 @@ export default defineConfig({
|
|
26 |
// mdi: () => import('@iconify/json/mid.json').then(i => i.default),
|
27 |
// logos: () => import('@iconify/json/logos.json').then(i => i.default),
|
28 |
// twemoji: () => import('@iconify/json/twemoji.json').then(i => i.default),
|
29 |
-
mdi: () => import('@iconify/json/json/mdi.json').then(i => i.default),
|
30 |
-
logos: () => import('@iconify/json/json/logos.json').then(i => i.default),
|
31 |
-
twemoji: () => import('@iconify/json/json/twemoji.json').then(i => i.default),
|
32 |
-
vscode: () => import('@iconify/json/json/vscode-icons.json').then(i => i.default),
|
33 |
fa: () => import('@iconify/json/json/fa-solid.json').then(i => i.default),
|
|
|
|
|
34 |
},
|
35 |
}),
|
36 |
presetWebFonts({
|
|
|
10 |
import presetIcons from '@unocss/preset-icons/browser'
|
11 |
|
12 |
export default defineConfig({
|
13 |
+
rules: [
|
14 |
+
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
|
15 |
+
[/^w-screen-(\d+)$/, ([, d]) => ({ width: `${d}vw` })],
|
16 |
+
[/^h-screen-(\d+)$/, ([, d]) => ({ height: `${d}vh` })],
|
17 |
+
],
|
18 |
presets: [
|
19 |
presetUno(),
|
20 |
presetIcons({
|
|
|
31 |
// mdi: () => import('@iconify/json/mid.json').then(i => i.default),
|
32 |
// logos: () => import('@iconify/json/logos.json').then(i => i.default),
|
33 |
// twemoji: () => import('@iconify/json/twemoji.json').then(i => i.default),
|
34 |
+
// mdi: () => import('@iconify/json/json/mdi.json').then(i => i.default),
|
35 |
+
// logos: () => import('@iconify/json/json/logos.json').then(i => i.default),
|
36 |
+
// twemoji: () => import('@iconify/json/json/twemoji.json').then(i => i.default),
|
37 |
+
// vscode: () => import('@iconify/json/json/vscode-icons.json').then(i => i.default),
|
38 |
fa: () => import('@iconify/json/json/fa-solid.json').then(i => i.default),
|
39 |
+
fad: () => import('@iconify/json/json/fad.json').then(i => i.default),
|
40 |
+
foundation: () => import('@iconify/json/json/foundation.json').then(i => i.default),
|
41 |
},
|
42 |
}),
|
43 |
presetWebFonts({
|