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
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> |