You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenProcessing上p5.js音频Sketch桌面正常移动端报错求助

移动端p5.js音频播放器报错问题及修复方案

问题背景

我在OpenProcessing上开发了一款用于舞蹈课程的音频播放器Sketch,桌面端运行正常,但存在以下问题:

  • 核心问题:实际手机/平板设备运行时控制台提示Media play method encountered an unexpected error,开发者工具模拟移动端时正常;
  • 移动端按钮需长按至少1秒才生效(疑似300ms延迟);
  • 桌面端需先点击画布才能触发"Upload File"按钮。

核心问题修复:移动端音频播放报错

原因分析

移动端浏览器有严格的自动播放安全策略:音频播放必须由用户的直接交互(点击、触摸)触发,且音频上下文默认处于挂起状态,需要手动唤醒才能正常播放。

修复步骤

  1. 在用户交互上下文内唤醒音频上下文
    修改buttonPressed函数中的播放逻辑,添加音频上下文唤醒处理,确保播放操作符合移动端浏览器要求:

    if (trackControls === 'playTrack') {
        if (!playStatus) {
            // 唤醒挂起的音频上下文
            if (typeof audioPlayer.elt.context !== 'undefined') {
                audioPlayer.elt.context.resume().then(() => {
                    audioPlayer.loop();
                }).catch(err => {
                    console.error('音频上下文唤醒失败:', err);
                    alert('无法播放音频,请检查浏览器权限或重新上传文件');
                });
            } else {
                // 兼容不支持audioContext的场景
                audioPlayer.loop().catch(err => {
                    console.error('播放失败:', err);
                    alert('无法播放音频,请检查浏览器权限或重新上传文件');
                });
            }
            OPC.button('playTrack', 'Pause');
            playStatus = true;
        } else {
            audioPlayer.pause();
            OPC.button('playTrack', 'Play');
            playStatus = false;
        }
    }
    
  2. 优化音频加载逻辑
    在handleFile函数中添加音频预加载,避免播放时因未加载完成导致报错:

    function handleFile(file) {
        if (file.type === 'audio') {
            audioPlayer.src = file.data;
            audioPlayer.load(); // 提前加载音频资源
            fileName = file.name;
        } else {
            alert('Not a valid audio file!');
            fileName = '';
        }
    }
    

次要问题修复

问题2:移动端按钮300ms延迟

在setup函数末尾添加CSS属性,消除移动端浏览器的点击延迟:

// 消除移动端点击延迟
document.body.style.touchAction = 'manipulation';

问题3:桌面端需点击画布才能触发上传

修改buttonPressed中的上传逻辑,确保文件选择框的点击触发在用户交互上下文内:

if (trackControls === 'uploadTrack') {
    setTimeout(() => {
        fileInput.elt.click();
    }, 0);
    OPC.button('playTrack', 'Play');
    playStatus = false;
}

同时在mousePressed中添加画布点击时的权限初始化:

function mousePressed() {
    handlePress(mouseX, mouseY);
    // 点击画布时赋予文件选择框交互权限
    if (!fileName) {
        fileInput.elt.focus();
    }
}

完整修改后代码

let audioPlayer;
let playStatus = false;
let fileInput;
let fileName = '';
let currentTime = 0;
let totalDuration = 1; // Placeholder, will be updated when the track is loaded

let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;

let circleX, circleY, displaySpeed;

let nunitoFont;

OPC.slider({
    name: 'speed',
    value: 1,
    min: 0.50,
    max: 2.01,
    step: 0.01,
    label: 'Speed',
    description: 'Changes the initial speed of the track'
});

OPC.slider({
    name: 'volume',
    value: 0.5,
    min: 0,
    max: 1.05,
    step: 0.05,
    label: 'Volume',
    description: 'Volume adjustment'
});

OPC.slider({
    name: 'seedNum',
    value: 536,
    min: 0,
    max: 2024,
    step: 1,
    label: 'Seed',
    description: 'Randomizes the Peaks & Valleys graph'
});

OPC.button('playTrack', 'Play');
OPC.button('stopTrack', 'Stop');
OPC.button({
    name: 'uploadTrack',
    value: 'Upload Track',
    description: 'Upload button not working? Try clicking the centre of the canvas and then the button.'
});

OPC.select({
    name: 'graphType',
    label: 'Graph',
    options: {
        "Stable": 0,
        "Accelerated": 1,
        "Decelerated": 2,
        "Peaks & Valleys": 3
    },
    value: 0,
    description: 'Choose speed graph'
});

function preload() {
    nunitoFont = loadFont('./Nunito-Regular.ttf');
}

function setup() {
    createCanvas(windowWidth, windowHeight);
    textFont(nunitoFont);

    // Create p5.js file input button and then hide it.
    // OP Configurator 3000 button will trigger this button.
    // Need to fix this circuitous route.
    fileInput = createFileInput(handleFile);
    fileInput.position(10, 10);
    fileInput.hide();
    fileInput.style('opacity', '0');
    fileInput.style('width', '1px');
    fileInput.style('height', '1px');
    // Initialize audio player
    audioPlayer = createAudio();
    audioPlayer.elt.addEventListener('loadedmetadata', () => {
        totalDuration = audioPlayer.duration();
    });

    // 消除移动端点击延迟
    document.body.style.touchAction = 'manipulation';
}

function draw() {
    background(55); // Clear the background
    fill(255);
    audioPlayer.volume(volume);

    if (fileName) {
        let baseTextSize = 32;
        textSize(baseTextSize);
        textAlign(CENTER, CENTER);

        // Measure the width of the text
        let measuredTextWidth = textWidth(fileName);

        // Adjust the text size if it exceeds the width of the screen
        while (measuredTextWidth > width * 0.9) { // 90% of screen width as padding
            baseTextSize -= 1; // Decrease text size
            textSize(baseTextSize);
            measuredTextWidth = textWidth(fileName); // Update measurement
        }

        // Draw the text in the center of the canvas
        text(fileName, width / 2, height / 4);

        // Display the adjusted current time and total duration
        textSize(16);
        let currentMinutes = floor(currentTime / 60);
        let currentSeconds = floor(currentTime % 60);
        let totalMinutes = floor(totalDuration / 60);
        let totalSeconds = floor(totalDuration % 60);
        let timeString = nf(currentMinutes, 2) + ':' + nf(currentSeconds, 2) + ' / ' + nf(totalMinutes, 2) + ':' + nf(totalSeconds, 2);
        text(timeString, width / 2, height / 4 + 40);
    }
    noStroke();
    rect(0, height / 2, width, height / 2); // Graph screen set up on the bottom half of the screen
    fill(0, 150);
    textAlign(CENTER);
    stroke(100, 30)
    line(width * 0.5, height * 0.5, width * 0.5, height); // Sets up a vertical line at centre of the speed graph.
    stroke(100, 105);
    textSize(12);
    if (graphType != 3) { // Sets up a horizontal centre line in the speed graph.
        text("SPEED SETTING LINE", width * 0.5, height * 0.75);
        line(0, height * 0.75, width, height * 0.75);
    } else { // Case 3 has a different centre line as it goes from 0.50 min to 2.00 max, whereas other cases go from 0.0 to 2.00
        text("SPEED SETTING LINE", width * 0.5, height * 0.833);
        line(0, height * 0.833, width, height * 0.833);
    }

    currentTime = audioPlayer.time(); // Keeps track of current time in the uploaded audio player

    // Adjust speed based on the selected graphType
    switch (graphType) {
        case 0: // Stable -- maintains the tempo of the file according to the speed set
            audioPlayer.speed(speed);
            stroke(5, 89, 137);
            line(0, height / 2 + height / 4, width, height / 2 + height / 4); // Draw horizontal line
            circleX = map(currentTime, 0, totalDuration, 0, width);
            circleY = height / 2 + height / 4; // Keep circle vertical position constant
            displaySpeed = speed;
            break;
        case 1: // Accelerated -- gradually increases speed to 2.00x over the length of the track
            let accelSpeed = speed + (2 * speed - speed) * (currentTime / totalDuration);
            audioPlayer.speed(accelSpeed);
            stroke(5, 89, 137);
            line(0, height / 2 + height / 4, width, height / 2); // Draw angled line upwards from centre
            circleX = map(currentTime, 0, totalDuration, 0, width);
            circleY = map(currentTime, 0, totalDuration, height / 2 + height / 4, height / 2);
            displaySpeed = accelSpeed;
            break;
        case 2: // Decelerated -- gradually decreases speed to 0.00x over the length of the track. May require reset of program after it hits 0.
            let decelSpeed = speed - (speed - 0.5 * speed) * (currentTime / totalDuration);
            audioPlayer.speed(decelSpeed);
            stroke(5, 89, 137);
            line(0, height / 2 + height / 4, width, height); // Draw angled line downwards from centre
            circleX = map(currentTime, 0, totalDuration, 0, width);
            circleY = map(currentTime, 0, totalDuration, height / 2 + height / 4, height);
            displaySpeed = decelSpeed;
            break;
        case 3: // Peaks & Valleys -- creates a mountainous speed for the track.
            stroke(5, 89, 137);
            noFill();
            var speedRand = seedNum;
            noiseSeed(speedRand);
            beginShape();
            let xoff = 0;
            let yValues = [];
            for (let x = 0; x <= width; x += 10) {
                let y = constrain(map(noise(xoff), 0.25, 0.75, height, height / 2), height / 2, height); // sets the Y-axis of the graph
                vertex(x, y);
                yValues.push(y);
                xoff += 0.05;
            }
            endShape();
            // Tracks tempo according to the mountainous line generated
            let index = map(currentTime, 0, totalDuration, 0, yValues.length - 1);
            let lowerIndex = Math.floor(index);
            let upperIndex = Math.ceil(index);
            let t = index - lowerIndex;
            circleY = lerp(yValues[lowerIndex], yValues[upperIndex], t);
            circleX = map(currentTime, 0, totalDuration, 0, width);

            let minSpeed = 0.5 * speed;
            let maxSpeed = 2 * speed;
            let contourSpeed = map(circleY, height, height / 2, minSpeed, maxSpeed);

            audioPlayer.speed(contourSpeed);
            displaySpeed = contourSpeed;
            break;
    }

    stroke(200, 8, 21);
    fill(200, 8, 21);
    ellipse(circleX, circleY, 10, 10); // a helpful little circle that shows where in the graph you are.

    // Check if the mouse is hovering over the circle
    let d = dist(mouseX, mouseY, circleX, circleY);
    if (d < 5) {
        cursor('pointer'); // HANDS cursor
    } else {
        cursor(ARROW); // Default pointy cursor
    }

    noStroke();
    textSize(12);
    textAlign(LEFT, CENTER);
    text(nf(displaySpeed, 1, 2) + 'x SPEED', circleX, circleY + 15); // displays text next to the circle to show current speed.
}

function buttonPressed(trackControls) {
    if (trackControls !== 'uploadTrack' && !fileName) {
        alert('Please upload a file before playing or stopping.');
        return;
    }

    if (trackControls === 'playTrack') {
        if (!playStatus) {
            // 唤醒音频上下文(针对移动端浏览器限制)
            if (typeof audioPlayer.elt.context !== 'undefined') {
                audioPlayer.elt.context.resume().then(() => {
                    audioPlayer.loop();
                }).catch(err => {
                    console.error('Failed to resume audio context:', err);
                    alert('无法播放音频,请检查浏览器权限或重新上传文件');
                });
            } else {
                // 兼容不支持audioContext的情况
                audioPlayer.loop().catch(err => {
                    console.error('Playback error:', err);
                    alert('无法播放音频,请检查浏览器权限或重新上传文件');
                });
            }
            OPC.button('playTrack', 'Pause');
            playStatus = true;
        } else {
            audioPlayer.pause();
            OPC.button('playTrack', 'Play');
            playStatus = false;
        }
    }

    if (trackControls === 'stopTrack') {
        audioPlayer.stop();
        OPC.button('playTrack', 'Play');
        playStatus = false;
    }

    if (trackControls === 'uploadTrack') {
        // 确保fileInput的点击在用户交互上下文内
        setTimeout(() => {
            fileInput.elt.click();
        }, 0);
        OPC.button('playTrack', 'Play');
        playStatus = false;
    }
}

function handleFile(file) {
    if (file.type === 'audio') {
        audioPlayer.src = file.data;
        // 提前加载音频,避免播放时延迟
        audioPlayer.load();
        fileName = file.name;
    } else {
        alert('Not a valid audio file!');
        fileName = '';
    }
}

// Mouse and touch event handlers for dragging the circle
function mousePressed() {
    handlePress(mouseX, mouseY);
    // 备选:点击画布后赋予fileInput交互权限
    if (!fileName) {
        fileInput.elt.focus();
    }
}

function mouseDragged() {
    handleDrag(mouseX, mouseY);
}

function mouseReleased() {
    isDragging = false;
}

function touchStarted() {
    if (touches.length > 0) {
        handlePress(touches[0].x, touches[0].y);
    }
    return false;
}

function touchMoved() {
    if (touches.length > 0) {
        handleDrag(touches[0].x, touches[0].y);
    }
    return false;
}

function touchEnded() {
    isDragging = false;
    return false;
}

function handlePress(x, y) {
    // Check if the mouse or touch is pressed within the circle
    let d = dist(x, y, circleX, circleY);
    if (d < 10) {
        isDragging = true;
        dragOffsetX = x - circleX;
        dragOffsetY = y - circleY;
    }
}

function handleDrag(x, y) {
    if (isDragging) {
        // Update the circle's position based on mouse or touch movement
        circleX = x - dragOffsetX;
        circleY = y - dragOffsetY;
        circleX = constrain(circleX, 0, width);
        circleY = constrain(circleY, height / 2, height);
        currentTime = map(circleX, 0, width, 0, totalDuration);
        audioPlayer.time(currentTime);
    }
}

// For desktop and switching between portrait and landscape on phones / tablets
function windowRes
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 11:36:55