4 | * Available under the MIT License
5 | * ECMAScript compliant, uniform cross-browser split method
6 | */
7 |
--------------------------------------------------------------------------------
/examples/basic-express/public/javascripts/track.js:
--------------------------------------------------------------------------------
1 | import WaveformPlaylist from "waveform-playlist";
2 |
3 | async function main() {
4 | const playlist = WaveformPlaylist({
5 | container: document.getElementById("playlist"),
6 | timescale: true,
7 | state: "cursor",
8 | samplesPerPixel: 1024,
9 | controls: {
10 | show: true,
11 | width: 200,
12 | },
13 | colors: {
14 | waveOutlineColor: "#E0EFF1",
15 | timeColor: "grey",
16 | fadeColor: "black",
17 | },
18 | });
19 |
20 | const ee = playlist.getEventEmitter();
21 |
22 | document.querySelector(".btn-play").addEventListener("click", () => {
23 | ee.emit("play");
24 | });
25 |
26 | document.querySelector(".btn-pause").addEventListener("click", () => {
27 | ee.emit("pause");
28 | });
29 |
30 | document.querySelector(".btn-stop").addEventListener("click", () => {
31 | ee.emit("stop");
32 | });
33 |
34 | document.querySelector(".btn-rewind").addEventListener("click", () => {
35 | ee.emit("rewind");
36 | });
37 |
38 | document.querySelector(".btn-fast-forward").addEventListener("click", () => {
39 | ee.emit("fastforward");
40 | });
41 |
42 | playlist.load([
43 | {
44 | name: "Sonnet",
45 | src: "/media/123",
46 | },
47 | ]);
48 | }
49 |
50 | main();
51 |
--------------------------------------------------------------------------------
/examples/basic-express/public/media:
--------------------------------------------------------------------------------
1 | ../../../ghpages/media
--------------------------------------------------------------------------------
/examples/basic-express/public/stylesheets/main.css:
--------------------------------------------------------------------------------
1 | ../../../../dist/waveform-playlist/css/main.css
--------------------------------------------------------------------------------
/examples/basic-express/routes/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET home page. */
5 | router.get('/', function(req, res, next) {
6 | res.render('index', { title: 'Sonnet' });
7 | });
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/examples/basic-express/routes/media.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const fs = require("fs");
3 | const router = express.Router();
4 |
5 | router.get("/annotations", function (req, res, next) {
6 | //send back a json file or something.
7 | const notes = [
8 | {
9 | begin: "0.000",
10 | children: [],
11 | end: "2.680",
12 | id: "f000001",
13 | language: "eng",
14 | lines: ["1"],
15 | },
16 | {
17 | begin: "2.680",
18 | children: [],
19 | end: "5.880",
20 | id: "f000002",
21 | language: "eng",
22 | lines: ["From fairest creatures we desire increase,"],
23 | },
24 | {
25 | begin: "5.880",
26 | children: [],
27 | end: "9.240",
28 | id: "f000003",
29 | language: "eng",
30 | lines: ["That thereby beauty's rose might never die,"],
31 | },
32 | {
33 | begin: "9.240",
34 | children: [],
35 | end: "11.920",
36 | id: "f000004",
37 | language: "eng",
38 | lines: ["But as the riper should by time decease,"],
39 | },
40 | {
41 | begin: "11.920",
42 | children: [],
43 | end: "15.280",
44 | id: "f000005",
45 | language: "eng",
46 | lines: ["His tender heir might bear his memory:"],
47 | },
48 | {
49 | begin: "15.280",
50 | children: [],
51 | end: "18.600",
52 | id: "f000006",
53 | language: "eng",
54 | lines: ["But thou contracted to thine own bright eyes,"],
55 | },
56 | {
57 | begin: "18.600",
58 | children: [],
59 | end: "22.800",
60 | id: "f000007",
61 | language: "eng",
62 | lines: ["Feed'st thy light's flame with self-substantial fuel,"],
63 | },
64 | {
65 | begin: "22.800",
66 | children: [],
67 | end: "25.680",
68 | id: "f000008",
69 | language: "eng",
70 | lines: ["Making a famine where abundance lies,"],
71 | },
72 | {
73 | begin: "25.680",
74 | children: [],
75 | end: "31.240",
76 | id: "f000009",
77 | language: "eng",
78 | lines: ["Thy self thy foe, to thy sweet self too cruel:"],
79 | },
80 | {
81 | begin: "31.240",
82 | children: [],
83 | end: "34.280",
84 | id: "f000010",
85 | language: "eng",
86 | lines: ["Thou that art now the world's fresh ornament,"],
87 | },
88 | {
89 | begin: "34.280",
90 | children: [],
91 | end: "36.960",
92 | id: "f000011",
93 | language: "eng",
94 | lines: ["And only herald to the gaudy spring,"],
95 | },
96 | {
97 | begin: "36.960",
98 | children: [],
99 | end: "40.680",
100 | id: "f000012",
101 | language: "eng",
102 | lines: ["Within thine own bud buriest thy content,"],
103 | },
104 | {
105 | begin: "40.680",
106 | children: [],
107 | end: "44.560",
108 | id: "f000013",
109 | language: "eng",
110 | lines: ["And tender churl mak'st waste in niggarding:"],
111 | },
112 | {
113 | begin: "44.560",
114 | children: [],
115 | end: "48.080",
116 | id: "f000014",
117 | language: "eng",
118 | lines: ["Pity the world, or else this glutton be,"],
119 | },
120 | {
121 | begin: "48.080",
122 | children: [],
123 | end: "53.240",
124 | id: "f000015",
125 | language: "eng",
126 | lines: ["To eat the world's due, by the grave and thee."],
127 | },
128 | ];
129 | res.json(notes);
130 | });
131 |
132 | // adapted from https://stackoverflow.com/questions/42590683/node-cant-seek-audio-stream#answer-42591021
133 | router.get("/:mediaId", function (req, res, next) {
134 | // use an id to get your track etc.
135 | const mediaId = req.params.mediaId;
136 |
137 | const filePath = __dirname + "/../public/media/audio/Vocals30.mp3";
138 | const stat = fs.statSync(filePath);
139 | const total = stat.size;
140 | if (req.headers.range) {
141 | const range = req.headers.range;
142 | const parts = range.replace(/bytes=/, "").split("-");
143 | const partialstart = parts[0];
144 | const partialend = parts[1];
145 |
146 | const start = parseInt(partialstart, 10);
147 | const end = partialend ? parseInt(partialend, 10) : total - 1;
148 | const chunksize = end - start + 1;
149 | const readStream = fs.createReadStream(filePath, {
150 | start: start,
151 | end: end,
152 | });
153 | res.writeHead(206, {
154 | "Content-Range": "bytes " + start + "-" + end + "/" + total,
155 | "Accept-Ranges": "bytes",
156 | "Content-Length": chunksize,
157 | "Content-Type": "audio/mp3",
158 | });
159 | readStream.pipe(res);
160 | } else {
161 | res.writeHead(200, {
162 | "Content-Length": total,
163 | "Content-Type": "audio/mp3",
164 | });
165 | fs.createReadStream(filePath).pipe(res);
166 | }
167 | });
168 |
169 | module.exports = router;
170 |
--------------------------------------------------------------------------------
/examples/basic-express/routes/track.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET track page. */
5 | router.get('/', function(req, res, next) {
6 | res.render('track', { title: 'Load a track' });
7 | });
8 |
9 | module.exports = router;
--------------------------------------------------------------------------------
/examples/basic-express/views/error.hbs:
--------------------------------------------------------------------------------
1 | {{message}}
2 | {{error.status}}
3 | {{error.stack}}
4 |
--------------------------------------------------------------------------------
/examples/basic-express/views/index.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{title}} Annotations
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/basic-express/views/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{title}}
6 |
7 |
8 |
9 |
10 |
11 | {{{body}}}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/basic-express/views/track.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{title}}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/basic-express/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | module.exports = {
3 | entry: {
4 | index: "./public/javascripts/index.js",
5 | track: "./public/javascripts/track.js",
6 | },
7 | output: {
8 | path: path.resolve(__dirname, "public/javascripts"),
9 | filename: "[name].bundle.js",
10 | },
11 | mode: 'production'
12 | };
13 |
--------------------------------------------------------------------------------
/examples/basic-html/hello.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-html/hello.mp3
--------------------------------------------------------------------------------
/examples/basic-html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Waveform Playlist Basic HTML
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | }
4 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic-nextjs",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build && next export",
7 | "start": "next start",
8 | "lint": "next lint"
9 | },
10 | "dependencies": {
11 | "@fortawesome/fontawesome-free": "^6.0.0",
12 | "events": "^3.3.0",
13 | "file-saver": "^2.0.5",
14 | "next": "^12.1.0",
15 | "react": "17.0.2",
16 | "react-dom": "17.0.2",
17 | "waveform-playlist": "^4.3.1"
18 | },
19 | "devDependencies": {
20 | "eslint": "8.8.0",
21 | "eslint-config-next": "12.0.10"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 |
3 | // https://fontawesome.com/docs/web/setup/host-yourself/webfonts
4 | import "../styles/fontawesome/css/all.css";
5 |
6 | import "waveform-playlist/styles/playlist.css";
7 |
8 | export default function MyApp({ Component, pageProps }) {
9 | return ;
10 | }
11 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState, useRef } from "react";
2 | import Script from "next/script";
3 | import EventEmitter from "events";
4 | import WaveformPlaylist from "waveform-playlist";
5 | import { saveAs } from "file-saver";
6 |
7 | export default function Home() {
8 | const [ee] = useState(new EventEmitter());
9 | const [toneCtx, setToneCtx] = useState(null);
10 | const setUpChain = useRef();
11 |
12 | const container = useCallback(
13 | (node) => {
14 | if (node !== null && toneCtx !== null) {
15 | const playlist = WaveformPlaylist(
16 | {
17 | ac: toneCtx.rawContext,
18 | samplesPerPixel: 100,
19 | mono: true,
20 | waveHeight: 100,
21 | container: node,
22 | state: "cursor",
23 | colors: {
24 | waveOutlineColor: "#E0EFF1",
25 | timeColor: "grey",
26 | fadeColor: "black",
27 | },
28 | controls: {
29 | show: true,
30 | width: 150,
31 | },
32 | zoomLevels: [100, 300, 500],
33 | },
34 | ee
35 | );
36 |
37 | ee.on("audiorenderingstarting", function (offlineCtx, a) {
38 | // Set Tone offline to render effects properly.
39 | const offlineContext = new Tone.OfflineContext(offlineCtx);
40 | Tone.setContext(offlineContext);
41 | setUpChain.current = a;
42 | });
43 |
44 | ee.on("audiorenderingfinished", function (type, data) {
45 | //restore original ctx for further use.
46 | Tone.setContext(toneCtx);
47 | if (type === "wav") {
48 | saveAs(data, "test.wav");
49 | }
50 | });
51 |
52 | playlist.load([
53 | {
54 | src: "hello.mp3",
55 | name: "Hello",
56 | effects: function (graphEnd, masterGainNode, isOffline) {
57 | const reverb = new Tone.Reverb(1.2);
58 |
59 | if (isOffline) {
60 | setUpChain.current.push(reverb.ready);
61 | }
62 |
63 | Tone.connect(graphEnd, reverb);
64 | Tone.connect(reverb, masterGainNode);
65 |
66 | return function cleanup() {
67 | reverb.disconnect();
68 | reverb.dispose();
69 | };
70 | },
71 | },
72 | ]);
73 |
74 | //initialize the WAV exporter.
75 | playlist.initExporter();
76 | }
77 | },
78 | [ee, toneCtx]
79 | );
80 |
81 | function handleLoad() {
82 | setToneCtx(Tone.getContext());
83 | }
84 |
85 | return (
86 | <>
87 |
91 |
92 |
93 | {
95 | ee.emit("play");
96 | }}
97 | >
98 | Play
99 |
100 |
101 |
102 | {
104 | ee.emit("startaudiorendering", "wav");
105 | }}
106 | >
107 | Download
108 |
109 |
110 |
111 |
112 | >
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/public/favicon.ico
--------------------------------------------------------------------------------
/examples/basic-nextjs/public/hello.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/public/hello.mp3
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | }
47 |
48 | .title,
49 | .description {
50 | text-align: center;
51 | }
52 |
53 | .description {
54 | margin: 4rem 0;
55 | line-height: 1.5;
56 | font-size: 1.5rem;
57 | }
58 |
59 | .code {
60 | background: #fafafa;
61 | border-radius: 5px;
62 | padding: 0.75rem;
63 | font-size: 1.1rem;
64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
65 | Bitstream Vera Sans Mono, Courier New, monospace;
66 | }
67 |
68 | .grid {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | flex-wrap: wrap;
73 | max-width: 800px;
74 | }
75 |
76 | .card {
77 | margin: 1rem;
78 | padding: 1.5rem;
79 | text-align: left;
80 | color: inherit;
81 | text-decoration: none;
82 | border: 1px solid #eaeaea;
83 | border-radius: 10px;
84 | transition: color 0.15s ease, border-color 0.15s ease;
85 | max-width: 300px;
86 | }
87 |
88 | .card:hover,
89 | .card:focus,
90 | .card:active {
91 | color: #0070f3;
92 | border-color: #0070f3;
93 | }
94 |
95 | .card h2 {
96 | margin: 0 0 1rem 0;
97 | font-size: 1.5rem;
98 | }
99 |
100 | .card p {
101 | margin: 0;
102 | font-size: 1.25rem;
103 | line-height: 1.5;
104 | }
105 |
106 | .logo {
107 | height: 1em;
108 | margin-left: 0.5rem;
109 | }
110 |
111 | @media (max-width: 600px) {
112 | .grid {
113 | width: 100%;
114 | flex-direction: column;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/regular.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | :root, :host {
7 | --fa-font-regular: normal 400 1em/1 "Font Awesome 6 Free"; }
8 |
9 | @font-face {
10 | font-family: 'Font Awesome 6 Free';
11 | font-style: normal;
12 | font-weight: 400;
13 | font-display: block;
14 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
15 |
16 | .far,
17 | .fa-regular {
18 | font-family: 'Font Awesome 6 Free';
19 | font-weight: 400; }
20 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/regular.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | :host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-family:"Font Awesome 6 Free";font-weight:400}
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/solid.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | :root, :host {
7 | --fa-font-solid: normal 900 1em/1 "Font Awesome 6 Free"; }
8 |
9 | @font-face {
10 | font-family: 'Font Awesome 6 Free';
11 | font-style: normal;
12 | font-weight: 900;
13 | font-display: block;
14 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
15 |
16 | .fas,
17 | .fa-solid {
18 | font-family: 'Font Awesome 6 Free';
19 | font-weight: 900; }
20 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/solid.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | :host,:root{--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-family:"Font Awesome 6 Free";font-weight:900}
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/v4-font-face.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | @font-face {
7 | font-family: "FontAwesome";
8 | font-display: block;
9 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
10 |
11 | @font-face {
12 | font-family: "FontAwesome";
13 | font-display: block;
14 | src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
15 |
16 | @font-face {
17 | font-family: "FontAwesome";
18 | font-display: block;
19 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
20 | unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
21 |
22 | @font-face {
23 | font-family: "FontAwesome";
24 | font-display: block;
25 | src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
26 | unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F250,U+F252,U+F27A; }
27 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/v4-font-face.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | @font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f250,u+f252,u+f27a}
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/v5-font-face.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | @font-face {
7 | font-family: "Font Awesome 5 Brands";
8 | font-display: block;
9 | font-weight: 400;
10 | src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
11 |
12 | @font-face {
13 | font-family: "Font Awesome 5 Free";
14 | font-display: block;
15 | font-weight: 900;
16 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
17 |
18 | @font-face {
19 | font-family: "Font Awesome 5 Free";
20 | font-display: block;
21 | font-weight: 400;
22 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
23 |
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/css/v5-font-face.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2022 Fonticons, Inc.
5 | */
6 | @font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-v4compatibility.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-v4compatibility.ttf
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/fontawesome/webfonts/fa-v4compatibility.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/examples/basic-nextjs/styles/fontawesome/webfonts/fa-v4compatibility.woff2
--------------------------------------------------------------------------------
/examples/basic-nextjs/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/experiments/dbfs.js:
--------------------------------------------------------------------------------
1 | /*
2 | Test script to display a meter in dBFS (dB)
3 | */
4 | var ctx = new AudioContext(),
5 | url = '../media/Drink.mp3',
6 | audio = new Audio(url),
7 | processor = ctx.createScriptProcessor(256, 2, 2),
8 | meter = document.getElementById('meter'),
9 | db = document.getElementById('db'),
10 | max = document.getElementById('max'),
11 | source;
12 |
13 | audio.addEventListener('canplaythrough', function(){
14 | source = ctx.createMediaElementSource(audio);
15 | source.connect(processor);
16 | processor.connect(ctx.destination);
17 | audio.play();
18 | }, false);
19 |
20 | //calculate average volume for a buffer.
21 | processor.onaudioprocess = function(evt){
22 | var input,
23 | output,
24 | len,
25 | total,
26 | channel,
27 | rms,
28 | decibel,
29 | percent,
30 | slice = Array.prototype.slice;
31 |
32 | for (channel = 0, len = evt.outputBuffer.numberOfChannels; channel < len; channel++) {
33 | input = evt.inputBuffer.getChannelData(channel);
34 | output = evt.outputBuffer.getChannelData(channel);
35 | output.set(input);
36 | }
37 |
38 | maxPeak = Math.max.apply(Math, input);
39 |
40 | rms = Math.sqrt(maxPeak);
41 | decibel = 20 * Math.log(rms) / Math.LN10;
42 |
43 | //scale is from -96db to 0db
44 | //percent = (1/96) * decibel + 1.0
45 | //percent = percent < 0 ? 0 : percent * 100;
46 |
47 | db.innerHTML = decibel;
48 | //max.innerHTML = maxPeak;
49 | //meter.style.width = (percent) + '%';
50 | };
--------------------------------------------------------------------------------
/experiments/dbmeter.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DB meter
6 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/experiments/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Drawer Tests
6 |
7 |
8 |
9 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ghpages/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | .sass-cache
3 | .jekyll-metadata
4 |
--------------------------------------------------------------------------------
/ghpages/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely need to edit after that.
5 | # For technical reasons, this file is *NOT* reloaded automatically when you use
6 | # 'jekyll serve'. If you change this file, please restart the server process.
7 |
8 | # Site settings
9 | title: Waveform Playlist
10 | email: naomiaro@gmail.com
11 | description: > # this means to ignore newlines until "baseurl:"
12 | Waveform Playlist: The multitrack javascript web audio editor and player.
13 | baseurl: "/waveform-playlist" # the subpath of your site, e.g. /blog
14 | url: "https://naomiaro.github.io" # the base hostname & protocol for your site
15 | twitter_username: naomiaro
16 | github_username: naomiaro
17 |
18 | # Build settings
19 | markdown: kramdown
20 | collections:
21 | examples:
22 | output: true
23 | output_ext: html
24 |
25 |
--------------------------------------------------------------------------------
/ghpages/_examples/01minimal.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Minimal Editor
4 | lead: One audio file with combined channel waveform visual and basic cursor selection for start.
5 | excerpt: Basic web audio single track mono display waveform editor. Play, stop, pause audio. Live seeking enabled.
6 | permalink: minimal.html
7 | javascript: minimal.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ghpages/_examples/02preselected.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Load Track With Selection
4 | lead: Pass a start time and end time in seconds to pre select an area on a track.
5 | excerpt: Preselect a section of audio on the track for playback.
6 | permalink: preselected.html
7 | javascript: preselected.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ghpages/_examples/03stationarytrack.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Individual Track State
4 | excerpt: Control the different tracks in the playlist by enabling or disabling interaction states.
5 | lead: Vocals track can not be shifted in time, only the Drums track.
6 | permalink: stationarytrack.html
7 | javascript: stationary-track.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 | {% include buttons/states.html %}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/ghpages/_examples/04stemtracks.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Stem Tracks
4 | lead: Multitrack scheduling allows to layer audio for simultaneous playback. Mute, solo, or set different volume levels for each audio track that makes up the composition. Control the master volume of the mix.
5 | excerpt: Create a playlist with multiple stem tracks. Mute, solo, or set different volume levels for each audio track. Multitrack scheduling audio simultaneous playback. Master volume adjustment.
6 | permalink: stem-tracks.html
7 | javascript: stem-tracks.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
22 | {% include timeformat.html %}
23 |
--------------------------------------------------------------------------------
/ghpages/_examples/05fades.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Track Fades
4 | lead: Set fade in and fade out.
5 | excerpt: Set the track's fade in and fade out. Choose from linear, logartihmic, s-curve and exponential fade types.
6 | permalink: fades.html
7 | javascript: fades.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 | {% include buttons/states.html %}
16 | {% include buttons/fades.html %}
17 |
18 |
19 |
20 |
21 | {% include timeformat.html %}
22 |
23 |
--------------------------------------------------------------------------------
/ghpages/_examples/06newtracks.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Working With Local Files
4 | lead: Drag one or many audio files into the bottom container!
5 | excerpt: Work with local files added in the playlist. File objects can be added using the EventEmitter.
6 | permalink: newtracks.html
7 | javascript: newtracks.js
8 | ---
9 |
10 |
11 |
12 | {% include buttons/playback.html %}
13 | {% include buttons/zoom.html %}
14 | {% include buttons/states.html %}
15 | {% include buttons/fades.html %}
16 |
17 |
18 |
19 |
20 | {% include timeformat.html %}
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/ghpages/_examples/07trackzoom.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Track Zoom Configuration
4 | lead: Set an array of zoom levels in samples per pixel. Multiple canvas elements are used to avoid maximum dimension limits.
5 | excerpt: Set an array of zoom levels in samples per pixel. Multiple canvas elements are used to avoid maximum dimension limits.
6 | permalink: track-zoom.html
7 | javascript: track-zoom.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 | {% include buttons/zoom.html %}
16 |
17 |
18 |
19 |
20 | {% include timeformat.html %}
21 |
22 |
--------------------------------------------------------------------------------
/ghpages/_examples/08record.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Record An Audio Track
4 | lead: Record a track in MediaRecorder enabled browsers.
5 | Allows recording over tracks, shows real-time recording progress.
6 | excerpt: Waveform Playlist experimental recording feature using MediaRecorder.
7 | permalink: record.html
8 | javascript: record.js
9 | ---
10 |
11 | Experimental recording feature using
12 |
13 | MediaRecorder , Tested in Firefox >= 41.0.2 & Chrome >= 58
14 | PAGE NEEDS HTTPS ACCESS (or localhost is also fine)
15 | Can record a track on top of the previously recorded track!
16 | Drop some tracks into the container below if
17 | you'd like to try recording over them.
18 |
19 |
20 |
21 | {% include buttons/playback.html %}
22 | {% include buttons/zoom.html %}
23 | {% include buttons/states.html %}
24 | {% include buttons/fades.html %}
25 |
26 |
27 |
28 |
29 | {% include timeformat.html %}
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/ghpages/_examples/10webaudioeditorfill.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Waveform Editor with seek style set to fill
4 | lead: Initialization property 'seekStyle' set to 'fill'.
5 | excerpt: Waveform Playlist, the multitrack javascript web audio editor and player. Set audio cue in and cue out. Set linear, exponential, logarithmic, and s-curve fades. Shift audio in time. Zoom in and zoom out on the waveform. Play, stop, pause and seek inside the audio tracks.
6 | permalink: web-audio-editor-fill.html
7 | javascript: web-audio-editor-fill.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
19 | {% include timeformat.html %}
20 |
21 |
--------------------------------------------------------------------------------
/ghpages/_examples/11exclusivesolo.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Stem Tracks with exclusive solo
4 | lead: Mute or exclusive solo the different tracks that make up the composition.
5 | excerpt: Create a playlist with multiple stem tracks. Mute or solo the different stems in the playlist.
6 | permalink: exclusive-solo.html
7 | javascript: exclusivesolo.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/ghpages/_examples/12multichannel.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Stereo Channel Editor
4 | lead: One audio file with stereo channel waveform visualization.
5 | excerpt: Basic web audio single track stereo display waveform editor. Play, stop, pause audio. Live seeking enabled.
6 | permalink: multi-channel.html
7 | javascript: multi-channel.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ghpages/_examples/13annotations.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Playlist Annotations
4 | excerpt: Annotate the playlist with timed text segments. Drag boundries to adjust timing. Enhance the annotation editing process with custom user defined functionality. Aeneas support.
5 | lead: Annotate the playlist with timed text segments. Annotations will scroll and highlight during playback. Drag the annotation's boundries in the UI to adjust time points. Enhance the annotation editing process with custom user defined functionality. Optionally edit live the annotation text using contenteditable.
6 | permalink: annotations.html
7 | javascript: annotations.js
8 | audio_title: Sonnet
9 | audio_artist: LibriVox
10 | image: https://raw.githubusercontent.com/naomiaro/waveform-playlist/master/img/annotations.png
11 | image_width: 1008
12 | image_height: 401
13 | ---
14 |
15 | Currently supporting input and output in the
Aeneas JSON format.
16 |
17 |
26 |
27 |
33 | {% include timeformat.html %}
34 |
35 |
36 |
37 |
38 | *The annotation plugin has been sponsored by a fond Italian TED volunteer transcriber hoping to make the transcription process of TEDx talks easier and more fun.
39 |
40 |
--------------------------------------------------------------------------------
/ghpages/_examples/14stereopan.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Stereo Panner
4 | lead: Top audio file uses full stereo left pan, bottom audio file uses full stereo right pan.
5 | excerpt: Change the panning of audio from left to right.
6 | permalink: stereopan.html
7 | javascript: stereopan.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ghpages/_examples/15bars.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Bars
4 | lead: Render the waveform in "bars".
5 | excerpt: Basic web audio single track mono display waveform editor. Play, stop, pause audio. Live seeking enabled.
6 | permalink: bars.html
7 | javascript: bars.js
8 | audio_title: Sonnet
9 | audio_artist: LibriVox
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ghpages/_examples/16effects.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Tone.js Effects + Custom Webaudio Graphs
4 | lead: Add effects to each track or analyse the master output. Frequency bar graph code adapted from MDN .
5 | excerpt: Create your own custom webaudio graph with Tone.js effects or other libraries compatible with hooking into the web audio graph..
6 | permalink: effects.html
7 | javascript: effects.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 | {% include buttons/random.html %}
16 |
17 |
18 |
19 |
20 |
23 | {% include timeformat.html %}
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ghpages/_examples/99webaudioeditor.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: Full Waveform Editor
4 | lead: Every control and state available.
5 | excerpt: Waveform Playlist, the multitrack javascript web audio editor and player. Set audio cue in and cue out. Set linear, exponential, logarithmic, and s-curve fades. Shift audio in time. Zoom in and zoom out on the waveform. Play, stop, pause and seek inside the audio tracks.
6 | permalink: web-audio-editor.html
7 | javascript: web-audio-editor.js
8 | audio_title: Starlight
9 | audio_artist: Muse
10 | ---
11 |
12 |
13 |
14 | {% include buttons/playback.html %}
15 | {% include buttons/zoom.html %}
16 | {% include buttons/states.html %}
17 | {% include buttons/fades.html %}
18 | {% include buttons/random.html %}
19 |
20 |
21 |
22 |
23 | {% include timeformat.html %}
24 |
28 |
29 | {% include forms/seek.html %}
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/ghpages/_includes/buttons/fades.html:
--------------------------------------------------------------------------------
1 |
2 | logarithmic
3 | linear
4 | exponential
5 | s-curve
6 |
7 |
--------------------------------------------------------------------------------
/ghpages/_includes/buttons/playback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/ghpages/_includes/buttons/random.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
14 | Trim
15 |
16 |
17 |
18 |
23 | Print
24 |
25 |
30 | Clear
31 |
32 |
33 |
34 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/ghpages/_includes/buttons/states.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
14 |
15 |
16 |
21 |
22 |
23 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ghpages/_includes/buttons/zoom.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/ghpages/_includes/checkboxes/automaticscroll.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Automatic Scroll
6 |
7 |
--------------------------------------------------------------------------------
/ghpages/_includes/checkboxes/continuousplay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Continuous Play
4 |
5 |
--------------------------------------------------------------------------------
/ghpages/_includes/checkboxes/linkendpoints.html:
--------------------------------------------------------------------------------
1 |
2 |
8 | Link Endpoints
9 |
10 |
--------------------------------------------------------------------------------
/ghpages/_includes/footer.html:
--------------------------------------------------------------------------------
1 |
2 | {% if page.audio_title and page.audio_artist %}
3 | Music: {{ page.audio_title }} by {{ page.audio_artist }}
4 | {% endif %}
5 |
6 |
7 | Waveform Playlist originally written for
8 | Airtime at
9 | Sourcefabric
10 |
11 | Licensed under the MIT License
12 |
13 |
14 | {% if page.layout == "page" %}
15 |
20 |
21 |
22 |
23 | {% endif %}
24 |
--------------------------------------------------------------------------------
/ghpages/_includes/forms/seek.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/ghpages/_includes/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
13 |
17 |
18 |
19 | {% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape
20 | }}{% endif %}
21 |
22 |
26 |
27 |
33 |
34 |
38 |
40 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/ghpages/_includes/header.html:
--------------------------------------------------------------------------------
1 |
25 |
62 |
--------------------------------------------------------------------------------
/ghpages/_includes/sliders/mastergain.html:
--------------------------------------------------------------------------------
1 | Master Volume
2 |
10 |
--------------------------------------------------------------------------------
/ghpages/_includes/sponsors.html:
--------------------------------------------------------------------------------
1 | Sponsors
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ghpages/_includes/timeformat.html:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/ghpages/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% include head.html %}
4 |
5 |
6 | {% include header.html %}
7 |
8 |
9 | {{ content }}
10 |
11 | {% include sponsors.html %}
12 |
13 | {% include footer.html %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/ghpages/_layouts/page.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
7 |
11 | {{ content }}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/ghpages/_layouts/post.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
10 |
11 |
12 | {{ content }}
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/ghpages/img/logos/moises-ai.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ghpages/index.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 |
7 |
Waveform Playlist
8 |
Multitrack Web Audio Editor and Player
9 |
10 |
11 | {% for example in site.examples %}
12 |
13 |
16 | {{ example.lead }}
17 |
18 | {% endfor %}
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ghpages/js/annotations.js:
--------------------------------------------------------------------------------
1 | var notes = [
2 | {
3 | "begin": "0.000",
4 | "children": [],
5 | "end": "2.680",
6 | "id": "f000001",
7 | "language": "eng",
8 | "lines": [
9 | "1"
10 | ]
11 | },
12 | {
13 | "begin": "2.680",
14 | "children": [],
15 | "end": "5.880",
16 | "id": "f000002",
17 | "language": "eng",
18 | "lines": [
19 | "From fairest creatures we desire increase,"
20 | ]
21 | },
22 | {
23 | "begin": "5.880",
24 | "children": [],
25 | "end": "9.240",
26 | "id": "f000003",
27 | "language": "eng",
28 | "lines": [
29 | "That thereby beauty's rose might never die,"
30 | ]
31 | },
32 | {
33 | "begin": "9.240",
34 | "children": [],
35 | "end": "11.920",
36 | "id": "f000004",
37 | "language": "eng",
38 | "lines": [
39 | "But as the riper should by time decease,"
40 | ]
41 | },
42 | {
43 | "begin": "11.920",
44 | "children": [],
45 | "end": "15.280",
46 | "id": "f000005",
47 | "language": "eng",
48 | "lines": [
49 | "His tender heir might bear his memory:"
50 | ]
51 | },
52 | {
53 | "begin": "15.280",
54 | "children": [],
55 | "end": "18.600",
56 | "id": "f000006",
57 | "language": "eng",
58 | "lines": [
59 | "But thou contracted to thine own bright eyes,"
60 | ]
61 | },
62 | {
63 | "begin": "18.600",
64 | "children": [],
65 | "end": "22.800",
66 | "id": "f000007",
67 | "language": "eng",
68 | "lines": [
69 | "Feed'st thy light's flame with self-substantial fuel,"
70 | ]
71 | },
72 | {
73 | "begin": "22.800",
74 | "children": [],
75 | "end": "25.680",
76 | "id": "f000008",
77 | "language": "eng",
78 | "lines": [
79 | "Making a famine where abundance lies,"
80 | ]
81 | },
82 | {
83 | "begin": "25.680",
84 | "children": [],
85 | "end": "31.240",
86 | "id": "f000009",
87 | "language": "eng",
88 | "lines": [
89 | "Thy self thy foe, to thy sweet self too cruel:"
90 | ]
91 | },
92 | {
93 | "begin": "31.240",
94 | "children": [],
95 | "end": "34.280",
96 | "id": "f000010",
97 | "language": "eng",
98 | "lines": [
99 | "Thou that art now the world's fresh ornament,"
100 | ]
101 | },
102 | {
103 | "begin": "34.280",
104 | "children": [],
105 | "end": "36.960",
106 | "id": "f000011",
107 | "language": "eng",
108 | "lines": [
109 | "And only herald to the gaudy spring,"
110 | ]
111 | },
112 | {
113 | "begin": "36.960",
114 | "children": [],
115 | "end": "40.680",
116 | "id": "f000012",
117 | "language": "eng",
118 | "lines": [
119 | "Within thine own bud buriest thy content,"
120 | ]
121 | },
122 | {
123 | "begin": "40.680",
124 | "children": [],
125 | "end": "44.560",
126 | "id": "f000013",
127 | "language": "eng",
128 | "lines": [
129 | "And tender churl mak'st waste in niggarding:"
130 | ]
131 | },
132 | {
133 | "begin": "44.560",
134 | "children": [],
135 | "end": "48.080",
136 | "id": "f000014",
137 | "language": "eng",
138 | "lines": [
139 | "Pity the world, or else this glutton be,"
140 | ]
141 | },
142 | {
143 | "begin": "48.080",
144 | "children": [],
145 | "end": "53.240",
146 | "id": "f000015",
147 | "language": "eng",
148 | "lines": [
149 | "To eat the world's due, by the grave and thee."
150 | ]
151 | }
152 | ];
153 |
154 | var actions = [
155 | {
156 | class: 'fas.fa-minus',
157 | title: 'Reduce annotation end by 0.010s',
158 | action: (annotation, i, annotations, opts) => {
159 | var next;
160 | var delta = 0.010;
161 | annotation.end -= delta;
162 |
163 | if (opts.linkEndpoints) {
164 | next = annotations[i + 1];
165 | next && (next.start -= delta);
166 | }
167 | }
168 | },
169 | {
170 | class: 'fas.fa-plus',
171 | title: 'Increase annotation end by 0.010s',
172 | action: (annotation, i, annotations, opts) => {
173 | var next;
174 | var delta = 0.010;
175 | annotation.end += delta;
176 |
177 | if (opts.linkEndpoints) {
178 | next = annotations[i + 1];
179 | next && (next.start += delta);
180 | }
181 | }
182 | },
183 | {
184 | class: 'fas.fa-cut',
185 | title: 'Split annotation in half',
186 | action: (annotation, i, annotations) => {
187 | const halfDuration = (annotation.end - annotation.start) / 2;
188 |
189 | annotations.splice(i + 1, 0, {
190 | id: 'test',
191 | start: annotation.end - halfDuration,
192 | end: annotation.end,
193 | lines: ['----'],
194 | lang: 'en',
195 | });
196 |
197 | annotation.end = annotation.start + halfDuration;
198 | }
199 | },
200 | {
201 | class: 'fas.fa-trash',
202 | title: 'Delete annotation',
203 | action: (annotation, i, annotations) => {
204 | annotations.splice(i, 1);
205 | }
206 | }
207 | ];
208 |
209 | var playlist = WaveformPlaylist.init({
210 | container: document.getElementById("playlist"),
211 | timescale: true,
212 | state: 'select',
213 | samplesPerPixel: 1024,
214 | colors: {
215 | waveOutlineColor: '#005BBB',
216 | timeColor: 'grey',
217 | fadeColor: 'black'
218 | },
219 | annotationList: {
220 | annotations: notes,
221 | controls: actions,
222 | editable: true,
223 | isContinuousPlay: false,
224 | linkEndpoints: true
225 | }
226 | });
227 |
228 | playlist.load([
229 | {
230 | src: "media/audio/sonnet.mp3"
231 | }
232 | ]).then(function() {
233 | //can do stuff with the playlist.
234 | });
235 |
--------------------------------------------------------------------------------
/ghpages/js/bars.js:
--------------------------------------------------------------------------------
1 | var playlist;
2 |
3 | playlist = WaveformPlaylist.init({
4 | container: document.getElementById("playlist"),
5 | colors: {
6 | waveOutlineColor: "#005BBB",
7 | },
8 | barWidth: 3,
9 | barGap: 1,
10 | });
11 |
12 | playlist.load([
13 | {
14 | src: "media/audio/sonnet.mp3",
15 | }
16 | ]).then(function() {
17 | //can do stuff with the playlist.
18 | });
19 |
--------------------------------------------------------------------------------
/ghpages/js/effects.js:
--------------------------------------------------------------------------------
1 | var playlist;
2 | var toneCtx = Tone.getContext();
3 | var audioCtx = toneCtx.rawContext;
4 | var analyser = audioCtx.createAnalyser();
5 | var offlineSetup = [];
6 |
7 | var userMediaStream;
8 | var constraints = { audio: true };
9 |
10 | navigator.getUserMedia = (navigator.getUserMedia ||
11 | navigator.webkitGetUserMedia ||
12 | navigator.mozGetUserMedia ||
13 | navigator.msGetUserMedia);
14 |
15 | function gotStream(stream) {
16 | userMediaStream = stream;
17 | playlist.initRecorder(userMediaStream);
18 | $(".btn-record").removeClass("disabled");
19 | }
20 |
21 | function logError(err) {
22 | console.error(err);
23 | }
24 |
25 | playlist = WaveformPlaylist.init({
26 | ac: audioCtx,
27 | barWidth: 3,
28 | barGap: 1,
29 | container: document.getElementById("playlist"),
30 | colors: {
31 | waveOutlineColor: '#005BBB'
32 | },
33 | controls: {
34 | show: true,
35 | width: 200
36 | },
37 | zoomLevels: [500, 1000, 3000, 5000],
38 | samplesPerPixel: 1000,
39 | waveHeight: 100,
40 | isAutomaticScroll: true,
41 | timescale: true,
42 | state: "cursor",
43 | effects: function(masterGainNode, destination, isOffline) {
44 | // analyser nodes don't work offline.
45 | if (!isOffline) masterGainNode.connect(analyser);
46 | masterGainNode.connect(destination);
47 | }
48 | });
49 |
50 | //initialize the WAV exporter.
51 | playlist.initExporter();
52 |
53 | playlist.ee.on("audiorenderingstarting", function(offlineCtx, setup) {
54 | // Set Tone offline to render effects properly.
55 | const offlineContext = new Tone.OfflineContext(offlineCtx);
56 | Tone.setContext(offlineContext);
57 | offlineSetup = setup;
58 | });
59 |
60 | playlist.ee.on("audiorenderingfinished", function() {
61 | //restore original ctx for further use.
62 | Tone.setContext(toneCtx);
63 | });
64 |
65 | playlist
66 | .load([
67 | {
68 | src: "media/audio/Vocals30.mp3",
69 | name: "Vocals",
70 | effects: function vocalsEffects(graphEnd, masterGainNode, isOffline) {
71 | var autoWah = new Tone.AutoWah(50, 6, -30);
72 |
73 | Tone.connect(graphEnd, autoWah);
74 | Tone.connect(autoWah, masterGainNode);
75 |
76 | return function cleanup() {
77 | autoWah.disconnect();
78 | autoWah.dispose();
79 | }
80 | }
81 | },
82 | {
83 | src: "media/audio/Guitar30.mp3",
84 | name: "Guitar",
85 | effects: function(graphEnd, masterGainNode, isOffline) {
86 | var reverb = new Tone.Reverb(1.2);
87 |
88 | if (isOffline) {
89 | offlineSetup.push(reverb.ready);
90 | }
91 |
92 | Tone.connect(graphEnd, reverb);
93 | Tone.connect(reverb, masterGainNode);
94 |
95 | return function cleanup() {
96 | reverb.disconnect();
97 | reverb.dispose();
98 | }
99 | }
100 | },
101 | {
102 | src: "media/audio/PianoSynth30.mp3",
103 | name: "Pianos & Synth",
104 | },
105 | {
106 | src: "media/audio/BassDrums30.mp3",
107 | name: "Drums",
108 | effects: function(graphEnd, masterGainNode, isOffline) {
109 | var reverb = new Tone.Reverb(5);
110 |
111 | if (isOffline) {
112 | offlineSetup.push(reverb.ready);
113 | }
114 |
115 | Tone.connect(graphEnd, reverb);
116 | Tone.connect(reverb, masterGainNode);
117 |
118 | return function cleanup() {
119 | reverb.disconnect();
120 | reverb.dispose();
121 | }
122 | }
123 | },
124 | ])
125 | .then(function () {
126 | //can do stuff with the playlist.
127 |
128 | if (navigator.mediaDevices) {
129 | navigator.mediaDevices.getUserMedia(constraints)
130 | .then(gotStream)
131 | .catch(logError);
132 | } else if (navigator.getUserMedia && 'MediaRecorder' in window) {
133 | navigator.getUserMedia(
134 | constraints,
135 | gotStream,
136 | logError
137 | );
138 | }
139 | });
140 |
141 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
142 | // The following code is from Mozilla Developer Network:
143 | // This draws the frequency data to the canvas.
144 | analyser.fftSize = 256;
145 | var bufferLength = analyser.frequencyBinCount;
146 | var dataArray = new Uint8Array(bufferLength);
147 | var drawVisual;
148 | var canvas = document.querySelector('.visualizer');
149 | var canvasCtx = canvas.getContext("2d");
150 | var WIDTH = canvas.width;
151 | var HEIGHT = canvas.height;
152 | canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
153 |
154 | // added scale for retina
155 | var scale = Math.floor(window.devicePixelRatio);
156 | canvasCtx.scale(scale, scale);
157 |
158 | function draw() {
159 | drawVisual = requestAnimationFrame(draw);
160 | analyser.getByteFrequencyData(dataArray);
161 |
162 | canvasCtx.fillStyle = 'rgb(255, 255, 255)';
163 | canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
164 |
165 | var barWidth = WIDTH / scale / bufferLength - 1;
166 | var barHeight;
167 | var x = 0;
168 |
169 | for(var i = 0; i < bufferLength; i++) {
170 | barHeight = dataArray[i]/2/scale;
171 |
172 | canvasCtx.fillStyle = 'rgb('+(barHeight+100)+',50,50)';
173 | canvasCtx.fillRect(x,HEIGHT/scale-barHeight/2,barWidth,barHeight);
174 |
175 | x += barWidth + 1;
176 | }
177 | }
178 |
179 | draw();
180 |
--------------------------------------------------------------------------------
/ghpages/js/exclusivesolo.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | samplesPerPixel: 1000,
3 | waveHeight: 100,
4 | container: document.getElementById("playlist"),
5 | timescale: true,
6 | state: 'cursor',
7 | colors: {
8 | waveOutlineColor: '#005BBB'
9 | },
10 | controls: {
11 | show: true,
12 | width: 200
13 | },
14 | zoomLevels: [500, 1000, 3000, 5000],
15 | exclSolo: true //enabling exclusive solo
16 | });
17 |
18 | playlist.load([
19 | {
20 | "src": "media/audio/Vocals30.mp3",
21 | "name": "Vocals"
22 | },
23 | {
24 | "src": "media/audio/Guitar30.mp3",
25 | "name": "Guitar"
26 | },
27 | {
28 | "src": "media/audio/PianoSynth30.mp3",
29 | "name": "Pianos & Synth"
30 | },
31 | {
32 | "src": "media/audio/BassDrums30.mp3",
33 | "name": "Drums"
34 | }
35 | ]).then(function() {
36 | //can do stuff with the playlist.
37 | });
38 |
--------------------------------------------------------------------------------
/ghpages/js/fades.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | samplesPerPixel: 5000,
3 | zoomLevels: [1000, 5000, 9000],
4 | waveHeight: 100,
5 | container: document.getElementById("playlist"),
6 | state: 'cursor',
7 | colors: {
8 | waveOutlineColor: '#005BBB',
9 | timeColor: 'grey',
10 | fadeColor: 'black'
11 | },
12 | controls: {
13 | show: true, //whether or not to include the track controls
14 | width: 200 //width of controls in pixels
15 | }
16 | });
17 |
18 | playlist.load([
19 | {
20 | "src": "media/audio/Vocals30.mp3",
21 | "name": "Vocals",
22 | "states": {
23 | "shift": false
24 | },
25 | "fadeIn": {
26 | "duration": 0.5
27 | },
28 | "fadeOut": {
29 | "duration": 0.5
30 | }
31 | },
32 | {
33 | "src": "media/audio/BassDrums30.mp3",
34 | "name": "Drums",
35 | "start": 30,
36 | "fadeIn": {
37 | "shape": "logarithmic",
38 | "duration": 0.75
39 | },
40 | "fadeOut": {
41 | "shape": "logarithmic",
42 | "duration": 1.5
43 | }
44 | }
45 | ]).then(function() {
46 | //can do stuff with the playlist.
47 | });
--------------------------------------------------------------------------------
/ghpages/js/loadingdata.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | container: document.getElementById("playlist"),
3 | controls: {
4 | show: true, //whether or not to include the track controls
5 | width: 200 //width of controls in pixels
6 | },
7 | waveHeight: 100,
8 | colors: {
9 | waveOutlineColor: '#005BBB'
10 | },
11 | });
12 |
13 | playlist.load([
14 | {
15 | "src": "media/audio/Vocals30.mp3",
16 | "name": "Vocals"
17 | },
18 | {
19 | "src": "media/audio/BassDrums30.mp3",
20 | "name": "Bass & Drums"
21 | }
22 | ]).then(function() {
23 | //can do stuff with the playlist.
24 | });
25 |
--------------------------------------------------------------------------------
/ghpages/js/minimal.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | container: document.getElementById("playlist"),
3 | colors: {
4 | waveOutlineColor: '#005BBB'
5 | },
6 | });
7 |
8 | playlist.load([
9 | {
10 | "src": "media/audio/BassDrums30.mp3"
11 | }
12 | ]).then(function() {
13 | //can do stuff with the playlist.
14 | });
15 |
--------------------------------------------------------------------------------
/ghpages/js/multi-channel.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | container: document.getElementById("playlist"),
3 | waveHeight: 80,
4 | mono: false,
5 | timescale: true,
6 | state: 'cursor',
7 | colors: {
8 | waveOutlineColor: '#005BBB'
9 | },
10 | controls: {
11 | show: true, //whether or not to include the track controls
12 | width: 200 //width of controls in pixels
13 | },
14 | });
15 |
16 | playlist.load([
17 | {
18 | src: "media/audio/BassDrums30.mp3",
19 | name: "Bass & Drums"
20 | }
21 | ]).then(function() {
22 | //can do stuff with the playlist.
23 | });
24 |
--------------------------------------------------------------------------------
/ghpages/js/newtracks.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | samplesPerPixel: 9000,
3 | zoomLevels: [1000, 5000, 9000],
4 | waveHeight: 100,
5 | container: document.getElementById("playlist"),
6 | timescale: true,
7 | state: 'cursor',
8 | colors: {
9 | waveOutlineColor: '#005BBB',
10 | timeColor: 'grey',
11 | fadeColor: 'black'
12 | },
13 | controls: {
14 | show: true, //whether or not to include the track controls
15 | width: 200 //width of controls in pixels
16 | }
17 | });
18 |
19 | //initialize the WAV exporter.
20 | playlist.initExporter();
--------------------------------------------------------------------------------
/ghpages/js/preselected.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | waveHeight: 100,
3 | container: document.getElementById("playlist"),
4 | colors: {
5 | waveOutlineColor: '#005BBB'
6 | },
7 | timescale: true
8 | });
9 |
10 | playlist.load([
11 | {
12 | src: "media/audio/BassDrums30.mp3",
13 | selected: {
14 | start: 5,
15 | end: 15
16 | },
17 | states: {
18 | cursor: false,
19 | select: false,
20 | shift: false,
21 | fadein: false,
22 | fadeout: false
23 | }
24 | }
25 | ]).then(function() {
26 | //can do stuff with the playlist.
27 | });
28 |
--------------------------------------------------------------------------------
/ghpages/js/record.js:
--------------------------------------------------------------------------------
1 | var userMediaStream;
2 | var playlist;
3 | var constraints = { audio: true };
4 |
5 | navigator.getUserMedia = (navigator.getUserMedia ||
6 | navigator.webkitGetUserMedia ||
7 | navigator.mozGetUserMedia ||
8 | navigator.msGetUserMedia);
9 |
10 | function gotStream(stream) {
11 | userMediaStream = stream;
12 | playlist.initRecorder(userMediaStream);
13 | $(".btn-record").removeClass("disabled");
14 | }
15 |
16 | function logError(err) {
17 | console.error(err);
18 | }
19 |
20 | playlist = WaveformPlaylist.init({
21 | samplesPerPixel: 5000,
22 | zoomLevels: [1000, 5000, 9000],
23 | waveHeight: 100,
24 | container: document.getElementById("playlist"),
25 | state: 'cursor',
26 | colors: {
27 | waveOutlineColor: '#005BBB',
28 | timeColor: 'grey',
29 | fadeColor: 'black'
30 | },
31 | controls: {
32 | show: true, //whether or not to include the track controls
33 | width: 200 //width of controls in pixels
34 | }
35 | });
36 |
37 | //initialize the WAV exporter.
38 | playlist.initExporter();
39 |
40 | if (navigator.mediaDevices) {
41 | navigator.mediaDevices.getUserMedia(constraints)
42 | .then(gotStream)
43 | .catch(logError);
44 | } else if (navigator.getUserMedia && 'MediaRecorder' in window) {
45 | navigator.getUserMedia(
46 | constraints,
47 | gotStream,
48 | logError
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/ghpages/js/stationary-track.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | samplesPerPixel: 3000,
3 | zoomLevels: [500, 1000, 3000, 5000],
4 | mono: true,
5 | waveHeight: 100,
6 | container: document.getElementById("playlist"),
7 | state: 'shift',
8 | waveOutlineColor: '#E0EFF1',
9 | colors: {
10 | waveOutlineColor: '#005BBB',
11 | timeColor: 'grey',
12 | fadeColor: 'black'
13 | },
14 | controls: {
15 | show: true, //whether or not to include the track controls
16 | width: 200, //width of controls in pixels
17 | widgets: {
18 | muteOrSolo: true,
19 | volume: true,
20 | stereoPan: false,
21 | collapse: false,
22 | remove: false,
23 | }
24 | }
25 | });
26 |
27 | playlist.load([
28 | {
29 | "src": "media/audio/Vocals30.mp3",
30 | "name": "Vocals",
31 | "states": {
32 | "shift": false
33 | }
34 | },
35 | {
36 | "src": "media/audio/BassDrums30.mp3",
37 | "name": "Drums",
38 | "start": 30
39 | }
40 | ]).then(function() {
41 | //can do stuff with the playlist.
42 | });
43 |
--------------------------------------------------------------------------------
/ghpages/js/stem-tracks.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | samplesPerPixel: 1000,
3 | waveHeight: 100,
4 | container: document.getElementById("playlist"),
5 | timescale: true,
6 | state: "cursor",
7 | colors: {
8 | waveOutlineColor: "#005BBB",
9 | },
10 | controls: {
11 | show: true, //whether or not to include the track controls
12 | width: 200, //width of controls in pixels
13 | },
14 | zoomLevels: [500, 1000, 3000, 5000],
15 | });
16 |
17 | playlist
18 | .load([
19 | {
20 | src: "media/audio/Vocals30.mp3",
21 | name: "Vocals",
22 | },
23 | {
24 | src: "media/audio/Guitar30.mp3",
25 | name: "Guitar",
26 | },
27 | {
28 | src: "media/audio/PianoSynth30.mp3",
29 | name: "Pianos & Synth",
30 | },
31 | {
32 | src: "media/audio/BassDrums30.mp3",
33 | name: "Drums",
34 | },
35 | ])
36 | .then(function () {
37 | //can do stuff with the playlist.
38 | });
39 |
--------------------------------------------------------------------------------
/ghpages/js/stereopan.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | container: document.getElementById('playlist'),
3 | controls: {
4 | show: true, // whether or not to include the track controls
5 | width: 200, // width of controls in pixels
6 | },
7 | colors: {
8 | waveOutlineColor: '#005BBB'
9 | },
10 | });
11 |
12 | playlist
13 | .load([
14 | {
15 | name: 'Left Panned Track',
16 | src: 'media/audio/PianoSynth30.mp3',
17 | stereoPan: -1,
18 | },
19 | {
20 | name: 'Right Panned Track',
21 | src: 'media/audio/BassDrums30.mp3',
22 | stereoPan: 1,
23 | },
24 | ])
25 | .then(() => {
26 | // can do stuff with the playlist.
27 | });
28 |
--------------------------------------------------------------------------------
/ghpages/js/track-zoom.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | samplesPerPixel: 256, //samples per pixel to draw, must be an entry in zoomLevels.
3 | waveHeight: 100,
4 | container: document.getElementById("playlist"),
5 | timescale: true,
6 | colors: {
7 | waveOutlineColor: '#005BBB'
8 | },
9 | zoomLevels: [128, 256, 512, 1024, 2048, 4096] //zoom levels in samples per pixel
10 | });
11 |
12 | playlist.load([
13 | {
14 | "src": "media/audio/BassDrums30.mp3"
15 | }
16 | ]).then(function() {
17 | //can do stuff with the playlist.
18 | });
--------------------------------------------------------------------------------
/ghpages/js/web-audio-editor-fill.js:
--------------------------------------------------------------------------------
1 | var playlist = WaveformPlaylist.init({
2 | samplesPerPixel: 3000,
3 | mono: false,
4 | waveHeight: 100,
5 | container: document.getElementById("playlist"),
6 | state: 'cursor',
7 | colors: {
8 | waveOutlineColor: '#005BBB',
9 | timeColor: 'grey',
10 | fadeColor: 'black'
11 | },
12 | timescale: true,
13 | controls: {
14 | show: true, //whether or not to include the track controls
15 | width: 200 //width of controls in pixels
16 | },
17 | seekStyle : 'fill',
18 | zoomLevels: [500, 1000, 3000, 5000]
19 | });
20 |
21 | playlist.load([
22 | {
23 | "src": "media/audio/Vocals30.mp3",
24 | "name": "Vocals",
25 | "fadeIn": {
26 | "duration": 0.5
27 | },
28 | "fadeOut": {
29 | "duration": 0.5
30 | },
31 | "cuein": 5.918,
32 | "cueout": 14.5
33 | },
34 | {
35 | "src": "media/audio/BassDrums30.mp3",
36 | "name": "Drums",
37 | "start": 8.5,
38 | "fadeIn": {
39 | "shape": "logarithmic",
40 | "duration": 0.5
41 | },
42 | "fadeOut": {
43 | "shape": "logarithmic",
44 | "duration": 0.5
45 | }
46 | },
47 | {
48 | "src": "media/audio/Guitar30.mp3",
49 | "name": "Guitar",
50 | "start": 23.5,
51 | "fadeOut": {
52 | "shape": "linear",
53 | "duration": 0.5
54 | },
55 | "cuein": 15
56 | }
57 | ]).then(function() {
58 | //can do stuff with the playlist.
59 | });
--------------------------------------------------------------------------------
/ghpages/js/web-audio-editor.js:
--------------------------------------------------------------------------------
1 | var userMediaStream;
2 | var playlist;
3 | var constraints = { audio: true };
4 |
5 | navigator.getUserMedia = (navigator.getUserMedia ||
6 | navigator.webkitGetUserMedia ||
7 | navigator.mozGetUserMedia ||
8 | navigator.msGetUserMedia);
9 |
10 | function gotStream(stream) {
11 | userMediaStream = stream;
12 | playlist.initRecorder(userMediaStream);
13 | $(".btn-record").removeClass("disabled");
14 | }
15 |
16 | function logError(err) {
17 | console.error(err);
18 | }
19 |
20 | playlist = WaveformPlaylist.init({
21 | samplesPerPixel: 3000,
22 | waveHeight: 100,
23 | container: document.getElementById("playlist"),
24 | state: 'cursor',
25 | colors: {
26 | waveOutlineColor: '#005BBB',
27 | timeColor: 'grey',
28 | fadeColor: 'black'
29 | },
30 | timescale: true,
31 | controls: {
32 | show: true, //whether or not to include the track controls
33 | width: 200 //width of controls in pixels
34 | },
35 | seekStyle : 'line',
36 | zoomLevels: [500, 1000, 3000, 5000]
37 | });
38 |
39 | playlist.load([
40 | {
41 | "src": "media/audio/Vocals30.mp3",
42 | "name": "Vocals",
43 | "fadeIn": {
44 | "duration": 0.5
45 | },
46 | "fadeOut": {
47 | "duration": 0.5
48 | },
49 | "cuein": 5.918,
50 | "cueout": 14.5,
51 | "customClass": "vocals",
52 | "waveOutlineColor": '#c0dce0'
53 | },
54 | {
55 | "src": "media/audio/BassDrums30.mp3",
56 | "name": "Drums",
57 | "start": 8.5,
58 | "fadeIn": {
59 | "shape": "logarithmic",
60 | "duration": 0.5
61 | },
62 | "fadeOut": {
63 | "shape": "logarithmic",
64 | "duration": 0.5
65 | }
66 | },
67 | {
68 | "src": "media/audio/Guitar30.mp3",
69 | "name": "Guitar",
70 | "start": 23.5,
71 | "fadeOut": {
72 | "shape": "linear",
73 | "duration": 0.5
74 | },
75 | "cuein": 15
76 | }
77 | ]).then(function() {
78 | //can do stuff with the playlist.
79 |
80 | //initialize the WAV exporter.
81 | playlist.initExporter();
82 |
83 | if (navigator.mediaDevices) {
84 | navigator.mediaDevices.getUserMedia(constraints)
85 | .then(gotStream)
86 | .catch(logError);
87 | } else if (navigator.getUserMedia && 'MediaRecorder' in window) {
88 | navigator.getUserMedia(
89 | constraints,
90 | gotStream,
91 | logError
92 | );
93 | }
94 | });
--------------------------------------------------------------------------------
/ghpages/media/audio/BassDrums30.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/ghpages/media/audio/BassDrums30.mp3
--------------------------------------------------------------------------------
/ghpages/media/audio/Guitar30.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/ghpages/media/audio/Guitar30.mp3
--------------------------------------------------------------------------------
/ghpages/media/audio/PianoSynth30.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/ghpages/media/audio/PianoSynth30.mp3
--------------------------------------------------------------------------------
/ghpages/media/audio/Vocals30.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/ghpages/media/audio/Vocals30.mp3
--------------------------------------------------------------------------------
/ghpages/media/audio/sonnet.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/ghpages/media/audio/sonnet.mp3
--------------------------------------------------------------------------------
/ghpages/media/image/vocals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/ghpages/media/image/vocals.png
--------------------------------------------------------------------------------
/ghpages/media/vocals.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/ghpages/media/vocals.dat
--------------------------------------------------------------------------------
/ghpages/scss/_base.scss:
--------------------------------------------------------------------------------
1 | a {
2 | color:#1d70b8;
3 | cursor: pointer;
4 | text-decoration: underline;
5 | }
6 | a:link {
7 | color: #1d70b8;
8 | }
9 | a:visited {
10 | color: #4c2c92;
11 | }
12 | a:hover {
13 | color: #003078;
14 | }
15 | a:active {
16 | color: #0b0c0c;
17 | }
18 | a:focus {
19 | color: #0b0c0c;
20 | }
21 |
22 | .breadcrumb {
23 | background-color: #f4f4f4;
24 | }
25 |
--------------------------------------------------------------------------------
/ghpages/scss/main.scss:
--------------------------------------------------------------------------------
1 | @charset 'UTF-8';
2 | @use 'playlist' with (
3 | $wp-channel-color: #FFD500,
4 | $wp-tracks-container-background-color: #005BBB
5 | );
6 | @import 'base';
7 |
8 | .playlist .vocals {
9 | background-color: #c0dce0;
10 | }
11 |
12 | .track-drop {
13 | border: 2px dashed blue;
14 | height: 100px;
15 | width: 200px;
16 | margin: 1em 0;
17 |
18 | &::before {
19 | content: "Drop audio file(s) here!";
20 | }
21 |
22 | &.drag-enter {
23 | border: 2px solid orange;
24 | }
25 | }
26 |
27 | footer {
28 | margin-top: 2em;
29 | }
--------------------------------------------------------------------------------
/img/annotations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/img/annotations.png
--------------------------------------------------------------------------------
/img/effects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/img/effects.png
--------------------------------------------------------------------------------
/img/stemtracks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/img/stemtracks.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "waveform-playlist",
3 | "description": "Multiple track web audio editor and player with waveform preview",
4 | "version": "4.3.3",
5 | "main": "build/waveform-playlist.umd.js",
6 | "author": "Naomi Aro",
7 | "license": "MIT",
8 | "website": "http://naomiaro.github.io",
9 | "devDependencies": {
10 | "@babel/cli": "^7.17.6",
11 | "@babel/core": "^7.17.5",
12 | "@babel/plugin-transform-runtime": "^7.17.0",
13 | "@babel/preset-env": "^7.16.11",
14 | "ajv": "^8.9.0",
15 | "babel-loader": "^8.2.3",
16 | "gh-pages": "^3.2.3",
17 | "mkdirp": "^1.0.4",
18 | "parallel-webpack": "^2.6.0",
19 | "prettier": "2.3.2",
20 | "sass": "^1.50.0",
21 | "webpack": "^5.68.0",
22 | "webpack-cli": "^4.9.2",
23 | "webpack-dev-server": "^4.7.3"
24 | },
25 | "directories": {
26 | "build": "build",
27 | "lib": "lib",
28 | "css": "styles"
29 | },
30 | "dependencies": {
31 | "@babel/runtime": "^7.17.2",
32 | "event-emitter": "^0.3.4",
33 | "fade-curves": "^1.0.2",
34 | "fade-maker": "^1.0.3",
35 | "inline-worker": "^1.1.0",
36 | "lodash.assign": "^4.0.0",
37 | "lodash.defaultsdeep": "^4.6.1",
38 | "lodash.forown": "^4.0.0",
39 | "tone": "^14.7.77",
40 | "uuid": "^8.3.2",
41 | "virtual-dom": "^2.1.1",
42 | "webaudio-peaks": "^1.0.0"
43 | },
44 | "scripts": {
45 | "clean": "rm -Rf dist && rm -Rf lib && rm -Rf build && rm -Rf styles",
46 | "styles": "mkdirp styles && cp ghpages/scss/_playlist.scss styles/playlist.scss && sass styles/",
47 | "jekyll:scss": "mkdirp dist/waveform-playlist/css/ && sass ghpages/scss/:dist/waveform-playlist/css/ --watch &",
48 | "jekyll": "jekyll build -s ghpages -d dist/waveform-playlist && mkdirp dist/waveform-playlist/css/ && sass ghpages/scss/:dist/waveform-playlist/css/",
49 | "jekyll:dev": "jekyll build -s ghpages -d dist/waveform-playlist --watch &",
50 | "ghpages": "gh-pages --repo https://$GH_TOKEN@github.com/naomiaro/waveform-playlist.git -d dist/waveform-playlist",
51 | "build": "npm run clean && npm run jekyll && webpack",
52 | "preversion": "npm run clean",
53 | "version": "npm run build && git add -A dist",
54 | "postversion": "git push && git push --tags",
55 | "prepare": "npm run compile && npm run styles && npm run webpack:unpkg",
56 | "compile": "babel src --out-dir lib",
57 | "webpack:unpkg": "webpack --config webpack.config.unpkg.js",
58 | "webpack:server": "webpack serve --static dist/ --color --host 0.0.0.0 --config webpack.config.dev.js",
59 | "start": "npm run webpack:server",
60 | "dev": "npm run jekyll:dev && npm run jekyll:scss && npm run webpack:server",
61 | "website": "npm run build && npm run ghpages"
62 | },
63 | "repository": {
64 | "type": "git",
65 | "url": "git+https://github.com/naomiaro/waveform-playlist.git"
66 | },
67 | "keywords": [
68 | "waveform",
69 | "audio",
70 | "audacity",
71 | "stem",
72 | "tracks",
73 | "multitrack",
74 | "playlist",
75 | "music",
76 | "editor",
77 | "record",
78 | "recording",
79 | "player",
80 | "webaudio"
81 | ],
82 | "bugs": {
83 | "url": "https://github.com/naomiaro/waveform-playlist/issues"
84 | },
85 | "homepage": "http://naomiaro.github.io/waveform-playlist"
86 | }
87 |
--------------------------------------------------------------------------------
/src/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | examples
3 | experiments
4 | ghpages
5 | img
6 |
--------------------------------------------------------------------------------
/src/Playout.js:
--------------------------------------------------------------------------------
1 | import { FADEIN, FADEOUT, createFadeIn, createFadeOut } from "fade-maker";
2 |
3 | function noEffects(node1, node2) {
4 | node1.connect(node2);
5 | }
6 |
7 | export default class {
8 | constructor(ac, buffer, masterGain = ac.createGain()) {
9 | this.ac = ac;
10 | this.gain = 1;
11 | this.effectsGraph = noEffects;
12 | this.masterEffectsGraph = noEffects;
13 | this.buffer = buffer;
14 | this.masterGain = masterGain;
15 | this.destination = this.ac.destination;
16 | }
17 |
18 | applyFade(type, start, duration, shape = "logarithmic") {
19 | if (type === FADEIN) {
20 | createFadeIn(this.fadeGain.gain, shape, start, duration);
21 | } else if (type === FADEOUT) {
22 | createFadeOut(this.fadeGain.gain, shape, start, duration);
23 | } else {
24 | throw new Error("Unsupported fade type");
25 | }
26 | }
27 |
28 | applyFadeIn(start, duration, shape = "logarithmic") {
29 | this.applyFade(FADEIN, start, duration, shape);
30 | }
31 |
32 | applyFadeOut(start, duration, shape = "logarithmic") {
33 | this.applyFade(FADEOUT, start, duration, shape);
34 | }
35 |
36 | isPlaying() {
37 | return this.source !== undefined;
38 | }
39 |
40 | getDuration() {
41 | return this.buffer.duration;
42 | }
43 |
44 | setAudioContext(ac) {
45 | this.ac = ac;
46 | this.destination = this.ac.destination;
47 | }
48 |
49 | createStereoPanner() {
50 | if (this.ac.createStereoPanner) {
51 | return this.ac.createStereoPanner();
52 | }
53 | return this.ac.createPanner();
54 | }
55 |
56 | setUpSource() {
57 | this.source = this.ac.createBufferSource();
58 | this.source.buffer = this.buffer;
59 |
60 | let cleanupEffects;
61 | let cleanupMasterEffects;
62 |
63 | const sourcePromise = new Promise((resolve) => {
64 | // keep track of the buffer state.
65 | this.source.onended = () => {
66 | this.source.disconnect();
67 | this.fadeGain.disconnect();
68 | this.volumeGain.disconnect();
69 | this.shouldPlayGain.disconnect();
70 | this.panner.disconnect();
71 | // this.masterGain.disconnect();
72 |
73 | if (cleanupEffects) cleanupEffects();
74 | if (cleanupMasterEffects) cleanupMasterEffects();
75 |
76 | this.source = undefined;
77 | this.fadeGain = undefined;
78 | this.volumeGain = undefined;
79 | this.shouldPlayGain = undefined;
80 | this.panner = undefined;
81 |
82 | resolve();
83 | };
84 | });
85 |
86 | this.fadeGain = this.ac.createGain();
87 | // used for track volume slider
88 | this.volumeGain = this.ac.createGain();
89 | // used for solo/mute
90 | this.shouldPlayGain = this.ac.createGain();
91 | this.panner = this.createStereoPanner();
92 |
93 | this.source.connect(this.fadeGain);
94 | this.fadeGain.connect(this.volumeGain);
95 | this.volumeGain.connect(this.shouldPlayGain);
96 | this.shouldPlayGain.connect(this.panner);
97 |
98 | cleanupEffects = this.effectsGraph(
99 | this.panner,
100 | this.masterGain,
101 | this.ac instanceof (window.OfflineAudioContext || window.webkitOfflineAudioContext)
102 | );
103 | cleanupMasterEffects = this.masterEffectsGraph(
104 | this.masterGain,
105 | this.destination,
106 | this.ac instanceof (window.OfflineAudioContext || window.webkitOfflineAudioContext)
107 | );
108 |
109 | return sourcePromise;
110 | }
111 |
112 | setVolumeGainLevel(level) {
113 | if (this.volumeGain) {
114 | this.volumeGain.gain.value = level;
115 | }
116 | }
117 |
118 | setShouldPlay(bool) {
119 | if (this.shouldPlayGain) {
120 | this.shouldPlayGain.gain.value = bool ? 1 : 0;
121 | }
122 | }
123 |
124 | setMasterGainLevel(level) {
125 | if (this.masterGain) {
126 | this.masterGain.gain.value = level;
127 | }
128 | }
129 |
130 | setStereoPanValue(pan = 0) {
131 | if (this.panner) {
132 | if (this.panner.pan !== undefined) {
133 | this.panner.pan.value = pan;
134 | } else {
135 | this.panner.panningModel = "equalpower";
136 | this.panner.setPosition(pan, 0, 1 - Math.abs(pan));
137 | }
138 | }
139 | }
140 |
141 | setEffects(effectsGraph = noEffects) {
142 | this.effectsGraph = effectsGraph;
143 | }
144 |
145 | setMasterEffects(effectsGraph = noEffects) {
146 | this.masterEffectsGraph = effectsGraph;
147 | }
148 |
149 | /*
150 | source.start is picky when passing the end time.
151 | If rounding error causes a number to make the source think
152 | it is playing slightly more samples than it has it won't play at all.
153 | Unfortunately it doesn't seem to work if you just give it a start time.
154 | */
155 | play(when, start, duration) {
156 | this.source.start(when, start, duration);
157 | }
158 |
159 | stop(when = 0) {
160 | if (this.source) {
161 | this.source.stop(when);
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/TimeScale.js:
--------------------------------------------------------------------------------
1 | import h from "virtual-dom/h";
2 |
3 | import { secondsToPixels } from "./utils/conversions";
4 | import TimeScaleHook from "./render/TimeScaleHook";
5 |
6 | class TimeScale {
7 | constructor(
8 | duration,
9 | offset,
10 | samplesPerPixel,
11 | sampleRate,
12 | marginLeft = 0,
13 | colors
14 | ) {
15 | this.duration = duration;
16 | this.offset = offset;
17 | this.samplesPerPixel = samplesPerPixel;
18 | this.sampleRate = sampleRate;
19 | this.marginLeft = marginLeft;
20 | this.colors = colors;
21 |
22 | this.timeinfo = {
23 | 20000: {
24 | marker: 30000,
25 | bigStep: 10000,
26 | smallStep: 5000,
27 | secondStep: 5,
28 | },
29 | 12000: {
30 | marker: 15000,
31 | bigStep: 5000,
32 | smallStep: 1000,
33 | secondStep: 1,
34 | },
35 | 10000: {
36 | marker: 10000,
37 | bigStep: 5000,
38 | smallStep: 1000,
39 | secondStep: 1,
40 | },
41 | 5000: {
42 | marker: 5000,
43 | bigStep: 1000,
44 | smallStep: 500,
45 | secondStep: 1 / 2,
46 | },
47 | 2500: {
48 | marker: 2000,
49 | bigStep: 1000,
50 | smallStep: 500,
51 | secondStep: 1 / 2,
52 | },
53 | 1500: {
54 | marker: 2000,
55 | bigStep: 1000,
56 | smallStep: 200,
57 | secondStep: 1 / 5,
58 | },
59 | 700: {
60 | marker: 1000,
61 | bigStep: 500,
62 | smallStep: 100,
63 | secondStep: 1 / 10,
64 | },
65 | };
66 | }
67 |
68 | getScaleInfo(resolution) {
69 | let keys = Object.keys(this.timeinfo).map((item) => parseInt(item, 10));
70 |
71 | // make sure keys are numerically sorted.
72 | keys = keys.sort((a, b) => a - b);
73 |
74 | for (let i = 0; i < keys.length; i += 1) {
75 | if (resolution <= keys[i]) {
76 | return this.timeinfo[keys[i]];
77 | }
78 | }
79 |
80 | return this.timeinfo[keys[0]];
81 | }
82 |
83 | /*
84 | Return time in format mm:ss
85 | */
86 | static formatTime(milliseconds) {
87 | const seconds = milliseconds / 1000;
88 | let s = seconds % 60;
89 | const m = (seconds - s) / 60;
90 |
91 | if (s < 10) {
92 | s = `0${s}`;
93 | }
94 |
95 | return `${m}:${s}`;
96 | }
97 |
98 | render() {
99 | const widthX = secondsToPixels(
100 | this.duration,
101 | this.samplesPerPixel,
102 | this.sampleRate
103 | );
104 | const pixPerSec = this.sampleRate / this.samplesPerPixel;
105 | const pixOffset = secondsToPixels(
106 | this.offset,
107 | this.samplesPerPixel,
108 | this.sampleRate
109 | );
110 | const scaleInfo = this.getScaleInfo(this.samplesPerPixel);
111 | const canvasInfo = {};
112 | const timeMarkers = [];
113 | const end = widthX + pixOffset;
114 | let counter = 0;
115 |
116 | for (let i = 0; i < end; i += pixPerSec * scaleInfo.secondStep) {
117 | const pixIndex = Math.floor(i);
118 | const pix = pixIndex - pixOffset;
119 |
120 | if (pixIndex >= pixOffset) {
121 | // put a timestamp every 30 seconds.
122 | if (scaleInfo.marker && counter % scaleInfo.marker === 0) {
123 | timeMarkers.push(
124 | h(
125 | "div.time",
126 | {
127 | attributes: {
128 | style: `position: absolute; left: ${pix}px;`,
129 | },
130 | },
131 | [TimeScale.formatTime(counter)]
132 | )
133 | );
134 |
135 | canvasInfo[pix] = 10;
136 | } else if (scaleInfo.bigStep && counter % scaleInfo.bigStep === 0) {
137 | canvasInfo[pix] = 5;
138 | } else if (scaleInfo.smallStep && counter % scaleInfo.smallStep === 0) {
139 | canvasInfo[pix] = 2;
140 | }
141 | }
142 |
143 | counter += 1000 * scaleInfo.secondStep;
144 | }
145 |
146 | return h(
147 | "div.playlist-time-scale",
148 | {
149 | attributes: {
150 | style: `position: relative; left: 0; right: 0; margin-left: ${this.marginLeft}px;`,
151 | },
152 | },
153 | [
154 | timeMarkers,
155 | h("canvas", {
156 | attributes: {
157 | width: widthX,
158 | height: 30,
159 | style: "position: absolute; left: 0; right: 0; top: 0; bottom: 0;",
160 | },
161 | hook: new TimeScaleHook(
162 | canvasInfo,
163 | this.offset,
164 | this.samplesPerPixel,
165 | this.duration,
166 | this.colors
167 | ),
168 | }),
169 | ]
170 | );
171 | }
172 | }
173 |
174 | export default TimeScale;
175 |
--------------------------------------------------------------------------------
/src/annotation/input/aeneas.js:
--------------------------------------------------------------------------------
1 | /*
2 | {
3 | "begin": "5.759",
4 | "end": "9.155",
5 | "id": "002",
6 | "language": "en",
7 | "lines": [
8 | "I just wanted to hold"
9 | ]
10 | },
11 | */
12 |
13 | import { v4 as uuidv4 } from "uuid";
14 |
15 | export default function (aeneas) {
16 | const annotation = {
17 | id: aeneas.id || uuidv4(),
18 | start: Number(aeneas.begin) || 0,
19 | end: Number(aeneas.end) || 0,
20 | lines: aeneas.lines || [""],
21 | lang: aeneas.language || "en",
22 | };
23 |
24 | return annotation;
25 | }
26 |
--------------------------------------------------------------------------------
/src/annotation/output/aeneas.js:
--------------------------------------------------------------------------------
1 | /*
2 | {
3 | "begin": "5.759",
4 | "end": "9.155",
5 | "id": "002",
6 | "language": "en",
7 | "lines": [
8 | "I just wanted to hold"
9 | ]
10 | },
11 | */
12 |
13 | export default function (annotation) {
14 | return {
15 | begin: String(annotation.start.toFixed(3)),
16 | end: String(annotation.end.toFixed(3)),
17 | id: String(annotation.id),
18 | language: annotation.lang,
19 | lines: annotation.lines,
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/src/annotation/render/ScrollTopHook.js:
--------------------------------------------------------------------------------
1 | /*
2 | * virtual-dom hook for scrolling to the text annotation.
3 | */
4 | const Hook = function ScrollTopHook() {};
5 | Hook.prototype.hook = function hook(node) {
6 | const el = node.querySelector(".current");
7 | if (el) {
8 | const box = node.getBoundingClientRect();
9 | const row = el.getBoundingClientRect();
10 | const diff = row.top - box.top;
11 | const list = node;
12 | list.scrollTop += diff;
13 | }
14 | };
15 |
16 | export default Hook;
17 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import _defaults from "lodash.defaultsdeep";
2 | import createElement from "virtual-dom/create-element";
3 | import EventEmitter from "event-emitter";
4 | import Playlist from "./Playlist";
5 |
6 | export function init(options = {}, ee = EventEmitter()) {
7 | if (options.container === undefined) {
8 | throw new Error("DOM element container must be given.");
9 | }
10 |
11 | const defaults = {
12 | samplesPerPixel: 4096,
13 | mono: true,
14 | fadeType: "logarithmic",
15 | exclSolo: false,
16 | timescale: false,
17 | controls: {
18 | show: false,
19 | width: 150,
20 | widgets: {
21 | muteOrSolo: true,
22 | volume: true,
23 | stereoPan: true,
24 | collapse: true,
25 | remove: true,
26 | },
27 | },
28 | colors: {
29 | waveOutlineColor: "white",
30 | timeColor: "grey",
31 | fadeColor: "black",
32 | },
33 | seekStyle: "line",
34 | waveHeight: 128,
35 | collapsedWaveHeight: 30,
36 | barWidth: 1,
37 | barGap: 0,
38 | state: "cursor",
39 | zoomLevels: [512, 1024, 2048, 4096],
40 | annotationList: {
41 | annotations: [],
42 | controls: [],
43 | editable: false,
44 | linkEndpoints: false,
45 | isContinuousPlay: false,
46 | },
47 | isAutomaticScroll: false,
48 | };
49 |
50 | const config = _defaults({}, options, defaults);
51 | const zoomIndex = config.zoomLevels.indexOf(config.samplesPerPixel);
52 |
53 | if (zoomIndex === -1) {
54 | throw new Error(
55 | "initial samplesPerPixel must be included in array zoomLevels"
56 | );
57 | }
58 |
59 | const playlist = new Playlist();
60 | const ctx = config.ac || new AudioContext();
61 | playlist.setAudioContext(ctx);
62 | playlist.setSampleRate(config.sampleRate || ctx.sampleRate);
63 | playlist.setSamplesPerPixel(config.samplesPerPixel);
64 | playlist.setEventEmitter(ee);
65 | playlist.setUpEventEmitter();
66 | playlist.setTimeSelection(0, 0);
67 | playlist.setState(config.state);
68 | playlist.setControlOptions(config.controls);
69 | playlist.setWaveHeight(config.waveHeight);
70 | playlist.setCollapsedWaveHeight(config.collapsedWaveHeight);
71 | playlist.setColors(config.colors);
72 | playlist.setZoomLevels(config.zoomLevels);
73 | playlist.setZoomIndex(zoomIndex);
74 | playlist.setMono(config.mono);
75 | playlist.setExclSolo(config.exclSolo);
76 | playlist.setShowTimeScale(config.timescale);
77 | playlist.setSeekStyle(config.seekStyle);
78 | playlist.setAnnotations(config.annotationList);
79 | playlist.setBarGap(config.barGap);
80 | playlist.setBarWidth(config.barWidth);
81 | playlist.isAutomaticScroll = config.isAutomaticScroll;
82 | playlist.isContinuousPlay = config.isContinuousPlay;
83 | playlist.linkedEndpoints = config.linkedEndpoints;
84 |
85 | if (config.effects) {
86 | playlist.setEffects(config.effects);
87 | }
88 |
89 | // take care of initial virtual dom rendering.
90 |
91 | const tree = playlist.render();
92 | const rootNode = createElement(tree);
93 |
94 | config.container.appendChild(rootNode);
95 | playlist.tree = tree;
96 | playlist.rootNode = rootNode;
97 |
98 | return playlist;
99 | }
100 |
101 | export default function (options = {}, ee = EventEmitter()) {
102 | return init(options, ee);
103 | }
104 |
--------------------------------------------------------------------------------
/src/interaction/DragInteraction.js:
--------------------------------------------------------------------------------
1 | import { pixelsToSeconds } from "../utils/conversions";
2 |
3 | export default class {
4 | constructor(playlist, data = {}) {
5 | this.playlist = playlist;
6 | this.data = data;
7 | this.active = false;
8 |
9 | this.ondragover = (e) => {
10 | if (this.active) {
11 | e.preventDefault();
12 | this.emitDrag(e.clientX);
13 | }
14 | };
15 | }
16 |
17 | emitDrag(x) {
18 | const deltaX = x - this.prevX;
19 |
20 | // emit shift event if not 0
21 | if (deltaX) {
22 | const deltaTime = pixelsToSeconds(
23 | deltaX,
24 | this.playlist.samplesPerPixel,
25 | this.playlist.sampleRate
26 | );
27 | this.prevX = x;
28 | this.playlist.ee.emit("dragged", deltaTime, this.data);
29 | }
30 | }
31 |
32 | complete() {
33 | this.active = false;
34 | document.removeEventListener("dragover", this.ondragover);
35 | }
36 |
37 | dragstart(e) {
38 | const ev = e;
39 | this.active = true;
40 | this.prevX = e.clientX;
41 |
42 | ev.dataTransfer.dropEffect = "move";
43 | ev.dataTransfer.effectAllowed = "move";
44 | ev.dataTransfer.setData("text/plain", "");
45 | document.addEventListener("dragover", this.ondragover);
46 | }
47 |
48 | dragend(e) {
49 | if (this.active) {
50 | e.preventDefault();
51 | this.complete();
52 | }
53 | }
54 |
55 | static getClass() {
56 | return ".shift";
57 | }
58 |
59 | static getEvents() {
60 | return ["dragstart", "dragend"];
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/render/CanvasHook.js:
--------------------------------------------------------------------------------
1 | /*
2 | * virtual-dom hook for drawing to the canvas element.
3 | */
4 | class CanvasHook {
5 | constructor(peaks, offset, bits, color, scale, height, barWidth, barGap) {
6 | this.peaks = peaks;
7 | // http://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element
8 | this.offset = offset;
9 | this.color = color;
10 | this.bits = bits;
11 | this.scale = scale;
12 | this.height = height;
13 | this.barWidth = barWidth;
14 | this.barGap = barGap;
15 | }
16 |
17 | static drawFrame(cc, h2, x, minPeak, maxPeak, width, gap) {
18 | const min = Math.abs(minPeak * h2);
19 | const max = Math.abs(maxPeak * h2);
20 |
21 | // draw max
22 | cc.fillRect(x, 0, width, h2 - max);
23 | // draw min
24 | cc.fillRect(x, h2 + min, width, h2 - min);
25 | // draw gap
26 | if (gap !== 0) {
27 | cc.fillRect(x + width, 0, gap, h2 * 2);
28 | }
29 | }
30 |
31 | hook(canvas, prop, prev) {
32 | // canvas is up to date
33 | if (
34 | prev !== undefined &&
35 | prev.peaks === this.peaks &&
36 | prev.scale === this.scale &&
37 | prev.height === this.height
38 | ) {
39 | return;
40 | }
41 |
42 | const scale = this.scale;
43 | const len = canvas.width / scale;
44 | const cc = canvas.getContext("2d");
45 | const h2 = canvas.height / scale / 2;
46 | const maxValue = 2 ** (this.bits - 1);
47 | const width = this.barWidth;
48 | const gap = this.barGap;
49 | const barStart = width + gap;
50 |
51 | cc.clearRect(0, 0, canvas.width, canvas.height);
52 |
53 | cc.save();
54 | cc.fillStyle = this.color;
55 | cc.scale(scale, scale);
56 |
57 | for (let pixel = 0; pixel < len; pixel += barStart) {
58 | const minPeak = this.peaks[(pixel + this.offset) * 2] / maxValue;
59 | const maxPeak = this.peaks[(pixel + this.offset) * 2 + 1] / maxValue;
60 | CanvasHook.drawFrame(cc, h2, pixel, minPeak, maxPeak, width, gap);
61 | }
62 |
63 | cc.restore();
64 | }
65 | }
66 |
67 | export default CanvasHook;
68 |
--------------------------------------------------------------------------------
/src/render/FadeCanvasHook.js:
--------------------------------------------------------------------------------
1 | import {
2 | FADEIN,
3 | FADEOUT,
4 | SCURVE,
5 | LINEAR,
6 | EXPONENTIAL,
7 | LOGARITHMIC,
8 | } from "fade-maker";
9 | import { sCurve, logarithmic, linear, exponential } from "fade-curves";
10 |
11 | /*
12 | * virtual-dom hook for drawing the fade curve to the canvas element.
13 | */
14 | class FadeCanvasHook {
15 | constructor(type, shape, duration, samplesPerPixel) {
16 | this.type = type;
17 | this.shape = shape;
18 | this.duration = duration;
19 | this.samplesPerPixel = samplesPerPixel;
20 | }
21 |
22 | static createCurve(shape, type, width) {
23 | let reflection;
24 | let curve;
25 |
26 | switch (type) {
27 | case FADEIN: {
28 | reflection = 1;
29 | break;
30 | }
31 | case FADEOUT: {
32 | reflection = -1;
33 | break;
34 | }
35 | default: {
36 | throw new Error("Unsupported fade type.");
37 | }
38 | }
39 |
40 | switch (shape) {
41 | case SCURVE: {
42 | curve = sCurve(width, reflection);
43 | break;
44 | }
45 | case LINEAR: {
46 | curve = linear(width, reflection);
47 | break;
48 | }
49 | case EXPONENTIAL: {
50 | curve = exponential(width, reflection);
51 | break;
52 | }
53 | case LOGARITHMIC: {
54 | curve = logarithmic(width, 10, reflection);
55 | break;
56 | }
57 | default: {
58 | throw new Error("Unsupported fade shape");
59 | }
60 | }
61 |
62 | return curve;
63 | }
64 |
65 | hook(canvas, prop, prev) {
66 | // node is up to date.
67 | if (
68 | prev !== undefined &&
69 | prev.shape === this.shape &&
70 | prev.type === this.type &&
71 | prev.duration === this.duration &&
72 | prev.samplesPerPixel === this.samplesPerPixel
73 | ) {
74 | return;
75 | }
76 |
77 | const ctx = canvas.getContext("2d");
78 | const width = canvas.width;
79 | const height = canvas.height;
80 | const curve = FadeCanvasHook.createCurve(this.shape, this.type, width);
81 | const len = curve.length;
82 | let y = height - curve[0] * height;
83 |
84 | ctx.clearRect(0, 0, canvas.width, canvas.height);
85 | ctx.save();
86 |
87 | ctx.strokeStyle = "black";
88 | ctx.beginPath();
89 | ctx.moveTo(0, y);
90 |
91 | for (let i = 1; i < len; i += 1) {
92 | y = height - curve[i] * height;
93 | ctx.lineTo(i, y);
94 | }
95 | ctx.stroke();
96 | ctx.restore();
97 | }
98 | }
99 |
100 | export default FadeCanvasHook;
101 |
--------------------------------------------------------------------------------
/src/render/ScrollHook.js:
--------------------------------------------------------------------------------
1 | import { secondsToPixels, pixelsToSeconds } from "../utils/conversions";
2 |
3 | /*
4 | * virtual-dom hook for scrolling the track container.
5 | */
6 | export default class {
7 | constructor(playlist) {
8 | this.playlist = playlist;
9 | }
10 |
11 | hook(node) {
12 | const playlist = this.playlist;
13 | if (!playlist.isScrolling) {
14 | const el = node;
15 |
16 | if (playlist.isAutomaticScroll) {
17 | const rect = node.getBoundingClientRect();
18 | const controlWidth = playlist.controls.show
19 | ? playlist.controls.width
20 | : 0;
21 | const width = pixelsToSeconds(
22 | rect.width - controlWidth,
23 | playlist.samplesPerPixel,
24 | playlist.sampleRate
25 | );
26 |
27 | const timePoint = playlist.isPlaying()
28 | ? playlist.playbackSeconds
29 | : playlist.getTimeSelection().start;
30 |
31 | if (
32 | timePoint < playlist.scrollLeft ||
33 | timePoint >= playlist.scrollLeft + width
34 | ) {
35 | playlist.scrollLeft = Math.min(timePoint, playlist.duration - width);
36 | }
37 | }
38 |
39 | const left = secondsToPixels(
40 | playlist.scrollLeft,
41 | playlist.samplesPerPixel,
42 | playlist.sampleRate
43 | );
44 |
45 | el.scrollLeft = left;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/render/StereoPanSliderHook.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /*
3 | * virtual-dom hook for setting the stereoPan input programmatically.
4 | */
5 | export default class {
6 | constructor(stereoPan) {
7 | this.stereoPan = stereoPan;
8 | }
9 |
10 | hook(stereoPanInput) {
11 | stereoPanInput.value = this.stereoPan * 100;
12 |
13 | let panOrientation;
14 | if (this.stereoPan === 0) {
15 | panOrientation = "Center";
16 | } else if (this.stereoPan < 0) {
17 | panOrientation = "Left";
18 | } else {
19 | panOrientation = "Right";
20 | }
21 | const percentage = `${Math.abs(Math.round(this.stereoPan * 100))}% `;
22 | stereoPanInput.title = `Pan: ${
23 | this.stereoPan !== 0 ? percentage : ""
24 | }${panOrientation}`;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/render/TimeScaleHook.js:
--------------------------------------------------------------------------------
1 | /*
2 | * virtual-dom hook for rendering the time scale canvas.
3 | */
4 | export default class {
5 | constructor(tickInfo, offset, samplesPerPixel, duration, colors) {
6 | this.tickInfo = tickInfo;
7 | this.offset = offset;
8 | this.samplesPerPixel = samplesPerPixel;
9 | this.duration = duration;
10 | this.colors = colors;
11 | }
12 |
13 | hook(canvas, prop, prev) {
14 | // canvas is up to date
15 | if (
16 | prev !== undefined &&
17 | prev.offset === this.offset &&
18 | prev.duration === this.duration &&
19 | prev.samplesPerPixel === this.samplesPerPixel
20 | ) {
21 | return;
22 | }
23 |
24 | const width = canvas.width;
25 | const height = canvas.height;
26 | const ctx = canvas.getContext("2d");
27 |
28 | ctx.clearRect(0, 0, width, height);
29 | ctx.fillStyle = this.colors.timeColor;
30 |
31 | Object.keys(this.tickInfo).forEach((x) => {
32 | const scaleHeight = this.tickInfo[x];
33 | const scaleY = height - scaleHeight;
34 | ctx.fillRect(x, scaleY, 1, scaleHeight);
35 | });
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/render/VolumeSliderHook.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /*
3 | * virtual-dom hook for setting the volume input programmatically.
4 | */
5 | export default class {
6 | constructor(gain) {
7 | this.gain = gain;
8 | }
9 |
10 | hook(volumeInput) {
11 | volumeInput.value = this.gain * 100;
12 | volumeInput.title = `${Math.round(this.gain * 100)}% volume`;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/track/loader/BlobLoader.js:
--------------------------------------------------------------------------------
1 | import Loader from "./Loader";
2 |
3 | export default class extends Loader {
4 | /*
5 | * Loads an audio file via a FileReader
6 | */
7 | load() {
8 | return new Promise((resolve, reject) => {
9 | if (
10 | this.src.type.match(/audio.*/) ||
11 | // added for problems with Firefox mime types + ogg.
12 | this.src.type.match(/video\/ogg/)
13 | ) {
14 | const fr = new FileReader();
15 |
16 | fr.readAsArrayBuffer(this.src);
17 |
18 | fr.addEventListener("progress", (e) => {
19 | super.fileProgress(e);
20 | });
21 |
22 | fr.addEventListener("load", (e) => {
23 | const decoderPromise = super.fileLoad(e);
24 |
25 | decoderPromise
26 | .then((audioBuffer) => {
27 | resolve(audioBuffer);
28 | })
29 | .catch(reject);
30 | });
31 |
32 | fr.addEventListener("error", reject);
33 | } else {
34 | reject(Error(`Unsupported file type ${this.src.type}`));
35 | }
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/track/loader/IdentityLoader.js:
--------------------------------------------------------------------------------
1 | import Loader from "./Loader";
2 |
3 | export default class IdentityLoader extends Loader {
4 | load() {
5 | return Promise.resolve(this.src);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/track/loader/Loader.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from "event-emitter";
2 |
3 | export const STATE_UNINITIALIZED = 0;
4 | export const STATE_LOADING = 1;
5 | export const STATE_DECODING = 2;
6 | export const STATE_FINISHED = 3;
7 |
8 | export default class {
9 | constructor(src, audioContext, ee = EventEmitter()) {
10 | this.src = src;
11 | this.ac = audioContext;
12 | this.audioRequestState = STATE_UNINITIALIZED;
13 | this.ee = ee;
14 | }
15 |
16 | setStateChange(state) {
17 | this.audioRequestState = state;
18 | this.ee.emit("audiorequeststatechange", this.audioRequestState, this.src);
19 | }
20 |
21 | fileProgress(e) {
22 | let percentComplete = 0;
23 |
24 | if (this.audioRequestState === STATE_UNINITIALIZED) {
25 | this.setStateChange(STATE_LOADING);
26 | }
27 |
28 | if (e.lengthComputable) {
29 | percentComplete = (e.loaded / e.total) * 100;
30 | }
31 |
32 | this.ee.emit("loadprogress", percentComplete, this.src);
33 | }
34 |
35 | fileLoad(e) {
36 | const audioData = e.target.response || e.target.result;
37 |
38 | this.setStateChange(STATE_DECODING);
39 |
40 | return new Promise((resolve, reject) => {
41 | this.ac.decodeAudioData(
42 | audioData,
43 | (audioBuffer) => {
44 | this.audioBuffer = audioBuffer;
45 | this.setStateChange(STATE_FINISHED);
46 |
47 | resolve(audioBuffer);
48 | },
49 | (err) => {
50 | if (err === null) {
51 | // Safari issues with null error
52 | reject(Error("MediaDecodeAudioDataUnknownContentType"));
53 | } else {
54 | reject(err);
55 | }
56 | }
57 | );
58 | });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/track/loader/LoaderFactory.js:
--------------------------------------------------------------------------------
1 | import BlobLoader from "./BlobLoader";
2 | import IdentityLoader from "./IdentityLoader";
3 | import XHRLoader from "./XHRLoader";
4 |
5 | export default class {
6 | static createLoader(src, audioContext, ee) {
7 | if (src instanceof Blob) {
8 | return new BlobLoader(src, audioContext, ee);
9 | } else if (src instanceof AudioBuffer) {
10 | return new IdentityLoader(src, audioContext, ee);
11 | } else if (typeof src === "string") {
12 | return new XHRLoader(src, audioContext, ee);
13 | }
14 |
15 | throw new Error("Unsupported src type");
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/track/loader/XHRLoader.js:
--------------------------------------------------------------------------------
1 | import Loader from "./Loader";
2 |
3 | export default class extends Loader {
4 | /**
5 | * Loads an audio file via XHR.
6 | */
7 | load() {
8 | return new Promise((resolve, reject) => {
9 | const xhr = new XMLHttpRequest();
10 |
11 | xhr.open("GET", this.src, true);
12 | xhr.responseType = "arraybuffer";
13 | xhr.send();
14 |
15 | xhr.addEventListener("progress", (e) => {
16 | super.fileProgress(e);
17 | });
18 |
19 | xhr.addEventListener("load", (e) => {
20 | const decoderPromise = super.fileLoad(e);
21 |
22 | decoderPromise
23 | .then((audioBuffer) => {
24 | resolve(audioBuffer);
25 | })
26 | .catch(reject);
27 | });
28 |
29 | xhr.addEventListener("error", () => {
30 | reject(Error(`Track ${this.src} failed to load`));
31 | });
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/track/states.js:
--------------------------------------------------------------------------------
1 | import cursor from "./states/CursorState";
2 | import select from "./states/SelectState";
3 | import shift from "./states/ShiftState";
4 | import fadein from "./states/FadeInState";
5 | import fadeout from "./states/FadeOutState";
6 |
7 | export default {
8 | cursor,
9 | select,
10 | shift,
11 | fadein,
12 | fadeout,
13 | };
14 |
--------------------------------------------------------------------------------
/src/track/states/CursorState.js:
--------------------------------------------------------------------------------
1 | import { pixelsToSeconds } from "../../utils/conversions";
2 |
3 | export default class {
4 | constructor(track) {
5 | this.track = track;
6 | }
7 |
8 | setup(samplesPerPixel, sampleRate) {
9 | this.samplesPerPixel = samplesPerPixel;
10 | this.sampleRate = sampleRate;
11 | }
12 |
13 | click(e) {
14 | e.preventDefault();
15 |
16 | const startX = e.offsetX;
17 | const startTime = pixelsToSeconds(
18 | startX,
19 | this.samplesPerPixel,
20 | this.sampleRate
21 | );
22 |
23 | this.track.ee.emit("select", startTime, startTime, this.track);
24 | }
25 |
26 | static getClass() {
27 | return ".state-cursor";
28 | }
29 |
30 | static getEvents() {
31 | return ["click"];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/track/states/FadeInState.js:
--------------------------------------------------------------------------------
1 | import { pixelsToSeconds } from "../../utils/conversions";
2 |
3 | export default class {
4 | constructor(track) {
5 | this.track = track;
6 | }
7 |
8 | setup(samplesPerPixel, sampleRate) {
9 | this.samplesPerPixel = samplesPerPixel;
10 | this.sampleRate = sampleRate;
11 | }
12 |
13 | click(e) {
14 | const startX = e.offsetX;
15 | const time = pixelsToSeconds(startX, this.samplesPerPixel, this.sampleRate);
16 |
17 | if (time > this.track.getStartTime() && time < this.track.getEndTime()) {
18 | this.track.ee.emit(
19 | "fadein",
20 | time - this.track.getStartTime(),
21 | this.track
22 | );
23 | }
24 | }
25 |
26 | static getClass() {
27 | return ".state-fadein";
28 | }
29 |
30 | static getEvents() {
31 | return ["click"];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/track/states/FadeOutState.js:
--------------------------------------------------------------------------------
1 | import { pixelsToSeconds } from "../../utils/conversions";
2 |
3 | export default class {
4 | constructor(track) {
5 | this.track = track;
6 | }
7 |
8 | setup(samplesPerPixel, sampleRate) {
9 | this.samplesPerPixel = samplesPerPixel;
10 | this.sampleRate = sampleRate;
11 | }
12 |
13 | click(e) {
14 | const startX = e.offsetX;
15 | const time = pixelsToSeconds(startX, this.samplesPerPixel, this.sampleRate);
16 |
17 | if (time > this.track.getStartTime() && time < this.track.getEndTime()) {
18 | this.track.ee.emit("fadeout", this.track.getEndTime() - time, this.track);
19 | }
20 | }
21 |
22 | static getClass() {
23 | return ".state-fadeout";
24 | }
25 |
26 | static getEvents() {
27 | return ["click"];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/track/states/RecordState.js:
--------------------------------------------------------------------------------
1 | export default class {
2 | constructor(track) {
3 | this.track = track;
4 | }
5 |
6 | static getClass() {
7 | return ".state-record";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/track/states/SelectState.js:
--------------------------------------------------------------------------------
1 | import { pixelsToSeconds } from "../../utils/conversions";
2 |
3 | export default class {
4 | constructor(track) {
5 | this.track = track;
6 | this.active = false;
7 | }
8 |
9 | setup(samplesPerPixel, sampleRate) {
10 | this.samplesPerPixel = samplesPerPixel;
11 | this.sampleRate = sampleRate;
12 | }
13 |
14 | emitSelection(x) {
15 | const minX = Math.min(x, this.startX);
16 | const maxX = Math.max(x, this.startX);
17 | const startTime = pixelsToSeconds(
18 | minX,
19 | this.samplesPerPixel,
20 | this.sampleRate
21 | );
22 | const endTime = pixelsToSeconds(
23 | maxX,
24 | this.samplesPerPixel,
25 | this.sampleRate
26 | );
27 |
28 | this.track.ee.emit("select", startTime, endTime, this.track);
29 | }
30 |
31 | complete(x) {
32 | this.emitSelection(x);
33 | this.active = false;
34 | }
35 |
36 | mousedown(e) {
37 | e.preventDefault();
38 | this.active = true;
39 |
40 | this.startX = e.offsetX;
41 | const startTime = pixelsToSeconds(
42 | this.startX,
43 | this.samplesPerPixel,
44 | this.sampleRate
45 | );
46 |
47 | this.track.ee.emit("select", startTime, startTime, this.track);
48 | }
49 |
50 | mousemove(e) {
51 | if (this.active) {
52 | e.preventDefault();
53 | this.emitSelection(e.offsetX);
54 | }
55 | }
56 |
57 | mouseup(e) {
58 | if (this.active) {
59 | e.preventDefault();
60 | this.complete(e.offsetX);
61 | }
62 | }
63 |
64 | mouseleave(e) {
65 | if (this.active) {
66 | e.preventDefault();
67 | this.complete(e.offsetX);
68 | }
69 | }
70 |
71 | static getClass() {
72 | return ".state-select";
73 | }
74 |
75 | static getEvents() {
76 | return ["mousedown", "mousemove", "mouseup", "mouseleave"];
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/track/states/ShiftState.js:
--------------------------------------------------------------------------------
1 | import { pixelsToSeconds } from "../../utils/conversions";
2 |
3 | export default class {
4 | constructor(track) {
5 | this.track = track;
6 | this.active = false;
7 | }
8 |
9 | setup(samplesPerPixel, sampleRate) {
10 | this.samplesPerPixel = samplesPerPixel;
11 | this.sampleRate = sampleRate;
12 | }
13 |
14 | emitShift(x) {
15 | const deltaX = x - this.prevX;
16 | const deltaTime = pixelsToSeconds(
17 | deltaX,
18 | this.samplesPerPixel,
19 | this.sampleRate
20 | );
21 | this.prevX = x;
22 | this.track.ee.emit("shift", deltaTime, this.track);
23 | }
24 |
25 | complete(x) {
26 | this.emitShift(x);
27 | this.active = false;
28 | }
29 |
30 | mousedown(e) {
31 | e.preventDefault();
32 |
33 | this.active = true;
34 | this.el = e.target;
35 | this.prevX = e.offsetX;
36 | }
37 |
38 | mousemove(e) {
39 | if (this.active) {
40 | e.preventDefault();
41 | this.emitShift(e.offsetX);
42 | }
43 | }
44 |
45 | mouseup(e) {
46 | if (this.active) {
47 | e.preventDefault();
48 | this.complete(e.offsetX);
49 | }
50 | }
51 |
52 | mouseleave(e) {
53 | if (this.active) {
54 | e.preventDefault();
55 | this.complete(e.offsetX);
56 | }
57 | }
58 |
59 | static getClass() {
60 | return ".state-shift";
61 | }
62 |
63 | static getEvents() {
64 | return ["mousedown", "mousemove", "mouseup", "mouseleave"];
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils/audioData.js:
--------------------------------------------------------------------------------
1 | export function resampleAudioBuffer(audioBuffer, targetSampleRate) {
2 | // `ceil` is needed because `length` must be in integer greater than 0 and
3 | // resampling a single sample to a lower sample rate will yield a value value < 1.
4 | const length = Math.ceil(audioBuffer.duration * targetSampleRate);
5 | const ac = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(audioBuffer.numberOfChannels, length, targetSampleRate);
6 | const src = ac.createBufferSource();
7 | src.buffer = audioBuffer;
8 | src.connect(ac.destination);
9 | src.start();
10 | return ac.startRendering();
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/conversions.js:
--------------------------------------------------------------------------------
1 | export function samplesToSeconds(samples, sampleRate) {
2 | return samples / sampleRate;
3 | }
4 |
5 | export function secondsToSamples(seconds, sampleRate) {
6 | return Math.ceil(seconds * sampleRate);
7 | }
8 |
9 | export function samplesToPixels(samples, resolution) {
10 | return Math.floor(samples / resolution);
11 | }
12 |
13 | export function pixelsToSamples(pixels, resolution) {
14 | return Math.floor(pixels * resolution);
15 | }
16 |
17 | export function pixelsToSeconds(pixels, resolution, sampleRate) {
18 | return (pixels * resolution) / sampleRate;
19 | }
20 |
21 | export function secondsToPixels(seconds, resolution, sampleRate) {
22 | return Math.ceil((seconds * sampleRate) / resolution);
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/exportWavWorker.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | let recLength = 0;
3 | let recBuffersL = [];
4 | let recBuffersR = [];
5 | let sampleRate;
6 |
7 | function init(config) {
8 | sampleRate = config.sampleRate;
9 | }
10 |
11 | function record(inputBuffer) {
12 | recBuffersL.push(inputBuffer[0]);
13 | recBuffersR.push(inputBuffer[1]);
14 | recLength += inputBuffer[0].length;
15 | }
16 |
17 | function writeString(view, offset, string) {
18 | for (let i = 0; i < string.length; i += 1) {
19 | view.setUint8(offset + i, string.charCodeAt(i));
20 | }
21 | }
22 |
23 | function floatTo16BitPCM(output, offset, input) {
24 | let writeOffset = offset;
25 | for (let i = 0; i < input.length; i += 1, writeOffset += 2) {
26 | const s = Math.max(-1, Math.min(1, input[i]));
27 | output.setInt16(writeOffset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
28 | }
29 | }
30 |
31 | function encodeWAV(samples, mono = false) {
32 | const buffer = new ArrayBuffer(44 + samples.length * 2);
33 | const view = new DataView(buffer);
34 |
35 | /* RIFF identifier */
36 | writeString(view, 0, "RIFF");
37 | /* file length */
38 | view.setUint32(4, 32 + samples.length * 2, true);
39 | /* RIFF type */
40 | writeString(view, 8, "WAVE");
41 | /* format chunk identifier */
42 | writeString(view, 12, "fmt ");
43 | /* format chunk length */
44 | view.setUint32(16, 16, true);
45 | /* sample format (raw) */
46 | view.setUint16(20, 1, true);
47 | /* channel count */
48 | view.setUint16(22, mono ? 1 : 2, true);
49 | /* sample rate */
50 | view.setUint32(24, sampleRate, true);
51 | /* byte rate (sample rate * block align) */
52 | view.setUint32(28, sampleRate * 4, true);
53 | /* block align (channel count * bytes per sample) */
54 | view.setUint16(32, 4, true);
55 | /* bits per sample */
56 | view.setUint16(34, 16, true);
57 | /* data chunk identifier */
58 | writeString(view, 36, "data");
59 | /* data chunk length */
60 | view.setUint32(40, samples.length * 2, true);
61 |
62 | floatTo16BitPCM(view, 44, samples);
63 |
64 | return view;
65 | }
66 |
67 | function mergeBuffers(recBuffers, length) {
68 | const result = new Float32Array(length);
69 | let offset = 0;
70 |
71 | for (let i = 0; i < recBuffers.length; i += 1) {
72 | result.set(recBuffers[i], offset);
73 | offset += recBuffers[i].length;
74 | }
75 | return result;
76 | }
77 |
78 | function interleave(inputL, inputR) {
79 | const length = inputL.length + inputR.length;
80 | const result = new Float32Array(length);
81 |
82 | let index = 0;
83 | let inputIndex = 0;
84 |
85 | while (index < length) {
86 | result[(index += 1)] = inputL[inputIndex];
87 | result[(index += 1)] = inputR[inputIndex];
88 | inputIndex += 1;
89 | }
90 |
91 | return result;
92 | }
93 |
94 | function exportWAV(type) {
95 | const bufferL = mergeBuffers(recBuffersL, recLength);
96 | const bufferR = mergeBuffers(recBuffersR, recLength);
97 | const interleaved = interleave(bufferL, bufferR);
98 | const dataview = encodeWAV(interleaved);
99 | const audioBlob = new Blob([dataview], { type });
100 |
101 | postMessage(audioBlob);
102 | }
103 |
104 | function clear() {
105 | recLength = 0;
106 | recBuffersL = [];
107 | recBuffersR = [];
108 | }
109 |
110 | onmessage = function onmessage(e) {
111 | switch (e.data.command) {
112 | case "init": {
113 | init(e.data.config);
114 | break;
115 | }
116 | case "record": {
117 | record(e.data.buffer);
118 | break;
119 | }
120 | case "exportWAV": {
121 | exportWAV(e.data.type);
122 | break;
123 | }
124 | case "clear": {
125 | clear();
126 | break;
127 | }
128 | default: {
129 | throw new Error("Unknown export worker command");
130 | }
131 | }
132 | };
133 | }
134 |
--------------------------------------------------------------------------------
/src/utils/recorderWorker.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | // http://jsperf.com/typed-array-min-max/2
3 | // plain for loop for finding min/max is way faster than anything else.
4 | /**
5 | * @param {TypedArray} array - Subarray of audio to calculate peaks from.
6 | */
7 | function findMinMax(array) {
8 | let min = Infinity;
9 | let max = -Infinity;
10 | let curr;
11 |
12 | for (let i = 0; i < array.length; i += 1) {
13 | curr = array[i];
14 | if (min > curr) {
15 | min = curr;
16 | }
17 | if (max < curr) {
18 | max = curr;
19 | }
20 | }
21 |
22 | return {
23 | min,
24 | max,
25 | };
26 | }
27 |
28 | /**
29 | * @param {Number} n - peak to convert from float to Int8, Int16 etc.
30 | * @param {Number} bits - convert to #bits two's complement signed integer
31 | */
32 | function convert(n, bits) {
33 | const max = 2 ** (bits - 1);
34 | const v = n < 0 ? n * max : n * max - 1;
35 | return Math.max(-max, Math.min(max - 1, v));
36 | }
37 |
38 | /**
39 | * @param {TypedArray} channel - Audio track frames to calculate peaks from.
40 | * @param {Number} samplesPerPixel - Audio frames per peak
41 | */
42 | function extractPeaks(channel, samplesPerPixel, bits) {
43 | const chanLength = channel.length;
44 | const numPeaks = Math.ceil(chanLength / samplesPerPixel);
45 | let start;
46 | let end;
47 | let segment;
48 | let max;
49 | let min;
50 | let extrema;
51 |
52 | // create interleaved array of min,max
53 | const peaks = new self[`Int${bits}Array`](numPeaks * 2);
54 |
55 | for (let i = 0; i < numPeaks; i += 1) {
56 | start = i * samplesPerPixel;
57 | end =
58 | (i + 1) * samplesPerPixel > chanLength
59 | ? chanLength
60 | : (i + 1) * samplesPerPixel;
61 |
62 | segment = channel.subarray(start, end);
63 | extrema = findMinMax(segment);
64 | min = convert(extrema.min, bits);
65 | max = convert(extrema.max, bits);
66 |
67 | peaks[i * 2] = min;
68 | peaks[i * 2 + 1] = max;
69 | }
70 |
71 | return peaks;
72 | }
73 |
74 | /**
75 | * @param {TypedArray} source - Source of audio samples for peak calculations.
76 | * @param {Number} samplesPerPixel - Number of audio samples per peak.
77 | * @param {Number} cueIn - index in channel to start peak calculations from.
78 | * @param {Number} cueOut - index in channel to end peak calculations from (non-inclusive).
79 | */
80 | function audioPeaks(source, samplesPerPixel = 10000, bits = 8) {
81 | if ([8, 16, 32].indexOf(bits) < 0) {
82 | throw new Error("Invalid number of bits specified for peaks.");
83 | }
84 |
85 | const peaks = [];
86 | const start = 0;
87 | const end = source.length;
88 | peaks.push(
89 | extractPeaks(source.subarray(start, end), samplesPerPixel, bits)
90 | );
91 |
92 | const length = peaks[0].length / 2;
93 |
94 | return {
95 | bits,
96 | length,
97 | data: peaks,
98 | };
99 | }
100 |
101 | onmessage = function onmessage(e) {
102 | const peaks = audioPeaks(e.data.samples, e.data.samplesPerPixel);
103 |
104 | postMessage(peaks);
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/src/utils/timeformat.js:
--------------------------------------------------------------------------------
1 | export default function (format) {
2 | function clockFormat(seconds, decimals) {
3 | const hours = parseInt(seconds / 3600, 10) % 24;
4 | const minutes = parseInt(seconds / 60, 10) % 60;
5 | const secs = (seconds % 60).toFixed(decimals);
6 |
7 | const sHours = hours < 10 ? `0${hours}` : hours;
8 | const sMinutes = minutes < 10 ? `0${minutes}` : minutes;
9 | const sSeconds = secs < 10 ? `0${secs}` : secs;
10 |
11 | return `${sHours}:${sMinutes}:${sSeconds}`;
12 | }
13 |
14 | const formats = {
15 | seconds(seconds) {
16 | return seconds.toFixed(0);
17 | },
18 | thousandths(seconds) {
19 | return seconds.toFixed(3);
20 | },
21 | "hh:mm:ss": function hhmmss(seconds) {
22 | return clockFormat(seconds, 0);
23 | },
24 | "hh:mm:ss.u": function hhmmssu(seconds) {
25 | return clockFormat(seconds, 1);
26 | },
27 | "hh:mm:ss.uu": function hhmmssuu(seconds) {
28 | return clockFormat(seconds, 2);
29 | },
30 | "hh:mm:ss.uuu": function hhmmssuuu(seconds) {
31 | return clockFormat(seconds, 3);
32 | },
33 | };
34 |
35 | return formats[format];
36 | }
37 |
--------------------------------------------------------------------------------
/test/media/silence.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/naomiaro/waveform-playlist/5d912cf3e1b7ed8bcc190902e1c1bd329083e9f9/test/media/silence.ogg
--------------------------------------------------------------------------------
/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | context: path.resolve(__dirname, "src"),
5 | entry: {
6 | "waveform-playlist": "./app.js",
7 | },
8 | output: {
9 | path: __dirname + "/dist/waveform-playlist/js",
10 | publicPath: "/waveform-playlist/js/",
11 | filename: "[name].js",
12 | library: {
13 | name: "WaveformPlaylist",
14 | type: "var",
15 | },
16 | },
17 | mode: "development",
18 | devtool: "eval-source-map",
19 | module: {
20 | rules: [
21 | {
22 | test: /\.m?js$/,
23 | exclude: /(node_modules|bower_components)/,
24 | use: {
25 | loader: "babel-loader",
26 | options: {
27 | presets: ["@babel/preset-env"],
28 | plugins: [["@babel/plugin-transform-runtime"]],
29 | },
30 | },
31 | },
32 | ],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | context: path.resolve(__dirname, "src"),
5 | entry: {
6 | "waveform-playlist": "./app.js",
7 | },
8 | output: {
9 | path: __dirname + "/dist/waveform-playlist/js",
10 | publicPath: "/waveform-playlist/js/",
11 | filename: "[name].js",
12 | library: {
13 | name: "WaveformPlaylist",
14 | type: "var",
15 | },
16 | },
17 | mode: "production",
18 | module: {
19 | rules: [
20 | {
21 | test: /\.m?js$/,
22 | exclude: /(node_modules|bower_components)/,
23 | use: {
24 | loader: "babel-loader",
25 | options: {
26 | presets: ["@babel/preset-env"],
27 | plugins: [["@babel/plugin-transform-runtime"]],
28 | },
29 | },
30 | },
31 | ],
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/webpack.config.unpkg.js:
--------------------------------------------------------------------------------
1 | const createVariants = require("parallel-webpack").createVariants;
2 | const TerserPlugin = require("terser-webpack-plugin");
3 |
4 | function createConfig(options) {
5 | const config = {
6 | entry: __dirname + "/src/app.js",
7 | output: {
8 | path: __dirname + "/build",
9 | filename:
10 | "waveform-playlist." +
11 | options.target +
12 | (options.minified ? ".min" : "") +
13 | ".js",
14 | library: {
15 | name: "WaveformPlaylist",
16 | type: options.target,
17 | },
18 | },
19 | optimization: {
20 | minimize: options.minified,
21 | minimizer: [new TerserPlugin()],
22 | },
23 | mode: "production",
24 | };
25 |
26 | if (options.target === "umd") {
27 | config.output.umdNamedDefine = true;
28 | config.output.globalObject = "this";
29 | }
30 |
31 | return config;
32 | }
33 |
34 | module.exports = createVariants(
35 | {
36 | minified: [true, false],
37 | target: ["var", "commonjs2", "umd", "amd"],
38 | },
39 | createConfig
40 | );
41 |
--------------------------------------------------------------------------------