The main website of PHENOM
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
PHENOMIC.net/pages/bytebeat.vue

303 lines
9.2 KiB

<template>
<v-layout column justify-center align-left>
<v-flex xs12 sm8 md6>
<h3 class="display-3">Bytebeat Generator</h3>
<span class="subheading">This small tool can create music from a single JavaScript Expression using the Web Audio API.</span>
<v-divider class="my-3"></v-divider>
<v-select
v-model="exampleSelected"
prepend-icon="mdi-file-music"
:items="bytebeatSaves"
return-object
item-text="name"
>
</v-select>
<v-btn @click="load()">Load</v-btn>
<v-btn @click="saveDialog = true" :disabled="!changed">Save As...</v-btn>
<v-btn color="red" :disabled="exampleSelected.isExample" @click="deleteSave()">Delete</v-btn>
<v-divider class="my-3"></v-divider>
<v-form>
<v-select
v-model="liveByteBeat.frequency"
prepend-icon="mdi-waves"
:items="frequencies"
label="Frequency (Hz)"
>
</v-select>
<v-text-field
v-model="liveByteBeat.duration"
label="Duration (seconds)"
placeholder="30"
prepend-icon="timer"
></v-text-field>
<v-text-field
v-model="liveByteBeat.ft"
label="JavaScript function (t)="
value='w=t>>9,k=32,m=2048,a=1-t/m%1,d=(14*t*t^t)%m*a,y=[3,3,4.7,2][p=w/k&3]*t/4,h="IQNNNN!!]]!Q!IW]WQNN??!!W]WQNNN?".charCodeAt(w/2&15|p/3<<4)/33*t-t,s=y*.98%80+y%80+(w>>7&&a*((5*t%m*a&128)*(0x53232323>>w/4&1)+(d&127)*(0xa444c444>>w/4&1)*1.5+(d*w&1)+(h%k+h*1.99%k+h*.49%k+h*.97%k-64)*(4-a-a))),s*s>>14?127:s'
hint="The variable t determines how the function behaves"
></v-text-field>
<v-slider
v-model="volume"
v-on:input="setVolume"
:disabled="!isContextAvailable()"
label="Volume"
append-icon="volume_up"
prepend-icon="volume_down"
></v-slider>
<p color="red" v-if="changed">Changes were made. Press on Generate to recompile the expression.</p>
<v-layout row wrap align-center>
<v-btn color="green" @click="regenerate()">
<v-icon>mdi-sync</v-icon>
<span>Generate</span>
</v-btn>
<v-btn-toggle v-model="playBtn" multiple>
<v-btn flat @click="playPause()" :disabled="!isGenerated()">
<v-icon>mdi-play-pause</v-icon>
</v-btn>
</v-btn-toggle>
<v-btn-toggle v-model="muteBtn" multiple>
<v-btn flat @click="mute()" :disabled="!isContextAvailable()">
<v-icon>mdi-volume-off</v-icon>
</v-btn>
</v-btn-toggle>
</v-layout>
</v-form>
<v-dialog
v-model="saveDialog"
max-width="290"
>
<v-card>
<v-card-title class="headline">Saving ByteBeat...</v-card-title>
<v-layout align-content-space-between>
<v-form>
<v-text-field
v-model="newName"
placeholder="Enter a Name"
label="Name"
></v-text-field>
</v-form>
</v-layout>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
flat="flat"
@click="saveDialog = false"
>
Cancel
</v-btn>
<v-btn
color="green darken-1"
flat="flat"
@click="onSave()"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-flex>
</v-layout>
</template>
<script>
import { mapGetters, mapState, mapMutations } from 'vuex'
/* eslint no-eval: 0 */
export default {
layout: 'phenomic',
data: () => ({
exampleSelected: {
id: 0,
isExample: true,
name: 'Default',
frequency: '8000',
duration: '30',
ft: 't * ((t>>12|t>>8)&63&t>>4)'
},
liveByteBeat: {
id: 0,
name: 'Default',
frequency: '8000',
duration: '30',
ft: 't * ((t>>12|t>>8)&63&t>>4)'
},
playBtn: [],
muteBtn: [],
frequencies: ['8000', '11025', '16000', '22050', '32000', '37800', '44056', '44100', '47250', '48000', '50000', '50400', '88200', '96000', '176400', '192000'],
audioCtx: null,
source: null,
gainNode: null,
volume: 50,
sampleBuffer: null,
saveDialog: false,
changed: false,
isLoading: false,
newName: ''
}),
watch: {
liveByteBeat: {
handler: function (newVal, oldVal) {
if (!this.isLoading) {
this.changed = true
} else {
this.changed = false
this.isLoading = false
}
},
deep: true
}
},
errorCaptured (err, vm, info) {
console.log(err)
},
computed: {
...mapState({
bytebeatSaves: 'bytebeatSaves'
}),
...mapGetters({
getNewId: 'getNewId'
})
},
methods: {
onChange (newVal) {
console.log('Change: ' + newVal)
this.changed = true
this.liveByteBeat.isExample = false
},
isGenerated () {
if (!(this.source === undefined || this.source === null)) {
return !(this.source.buffer === undefined || this.source.buffer === null)
}
return false
},
isContextAvailable () {
return !(this.audioCtx === undefined || this.audioCtx === null)
},
isExample () {
return this.liveByteBeat.isExample
},
isPlaying () {
if (this.audioCtx.state === 'running') {
return true
} else if (this.audioCtx.state === 'suspended') {
return false
}
},
regenerate () {
// Reset change indicator
this.changed = false
// Reset Playbutton
this.playBtn = []
if (process.browser) {
// Close Audio Context if already opened
if (this.isContextAvailable()) {
this.audioCtx.close()
}
// Create new Audio Context
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
this.audioCtx.suspend() // Stop instant playback
this.gainNode = this.audioCtx.createGain()
this.gainNode.connect(this.audioCtx.destination)
} else {
console.error("This is not a browser! Can't continue!")
return
}
// Create a new Audio Source
this.source = this.audioCtx.createBufferSource()
// Determines the behaviour when the Audio has finished playing
this.source.onended = (event) => {
this.audioCtx.suspend()
this.playBtn = []
}
// Here is the main clear on compare match linear function of the bytebeat algorithm
this.sampleBuffer = []
let f
eval('f = function (t) { return ' + this.liveByteBeat.ft + '}')
for (let t = 0; t < this.liveByteBeat.frequency * this.liveByteBeat.duration; t++) {
let sample = f(t)
sample = (sample & 0xff) * 256
if (sample < 0) sample = 0
if (sample > 65535) sample = 65535
this.sampleBuffer.push(sample / 65535)
}
// Fill Audio Buffer with Data
let audioBuffer = this.audioCtx.createBuffer(2, this.liveByteBeat.frequency * this.liveByteBeat.duration, this.liveByteBeat.frequency)
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
let channelBuffer = audioBuffer.getChannelData(channel)
for (let i = 0; i < audioBuffer.length; i++) {
channelBuffer[i] = this.sampleBuffer[i]
}
}
// Apply Audio Buffer to source
this.source.buffer = audioBuffer
// Apply Gain connection for Volume control
this.source.connect(this.gainNode)
// Start the source
this.source.start(0)
},
playPause () {
if (this.isGenerated()) {
if (this.isPlaying()) {
this.audioCtx.suspend()
this.playBtn = []
} else {
this.audioCtx.resume()
}
}
},
mute () {
if (this.muteBtn.length === 0) {
this.gainNode.gain.value = 0
} else {
this.gainNode.gain.value = this.volume / 100.0
}
},
setVolume (something) {
this.muteBtn = []
this.gainNode.gain.value = something / 100.0
},
onSave () {
this.saveDialog = false
this.liveByteBeat.name = this.newName
this.liveByteBeat.id = this.getNewId
this.saveBeat(this.liveByteBeat)
this.exampleSelected = this.liveByteBeat
this.isCurrentExample = false
},
load () {
this.isLoading = true
this.liveByteBeat.id = this.exampleSelected.id
this.liveByteBeat.duration = this.exampleSelected.duration
this.liveByteBeat.frequency = this.exampleSelected.frequency
this.liveByteBeat.ft = this.exampleSelected.ft
this.liveByteBeat.name = this.exampleSelected.name
this.isCurrentExample = this.exampleSelected.isExample
},
deleteSave () {
if (!this.isCurrentExample) {
this.deleteBeat(this.exampleSelected.id)
}
},
...mapMutations({
saveBeat: 'saveByteBeat',
deleteBeat: 'deleteByteBeatById'
})
},
beforeRouteLeave (to, from, next) {
// Close Audio Context if already opened
if (this.isContextAvailable()) {
this.audioCtx.close()
}
next()
}
}
</script>