ffmpeg+wavesurfer.js develops an audio cutting web tool

Scene

Sometimes I want to process the cutting of some audio files, but I don’t want to download professional software such as Audition. So I opened Baidu to look for such a web tool, and found that most of them only display the time of the audio, without the waveform diagram, which brings inconvenience to the cutting. In order to solve this problem, implement a tool like this yourself

Technology stack

Frontend: ElementUI, wavesurfer.js
Backend: ffmpeg, php

Thoughts

Install ffmpeg on the server, after the front-end uploads the file to the server, the server runs the cutting command and returns the cut file to the front-end for download

The interface is as follows

Front-end code

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Audio cutting</title>
    <script src="//i2.wp.com/unpkg.com/wavesurfer.js"></script>

    <script src="//i2.wp.com/code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- import style -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <!-- First introduce Vue -->
    <script src="//i2.wp.com/cdn.staticfile.org/vue/2.4.2/vue.min.js"></script>
    <script src="//i2.wp.com/unpkg.com/element-ui/lib/index.js"></script>

    <!-- Import component library -->
    <script src="//i2.wp.com/unpkg.com/element-ui/lib/index.js"></script>
    <style>
        #waveform {<!-- -->
            width: 600px;
            height: 200px;
            position: relative;
        }

        .block {<!-- -->
            margin-bottom: 20px;
        }
        .overlay {<!-- -->
            position: absolute;
            width: 600px;
            height: 200px;
            z-index: 100;
            border: 1px solid #0B102C;
            opacity: 0.3;
        }

    </style>
</head>
<body>


<div id="app">
    <!-- audio waveform display container -->
    <div class="block">
        <div id="waveform">
            <canvas id="myCanvas" width="600" height="100" class="overlay"
                    style="border:1px solid #000000;">
            </canvas>

        </div>
    </div>

    <div class="block">
        <el-slider
                style="width: 600px"
                v-model="rangeValue"
                range
                show-stops
                @change="changeSlider"
                :max="1000">
        </el-slider>
    </div>

    <div class="block">
        <el-upload
                class="upload-demo"
                action="upload.php"
                :on-preview="handlePreview"
                :on-remove="handleRemove"
                :before-remove="beforeRemove"
                :on-success="uploadSuccess"
                :file-list="fileList">
            <el-button size="small" type="primary">Click to upload audio file</el-button>
            <div slot="tip" class="el-upload__tip">Only upload wav/mp3/ogg files, and no more than 100mb</div>
        </el-upload>
    </div>

    <div class="block">
        <el-button size="small" type="primary" @click="cutAudio">Cut and export</el-button>
    </div>

</div>

</body>
</html>
<script>
    new Vue({<!-- -->
        el: '#app',
        data() {<!-- -->
            return {<!-- -->
                divWidth: 600,
                overlayLeft: '50px',
                filePath: '',
                maxValue: 1000,
                audioDuration: 0,
                rangeValue: [1, 1000],
                wavesurfer: null,
                fileList: []
            };
        },
        mounted() {<!-- -->
            this.wavesurfer = WaveSurfer.create({<!-- -->
                container: '#waveform',
                waveColor: 'violet',
                progressColor: 'purple'
            });
            this. drawRect(0, 0, this. divWidth, 100)
        },
        methods: {<!-- -->
            drawRect(x, y, w, h) {<!-- -->
                var c = document. getElementById("myCanvas");
                var ctx = c. getContext("2d");
                ctx. clearRect(0, 0, c. width, c. height)
                ctx.fillStyle = "#FF0000";
                ctx. fillRect(x, y, w, 100);
            },
            cutAudio() {<!-- -->
                // Calculate the cutting time point
                this.$message.error('There is no file that can be cut');
                let duration = this.wavesurfer.getDuration();
                if(!duration){<!-- -->
                    return;
                }
                this.$message('cutting, export later...');
                let startTime = duration * parseFloat(this. rangeValue[0]) / this. maxValue;
                let endTime = duration * parseFloat(this. rangeValue[1]) / this. maxValue;
                $.ajax({<!-- -->
                    url: 'cut.php',
                    method: 'POST',
                    data: {<!-- -->start_time: startTime, end_time: endTime, filePath: this.filePath},
                    success: (res) => {<!-- -->
                        // process the response result
                        let res1 = JSON. parse(res)
                        let a = document. createElement('a')
                        var filePath = res1. file_path
                        let path = filePath. lastIndexOf('/')
                        a.download = filePath.substr(path + 1)//Set the downloaded file name
                        a.href = res1.file_path; // Set the download address of the picture
                        a.click();//Trigger download event
                    },
                    error: (xhr, status, error) => {<!-- -->
                        // handle errors
                        console.log('cutting failed');
                        console. log(error);
                    }
                });
            },
            changeSlider(e) {<!-- -->

                let x = Math.ceil(this.rangeValue[0] / this.maxValue * this.divWidth)
                let tmpWidth = this.rangeValue[1] - this.rangeValue[0];
                let width = Math.ceil(tmpWidth / this.maxValue * this.divWidth)
                this. drawRect(x, 0, width, 100)

            },
            uploadSuccess(res, file, fileLis) {<!-- -->
                if(res.code ==500){<!-- -->
                    this.$message.error(res.msg);
                    this.fileList = [];
                    return;
                }
                this.wavesurfer.load(res.path);
                this.filePath = res.path
                this.audioDuration = this.wavesurfer.getDuration();
            },
            handleRemove(file, fileList) {<!-- -->
                console. log(file, fileList);
            },
            handlePreview(file) {<!-- -->
                console. log(file);
            },
            beforeRemove(file, fileList) {<!-- -->
                return this.$confirm(`Are you sure to remove ${<!-- -->file.name}?`);
            }
        }
    })


</script>


Code Analysis

1. To display the audio file, I used the library wavesurfer.js, as long as the file path is passed in, the waveform can be displayed
2. The starting and ending points of the interception use the slider component of elementUI,
Drag to select start point and end point
3. I covered a layer of canvas on the waveform graph for drawing the mask, and used the onchange event of the slider to trigger the drawing

Server

Environmental installation

install ffmpeg
Local development, install the windows version, Baidu is enough, this is required
add link description

When putting it on the linux server, Baidu linux ffmpeg method by yourself

php code

upload.php

<?php

class UploadFile
{
    /**
     * upload path
     * @var string
     */
    public $upload_path = 'uploads';

    /**
     * Obtain a document opening certificate
     * @param $file
     * @return mixed|string
     */
    function get_extension($file)
    {
        $info = pathinfo($file);
        return $info['extension'];
    }

    /**
     * Execute the upload
     * @return false|string
     */
    function doUpload()
    {
        $file = $_FILES['file'];

        if ($_FILES["file"]["error"] > 0) {
            exit( json_encode(['code' => 500, 'msg' => $_FILES["file"]["error"]]));
        }
        $allowedMimeTypes = ['audio/mpeg', 'audio/ogg', 'audio/wav'];

        // Check the MIME type of the uploaded file
        if (!in_array($file['type'], $allowedMimeTypes)) {
            exit( json_encode(['code' => 500, 'msg' => 'file type error']));
        }
        $ext = $this->get_extension($_FILES["file"]["name"]);

        if (!is_dir($this->upload_path)) {
            mkdir($this->upload_path, 777);
        }

        $path = "uploads/" . uniqid() . '.' . $ext;
        // If the file does not exist in the upload directory, upload the file to the upload directory
        move_uploaded_file($_FILES["file"]["tmp_name"], $path);
        exit( json_encode(['code' => 200, 'path' => $path]));

    }
}


$upload = new UploadFile();
return $upload->doUpload();


A very common upload class, used to receive files from the front end, and return the server file path for the front end to display waveforms

cut.php

<?php


$start_time = isset($_POST['start_time']) ? $_POST['start_time'] : 0; // default is 0
$end_time = isset($_POST['end_time']) ? $_POST['end_time'] : 0; // default is 0
$input_file = isset($_POST['filePath']) ? $_POST['filePath'] : 0; // default is 0
// Define input and output file paths
$output_file = 'output/'.date('Y-m-d-H-i-s').'.wav';

// Call FFmpeg for audio cutting
$ffmpeg_command = "ffmpeg -i {$input_file} -ss $start_time -to $end_time -c copy {$output_file}";
exec($ffmpeg_command);

// Return the cut file path to the front end
$response = array('file_path' => $output_file);
exit(json_encode($response));

When the export button is clicked, the cutting command of ffmpeg is executed, and the cut file is returned to the front end

Summary

So far, a simple web version of ffmpeg audio cutting has been implemented. ffmpeg has many other functions, such as converting audio and video formats, adding watermarks to videos, etc., you can use this demo to transform

Example project address

https://gitee.com/ghjkg546/ffmpeg-audio-cut