Update version to 3.0.0

- use MP3 instead of WAV
 - new callbacks & properties
 - refactoring
This commit is contained in:
Gennady Grishkovtsov
2018-12-16 23:28:36 +03:00
parent 7e89d8a33a
commit db90e87dff
16 changed files with 304 additions and 277 deletions

View File

@@ -29,22 +29,23 @@ npm i vue-audio-recorder --save
## AudioRecorder props ## AudioRecorder props
| Prop | Type | Description | | Prop | Type | Description |
| --------------------- | -------- | --------------------------------------------------------------- | | --------------------- | -------- | ------------------------------------------------------------------------ |
| attempts | Number | Number of recording attempts | | attempts | Number | Number of recording attempts |
| compact | Boolean | Hide the download and upload buttons | | headers | Object | HTTP headers |
| headers | Object | HTTP headers | | time | Number | Time limit for the record (minutes) |
| time | Number | Time limit for the record (minutes) | | filename | String | Download/Upload filename |
| upload-url | String | URL for uploading | | upload-url | String | URL for uploading |
| start-record | Function | Fires after click the record button | | show-download-button | Boolean | If it is true show a download button. Default: true |
| stop-record | Function | Fires after click the stop button or exceeding the time limit | | show-upload-button | Boolean | If it is true show an upload button. Default: true |
| start-upload | Function | Fires after start uploading | | before-upload | Function | Callback fires before uploading |
| attempts-limit | Function | Fires after exceeding the attempts | | successful-upload | Function | Callback fires after successful uploading |
| failed-upload | Function | Fires after a failure uploading | | failed-upload | Function | Callback fires after failure uploading |
| mic-failed | Function | Fires if your microphone doesn't work | | mic-failed | Function | Callback fires if your microphone doesn't work |
| successful-upload | Function | Fires after a successful uploading | | before-recording | Function | Callback fires after click the record button |
| successful-upload-msg | String | Displays the message after a successful uploading | | pause-recording | Function | Callback fires after pause recording |
| failed-upload-msg | String | Displays the message after a failure uploading | | after-recording | Function | Callback fires after click the stop button or exceeding the time limit |
| select-record | Function | Callback fires after choise a record. Returns the record |
## AudioPlayer props ## AudioPlayer props
| Prop | Type | Description | | Prop | Type | Description |
@@ -59,15 +60,25 @@ npm i vue-audio-recorder --save
Vue.use(AudioRecorder) Vue.use(AudioRecorder)
``` ```
```js
methods: {
callback (data) {
console.debug(data)
}
}
```
```html ```html
<audio-recorder <audio-recorder
upload-url="YOUR_API_URL" upload-url="YOUR_API_URL"
:attempts="3" :attempts="3"
:headers="headers"
:time="2" :time="2"
:start-record="callback" :headers="headers"
:stop-record="callback" :before-recording="callback"
:start-upload="callback" :pause-recording="callback"
:after-recording="callback"
:select-record="callback"
:before-upload="callback"
:successful-upload="callback" :successful-upload="callback"
:failed-upload="callback"/> :failed-upload="callback"/>
``` ```

View File

@@ -14,9 +14,11 @@
:attempts="3" :attempts="3"
:time="2" :time="2"
:headers="headers" :headers="headers"
:start-record="callback" :before-recording="callback"
:stop-record="callback" :pause-recording="callback"
:start-upload="callback" :after-recording="callback"
:select-record="callback"
:before-upload="callback"
:successful-upload="callback" :successful-upload="callback"
:failed-upload="callback"/> :failed-upload="callback"/>

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +1,16 @@
{ {
"name": "vue-audio-recorder", "name": "vue-audio-recorder",
"description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server", "description": "Audio recorder for Vue.js. It allows to create, play, download and store records on a server",
"version": "2.2.0", "version": "3.0.0",
"author": "Gennady Grishkovtsov <grishkovelli@gmail.com>", "author": "Gennady Grishkovtsov <grishkovelli@gmail.com>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "webpack-dev-server --env.NODE_ENV=development --mode development --open --hot --https", "dev": "webpack-dev-server --env.NODE_ENV=development --mode development --open --hot --https",
"build": "webpack --env.NODE_ENV=production --mode production --progress --hide-modules" "build": "webpack --env.NODE_ENV=production --mode production --progress --hide-modules"
}, },
"dependencies": {}, "dependencies": {
"lamejs": "^1.2.0"
},
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,37 @@
<style lang="scss">
@import '../scss/icons';
</style>
<template>
<icon-button
id="download"
class="ar-icon ar-icon__xs ar-icon--no-border"
name="download"
@click.native="download"/>
</template>
<script>
import IconButton from './icon-button'
export default {
props: {
record : { type: Object },
filename : { type: String }
},
components: {
IconButton
},
methods: {
download () {
if (!this.record.url) {
return
}
const link = document.createElement('a')
link.href = this.record.url
link.download = `${this.filename}.mp3`
link.click()
}
}
}
</script>

View File

@@ -35,7 +35,7 @@
}, },
methods: { methods: {
onMouseDown (ev) { onMouseDown (ev) {
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId]) const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
this.$emit('change-linehead', seekPos) this.$emit('change-linehead', seekPos)
document.addEventListener('mousemove', this.onMouseMove) document.addEventListener('mousemove', this.onMouseMove)
document.addEventListener('mouseup', this.onMouseUp) document.addEventListener('mouseup', this.onMouseUp)
@@ -43,17 +43,17 @@
onMouseUp (ev) { onMouseUp (ev) {
document.removeEventListener('mouseup', this.onMouseUp) document.removeEventListener('mouseup', this.onMouseUp)
document.removeEventListener('mousemove', this.onMouseMove) document.removeEventListener('mousemove', this.onMouseMove)
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId]) const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
this.$emit('change-linehead', seekPos) this.$emit('change-linehead', seekPos)
}, },
onMouseMove (ev) { onMouseMove (ev) {
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId]) const seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
this.$emit('change-linehead', seekPos) this.$emit('change-linehead', seekPos)
} }
}, },
computed: { computed: {
calculateSize () { calculateSize () {
let value = this.percentage < 1 ? this.percentage * 100 : this.percentage const value = this.percentage < 1 ? this.percentage * 100 : this.percentage
return `${this.rowDirection ? 'width' : 'height'}: ${value}%` return `${this.rowDirection ? 'width' : 'height'}: ${value}%`
} }
} }

View File

@@ -1,16 +1,26 @@
<style lang="scss"> <style lang="scss">
.ar-player { .ar-player {
width: 380px; width: 380px;
height: 120px; height: unset;
border: 1px solid #E8E8E8; border: 0;
border-radius: 24px; border-radius: 0;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: #FAFAFA; background-color: unset;
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
& > .ar-player-bar {
border: 1px solid #E8E8E8;
border-radius: 24px;
margin: 0 0 0 5px;
& > .ar-player__progress {
width: 125px;
}
}
&-bar { &-bar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -26,33 +36,6 @@
justify-content: space-around; justify-content: space-around;
} }
&--compact {
height: unset;
flex-direction: row;
border: 0;
border-radius: 0;
background-color: unset;
& > .ar-player-actions {
width: unset;
& > #download,
& > #upload {
display: none;
}
}
& > .ar-player-bar {
border: 1px solid #E8E8E8;
border-radius: 24px;
margin: 0 0 0 5px;
& > .ar-player__progress {
width: 125px;
}
}
}
&__progress { &__progress {
width: 160px; width: 160px;
margin: 0 8px; margin: 0 8px;
@@ -81,27 +64,14 @@
</style> </style>
<template> <template>
<div class="ar-player" :class="{'ar-player--compact': compact}"> <div class="ar-player">
<div class="ar-player-actions"> <div class="ar-player-actions">
<icon-button
id="download"
class="ar-icon ar-icon__sm"
name="download"
@click.native="decorator(download)"/>
<icon-button <icon-button
id="play" id="play"
class="ar-icon ar-icon__lg ar-player__play" class="ar-icon ar-icon__lg ar-player__play"
:name="playBtnIcon" :name="playBtnIcon"
:class="{'ar-player__play--active': isPlaying}" :class="{'ar-player__play--active': isPlaying}"
@click.native="decorator(playback)"/> @click.native="playback"/>
<uploader
id="upload"
class="ar-icon ar-icon__sm"
:record="record"
:options="uploaderOptions"/>
</div> </div>
<div class="ar-player-bar"> <div class="ar-player-bar">
@@ -122,16 +92,14 @@
<script> <script>
import IconButton from './icon-button' import IconButton from './icon-button'
import LineControl from './line-control' import LineControl from './line-control'
import Uploader from './uploader'
import VolumeControl from './volume-control' import VolumeControl from './volume-control'
import { convertTimeMMSS } from '@/lib/utils' import { convertTimeMMSS } from '@/lib/utils'
export default { export default {
props: { props: {
src : { type: String }, src : { type: String },
record : { type: Object }, record : { type: Object },
compact : { type: Boolean, default: true }, filename : { type: String }
uploaderOptions : { type: Object, default: () => new Object }
}, },
data () { data () {
return { return {
@@ -144,7 +112,6 @@
components: { components: {
IconButton, IconButton,
LineControl, LineControl,
Uploader,
VolumeControl VolumeControl
}, },
mounted: function() { mounted: function() {
@@ -167,7 +134,7 @@
}, },
computed: { computed: {
audioSource () { audioSource () {
let url = this.src || this.record.url const url = this.src || this.record.url
if (url) { if (url) {
return url return url
} else { } else {
@@ -183,6 +150,10 @@
}, },
methods: { methods: {
playback () { playback () {
if (!this.audioSource) {
return
}
if (this.isPlaying) { if (this.isPlaying) {
this.player.pause() this.player.pause()
} else { } else {
@@ -191,18 +162,6 @@
this.isPlaying = !this.isPlaying this.isPlaying = !this.isPlaying
}, },
download () {
let link = document.createElement('a')
link.href = this.record.url
link.download = 'record.wav'
link.click()
},
decorator (func) {
if (!this.audioSource) {
return
}
func()
},
_resetProgress () { _resetProgress () {
if (this.isPlaying) { if (this.isPlaying) {
this.player.pause() this.player.pause()

View File

@@ -72,7 +72,7 @@
&__records-limit { &__records-limit {
position: absolute; position: absolute;
color: #AEAEAE; color: #AEAEAE;
font-size: 12px; font-size: 13px;
top: 78px; top: 78px;
} }
} }
@@ -160,6 +160,22 @@
top: 0; top: 0;
color: rgb(244, 120, 90); color: rgb(244, 120, 90);
} }
&__downloader,
&__uploader {
position: absolute;
top: 0;
bottom: 0;
margin: auto;
}
&__downloader {
right: 115px;
}
&__uploader {
right: 85px;
}
} }
@import '../scss/icons'; @import '../scss/icons';
@@ -168,7 +184,6 @@
<template> <template>
<div class="ar"> <div class="ar">
<div class="ar__overlay" v-if="isUploading"></div> <div class="ar__overlay" v-if="isUploading"></div>
<div class="ar-spinner" v-if="isUploading"> <div class="ar-spinner" v-if="isUploading">
<div class="ar-spinner__dot"></div> <div class="ar-spinner__dot"></div>
<div class="ar-spinner__dot"></div> <div class="ar-spinner__dot"></div>
@@ -201,89 +216,91 @@
:class="{'ar-records__record--selected': record.id === selected.id}" :class="{'ar-records__record--selected': record.id === selected.id}"
:key="record.id" :key="record.id"
v-for="(record, idx) in recordList" v-for="(record, idx) in recordList"
@click="selected = record"> @click="choiceRecord(record)">
<div <div
class="ar__rm" class="ar__rm"
v-if="record.id === selected.id" v-if="record.id === selected.id"
@click="removeRecord(idx)">&times;</div> @click="removeRecord(idx)">&times;</div>
<div class="ar__text">Record {{idx + 1}}</div> <div class="ar__text">Record {{idx + 1}}</div>
<div class="ar__text">{{record.duration}}</div> <div class="ar__text">{{record.duration}}</div>
<downloader
v-if="record.id === selected.id && showDownloadButton"
class="ar__downloader"
:record="record"
:filename="filename"/>
<uploader
v-if="record.id === selected.id && showUploadButton"
class="ar__uploader"
:record="record"
:filename="filename"
:headers="headers"
:upload-url="uploadUrl"/>
</div> </div>
</div> </div>
<audio-player :compact="compact" :record="selected" :uploader-options="uploaderOptions"/> <audio-player :record="selected"/>
<div :class="uploadStatusClasses" v-if="uploadStatus">{{message}}</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import AudioPlayer from './player' import AudioPlayer from './player'
import Downloader from './downloader'
import IconButton from './icon-button' import IconButton from './icon-button'
import Recorder from '@/lib/recorder' import Recorder from '@/lib/recorder'
import Uploader from './uploader'
import UploaderPropsMixin from '@/mixins/uploader-props'
import { convertTimeMMSS } from '@/lib/utils' import { convertTimeMMSS } from '@/lib/utils'
export default { export default {
mixins: [UploaderPropsMixin],
props: { props: {
attempts : { type: Number }, attempts : { type: Number },
compact : { type: Boolean, default: false }, time : { type: Number },
time : { type: Number },
attemptsLimit : { type: Function }, showDownloadButton : { type: Boolean, default: true },
micFailed : { type: Function }, showUploadButton : { type: Boolean, default: true },
startRecord : { type: Function },
stopRecord : { type: Function },
micFailed : { type: Function },
beforeRecording : { type: Function },
pauseRecording : { type: Function },
afterRecording : { type: Function },
failedUpload : { type: Function }, failedUpload : { type: Function },
headers : { type: Object }, beforeUpload : { type: Function },
startUpload : { type: Function },
successfulUpload : { type: Function }, successfulUpload : { type: Function },
uploadUrl : { type: String }, selectRecord : { type: Function }
successfulUploadMsg : { type: String, default: 'Upload successful' },
failedUploadMsg : { type: String, default: 'Upload fail' }
}, },
data () { data () {
return { return {
isUploading : false, isUploading : false,
recorder : new Recorder({ recorder : this._initRecorder(),
afterStop: () => { recordList : [],
this.recordList = this.recorder.recordList() selected : {},
if (this.stopRecord) { uploadStatus : null,
this.stopRecord('stop record')
}
},
attempts: this.attempts,
time: this.time
}),
recordList : [],
selected : {},
uploadStatus : null,
uploaderOptions : {}
} }
}, },
components: { components: {
AudioPlayer, AudioPlayer,
IconButton Downloader,
IconButton,
Uploader
}, },
created () { mounted () {
this.uploaderOptions = {
failedUpload : this.failedUpload,
headers : this.headers,
startUpload : this.startUpload,
successfulUpload : this.successfulUpload,
uploadUrl : this.uploadUrl
}
this.$eventBus.$on('start-upload', () => { this.$eventBus.$on('start-upload', () => {
this.isUploading = true this.isUploading = true
this.beforeUpload && this.beforeUpload('before upload')
}) })
this.$eventBus.$on('end-upload', (resp) => { this.$eventBus.$on('end-upload', (msg) => {
this.isUploading = false this.isUploading = false
this.uploadStatus = status
setTimeout(() => {this.uploadStatus = null}, 1500) if (msg.status === 'success') {
this.successfulUpload && this.successfulUpload(msg.response)
} else {
this.failedUpload && this.failedUpload(msg.response)
}
}) })
}, },
beforeDestroy () { beforeDestroy () {
@@ -297,14 +314,8 @@
if (!this.isRecording || (this.isRecording && this.isPause)) { if (!this.isRecording || (this.isRecording && this.isPause)) {
this.recorder.start() this.recorder.start()
if (this.startRecord) {
this.startRecord('start record')
}
} else { } else {
this.recorder.pause() this.recorder.pause()
if (this.startRecord) {
this.startRecord('pause record')
}
} }
}, },
stopRecorder () { stopRecorder () {
@@ -313,16 +324,32 @@
} }
this.recorder.stop() this.recorder.stop()
this.recordList = this.recorder.recordList()
}, },
removeRecord (idx) { removeRecord (idx) {
this.recordList.splice(idx, 1) this.recordList.splice(idx, 1)
this.$set(this.selected, 'url', null) this.$set(this.selected, 'url', null)
this.$eventBus.$emit('remove-record') this.$eventBus.$emit('remove-record')
},
choiceRecord (record) {
if (this.selected === record) {
return
}
this.selected = record
this.selectRecord && this.selectRecord(record)
},
_initRecorder () {
return new Recorder({
beforeRecording : this.beforeRecording,
afterRecording : this.afterRecording,
pauseRecording : this.pauseRecording,
micFailed : this.micFailed
})
} }
}, },
computed: { computed: {
attemptsLeft () { attemptsLeft () {
return this.attempts - this.recorder.records.length return this.attempts - this.recordList.length
}, },
iconButtonType () { iconButtonType () {
return this.isRecording && this.isPause ? 'mic' : this.isRecording ? 'pause' : 'mic' return this.isRecording && this.isPause ? 'mic' : this.isRecording ? 'pause' : 'mic'
@@ -333,20 +360,12 @@
isRecording () { isRecording () {
return this.recorder.isRecording return this.recorder.isRecording
}, },
message () {
return this.uploadStatus === 'success' ? this.successfulUploadMsg : this.failedUploadMsg
},
recordedTime () { recordedTime () {
if (this.time && this.recorder.duration >= this.time * 60) { if (this.time && this.recorder.duration >= this.time * 60) {
this.stopRecorder() this.stopRecorder()
} }
return convertTimeMMSS(this.recorder.duration) return convertTimeMMSS(this.recorder.duration)
}, },
uploadStatusClasses () {
let classes = ['ar__upload-status']
classes.push(this.uploadStatus === 'success' ? 'ar__upload-status--success' : 'ar__upload-status--fail')
return classes.join(' ')
},
volume () { volume () {
return parseFloat(this.recorder.volume) return parseFloat(this.recorder.volume)
} }

View File

@@ -3,16 +3,17 @@
</style> </style>
<template> <template>
<icon-button name="save" @click.native="upload"/> <icon-button name="save" class="ar-icon ar-icon__xs ar-icon--no-border" @click.native="upload"/>
</template> </template>
<script> <script>
import IconButton from './icon-button' import IconButton from './icon-button'
import UploaderPropsMixin from '@/mixins/uploader-props'
export default { export default {
mixins: [UploaderPropsMixin],
props: { props: {
options : { type: Object }, record: { type: Object }
record : { type: Object }
}, },
components: { components: {
IconButton IconButton
@@ -25,28 +26,16 @@
this.$eventBus.$emit('start-upload') this.$eventBus.$emit('start-upload')
if (this.options.startUpload) { const data = new FormData()
this.options.startUpload() data.append('audio', this.record.blob, `${this.filename}.mp3`)
}
let data = new FormData() const headers = Object.assign(this.headers, {})
data.append('audio', this.record.blob, 'my-record')
let headers = Object.assign(this.options.headers, {})
headers['Content-Type'] = `multipart/form-data; boundary=${data._boundary}` headers['Content-Type'] = `multipart/form-data; boundary=${data._boundary}`
this.$http.post(this.options.uploadUrl, data, { headers: headers }).then(resp => { this.$http.post(this.uploadUrl, data, { headers: headers }).then(resp => {
this.$eventBus.$emit('end-upload', 'success') this.$eventBus.$emit('end-upload', { status: 'success', response: resp })
if (this.options.successfulUpload) {
this.options.successfulUpload(resp)
}
}).catch(error => { }).catch(error => {
this.$eventBus.$emit('end-upload', 'fail') this.$eventBus.$emit('end-upload', { status: 'fail', response: error })
if (this.options.failedUpload) {
this.options.failedUpload(error)
}
}) })
} }
} }

49
src/lib/encoder.js Normal file
View File

@@ -0,0 +1,49 @@
import { Mp3Encoder } from 'lamejs'
export default class {
constructor(config) {
this.bitRate = config.bitRate || 128
this.sampleRate = config.sampleRate || 44100
this.dataBuffer = []
this.encoder = new Mp3Encoder(1, this.sampleRate, this.bitRate)
}
encode(arrayBuffer) {
const maxSamples = 1152
const samples = this._convertBuffer(arrayBuffer)
let remaining = samples.length
for (let i = 0; remaining >= 0; i += maxSamples) {
const left = samples.subarray(i, i + maxSamples)
const buffer = this.encoder.encodeBuffer(left)
this.dataBuffer.push(new Int8Array(buffer))
remaining -= maxSamples
}
}
finish() {
this.dataBuffer.push(this.encoder.flush())
const blob = new Blob(this.dataBuffer, { type: 'audio/mp3' })
this.dataBuffer = []
return {
id : Date.now(),
blob : blob,
url : URL.createObjectURL(blob)
}
}
_floatTo16BitPCM(input, output) {
for (let i = 0; i < input.length; i++) {
const s = Math.max(-1, Math.min(1, input[i]))
output[i] = (s < 0 ? s * 0x8000 : s * 0x7FFF)
}
}
_convertBuffer(arrayBuffer) {
const data = new Float32Array(arrayBuffer)
const out = new Int16Array(arrayBuffer.length)
this._floatTo16BitPCM(data, out)
return out
}
}

View File

@@ -1,17 +1,18 @@
import WavEncoder from './wav-encoder' import Encoder from './encoder'
import { convertTimeMMSS } from './utils' import { convertTimeMMSS } from './utils'
export default class { export default class {
constructor (options = {}) { constructor (options = {}) {
this.afterStop = options.afterStop this.beforeRecording = options.beforeRecording
this.micFailed = options.micFailed this.pauseRecording = options.pauseRecording
this.afterRecording = options.afterRecording
this.micFailed = options.micFailed
this.bufferSize = 4096 this.bufferSize = 4096
this.records = [] this.records = []
this.samples = []
this.isPause = false this.isPause = false
this.isRecording = false this.isRecording = false
this.duration = 0 this.duration = 0
this.volume = 0 this.volume = 0
@@ -20,11 +21,23 @@ export default class {
} }
start () { start () {
navigator.mediaDevices.getUserMedia({audio: true}) const constraints = {
.then(this._micCaptured.bind(this)) video: false,
.catch(this._micError.bind(this)) audio: {
this.isPause = false channelCount: 1,
echoCancellation: false
}
}
this.beforeRecording && this.beforeRecording('start recording')
navigator.mediaDevices
.getUserMedia(constraints)
.then(this._micCaptured.bind(this))
.catch(this._micError.bind(this))
this.isPause = false
this.isRecording = true this.isRecording = true
this.lameEncoder = new Encoder({})
} }
stop () { stop () {
@@ -33,23 +46,9 @@ export default class {
this.processor.disconnect() this.processor.disconnect()
this.context.close() this.context.close()
let encoder = new WavEncoder({ const record = this.lameEncoder.finish()
bufferSize : this.bufferSize, record.duration = convertTimeMMSS(this.duration)
sampleRate : this.context.sampleRate, this.records.push(record)
samples : this.samples
})
let audioBlob = encoder.getData()
let audioUrl = URL.createObjectURL(audioBlob)
this.samples = []
this.records.push({
id : Date.now(),
blob : audioBlob,
duration : convertTimeMMSS(this.duration),
url : audioUrl
})
this._duration = 0 this._duration = 0
this.duration = 0 this.duration = 0
@@ -57,9 +56,7 @@ export default class {
this.isPause = false this.isPause = false
this.isRecording = false this.isRecording = false
if (this.afterStop) { this.afterRecording && this.afterRecording(record)
this.afterStop()
}
} }
pause () { pause () {
@@ -70,6 +67,8 @@ export default class {
this._duration = this.duration this._duration = this.duration
this.isPause = true this.isPause = true
this.pauseRecording && this.pauseRecording('pause recording')
} }
recordList () { recordList () {
@@ -88,8 +87,10 @@ export default class {
this.stream = stream this.stream = stream
this.processor.onaudioprocess = (ev) => { this.processor.onaudioprocess = (ev) => {
let sample = ev.inputBuffer.getChannelData(0) const sample = ev.inputBuffer.getChannelData(0)
let sum = 0.0 let sum = 0.0
this.lameEncoder.encode(sample)
for (let i = 0; i < sample.length; ++i) { for (let i = 0; i < sample.length; ++i) {
sum += sample[i] * sample[i] sum += sample[i] * sample[i]
@@ -97,7 +98,6 @@ export default class {
this.duration = parseFloat(this._duration) + parseFloat(this.context.currentTime.toFixed(2)) this.duration = parseFloat(this._duration) + parseFloat(this.context.currentTime.toFixed(2))
this.volume = Math.sqrt(sum / sample.length).toFixed(2) this.volume = Math.sqrt(sum / sample.length).toFixed(2)
this.samples.push(new Float32Array(sample))
} }
this.input.connect(this.processor) this.input.connect(this.processor)
@@ -105,8 +105,6 @@ export default class {
} }
_micError (error) { _micError (error) {
if (this.micFailed) { this.micFailed && this.micFailed(error)
this.micFailed(error)
}
} }
} }

View File

@@ -1,6 +1,6 @@
export function calculateLineHeadPosition (ev, element) { export function calculateLineHeadPosition (ev, element) {
let progressWidth = element.getBoundingClientRect().width const progressWidth = element.getBoundingClientRect().width
let leftPosition = ev.target.getBoundingClientRect().left const leftPosition = ev.target.getBoundingClientRect().left
let pos = (ev.clientX - leftPosition) / progressWidth let pos = (ev.clientX - leftPosition) / progressWidth
try { try {

View File

@@ -1,59 +0,0 @@
export default class {
constructor (options) {
this.bufferSize = options.bufferSize || 4096
this.sampleRate = options.sampleRate
this.samples = options.samples
}
getData () {
this._joinSamples()
let buffer = new ArrayBuffer(44 + this.samples.length * 2)
let view = new DataView(buffer)
this._writeString(view, 0, 'RIFF') // RIFF identifier
view.setUint32(4, 36 + this.samples.length * 2, true) // RIFF chunk length
this._writeString(view, 8, 'WAVE') // RIFF type
this._writeString(view, 12, 'fmt ') // format chunk identifier
view.setUint32(16, 16, true) // format chunk length
view.setUint16(20, 1, true) // sample format (raw)
view.setUint16(22, 1, true) // channel count
view.setUint32(24, this.sampleRate, true) // sample rate
view.setUint32(28, this.sampleRate * 4, true) // byte rate (sample rate * block align)
view.setUint16(32, 4, true) // block align (channel count * bytes per sample)
view.setUint16(34, 16, true) // bits per sample
this._writeString(view, 36, 'data') // data chunk identifier
view.setUint32(40, this.samples.length * 2, true) // data chunk length
this._floatTo16BitPCM(view, 44, this.samples)
return new Blob([view], {type: 'audio/wav'})
}
_floatTo16BitPCM (output, offset, input) {
for (let i = 0; i < input.length; i++, offset += 2) {
let s = Math.max(-1, Math.min(1, input[i]))
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
}
}
_joinSamples () {
let recordLength = this.samples.length * this.bufferSize
let joinedSamples = new Float64Array(recordLength)
let offset = 0
for (let i = 0; i < this.samples.length; i++) {
let sample = this.samples[i]
joinedSamples.set(sample, offset)
offset += sample.length
}
this.samples = joinedSamples
}
_writeString (view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
}

View File

@@ -0,0 +1,7 @@
export default {
props: {
filename : { type: String, default: 'record' },
headers : { type: Object },
uploadUrl : { type: String }
}
}

View File

@@ -7,6 +7,12 @@
cursor: pointer; cursor: pointer;
transition: .2s; transition: .2s;
&--no-border {
border: 0;
border-radius: 0;
padding: 0;
}
&--rec { &--rec {
fill: white; fill: white;
background-color: #FF6B64; background-color: #FF6B64;
@@ -15,7 +21,6 @@
&--pulse { &--pulse {
animation: ripple .5s linear infinite; animation: ripple .5s linear infinite;
@keyframes ripple { @keyframes ripple {
0% { 0% {
box-shadow: box-shadow:
@@ -32,14 +37,22 @@
} }
} }
&__xs {
width: 18px;
height: 18px;
line-height: 18px;
}
&__sm { &__sm {
width: 30px; width: 30px;
height: 30px; height: 30px;
line-height: 30px;
} }
&__lg { &__lg {
width: 45px; width: 45px;
height: 45px; height: 45px;
line-height: 45px;
box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5); box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5);
} }
} }