Подтвердить что ты не робот

Как сделать визуализацию формы волны более интересной?

Я написал визуализатор формы, который принимает аудиофайл и создает что-то вроде этого:

enter image description here

Логика довольно проста. Я вычисляю количество образцов аудио, необходимых для каждого пикселя, читаю эти образцы, усредняю ​​их и рисую столбцом пикселей в соответствии с полученным значением.

Как правило, я буду воспроизводить целую песню примерно на 600-800 пикселей, поэтому волна довольно сжата. К сожалению, это обычно приводит к непривлекательным визуальным эффектам, поскольку почти вся песня просто отображается практически на одной высоте. Изменений нет.

Интересно, что если вы посмотрите на осциллограммы на SoundCloud, почти ни один из них не скучен, как мои результаты. У всех есть некоторые варианты. Что может быть трюком здесь? Я не думаю, что они просто добавляют случайный шум.

4b9b3361

Ответ 1

Я не думаю, что SoundCloud делает что-то особенное. На их первой странице есть много песен, которые очень плоские. Это больше связано с тем, как воспринимается деталь и какова общая динамика песни. Основное различие заключается в том, что SoundCloud рисует абсолютное значение. (Отрицательная сторона изображения - просто зеркало.)

Для демонстрации здесь представлен базовый график белого шума с прямыми линиями:

regular plot

Теперь, как правило, заполнение используется, чтобы сделать общий контур более понятным. Это уже многое делает для внешнего вида:

fill

Большие осциллограммы (в частности, "увеличенные" ) обычно используют эффект зеркала, поскольку динамика становится более выраженной:

wrap

Бары - это еще один способ визуализации и может дать иллюзию детали:

step

Псевдо-процедура для типичного графического изображения (среднее значение абс и зеркала) может выглядеть так:

for (each pixel in width of image) {
    var sum = 0

    for (each sample in subset contained within pixel) {
        sum = sum + abs(sample)
    }

    var avg = sum / length of subset

    draw line(avg to -avg)
}

Это эффективно, как сжатие оси времени как RMS окна. (RMS также можно использовать, но они почти одинаковы.) Теперь форма волны показывает общую динамику.

Это не слишком отличается от того, что вы уже делаете, просто абс, зеркало и заливку. Для ящиков, таких как SoundCloud, вы будете рисовать прямоугольники.

Как бонус, вот MCVE, написанный на Java, для генерации формы волны с блоками, как описано. (Извините, если Java не является вашим языком.) Фактический код чертежа находится в верхней части. Эта программа также нормализуется, т.е. Форма волны "растягивается" до высоты изображения.

Этот простой вывод такой же, как в приведенной выше псевдо-процедуре:

normal output

Этот вывод с полями очень похож на SoundCloud:

box waveform

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.sound.sampled.*;

public class BoxWaveform {
    static int boxWidth = 4;
    static Dimension size = new Dimension(boxWidth == 1 ? 512 : 513, 97);

    static BufferedImage img;
    static JPanel view;

    // draw the image
    static void drawImage(float[] samples) {
        Graphics2D g2d = img.createGraphics();

        int numSubsets = size.width / boxWidth;
        int subsetLength = samples.length / numSubsets;

        float[] subsets = new float[numSubsets];

        // find average(abs) of each box subset
        int s = 0;
        for(int i = 0; i < subsets.length; i++) {

            double sum = 0;
            for(int k = 0; k < subsetLength; k++) {
                sum += Math.abs(samples[s++]);
            }

            subsets[i] = (float)(sum / subsetLength);
        }

        // find the peak so the waveform can be normalized
        // to the height of the image
        float normal = 0;
        for(float sample : subsets) {
            if(sample > normal)
                normal = sample;
        }

        // normalize and scale
        normal = 32768.0f / normal;
        for(int i = 0; i < subsets.length; i++) {
            subsets[i] *= normal;
            subsets[i] = (subsets[i] / 32768.0f) * (size.height / 2);
        }

        g2d.setColor(Color.GRAY);

        // convert to image coords and do actual drawing
        for(int i = 0; i < subsets.length; i++) {
            int sample = (int)subsets[i];

            int posY = (size.height / 2) - sample;
            int negY = (size.height / 2) + sample;

            int x = i * boxWidth;

            if(boxWidth == 1) {
                g2d.drawLine(x, posY, x, negY);
            } else {
                g2d.setColor(Color.GRAY);
                g2d.fillRect(x + 1, posY + 1, boxWidth - 1, negY - posY - 1);
                g2d.setColor(Color.DARK_GRAY);
                g2d.drawRect(x, posY, boxWidth, negY - posY);
            }
        }

        g2d.dispose();
        view.repaint();
        view.requestFocus();
    }

    // handle most WAV and AIFF files
    static void loadImage() {
        JFileChooser chooser = new JFileChooser();
        int val = chooser.showOpenDialog(null);
        if(val != JFileChooser.APPROVE_OPTION) {
            return;
        }

        File file = chooser.getSelectedFile();
        float[] samples;

        try {
            AudioInputStream in = AudioSystem.getAudioInputStream(file);
            AudioFormat fmt = in.getFormat();

            if(fmt.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
                throw new UnsupportedAudioFileException("unsigned");
            }

            boolean big = fmt.isBigEndian();
            int chans = fmt.getChannels();
            int bits = fmt.getSampleSizeInBits();
            int bytes = bits + 7 >> 3;

            int frameLength = (int)in.getFrameLength();
            int bufferLength = chans * bytes * 1024;

            samples = new float[frameLength];
            byte[] buf = new byte[bufferLength];

            int i = 0;
            int bRead;
            while((bRead = in.read(buf)) > -1) {

                for(int b = 0; b < bRead;) {
                    double sum = 0;

                    // (sums to mono if multiple channels)
                    for(int c = 0; c < chans; c++) {
                        if(bytes == 1) {
                            sum += buf[b++] << 8;

                        } else {
                            int sample = 0;

                            // (quantizes to 16-bit)
                            if(big) {
                                sample |= (buf[b++] & 0xFF) << 8;
                                sample |= (buf[b++] & 0xFF);
                                b += bytes - 2;
                            } else {
                                b += bytes - 2;
                                sample |= (buf[b++] & 0xFF);
                                sample |= (buf[b++] & 0xFF) << 8;
                            }

                            final int sign = 1 << 15;
                            final int mask = -1 << 16;
                            if((sample & sign) == sign) {
                                sample |= mask;
                            }

                            sum += sample;
                        }
                    }

                    samples[i++] = (float)(sum / chans);
                }
            }

        } catch(Exception e) {
            problem(e);
            return;
        }

        if(img == null) {
            img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
        }

        drawImage(samples);
    }

    static void problem(Object msg) {
        JOptionPane.showMessageDialog(null, String.valueOf(msg));
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("Box Waveform");
                JPanel content = new JPanel(new BorderLayout());
                frame.setContentPane(content);

                JButton load = new JButton("Load");
                load.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent ae) {
                        loadImage();
                    }
                });

                view = new JPanel() {
                    @Override
                    protected void paintComponent(Graphics g) {
                        super.paintComponent(g);

                        if(img != null) {
                            g.drawImage(img, 1, 1, img.getWidth(), img.getHeight(), null);
                        }
                    }
                };

                view.setBackground(Color.WHITE);
                view.setPreferredSize(new Dimension(size.width + 2, size.height + 2));

                content.add(view, BorderLayout.CENTER);
                content.add(load, BorderLayout.SOUTH);

                frame.pack();
                frame.setResizable(false);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}

Примечание: для простоты эта программа загружает весь аудиофайл в память. Некоторые JVM могут бросать OutOfMemoryError. Чтобы исправить это, запустите с увеличенным размером кучи как описано здесь.