【问题标题】:Show waveform of audio显示音频波形
【发布时间】:2016-12-09 05:36:01
【问题描述】:

我正在 android 中制作一个音乐应用程序。在这个来自服务器端的音乐列表中。我不知道如何在 android 中显示音频波形?就像在 soundcloud 网站中一样。我在下面附上了图片。

【问题讨论】:

  • 你好你有什么解决方案吗?我在录音应用程序中也遇到了同样的问题。如果您对此有任何了解,请提供帮助。
  • 运气好吗?

标签: android audio waveform


【解决方案1】:

也许,您可以在没有库的情况下实现此功能,当然如果您只想要音频样本的可视化。 例如:

public class PlayerVisualizerView extends View {

    /**
     * constant value for Height of the bar
     */
    public static final int VISUALIZER_HEIGHT = 28;

    /**
     * bytes array converted from file.
     */
    private byte[] bytes;

    /**
     * Percentage of audio sample scale
     * Should updated dynamically while audioPlayer is played
     */
    private float denseness;

    /**
     * Canvas painting for sample scale, filling played part of audio sample
     */
    private Paint playedStatePainting = new Paint();
    /**
     * Canvas painting for sample scale, filling not played part of audio sample
     */
    private Paint notPlayedStatePainting = new Paint();

    private int width;
    private int height;

    public PlayerVisualizerView(Context context) {
        super(context);
        init();
    }

    public PlayerVisualizerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        bytes = null;

        playedStatePainting.setStrokeWidth(1f);
        playedStatePainting.setAntiAlias(true);
        playedStatePainting.setColor(ContextCompat.getColor(getContext(), R.color.gray));
        notPlayedStatePainting.setStrokeWidth(1f);
        notPlayedStatePainting.setAntiAlias(true);
        notPlayedStatePainting.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
    }

    /**
     * update and redraw Visualizer view
     */
    public void updateVisualizer(byte[] bytes) {
        this.bytes = bytes;
        invalidate();
    }

    /**
     * Update player percent. 0 - file not played, 1 - full played
     *
     * @param percent
     */
    public void updatePlayerPercent(float percent) {
        denseness = (int) Math.ceil(width * percent);
        if (denseness < 0) {
            denseness = 0;
        } else if (denseness > width) {
            denseness = width;
        }
        invalidate();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        width = getMeasuredWidth();
        height = getMeasuredHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bytes == null || width == 0) {
            return;
        }
        float totalBarsCount = width / dp(3);
        if (totalBarsCount <= 0.1f) {
            return;
        }
        byte value;
        int samplesCount = (bytes.length * 8 / 5);
        float samplesPerBar = samplesCount / totalBarsCount;
        float barCounter = 0;
        int nextBarNum = 0;

        int y = (height - dp(VISUALIZER_HEIGHT)) / 2;
        int barNum = 0;
        int lastBarNum;
        int drawBarCount;

        for (int a = 0; a < samplesCount; a++) {
            if (a != nextBarNum) {
                continue;
            }
            drawBarCount = 0;
            lastBarNum = nextBarNum;
            while (lastBarNum == nextBarNum) {
                barCounter += samplesPerBar;
                nextBarNum = (int) barCounter;
                drawBarCount++;
            }

            int bitPointer = a * 5;
            int byteNum = bitPointer / Byte.SIZE;
            int byteBitOffset = bitPointer - byteNum * Byte.SIZE;
            int currentByteCount = Byte.SIZE - byteBitOffset;
            int nextByteRest = 5 - currentByteCount;
            value = (byte) ((bytes[byteNum] >> byteBitOffset) & ((2 << (Math.min(5, currentByteCount) - 1)) - 1));
            if (nextByteRest > 0) {
                value <<= nextByteRest;
                value |= bytes[byteNum + 1] & ((2 << (nextByteRest - 1)) - 1);
            }

            for (int b = 0; b < drawBarCount; b++) {
                int x = barNum * dp(3);
                float left = x;
                float top = y + dp(VISUALIZER_HEIGHT - Math.max(1, VISUALIZER_HEIGHT * value / 31.0f));
                float right = x + dp(2);
                float bottom = y + dp(VISUALIZER_HEIGHT);
                if (x < denseness && x + dp(2) < denseness) {
                    canvas.drawRect(left, top, right, bottom, notPlayedStatePainting);
                } else {
                    canvas.drawRect(left, top, right, bottom, playedStatePainting);
                    if (x < denseness) {
                        canvas.drawRect(left, top, right, bottom, notPlayedStatePainting);
                    }
                }
                barNum++;
            }
        }
    }

    public int dp(float value) {
        if (value == 0) {
            return 0;
        }
        return (int) Math.ceil(getContext().getResources().getDisplayMetrics().density * value);
    }
}

抱歉,使用少量 cmets 的代码,但它正在工作的可视化工具。您可以将它附加到任何您想要的玩家身上。

如何使用它:将此视图添加到您的 xml 布局中,然后您必须使用方法更新可视化器状态

public void updateVisualizer(byte[] bytes) {
    playerVisualizerView.updateVisualizer(bytes);
}

public void updatePlayerProgress(float percent) {
    playerVisualizerView.updatePlayerPercent(percent);
}

updateVisualizer 中,您将字节数组与音频样本一起传递,而在updatePlayerProgress 中,您在音频样本播放时动态传递百分比。

为了将文件转换为字节,您可以使用此帮助方法

public static byte[] fileToBytes(File file) {
    int size = (int) file.length();
    byte[] bytes = new byte[size];
    try {
        BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file));
        buf.read(bytes, 0, bytes.length);
        buf.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return bytes;
}

例如(很快),Mosby 库的样子:

public class AudioRecorderPresenter extends MvpBasePresenter<AudioRecorderView> {

public void onStopRecord() {
        // stopped and released MediaPlayer
        // ...
        // some preparation and saved audio file in audioFileName variable.

        getView().updateVisualizer(FileUtils.fileToBytes(new File(audioFileName)));
        }
    }
}

UPD:我创建了用于解决此案例 github.com/scrobot/SoundWaveView 的库。它仍处于“WIP”状态(正在进行中),但我很快就会完成它。

【讨论】:

  • Tanx,如何使用 PlayerVisualizerView?
  • 我认为你给出了最佳答案并给你的答案+50奖励,但请添加更多细节并显示用法。
  • 如果你在每一行注释代码并解释它会很好,特别是字节移动
  • @Scrobot 感谢您的大力帮助。如果你能添加一个代码来显示条形的波纹直线,我会非常感激,我的意思是,如果我们给出负值,我们会得到直线下面的条形。完成完整的声波(soundcloud 类型 UI)
  • @KathanShah 嗨!我正在努力))我接受了很多反馈,并决定为此功能创建库)因此,android 库将很快发布。 )) 特别是,负波也将“在盒子里”))
【解决方案2】:

我相信Scrobot's answer 不起作用。它假定输入音频采用某种(非常特殊的)编码(具有 5 位深度的单通道/单声道 PCM)。从波函数计算振幅的算法可能存在缺陷。如果将该算法与任何常用的音频文件格式一起使用,则只会得到随机数据。

事实是:它只是稍微复杂一点。

为了实现 OP 的目标,需要做些什么:

  1. 使用Android's MediaExtractor读取输入音频文件(支持多种格式/编码)
  2. 使用Android's MediaCodec将输入音频编码解码为具有一定位深度(通常为16位)的线性PCM编码
    • 只有在这一步之后,您才能获得一个字节数组,您可以从该数组中线性读取和计算幅度。
  3. 对 PCM 编码的数据应用响度测量。其中有很多,一些更复杂(例如 LUFS/LKFS),一些更基本(RMS)。我们以 RMS(= 均方根)为例:
    1. 确定每个柱的样本数。
    2. 读取单个条的所有样本。通常有 2 个通道,因此对于每个样本,您将获得 2 个用于 PCM-16 的短整数(16 位)。
    3. 为每个样本计算所有通道的平均值。
    4. 也许您希望以某种方式规范化该值,例如要获得介于 -1 和 1 之间的浮点值,您可以除以 (2 / (1
    5. 对每个样本进行平方(因此 RMS 中的“S”)
    6. 计算条形的所有样本的平均值(因此 RMS 中的“M”)
    7. 计算结果值的平方根(因此 RMS 中的“R”)
    8. 现在您获得了一个值,您可以将其作为条形高度的基础。
    9. 对所有条形重复步骤 2-8。

实现这是一项相当复杂的任务。我已经找不到任何提供整个过程的库。但至少 Android 的媒体 API 提供了读取任何格式的音频文件的算法。

注意:RMS 被认为是一种不太准确的响度测量值。但它似乎产生的结果至少与你实际听到的有些相关。对于许多应用程序来说,它应该已经足够好了。

【讨论】:

    【解决方案3】:

    您可以使用WaveformSeekBar 库在您的应用中绘制波形。该库允许您从本地音频文件、资源、URL 或自定义音频数据中绘制波形。

    您还可以使用Amplituda 库,该库将处理音频文件并返回您可以绘制自己的波形的数据。

    WaveformSeekBar 的结果波形:

    【讨论】:

      猜你喜欢
      • 2011-03-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-06
      • 2020-07-09
      • 1970-01-01
      • 2012-10-21
      • 2017-07-08
      相关资源
      最近更新 更多