Skip to content

Commit

Permalink
support streaming
Browse files Browse the repository at this point in the history
  • Loading branch information
pengzhendong committed Oct 15, 2024
1 parent 1f861d5 commit e2b1dfe
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
bash install.sh
echo ${{ inputs.version }} > VERSION
bash install.sh
python -m venv .venv
source .venv/bin/activate
python -m pip install -U setuptools wheel
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

wavesurfer/js
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
include requirements.txt
include VERSION
recursive-include wavesurfer/css *.css
recursive-include wavesurfer/js *.js
recursive-include wavesurfer/templates *.txt

7 changes: 7 additions & 0 deletions wavesurfer/css/bootstrap.min.css

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions wavesurfer/js/pcm-player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
(function() {
if (typeof PCMPlayer === 'undefined') {
class PCMPlayer {
constructor(option) {
this.option = Object.assign({}, {channels: 1, sampleRate: 16000, flushTime: 100}, option)
// 每隔 flushTime 毫秒调用一次 flush 函数
this.interval = setInterval(this.flush.bind(this), this.option.flushTime)
this.samples = new Float32Array()
this.all_samples = new Float32Array()
this.url

this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
this.gainNode = this.audioCtx.createGain()
this.gainNode.gain.value = 1.0 // 音量
this.gainNode.connect(this.audioCtx.destination)
this.startTime = this.audioCtx.currentTime
}

feed(base64Data) {
const binaryString = atob(base64Data)
const buffer = new ArrayBuffer(binaryString.length)
const bufferView = new Uint8Array(buffer)
for (let i = 0; i < binaryString.length; i++) {
bufferView[i] = binaryString.charCodeAt(i);
}
const data = Float32Array.from(new Int16Array(buffer)).map(item => item / 32768)
this.samples = new Float32Array([...this.samples, ...data])
this.all_samples = new Float32Array([...this.all_samples, ...data])

const wavBytes = getWavBytes(this.all_samples.buffer, {
isFloat: true,
numChannels: this.option.channels,
sampleRate: this.option.sampleRate,
})
this.url = URL.createObjectURL(new Blob([wavBytes], { type: 'audio/wav' }))
}

flush() {
if (!this.samples.length) return
var bufferSource = this.audioCtx.createBufferSource()
const length = this.samples.length / this.option.channels
const audioBuffer = this.audioCtx.createBuffer(this.option.channels, length, this.option.sampleRate)
for (let channel = 0; channel < this.option.channels; channel++) {
const audioData = audioBuffer.getChannelData(channel)
let offset = channel
for (let i = 0; i < length; i++) {
audioData[i] = this.samples[offset]
offset += this.option.channels
}
}

this.startTime = Math.max(this.startTime, this.audioCtx.currentTime)
bufferSource.buffer = audioBuffer
bufferSource.connect(this.gainNode)
bufferSource.start(this.startTime)
this.startTime += audioBuffer.duration
this.samples = new Float32Array()
}

async continue() {
await this.audioCtx.resume()
}

async pause() {
await this.audioCtx.suspend()
}

volume(volume) {
this.gainNode.gain.value = volume
}

destroy() {
if (this.interval) {
clearInterval(this.interval)
}
this.samples = null
this.audioCtx.close()
this.audioCtx = null
}
}
window.PCMPlayer = PCMPlayer;
}
})();

function getWavHeader(options) {
const numFrames = options.numFrames
const numChannels = options.numChannels || 2
const sampleRate = options.sampleRate || 44100
const bytesPerSample = options.isFloat? 4 : 2
const format = options.isFloat? 3 : 1
const blockAlign = numChannels * bytesPerSample
const byteRate = sampleRate * blockAlign
const dataSize = numFrames * blockAlign
const buffer = new ArrayBuffer(44)
const dv = new DataView(buffer)
let p = 0
function writeString(s) {
for (let i = 0; i < s.length; i++) {
dv.setUint8(p + i, s.charCodeAt(i))
}
p += s.length
}
function writeUint32(d) {
dv.setUint32(p, d, true)
p += 4
}
function writeUint16(d) {
dv.setUint16(p, d, true)
p += 2
}
writeString('RIFF') // ChunkID
writeUint32(dataSize + 36) // ChunkSize
writeString('WAVE') // Format
writeString('fmt ') // Subchunk1ID
writeUint32(16) // Subchunk1Size
writeUint16(format) // AudioFormat https://i.stack.imgur.com/BuSmb.png
writeUint16(numChannels) // NumChannels
writeUint32(sampleRate) // SampleRate
writeUint32(byteRate) // ByteRate
writeUint16(blockAlign) // BlockAlign
writeUint16(bytesPerSample * 8) // BitsPerSample
writeString('data') // Subchunk2ID
writeUint32(dataSize) // Subchunk2Size
return new Uint8Array(buffer)
}

function getWavBytes(buffer, options) {
const type = options.isFloat ? Float32Array : Uint16Array
const numFrames = buffer.byteLength / type.BYTES_PER_ELEMENT
const headerBytes = getWavHeader(Object.assign({}, options, { numFrames }))
const wavBytes = new Uint8Array(headerBytes.length + buffer.byteLength);
wavBytes.set(headerBytes, 0)
wavBytes.set(new Uint8Array(buffer), headerBytes.length)
return wavBytes
}
36 changes: 36 additions & 0 deletions wavesurfer/player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (c) 2024 Zhendong Peng ([email protected])
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from IPython.display import display, HTML


class Player:
def __init__(self, index):
self.player = f"player_{index}"
self.wavesurfer = f"wavesurfer_{index}"
self.display_id = None

def render(self, script):
html = HTML(f"<script>{script}</script>")
if self.display_id is None:
self.display_id = display(html, display_id=True)
else:
self.display_id.update(html)

def feed(self, base64_pcm):
self.render(f"{self.player}.feed('{base64_pcm}')")
self.render(f"{self.wavesurfer}.load({self.player}.url)")

def destroy(self):
self.render(f"{self.player}.destroy()")
106 changes: 61 additions & 45 deletions wavesurfer/templates/wavesurfer.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
<script>{{ wavesurfer_script }}</script>
<script>{{ hover_script }}</script>
<script>{{ minimap_script }}</script>
<script>{{ regions_script }}</script>
<script>{{ spectrogram_script }}</script>
<script>{{ timeline_script }}</script>
<script>{{ zoom_script }}</script>
<script>{{ plugins_script }}</script>
<script>{{ script }}</script>
<style>
{{ style }}
#waveform_{{ idx }} {
cursor: pointer;
position: relative;
Expand All @@ -24,11 +18,31 @@
#time_{{ idx }} { left: 0; }
#duration_{{ idx }} { right: 0; }
</style>
<br>
<div id="waveform_{{ idx }}" style="background-color: black; width: {{ width }}px;">
<div id="time_{{ idx }}">0:00</div>
<div id="duration_{{ idx }}">0:00</div>
</div>
{% if is_streaming %}
<button class="btn btn-primary my-3" id="button_{{ idx }}">
暂停 <i class="fas fa-pause"></i>
</button>
</br>
<script>
button_{{ idx }} = document.getElementById('button_{{ idx }}')
player_{{ idx }} = new PCMPlayer({ sampleRate: '{{ rate }}'});
isPlaying_{{ idx }} = true
button_{{ idx }}.addEventListener('click', function() {
if (isPlaying_{{ idx }}) {
player_{{ idx }}.pause()
button_{{ idx }}.innerHTML = '播放 <i class="fas fa-play"></i>'
} else {
player_{{ idx }}.continue()
button_{{ idx }}.innerHTML = '暂停 <i class="fas fa-pause"></i>'
}
isPlaying_{{ idx }} = !isPlaying_{{ idx }}
})
</script>
{% endif %}
<script>
wavesurfer_{{ idx }} = WaveSurfer.create({
container: '#waveform_{{ idx }}',
Expand All @@ -38,47 +52,49 @@
cursorWidth: 2,
progressColor: '#4BF2A7',
normalize: true,
url: '{{ audio.src_attr() }}',
sampleRate: '{{ sr }}',
{% if not is_streaming %}
url: '{{ audio }}',
sampleRate: '{{ rate }}',
{% endif %}
minPxPerSec: 100,
backend: 'WebAudio',
});
wavesurfer_{{ idx }}.on('interaction', () => { wavesurfer_{{ idx }}.playPause() });
wavesurfer_{{ idx }}.on('decode', (duration) => (document.querySelector('#duration_{{ idx }}').textContent = formatTime(duration)));
wavesurfer_{{ idx }}.on('timeupdate', (currentTime) => (document.querySelector('#time_{{ idx }}').textContent = formatTime(currentTime)));
{% if enable_hover %}
wavesurfer_{{ idx }}.registerPlugin(create_hover());
{% endif %}
{% if enable_timeline %}
wavesurfer_{{ idx }}.registerPlugin(create_timeline());
{% endif %}
{% if enable_minimap %}
wavesurfer_{{ idx }}.registerPlugin(create_minimap());
{% endif %}
{% if enable_spectrogram %}
wavesurfer_{{ idx }}.registerPlugin(create_spectrogram());
{% endif %}
{% if enable_zoom %}
wavesurfer_{{ idx }}.registerPlugin(create_zoom());
{% endif %}
{% if enable_regions %}
wavesurfer_{{ idx }}.registerPlugin(create_regions());
activeRegion_{{ idx }} = null;
regions_{{ idx }}.on('region-clicked', (region, e) => {
e.stopPropagation();
activeRegion_{{ idx }} = region;
region.play();
});
regions_{{ idx }}.on('region-out', (region) => {
wavesurfer_{{ idx }}.pause();
});
document.addEventListener('keydown', function(event) {
if (event.key == 'Backspace' || event.key == 'Delete') {
console.log(event.key)
if (activeRegion_{{ idx }}) {
activeRegion_{{ idx }}.remove();
}
{% if enable_hover %}
wavesurfer_{{ idx }}.registerPlugin(create_hover());
{% endif %}
{% if enable_timeline %}
wavesurfer_{{ idx }}.registerPlugin(create_timeline());
{% endif %}
{% if enable_minimap %}
wavesurfer_{{ idx }}.registerPlugin(create_minimap());
{% endif %}
{% if enable_spectrogram %}
wavesurfer_{{ idx }}.registerPlugin(create_spectrogram());
{% endif %}
{% if enable_zoom %}
wavesurfer_{{ idx }}.registerPlugin(create_zoom());
{% endif %}
{% if enable_regions %}
wavesurfer_{{ idx }}.registerPlugin(create_regions());
activeRegion_{{ idx }} = null;
regions_{{ idx }}.on('region-clicked', (region, e) => {
e.stopPropagation();
activeRegion_{{ idx }} = region;
region.play();
});
regions_{{ idx }}.on('region-out', (region) => {
wavesurfer_{{ idx }}.pause();
});
document.addEventListener('keydown', function(event) {
if (event.key == 'Backspace' || event.key == 'Delete') {
console.log(event.key)
if (activeRegion_{{ idx }}) {
activeRegion_{{ idx }}.remove();
}
});
{% endif %}
}
});
{% endif %}
</script>
Loading

0 comments on commit e2b1dfe

Please sign in to comment.