diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..9fc7ded --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,42 @@ +name: Docker Image CI + +on: + push: + branches: [ main ] + +jobs: + buildx: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - + name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - + name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push linux/amd64 + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64 + push: true + tags: noisedash/noisedash:latest + - + name: Build and push linux/arm/v7 + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/arm/v7 + push: true + tags: noisedash/noisedash:latest-armv7 diff --git a/README.md b/README.md index 421b021..791c3cc 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Self-hostable web tool for generating ambient noises # Features * Generate and customize ambient noises and user-uploadable samples (leveraging [Tone.js](https://github.com/Tonejs/Tone.js/)) -* Save "noise profiles" so you can easily switch between your created soundscapes +* Save "noise profiles" so you can easily switch between your created soundscapes. Import and export them for easy sharing, record them for use elsewhere * Fine-tune your noises with audio processing tools like filters, LFOs, and effects -* Upload audio samples (e.g rain, wind, thunder) to combine with your generated noises +* Upload and edit audio samples (e.g rain, wind, thunder) to combine with your generated noises * Use admin tools to manage multiple users * Mobile friendly diff --git a/src/components/Noise.vue b/src/components/Noise.vue index 0e05b1c..97aadfb 100644 --- a/src/components/Noise.vue +++ b/src/components/Noise.vue @@ -90,18 +90,16 @@ > - Profile Name + Save Profile As... - - - + @@ -168,6 +166,16 @@ Export Profile + + + mdi-record + + + Record Profile Audio + + @@ -252,7 +260,6 @@ :items="profileItems" return-object label="Profiles" - class="mx-3 mb-5" /> @@ -274,6 +281,90 @@ + + + + + + Start Recording + + + + + Select profile to record audio for. This is only supported on Chrome and Firefox. + + + + + + + + + + + + + Close + + + Record + + + + + + + + + + Recording + + + Time Elapsed: {{ recordingTimeElapsed }} Seconds + + + + + Cancel + + + Stop and Save + + + + @@ -345,7 +436,7 @@ label="Volume" thumb-label max="0" - min="-30" + min="-60" class="mx-3" @input="updateVolume" /> @@ -548,7 +639,7 @@ label="Volume" thumb-label max="0" - min="-30" + min="-60" class="mx-3" @input="updateSampleVolume(sample.id, index)" /> @@ -646,9 +737,7 @@ - - WARNING: Uploaded samples are publicly accessible. - + WARNING: Uploaded samples are publicly accessible. - - - + @@ -711,7 +798,7 @@ > - Edit Sample + Edit Samples @@ -722,7 +809,6 @@ item-text="name" return-object label="Samples" - class="mx-3" @change="loadPreviewSample" /> @@ -736,7 +822,6 @@ v-model="previewSampleLoopPointsEnabled" :disabled="previewSamplePlaying" label="Use Loop Points" - class="mx-3" /> @@ -745,7 +830,7 @@ v-model="previewSampleLoopStart" type="number" label="Loop Start Time" - class="mx-3" + class="mr-3" :disabled="!previewSampleLoopPointsEnabled || previewSamplePlaying" :rules="[rules.gt(-1)]" @change="updatePreviewSamplePlayerLoopPoints" @@ -755,7 +840,7 @@ v-model="previewSampleLoopEnd" type="number" label="Loop End Time" - class="mx-3" + class="ml-3" :disabled="!previewSampleLoopPointsEnabled || previewSamplePlaying" :rules="[rules.gt(-1), rules.lt(previewSampleLength)]" @change="updatePreviewSamplePlayerLoopPoints" @@ -767,7 +852,6 @@ v-model="previewSampleFadeIn" type="number" label="Fade In Time" - class="mx-3" :disabled="previewSamplePlaying" :rules="[rules.gt(-1)]" @change="updatePreviewSamplePlayerFadeIn" diff --git a/src/components/noise.js b/src/components/noise.js index 8a2b383..2aee06e 100644 --- a/src/components/noise.js +++ b/src/components/noise.js @@ -64,6 +64,12 @@ export default { previewSampleButtonText: 'Preview Sample', previewSampleLoading: true, previewSampleLength: 0, + startRecordingDialog: false, + recordingDialog: false, + recordingTimeElapsed: 0, + recordedProfile: {}, + recordingFileName: '', + isRecordingValid: false, errorSnackbar: false, errorSnackbarText: '', rules: { @@ -98,6 +104,7 @@ export default { this.players = new Tone.Players() this.samplePreviewPlayer = new Tone.Player().toDestination() this.samplePreviewPlayer.loop = true + this.recorder = new Tone.Recorder() this.populateProfileItems(0) this.populatePreviewSampleItems() @@ -249,6 +256,7 @@ export default { this.selectedProfile = this.profileItems.find(p => p.id === profileId) } this.exportedProfile = this.profileItems[0] + this.recordedProfile = this.profileItems[0] this.loadProfile() } } @@ -483,7 +491,7 @@ export default { } }) .catch(() => { - this.errorSnackbarText = 'Error Saving Profile' + this.errorSnackbarText = 'Error Importing Profile' this.errorSnackbar = true }) @@ -536,7 +544,7 @@ export default { } }) .catch(() => { - this.errorSnackbarText = 'Error Loading Profile' + this.errorSnackbarText = 'Error Exporting Profile' this.errorSnackbar = true }) @@ -631,6 +639,116 @@ export default { if (this.previewSampleLoopStart >= 0 && this.previewSampleLoopEnd <= this.previewSampleLength) { this.samplePreviewPlayer.setLoopPoints(this.previewSampleLoopStart, this.previewSampleLoopEnd) } + }, + openStartRecordingDialog () { + this.startRecordingDialog = true + this.profileMoreDialog = false + }, + startRecording () { + this.$http.get('/profiles/'.concat(this.recordedProfile.id)) + .then(async response => { + if (response.status === 200) { + const profile = response.data.profile + + this.isTimerEnabled = profile.isTimerEnabled + this.duration = profile.duration + this.volume = profile.volume + this.noiseColor = profile.noiseColor + this.isFilterEnabled = profile.isFilterEnabled + this.filterType = profile.filterType + this.filterCutoff = profile.filterCutoff + this.isLFOFilterCutoffEnabled = profile.isLFOFilterCutoffEnabled + this.lfoFilterCutoffFrequency = profile.lfoFilterCutoffFrequency + this.lfoFilterCutoffRange[0] = profile.lfoFilterCutoffLow + this.lfoFilterCutoffRange[1] = profile.lfoFilterCutoffHigh + this.isTremoloEnabled = profile.isTremoloEnabled + this.tremoloFrequency = profile.tremoloFrequency + this.tremoloDepth = profile.tremoloDepth + + this.loadedSamples = profile.samples + + this.startRecordingDialog = false + this.recordingDialog = true + this.recordingTimeElapsed = 0 + + await this.recorder.start() + this.recordingInterval = setInterval(() => this.recordingTimeElapsed++, 1000) + this.playProfileForRecording() + } + }) + .catch(() => { + this.errorSnackbarText = 'Error Recording Profile' + this.errorSnackbar = true + }) + }, + playProfileForRecording () { + this.playDisabled = true + Tone.Transport.cancel() + + if (!this.isFilterEnabled && !this.isTremoloEnabled) { + this.noise = new Tone.Noise({ volume: this.volume, type: this.noiseColor }).connect(this.recorder).toDestination() + } else if (!this.isFilterEnabled && this.isTremoloEnabled) { + this.tremolo = new Tone.Tremolo({ frequency: this.tremoloFrequency, depth: this.tremoloDepth }).connect(this.recorder).toDestination().start() + this.noise = new Tone.Noise({ volume: this.volume, type: this.noiseColor }).connect(this.tremolo) + } else if (this.isFilterEnabled && !this.isTremoloEnabled) { + this.filter = new Tone.Filter(this.filterCutoff, this.filterType).connect(this.recorder).toDestination() + this.noise = new Tone.Noise({ volume: this.volume, type: this.noiseColor }).connect(this.filter) + } else if (this.isFilterEnabled && this.isTremoloEnabled) { + this.tremolo = new Tone.Tremolo({ frequency: this.tremoloFrequency, depth: this.tremoloDepth }).connect(this.recorder).toDestination().start() + this.filter = new Tone.Filter(this.filterCutoff, this.filterType).connect(this.tremolo) + this.noise = new Tone.Noise({ volume: this.volume, type: this.noiseColor }).connect(this.filter) + } else { + this.tremolo = new Tone.Tremolo({ frequency: this.tremoloFrequency, depth: this.tremoloDepth }).connect(this.recorder).toDestination().start() + this.filter = new Tone.Filter(this.filterCutoff, this.filterType).connect(this.tremolo) + this.noise = new Tone.Noise({ volume: this.volume, type: this.noiseColor }).connect(this.filter) + } + + if (this.isLFOFilterCutoffEnabled) { + this.lfo = new Tone.LFO({ frequency: this.lfoFilterCutoffFrequency, min: this.lfoFilterCutoffRange[0], max: this.lfoFilterCutoffRange[1] }) + this.lfo.connect(this.filter.frequency).start() + } + + this.loadedSamples.forEach(s => { + this.players.player(s.id).loop = true + this.players.player(s.id).fadeIn = s.fadeIn + if (s.loopPointsEnabled) { + this.players.player(s.id).setLoopPoints(s.loopStart, s.loopEnd) + } + this.players.player(s.id).volume.value = s.volume + + this.players.player(s.id).connect(this.recorder) + this.players.player(s.id).unsync().sync().start(0) + }) + + this.noise.sync().start(0) + + Tone.Transport.start() + }, + async stopRecording () { + const recording = await this.recorder.stop() + + // Set active profile back to the selected one + this.loadProfile() + + const url = URL.createObjectURL(recording) + const anchor = document.createElement('a') + anchor.download = this.recordingFileName + '.webm' + anchor.href = url + anchor.click() + + clearInterval(this.recordingInterval) + this.recordingDialog = false + this.stop() + }, + async cancelRecording () { + await this.recorder.stop() + + // Set active profile back to the selected one + this.loadProfile() + + clearInterval(this.recordingInterval) + this.recordingDialog = false + this.stop() } } }
Select profile to record audio for. This is only supported on Chrome and Firefox.
WARNING: Uploaded samples are publicly accessible.