- スクリーンショット
- 背景
- 実装戦略・参考ページ
- サーバへの複数オーディオファイルのアップロード(ドラッグアンドドロップ)
- サーバ側でのファイルアップロード処理・ファイル保存
- クライアント側で、ローカルにあるオーディオファイルを再生
- 実装
- デモ
- 最後に
スクリーンショット
背景
ユーザがアップロードするオーディオファイルを使ってWebアプリを作成する時に、ファイルサイズによる通信帯域の圧迫、アップロードの処理待ちが問題になります。
具体的には、1つの楽曲のオーディオファイルは非可逆圧縮フォーマット(e.g. mp3, aac, etc.)で数MB、可逆圧縮フォーマット(e.g. wav, flac, etc.)で数十MB程度のファイルサイズです。 従って、1つの楽曲ファイルならあまり気になりませんが、可逆圧縮の楽曲ファイルや複数楽曲をサーバへアップロードし処理しようとすると、ユーザを長い間待たせてしまいます。そうなるとアプリケーション品質に致命的な影響を与えるため、なるべくアップロード高速化したり、ユーザの体感的待ち時間を短くしたりすることが重要となります。
以上より、今回は、サーバへアップロード中に、アップロードしているオーディオをブラウザ上で確認できる簡単なWebアプリを実装してみました。
なお、応用としては、WebAudioAPI
やJavaScriptの信号処理ライブラリを用いることで、処理負荷が低く単純な信号処理をクライアント側で、複雑で独自性のあるリッチな信号処理(e.g. 深層学習)をサーバで行うようなWebアプリが考えられます。
実装戦略・参考ページ
サーバへの複数オーディオファイルのアップロード(ドラッグアンドドロップ)
Flask-jquery.ajax
間の非同期通信を使用しています。
また、ドラッグアンドドロップによる複数ファイルのアップロードもjQueryで記述できるようです。
サーバ側でのファイルアップロード処理・ファイル保存
クライアント側で、ローカルにあるオーディオファイルを再生
HTML5の<audio>
を使っています。
stackoverflow.com
なお、Web音楽アプリのサーバ開発に‘‘‘Python‘‘‘を利用している理由としては、Flaskによる開発が楽なことと、PythonのNumpy, Scikit-learn, LibROSA, TensorFlowといった優れた信号処理向けライブラリを簡単に使いたいということが挙げられます。
実装
ディレクトリ構成
※開発環境の都合で、js, cssをresourcesに置いていますが、Flaskの定石通り、staticにおいても大丈夫です。
project_root ├ resources/ │ ├ dropzone_handler.js │ ├ dropzone_uploader.js │ └ style_dropzone.css ├ templates/ │ └ index.html └ run.py
dropzone_handler.js
参考ページに書いてあるように、破線内の領域にファイルがドラッグアンドドロップした時の挙動を記述しています。ファイルが領域内にドロップされると、dropzone_uploader.js
のhandleFileUpload()
でファイルのアップロードおよびオーディオファイル操作用の要素の生成が行われます。
クリックするとソースコードを展開
/* Detect file dropping inside the dropzone */ $(document).ready(function() { var filetype = 'audio.*' var obj = $("#dropzone"); obj.on('dragenter', function (e) { e.stopPropagation(); e.preventDefault(); $(this).css('border', '2px solid #0B85A1'); }); obj.on('dragover', function (e) { e.stopPropagation(); e.preventDefault(); }); obj.on('drop', function (e) { $(this).css('border', '2px dotted #0B85A1'); e.preventDefault(); var files = e.originalEvent.dataTransfer.files; //We need to send dropped files to Server handleFileUpload(files, obj, filetype); }); $(document).on('dragenter', function (e) { e.stopPropagation(); e.preventDefault(); }); $(document).on('dragover', function (e) { e.stopPropagation(); e.preventDefault(); obj.css('border', '2px dotted #0B85A1'); }); $(document).on('drop', function (e) { e.stopPropagation(); e.preventDefault(); }); });
dropzone_uploader.js
createAudioTrack
:各オーディオファイルの操作および情報提示用のオブジェクトを生成します。handleFileUpload
:各ファイルごとにfiletypeを確認してcreateAudioTrack()
でオーディオ操作用のオブジェクトを生成します。sendFileToServer
:jQuery.ajax()でファイルをアップロードしています。
クリックするとソースコードを展開
var n_tracks=0; function createAudioTrack(obj) { n_tracks++; this.track_id = "track"+('000'+n_tracks).slice( -3 ) this.track_src = this.track_id+"_src" this.audiotrack = $("<div class='audiotrack'></div>"); this.audio = $("<audio id='"+this.track_id+"' controls><source id='"+this.track_src+"' src=''></source></audio>").appendTo(this.audiotrack); this.filename = $("<div class='filename'></div>").appendTo(this.audiotrack); this.size = $("<div class='filesize'></div>").appendTo(this.audiotrack); this.state = $("<div class='state'><div>").appendTo(this.audiotrack); this.abort = $("<div class='abort'>Abort</div>").appendTo(this.state); this.completed = $("<div class='completed'>Completed</div>").appendTo(this.state); this.remove = $("<div class='remove'>Remove</div>").appendTo(this.state); this.completed.hide() this.progressBar = $("<div class='progressBar'><div></div></div>").appendTo(this.audiotrack); obj.after(this.audiotrack); this.setFileNameSize = function(name, size) { var sizeStr = ""; var sizeKB = size/1024; if(parseInt(sizeKB) > 1024) { var sizeMB = sizeKB/1024; sizeStr = sizeMB.toFixed(2)+" MB"; } else { sizeStr = sizeKB.toFixed(2)+" KB"; } this.filename.html(name); this.size.html(sizeStr); } this.setAudioPlayer = function(file) { var source = document.getElementById(this.track_src); source.src = URL.createObjectURL(file); } this.setProgress = function(progress) { var progressBarWidth =progress*this.progressBar.width()/ 100; this.progressBar.find('div').animate({ width: progressBarWidth }, 10).html(progress + "% "); if(parseInt(progress) >= 100) { this.abort.hide(); this.completed.show(); this.remove.show(); } } this.setAbort = function(jqxhr) { var at = this.audiotrack; this.abort.click(function() { jqxhr.abort(); sb.hide(); }); } } function handleFileUpload(files, obj, filetype) { for (var i = 0; i < files.length; i++) { if (!files[i].type.match(filetype)) { continue; } var fd = new FormData(); fd.append('file', files[i]); // Set audiotrack var audiotrack = new createAudioTrack(obj); audiotrack.setFileNameSize(files[i].name, files[i].size); audiotrack.setAudioPlayer(files[i]); // Upload the file sendFileToServer(fd, audiotrack); } } function sendFileToServer(formData, audio_track) { var jqXHR=$.ajax({ xhr: function() { var xhrobj = $.ajaxSettings.xhr(); if (xhrobj.upload) { xhrobj.upload.addEventListener('progress', function(event) { var percent = 0; var position = event.loaded || event.position; var total = event.total; if (event.lengthComputable) { percent = Math.ceil(position / total * 100); } audio_track.setProgress(percent); }, false); } return xhrobj; }, url: "./upload", type: "POST", contentType:false, processData: false, cache: false, data: formData, success: function(data){ audio_track.setProgress(100); } }); audio_track.setAbort(jqXHR); }
style_dropzone.css
クリックするとソースコードを展開
#dropzone { border: 2px dotted #0B85A1; width: 600px; height: 300px; color: #92AAB0; text-align: center; vertical-align: middle; padding: 10px 10px 10 10px; margin-bottom: 10px; font-size: 2.0rem; background-color: rgba(255, 255, 255, 0.8); } .progressBar { width: 200px; height: 22px; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; display:inline-block; margin:0px 10px 5px 5px; vertical-align:top; } .progressBar div { height: 100%; color: #fff; text-align: right; line-height: 22px; /* same as #progressBar height if we want text middle aligned */ width: 0; background-color: #0ba1b5; border-radius: 3px; } .audiotrack { border-top: 1px solid #A9CCD1; min-height: 25px; width: 700px; padding: 10px 10px 0px 10px; vertical-align: top; } .filename { display: inline-block; vertical-align: top; width: 250px; } .filesize { display: inline-block; vertical-align: top; color: #30693D; width: 100px; margin-left: 10px; margin-right: 5px; } .abort, .completed, .remove{ -moz-border-radius: 0.1rem; -webkit-border-radius: 0.1rem; border-radius: 0.1rem; display: inline-block; color: #fff; font-family: arial; font-size: 1.0rem; font-weight: normal; margin-right: 0.2rem; padding: 0.1rem 1rem; cursor: pointer; vertical-align: top; } .abort{ background-color: #A8352F; } .completed{ background-color: #2fa863; } .remove{ background-color: #444444; }
run.py (Flask)
クリックするとソースコードを展開
import os from flask import Flask from flask import render_template, request, send_from_directory from pprint import pprint app = Flask(__name__) basedir = os.path.abspath(os.path.dirname(__file__)) app.config.update( UPLOADED_PATH=os.path.join(basedir, 'uploads') ) @app.route('/') def index(): return render_template('index.html') @app.route("/resources/<path:filepath>") def send_resource(filepath): return send_from_directory("resources", filepath) @app.route('/upload', methods=['POST']) def upload_audio(): pprint(request.files) f = request.files['file'] filepath = os.path.join(app.config["UPLOADED_PATH"], f.filename) f.save(filepath) os.remove(filepath) return "", 204 if __name__ == '__main__': app.run("0.0.0.0", 80, debug=True)
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script type="text/javascript" src="resources/dropzone_upload.js"></script> <script type="text/javascript" src="resources/dropzone_handler.js"></script> <link type="text/css" href="resources/style_dropzone.css" rel="stylesheet"> </head> <body> <div id="dropzone">Drag and Drop Your Audio-files</div> </body> </html>
デモ
複数の楽曲ファイルをアップロードしてみてください。 ※以下のデモでは、アップロードしたファイルはアップロード完了後にサーバ側で即削除しております。 http://www.wizardcraft.works/dev/flask_dropzone_local_playback_and_upload/www.wizardcraft.works
最後に
ファイルのアップロードについては、サーバへの攻撃を防ぐため、ファイル名、フォーマット、サイズなどに注意する必要があります。 デモの方では、アップロードできるファイル数やサイズを制限しています。