自己动手,打造一款适合 Python 程序员的 Html5 音乐播放器

近期断断续续读了一些 Web Audio API 和 WebRTC API 的文档,发现了很多好玩的东西,有一种“忽入桃花源”的感觉。这一款 Html5 音乐播放器,就是基于 Web Audio 技术实现的,原型来自于 Web Audio API 的例子。为什么说适合 Python 程序员呢?因为使用者需要理解下面这一行命令:

python -m http.server

这个命令,将(命令行窗口)当前路径作为 WebRoot,启动了一个 Web 服务器,我们可以从浏览器访问 WebRoot 之下的所有文件和文件夹。这意味着,放在 WebRoot 之下的 html 文件,将以远程文件而不是本地文件的形式被浏览器打开。我们知道,JavaScript 支持的很多高级功能,比如 Ajax、 Web Audio 等,都必须依赖于服务端才能运行,有了这个快速构建 Web 服务器的手段,将会给启蒙学习、验证测试带来极大的便利。

播放器外观模拟了一台录音机,带音量调节和双声道均衡,纯 JavaScript (不是jQuery)实现,完全兼容Firefox/Chrome/Edge 等浏览器。除了样式表文件,全部代码只有130行,我已上传至github,如果有兴趣,可自行下载。

使用方法
  1. 在项目所在路径下打开一个m命令行窗口,运行:python -m http.server
  2. 在浏览器地址栏输入:http://127.0.0.1:8000
  3. 仅能播放项目所在路径下music文件夹内的音乐文件

在这里插入图片描述

index.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>Html5音乐播放器</title>
  <meta name="description" content="Audio basics demo for Web Audio API">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
  <div id="boombox">
    <div class="boombox-handle"></div>
    <div class="boombox-body">
      <section class="master-controls">
        <input type="range" id="volume" class="control-volume" min="0" max="2" value="1" list="gain-vals" step="0.01" data-action="volume" />
        <datalist id="gain-vals">
          <option value="0" label="min">
          <option value="2" label="max">
        </datalist>
        <label for="volume">VOL</label>
        <input type="range" id="panner" class="control-panner" list="pan-vals" min="-1" max="1" value="0" step="0.01" data-action="panner" />
        <datalist id="pan-vals">
          <option value="-1" label="left">
          <option value="1" label="right">
        </datalist>
        <label for="panner">PAN</label>
        <button class="control-power" role="switch" aria-checked="false" data-power="on">
          <span>On/Off</span>
        </button>
      </section>

      <section class="tape">
        <audio src="" crossorigin="anonymous" ></audio>
        <button data-playing="false" class="tape-controls-play" role="switch" aria-checked="false">
          <span>Play/Pause</span>
        </button>
      </section>
    </div>
    <div class="boombox-file">
      <input type="file" name="mp3fn" id="mp3fn" accept="audio/mp3, audio/wav" />
    </div>
  </div>

  <script type="text/javascript">
    console.clear();
    
    var burl = document.URL.split("/");
    burl.pop();
    burl = burl.join("/");
      
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    let audioCtx;

    const audioElement = document.querySelector('audio');
    let track;

    const playButton = document.querySelector('.tape-controls-play');
    playButton.disabled = true;

    const fileButton = document.querySelector('#mp3fn');

    fileButton.addEventListener('change', function() {
      let fn = fileButton.value.split("\\");
      fn = fn.pop();
      
      if(playButton.dataset.playing == 'true'){
        playButton.click();
      }
      
      audioElement.src = burl + "/" + fn;
      playButton.disabled = false;
      
      playButton.click();
    });

    // play pause audio
    playButton.addEventListener('click', function() {
      let fn = fileButton.value.split("\\");
      fn = fn.pop();
      audioElement.src = burl + "/music/" + fn;
      
      if(!audioCtx) {
        init();
      }

      if (audioCtx.state === 'suspended') {
        audioCtx.resume();
      }

      if (this.dataset.playing === 'false') {
        audioElement.play();
        this.dataset.playing = 'true';
      } else if (this.dataset.playing === 'true') {
        audioElement.pause();
        this.dataset.playing = 'false';
      }

      let state = this.getAttribute('aria-checked') === "true" ? true : false;
      this.setAttribute( 'aria-checked', state ? "false" : "true" );
    }, false);

    audioElement.addEventListener('ended', () => {
      playButton.dataset.playing = 'false';
      playButton.setAttribute( "aria-checked", "false" );
    }, false);

    function init() {
      audioCtx = new AudioContext();
      track = audioCtx.createMediaElementSource(audioElement);

      const gainNode = audioCtx.createGain();
      const volumeControl = document.querySelector('[data-action="volume"]');
      volumeControl.addEventListener('input', function() {
        gainNode.gain.value = this.value;
      }, false);

      const pannerOptions = { pan: 0 };
      const panner = new StereoPannerNode(audioCtx, pannerOptions);

      const pannerControl = document.querySelector('[data-action="panner"]');
      pannerControl.addEventListener('input', function() {
        panner.pan.value = this.value;
      }, false);
      
      track.connect(gainNode).connect(panner).connect(audioCtx.destination);
    }
  </script>
</body>
</html>
style.css
:root {
	--orange: hsla(32, 100%, 50%, 1);
	--yellow: hsla(49, 99%, 50%, 1);
	--lime: hsla(82, 90%, 45%, 1);
	--green: hsla(127, 81%, 41%, 1);
	--red: hsla(342, 93%, 53%, 1);
	--pink: hsla(314, 85%, 45%, 1);
	--blue: hsla(211, 92%, 52%, 1);
	--purple: hsla(283, 92%, 44%, 1);
	--cyan: hsla(195, 98%, 55%, 1);
	--white: hsla(0, 0%, 95%, 1);
	--black: hsla(0, 0%, 10%, 1);

	/* abstract our colours */
	--boxMain: var(--blue);
	--boxSecond: var(--cyan);
	--boxHigh: var(--orange);

	--border: 1vmin solid var(--black);
	--borderRad: 2px;


}
* {box-sizing: border-box;}
body {
	background-color: var(--white);
	padding: 4vmax;
	font-family: system-ui; color: var(--black);
}

#boombox {
	width: 82vw; max-width: 800px; margin: 0px auto;
}
@media screen and (max-width: 570px) {
	#boombox {max-width: 600px;}
}
.boombox-handle {
	margin: 0px 5vw; height: 12vh;
	background:
		var(--white)
		linear-gradient(180deg,
			var(--boxHigh) 4vmin,
			var(--black) 4vmin,
			var(--black) 5vmin,
			var(--white) 5vmin)
		no-repeat;
	border: var(--border); border-bottom-width: 0px;
	border-radius: var(--borderRad);
}
.boombox-body {
	/*gradient with two circles for speakers*/
/* 	padding-top: 3vh; */
	background: var(--boxMain) repeat-x bottom left;
	background-image:
		radial-gradient(circle,
			var(--boxMain) 2vmin,
			var(--black) 2vmin,
			var(--black) 3vmin,
			var(--boxSecond) 3vmin,
			var(--boxSecond) 9vmin,
			var(--black) 9vmin,
			var(--black) 9.5vmin,
			var(--boxSecond) 9.5vmin,
			var(--boxSecond) 12vmin,
			var(--black) 12vmin,
			var(--black) 13vmin,
			var(--boxMain) 13vmin);
	background-size: 33.3% 70%;
	border: 6px solid var(--black);
	border-radius: var(--borderRad);
}
/*small screen with one circle*/
@media screen and (max-width: 570px) {
	.boombox-body {
		background-repeat: no-repeat;
		background-position: center top;
		background-size: 70% 70%;
		background-image:
		radial-gradient(circle,
			var(--boxMain) 4vmin,
			var(--black) 4vmin,
			var(--black) 5vmin,
			var(--boxSecond) 5vmin,
			var(--boxSecond) 21vmin,
			var(--black) 21vmin,
			var(--black) 21.5vmin,
			var(--boxSecond) 21.5vmin,
			var(--boxSecond) 24vmin,
			var(--black) 24vmin,
			var(--black) 25vmin,
			var(--boxMain) 25vmin);
	}
}

.boombox-file {
	padding: 10px;
}

.master-controls {
	display: grid;
	grid-template-rows: repeat(2, auto);
	grid-template-columns: repeat(2, 1fr) 12%;
	/*name our grid areas so they are more manipulative later*/
	grid-template-areas:
		"volin panin power"
		"vollab panlab power";
	justify-items: center; align-items: center;
	padding: 2vmax;
	background-color: var(--boxSecond);
	border-bottom: var(--border);
}
/* change control grid areas for smaller boom box */
@media screen and (max-width: 570px) {
	.master-controls {
		grid-gap: 10px;
		grid-template-columns: 16% 1fr 12%;
		grid-template-areas:
			"vollab volin power"
			"panlab panin power";
	}
}

.master-controls input, .master-controls label {display: block;}
.master-controls input::before, .master-controls input::after {
	line-height: 5vh; color: var(--black);
}
.master-controls input::before {padding-right: 1vw;}
.master-controls input::after {padding-left: 1vw;}

.control-volume {grid-area: volin;}
.control-volume::before {content: 'min';}
.control-volume::after {content: 'max';}
label[for="volume"] {grid-area: vollab; margin-top: 15px;}
.control-panner {grid-area: panin;}
.control-panner::before {content: 'left';}
.control-panner::after {content: 'right';}
label[for="panner"] {grid-area: panlab; margin-top: 15px;}
@media screen and (max-width: 570px) {
	label[for="volume"], label[for="panner"] {margin-top: 0px;}
	.control-volume {margin-bottom: 20px;}
}

.control-power {
	grid-area: power; align-self: center;
	width: 40px; height: 40px;
	border: 3px solid var(--black); border-radius: 20px;
	cursor: pointer;
}
.control-power span {display: none;}


/* tape area ~~~~~~~~~~~~~~~~~~~~~~ */
.tape {
	display: grid;
	grid-template-rows: 24vh 6vh;
	grid-template-columns: repeat(5, 1fr);
	grid-template-areas: "tape tape tape tape tape"
		"back rewind play ff next";

	width: 26vw; margin: 0px auto;
	background: var(--boxMain) no-repeat center center;
	background-image:
		radial-gradient(circle at 30%,
			var(--boxSecond) 2vh,
			var(--black) 2vh,
			var(--black) 2.5vh,
			transparent 2.5vh),
		radial-gradient(circle at 70%,
			var(--boxSecond) 2vh,
			var(--black) 2vh,
			var(--black) 2.5vh,
			transparent 2.5vh),
		linear-gradient(180deg,
			transparent 9.1vh,
			var(--black) 9.1vh,
			var(--black) 10.1vh,
			var(--boxHigh) 10.1vh,
			var(--boxHigh) 20vh,
			var(--black) 20vh,
			var(--black) 21vh,
			transparent 21vh),
		radial-gradient(circle at 30%,
			var(--boxHigh) 5vh,
			var(--black) 5vh,
			var(--black) 6vh,
			transparent 6vh),
		radial-gradient(circle at 70%,
			var(--boxHigh) 5vh,
			var(--black) 5vh,
			var(--black) 6vh,
			transparent 6vh);
	background-size: 100% 100%, 100% 100%, 40% 100%, 100% 100%, 100% 100%;
	border: var(--border); border-bottom-width: 0px;
	border-radius: var(--borderRad);
	max-width: 300px;
}
@media screen and (max-width: 570px) {
	.tape {width: 80%; margin-top: 30vh;}
}
audio {
	grid-area: tape;
}
/*TODO GIVE BUTTONS CLASSES*/
[class^="tape-controls"] {
	border: none;
}
[class^="tape-controls"] span {display: none;}
.tape-controls-play {grid-area: play;}

/* ~~~~~~~~~~~~~~~~ All the crazy range styling */
input[type=range] {
  -webkit-appearance: none;
	width: 30vw;
  background: transparent;
}
/* set min & max for different screen sizes */
@media screen and (min-width: 1100px) {
	input[type=range] {width: 270px;}
}
@media(max-width: 680px) {
	input[type=range] {
		width: 180px;
	}
}
@media(max-width: 380px) {
	input[type=range] {
		width: 130px;
	}
}

input[type=range]::-ms-track {
  width: 100%;
  cursor: pointer;
  background: transparent;
  border-color: transparent;
  color: transparent;
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  margin-top: -1vh;
	height: 4vh; width: 2vw;
	border: 0.5vmin solid var(--black);
  border-radius: var(--borderRad);
  background-color: var(--boxMain);
  cursor: pointer;
}
input[type=range]::-moz-range-thumb {
  height: 4vh; width: 2vw;
	border: 0.5vmin solid var(--black);
  border-radius: var(--borderRad);
  background-color: var(--boxMain);
  cursor: pointer;
}
input[type=range]::-ms-thumb {
  height: 4vh; width: 2vw;
	border: 0.5vmin solid var(--black);
  border-radius: var(--borderRad);
  background-color: var(--boxMain);
  cursor: pointer;
}

input[type=range]::-webkit-slider-runnable-track {
  height: 2vh;
  cursor: pointer;
  background-color: var(--black);
  border-radius: var(--borderRad);
}
input[type=range]::-moz-range-track {
  height: 2vh;
  cursor: pointer;
  background-color: var(--black);
  border-radius: var(--borderRad);
}
input[type=range]::-ms-track {
  height: 2vh;
  cursor: pointer;
  background-color: var(--black);
  border-radius: var(--borderRad);
}

input[type=range]:focus {
  outline: none;
}
input[type=range]:focus::-webkit-slider-runnable-track {
  background: var(--boxMain);
}

/*~~~~~~~~~~~~~~~~ the play pause & power icons*/
[data-playing] {
		background: transparent url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z" fill="black" /></svg>') no-repeat center center;
		background-size: 60% 60%;
		cursor: pointer;
}
[data-playing]:hover {
	background: transparent url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z" fill="hsla(32, 100%, 50%, 1)" /></svg>') no-repeat center center;
	background-size: 60% 60%;
}
[data-playing="true"] {
	background: transparent url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z" fill="black" /></svg>') no-repeat center center;
	background-size: 60% 60%;
}
[data-playing="true"]:hover {
				background: transparent url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z" fill="hsla(32, 100%, 50%, 1)" /></svg>') no-repeat center center;
			background-size: 60% 60%;
}
[data-power] {
	background: var(--boxSecond) url('data:image/svg+xml; charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M400 54.1c63 45 104 118.6 104 201.9 0 136.8-110.8 247.7-247.5 248C120 504.3 8.2 393 8 256.4 7.9 173.1 48.9 99.3 111.8 54.2c11.7-8.3 28-4.8 35 7.7L162.6 90c5.9 10.5 3.1 23.8-6.6 31-41.5 30.8-68 79.6-68 134.9-.1 92.3 74.5 168.1 168 168.1 91.6 0 168.6-74.2 168-169.1-.3-51.8-24.7-101.8-68.1-134-9.7-7.2-12.4-20.5-6.5-30.9l15.8-28.1c7-12.4 23.2-16.1 34.8-7.8zM296 264V24c0-13.3-10.7-24-24-24h-32c-13.3 0-24 10.7-24 24v240c0 13.3 10.7 24 24 24h32c13.3 0 24-10.7 24-24z" fill="black" /></svg>') no-repeat center center;
	background-size: 60% 60%;
}
[data-power]:hover, [data-power="on"] {
	background: var(--boxHigh) url('data:image/svg+xml; charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M400 54.1c63 45 104 118.6 104 201.9 0 136.8-110.8 247.7-247.5 248C120 504.3 8.2 393 8 256.4 7.9 173.1 48.9 99.3 111.8 54.2c11.7-8.3 28-4.8 35 7.7L162.6 90c5.9 10.5 3.1 23.8-6.6 31-41.5 30.8-68 79.6-68 134.9-.1 92.3 74.5 168.1 168 168.1 91.6 0 168.6-74.2 168-169.1-.3-51.8-24.7-101.8-68.1-134-9.7-7.2-12.4-20.5-6.5-30.9l15.8-28.1c7-12.4 23.2-16.1 34.8-7.8zM296 264V24c0-13.3-10.7-24-24-24h-32c-13.3 0-24 10.7-24 24v240c0 13.3 10.7 24 24 24h32c13.3 0 24-10.7 24-24z" fill="black" /></svg>') no-repeat center center;
	background-size: 60% 60%;
}
发布了94 篇原创文章 · 获赞 1万+ · 访问量 141万+

猜你喜欢

转载自blog.csdn.net/xufive/article/details/104612617