You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

377 lines
17 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
<script src="https://unpkg.com/downloadjs@1.4.7"></script>
<script src="https://unpkg.com/tone@14.7.77/build/Tone.js" async></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
</head>
<body>
<div id="app">
<v-app>
<v-app-bar app>
<v-toolbar-title>
<b>Birdy Slim Ringtone Editor/Manager/Customizer</b>
<sub>by <a href="https://github.com/catSIXe">catSIXe</a></sub>
</v-toolbar-title>
</v-app-bar>
<v-content>
<v-container v-if="global.step == 0">
<v-file-input v-model="step0.file" @change="handleFileSelectionChange()" show-size truncate-length="39"></v-file-input>
<v-btn color="success" :disabled="!step0.selectedConfigFileValid" raised @click="readConfigFile()">Read Config</v-btn>
</v-container>
<v-container v-if="global.step == 1">
<v-row>
<v-col>
<v-btn @click="writeConfigFile()">Download Config</v-btn>
</v-col>
</v-row>
<v-row align="center">
<v-col class="d-flex" cols="12" sm="6">
<v-select :items="step1.birdyRingtoneSelection" v-model="step1.ringtoneSelection" label="Ring-Tone Selection" item-text="name" item-value="id"></v-select>
<v-btn disabled atr-disabled="step1.ringtoneSelection==null" @click="demoPlay()" >Play</v-btn>
</v-col>
<v-col class="d-flex" cols="12" sm="6">
<v-btn color="success" :disabled="step1.ringtoneSelection==null" raised @click="decodeRingtone()">Load</v-btn>
<v-btn color="warning" :disabled="step1.ringtoneSelection==null || !step1.dirty" raised @click="encodeRingtone()">Store</v-btn>
<v-btn color="error" :disabled="step1.ringtoneSelection==null" raised @click="cleanRingtone()">Clear</v-btn>
</v-col>
</v-row>
</v-container>
<v-container fluid v-if="global.step == 1">
<p v-if="step1_1.dirty">WARNING, you have unsaved ringtonedata, press [STORE] to Apply or [LOAD] to load it again from the config file</p>
<v-row dense>
<v-col cols="12">
<v-card>
<v-card-title>Ringtone-JSON</v-card-title>
<p v-if="step1_1.error">Your JSON is invalid! Correct it or press [LOAD] to load it again from the config file</p>
<v-textarea @keydown="testJSONRingtoneData()" @change="testJSONRingtoneData()" v-model="step1_1.ringtoneDataJSON_User"></v-textarea>
<v-card-actions>
<v-btn color="warning" :disabled="!step1_1.dirty || step1_1.error" raised @click="parseJSONtoRingtoneData()">Apply JSON to RingtoneData</v-btn>
<v-spacer></v-spacer>
<v-btn icon @click="copyClipboard(step1_1.ringtoneDataJSON_User)"><v-icon>mdi-clipboard-text</v-icon></v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<v-row dense>
<v-col cols="6">
<v-btn disabled><v-icon x-large>mdi-download</v-icon> JSON to RTTIL </v-btn>
<v-btn disabled><v-icon x-large>mdi-upload</v-icon> RTTTL to JSON </v-btn>
</v-col>
<v-col cols="6">
<v-btn disabled><v-icon x-large>mdi-download</v-icon> JSON to RTTIL </v-btn>
<v-btn disabled><v-icon x-large>mdi-upload</v-icon> RTTTL to JSON </v-btn>
</v-col>
</v-row>
<v-row dense>
<v-col cols="6">
<v-card>
<v-card-title>Ringtone-RTTTL</v-card-title>
<v-textarea disabled @keydown="testRTTTLRingtoneData()" @change="testRTTTLRingtoneData()" v-model="step1_2.ringtoneDataRTTTL_User"></v-textarea>
<v-card-actions>
<v-btn icon disabled @click="copyClipboard(step1_2.ringtoneDataRTTTL_User)"><v-icon>mdi-clipboard-text</v-icon></v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-col cols="6">
<v-card>
<v-card-title>Ringtone-BML</v-card-title>
<v-textarea disabled @keydown="testRTTTLRingtoneData()" @change="testRTTTLRingtoneData()" v-model="step1_2.ringtoneDataRTTTL_User"></v-textarea>
<v-card-actions>
<v-btn icon disabled @click="copyClipboard(step1_2.ringtoneDataRTTTL_User)"><v-icon>mdi-clipboard-text</v-icon></v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
<!--
hello to anyone, who reads this :3
<v-container>
<v-data-table must-sort :footer-props="footerProps" :options="options" :loading="loading" loading-text="Loading... Please wait" :headers="headers" item-key="_id" :items="accountData" :items-per-page="5"
class="elevation-1" :search="search" :custom-filter="filterOnlyCapsText">
<template v-slot:top>
<v-text-field v-model="search" label="Search" class="mx-4"></v-text-field>
</template>
<template v-slot:no-data>
No Accounts to show
</template>
<template v-slot:item.uuid="{ item }">
<tr>
<td :colspan="headers.length">
<img v-if="item.validState == 1" :alt="item.uuid"
:src="'https://www.mc-heads.net/avatar/' + item.uuid + '/48/nohelm.png'">
<img v-if="item.validState == -1" :src="'Barrier_2.png'">
</td>
</tr>
</template>
</v-data-table>
<hr>
Import Bulk Accounts
<v-textarea outlined v-model="bulkImportText" label="Bulk Import Data" placeholder="[username]:<user/email>:<password>
..."></v-textarea>
<v-btn :disabled="isBulkTextValid()" raised @click="bulkImport()">Import</v-btn>
</v-container>
-->
</v-content>
<v-footer>
<v-card flat tile width="100%" class="red lighten-1 text-center">
<v-card-text class="white--text">
{{ new Date().getFullYear() }} — <strong>catSIXe</strong> - disclaimer.txt
</v-card-text>
</v-card>
</v-footer>
</v-app>
</div>
<script>
//
const noteNames = [
'End',
'C', 'C#',
'D', 'D#',
'E',
'F', 'F#',
'G', 'G#',
'A', 'A',
'B',
'Interval'
]
// im working on a full version of this.
class TPL_BirdySlimConfig {
constructor() {
this.buffer = null
}
loadBuffer(buffer) {
this.buffer = buffer
return this
}
saveBuffer() {
return this.buffer
}
writeNotes(melody) {
console.log(melody)
let offset = 0x0AE0 + (melody.ringNumber * 90)
this.buffer[ offset++ ] = melody.melodyHeader
for(let i=0;i<45;i++) {
if (!(!!melody.notes[i])) {
this.buffer[ offset++ ] = 0x00
this.buffer[ offset++ ] = 0x00
} else {
let note = melody.notes[i] // 0note,1octave,2length
this.buffer[ offset++ ] = (note[ 1 ] << 4 & 0xf0) | (note[ 0 ] & 0xf)
this.buffer[ offset++ ] = note[ 2 ]
}
}
}
readNotesFromBuffer(ringNumber) {
const offset = 0x0AE0 + (ringNumber * 90)
let music = JSON.parse(JSON.stringify(this.buffer)).slice(offset,offset+1+(2 * 45))
//console.log(music.map(x=>x.toString(16).toUpperCase()).join(''))
/////if (typeof(Buffer)==='undefined') music = new Int8Array(music) // if in browser, we get arraybuffers
const notes = [], melodyHeader = music[0]
for (let i=1; i < music.length; i+=2) {
const note = music[i] >> 0 & 0xf,
octave = music[i] >> 4 & 0xf,
length = music[i+1] // 1 Byte for the Length
if (note == 0) break;
//console.log(`${ noteNames[note] } ${ octave } ${ length*10 }ms`)
notes.push([note, octave, length ])
}
return {
ringNumber,
melodyHeader,
notes,
}
}
}
let $buff=null,$buff2=null
function parseHexString(str) {
var result = [];
while (str.length >= 2) {
result.push(parseInt(str.substring(0, 2), 16))
str = str.substring(2, str.length)
}
return result
}
function parseRev2Config(text) {
if (window.DOMParser) {
parser = new DOMParser()
xmlDoc = parser.parseFromString(text, 'text/xml')
} else {// Internet Explorer
xmlDoc = new ActiveXObject('Microsoft.XMLDOM')
xmlDoc.async = false
xmlDoc.loadXML(text)
}
const dataTags = xmlDoc.getElementsByTagName("data")
let dataTagsSorted = []
for (let i=0;i<dataTags.length;i++) {
const tag = dataTags[ i ]
dataTagsSorted.push([parseInt(tag.attributes.ad.value), tag.textContent])
}
dataTagsSorted.sort((a,b) => a[0]-b[0])
const hexStream = dataTagsSorted.map(x=>x[1]).join('')
return parseHexString(hexStream)
}
function writeRev2Config(origXML, newBuffer) {
let hexStream = newBuffer.map(x=>x.toString(16).padStart(2, '0').toUpperCase()).join('')
if (window.DOMParser) {
parser = new DOMParser()
xmlDoc = parser.parseFromString(origXML, 'text/xml')
} else { // Internet Explorer
xmlDoc = new ActiveXObject('Microsoft.XMLDOM')
xmlDoc.async = false
xmlDoc.loadXML(origXML)
}
const dataTags = xmlDoc.getElementsByTagName("data")
let dataTagsSorted = []
for (let i=0;i<dataTags.length;i++) {
const tag = dataTags[ i ]
const hexChunk = hexStream.substring(0, tag.textContent.length)
hexStream = hexStream.substring(tag.textContent.length)
tag.textContent = hexChunk
}
return atob("PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwhLS1UUEwgU3lzdMOobWVzLCBmaWNoaWVyIHBlcnNvbm5hbGlzYXRpb24gLyBjdXN0b21pemF0aW9uIGZpbGUtLT4") + "\n" + xmlDoc.documentElement.outerHTML
}
new Vue({
el: '#app',
vuetify: new Vuetify(),
data() {
return {
step0: {
file: [],
selectedConfigFileValid: false,
},
step1: {
birdyRingtoneSelection: [],
ringtoneSelection: null,
dirty: false,
},
step1_1: {
ringtoneData: {},
dirty: false,
error: false,
ringtoneDataJSON_User: '-- no json --',
},
step1_2: {
ringtoneData: {},
dirty: false,
error: false,
ringtoneDataRTTTL_User: '-- no json --',
},
global: {
birdyConfig: null,
step: 0,
betaFeatures: false,
}
}
},
created() {
},
methods: {
// step0
handleFileSelectionChange() {
this.step0.selectedConfigFileValid = false
if (this.step0.file.name.indexOf('.birdyIOT.rev2') > 0) {
this.step0.selectedConfigFileValid = true
return
}
},
readConfigFile() {
if (!this.step0.selectedConfigFileValid) return
const reader = new FileReader()
reader.onload = async _ => {
this.step0.originalConfigText = reader.result
const arrayBuffer = parseRev2Config(reader.result)
$buff = arrayBuffer
this.global.birdyConfig = new TPL_BirdySlimConfig()
this.global.birdyConfig.loadBuffer(arrayBuffer)
this.global.step = 1
this.step1.birdyRingtoneSelection = [
{ name: `Messages (0) [${ this.global.birdyConfig.readNotesFromBuffer(0).notes.length }]`, id: 0 }
]
for(let id=1;id<=21;id++) {
const ring = this.global.birdyConfig.readNotesFromBuffer(id)
console.log(id, ring)
this.step1.birdyRingtoneSelection.push({ name: `Ring (${ id }) [${ ring.notes.length }]`, id })
}
}
reader.readAsText(this.step0.file)
},
writeConfigFile () {
const newConfig = writeRev2Config(this.step0.originalConfigText, this.global.birdyConfig.saveBuffer()) // write a new configXml
download(newConfig, this.step0.file.name.replace('.birdyIOT.rev2','') + "-rings.birdyIOT.rev2", "text/xml")
},
// step1
demoPlay() {
const synth = new Tone.Synth().toDestination();
const now = Tone.now()
let offset=0
this.global.birdyConfig.readNotesFromBuffer(this.step1.ringtoneSelection).notes.map((note)=>{
offset+=note[2]*10
if (noteNames[ note[0] ] === 'Interval') return
synth.triggerAttackRelease(noteNames[ note[0] ] + note[1], "8n", offset/1e3)
})
},
decodeRingtone() {
this.step1_1.dirty = false
this.step1.dirty = false
this.step1_1.ringtoneData = this.global.birdyConfig.readNotesFromBuffer(this.step1.ringtoneSelection)
this.step1_1.ringtoneDataJSON_User = JSON.stringify(this.step1_1.ringtoneData, null, "")
},
encodeRingtone() {
const d = JSON.parse(JSON.stringify(this.step1_1.ringtoneData)) // dirty copy hack
d.ringNumber = this.step1.ringtoneSelection // enforce selected ringnumber
this.global.birdyConfig.writeNotes(d)
this.step1_1.ringtoneData = this.global.birdyConfig.readNotesFromBuffer(this.step1.ringtoneSelection)
this.step1_1.ringtoneDataJSON_User = JSON.stringify(this.step1_1.ringtoneData, null, "")
},
cleanRingtone() {
this.global.birdyConfig.writeNotes({
ringNumber: this.step1.ringtoneSelection,
melodyHeader: 0,
notes: [],
})
this.step1_1.ringtoneData = this.global.birdyConfig.readNotesFromBuffer(this.step1.ringtoneSelection)
this.step1_1.ringtoneDataJSON_User = JSON.stringify(this.step1_1.ringtoneData, null, "")
},
parseJSONtoRingtoneData() {
let jsonText = this.step1_1.ringtoneDataJSON_User
try {
let newData = JSON.parse(jsonText)
this.step1_1.ringtoneData = newData
this.step1.dirty = JSON.stringify(this.step1_1.ringtoneData) != JSON.stringify(this.global.birdyConfig.readNotesFromBuffer(this.step1.ringtoneSelection))
} catch(e) {
alert('ERROR:' + e)
}
},
testJSONRingtoneData() {
let jsonText = this.step1_1.ringtoneDataJSON_User
try {
let newData = JSON.parse(jsonText)
this.step1_1.dirty = JSON.stringify(newData) == JSON.stringify(this.step1_1.ringtoneData)
this.step1_1.error = !(typeof(newData.ringNumber) === 'number' && typeof(newData.melodyHeader) === 'number' && typeof(newData.notes) === 'object')
console.log(newData)
} catch(e) {
this.step1_1.error = true
this.step1_1.dirty = false
return
}
this.step1_1.dirty = true
}
}
})
</script>
</body>
</html>