Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
Define the waveform component
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvkb committed Aug 14, 2021
1 parent 3ca3985 commit b9e95e6
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 0 deletions.
74 changes: 74 additions & 0 deletions src/components/AudioTrack/Waveform.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
ArgsTable,
Canvas,
Description,
Meta,
Story,
} from '@storybook/addon-docs'

import Waveform from '~/components/AudioTrack/Waveform.vue'

<Meta title="Components/Waveform" components={Waveform} />

export const Template = (args, { argTypes }) => ({
template: `<Waveform class="w-full h-30" v-bind="$props" v-on="$props"/>`,
components: { Waveform },
props: Object.keys(argTypes),
})

# Waveform

<Description of={Waveform} />

<ArgsTable of={Waveform} />

## Sampling

The waveform can automatically upsample and downsample the peaks data to the
required number of points, calculated using a combination of `barWidth` and
`barGap` values.

### Upsampling

Here 9 points are upsampled to as many as required to fill the viewport.

<Canvas>
<Story
name="Upsampling"
args={{
peaks: [0.5, 1, 0.5, 0, 0.5, 1, 0.5, 0, 0.5], // triangular wave with 9 points
percentage: 0.33,
}}
>
{Template.bind({})}
</Story>
</Canvas>

### Downsampling

Here 1000 points are downsampled to as many as required to fill the viewport.

<Canvas>
<Story
name="Downsampling"
args={{
peaks: Array.from(
{ length: 1000 },
(_, k) => 0.5 * Math.sin((k * 2 * Math.PI) / 500) + 0.5
), // sine wave with 1000 points
percentage: 0.67,
}}
>
{Template.bind({})}
</Story>
</Canvas>

## Sizing

The waveform always takes the height and width of its container.

- **Height:** the bars will elongate proportionally to scale vertically
- **Width:** the number of samples will be adjusted to scale horizontally

Thus the `barWidth` and `barGap` will be maintained even in the case of
horizontal scaling.
116 changes: 116 additions & 0 deletions src/components/AudioTrack/Waveform.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<div class="waveform bg-dark-charcoal-04">
<svg
class="w-full h-full"
xmlns="http://www.w3.org/2000/svg"
:viewBox="viewBox"
preserveAspectRatio="none"
>
<rect
class="fill-yellow"
x="0"
y="0"
:width="progressBarWidth"
:height="tallestPeak"
/>
<rect
v-for="(peak, index) in normalizedPeaks"
:key="index"
class="transform origin-bottom"
:class="[
spaceBefore(index) < progressBarWidth
? 'fill-black'
: 'fill-dark-charcoal-20',
]"
:x="spaceBefore(index)"
:y="spaceAbove(peak)"
:width="barWidth"
:height="peak"
/>
</svg>
</div>
</template>

<script>
import { upsampleArray, downsampleArray } from '~/utils/resampling.js'
/**
* Renders an SVG representation of the waveform given a list of heights for the
* bars.
*/
export default {
name: 'Waveform',
props: {
/**
* an array of heights of the bars; The waveform will be generated with
* bars of random length if the prop is not provided.
*/
peaks: {
type: Array,
required: false,
default: () => Array.from({ length: 100 }, () => Math.random()),
validator: (val) => val.every((item) => item >= 0 && item <= 1),
},
/**
* the percentage of the graph that has been played; This represents the
* seekbar of the audio player.
*/
percentage: {
type: Number,
validator: (val) => val >= 0 && val <= 100,
default: 0,
},
},
data: () => ({
barWidth: 2, // px
barGap: 2, // px
waveformWidth: 100, // dummy start value
observer: null, // ResizeObserver
}),
computed: {
peakCount() {
return Math.floor(
(this.waveformWidth - this.barGap) / (this.barWidth + this.barGap)
)
},
normalizedPeaks() {
if (this.peaks.length < this.peakCount) {
return upsampleArray(this.peaks, this.peakCount)
} else if (this.peaks.length > this.peakCount) {
return downsampleArray(this.peaks, this.peakCount)
}
return this.peaks
},
tallestPeak() {
return Math.max(...this.normalizedPeaks)
},
viewBox() {
return `0 0 ${this.waveformWidth} 1`
},
progressBarWidth() {
return this.waveformWidth * this.percentage
},
},
async mounted() {
this.updateWaveformWidth()
this.observer = new ResizeObserver(this.updateWaveformWidth)
this.observer.observe(this.$el)
},
beforeDestroy() {
this.observer.disconnect()
},
methods: {
spaceBefore(index = this.peakCount) {
return index * this.barWidth + (index + 1) * this.barGap
},
spaceAbove(peak) {
return 1 - peak
},
updateWaveformWidth() {
this.waveformWidth = this.$el.clientWidth
},
},
}
</script>

0 comments on commit b9e95e6

Please sign in to comment.