2014-10-19 5 views
13

я написал сигнал рендерер, который принимает звуковой файл и создает что-то вроде этого:Как сделать визуализацию формы волны более интересной?

enter image description here

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

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

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

+1

Вы пробовали вертикальную шкалу «обратного журнала»? Это будет подчеркивать вариацию. Также вы можете попробовать средние значения вместо avg и/или отрезать образцы верхнего и нижнего отсечения – radai

+0

@radai Я не думал о том, чтобы использовать средства, я попробую. Какой обратный журнал вы имеете в виду? Это будет экспоненциальная функция? – Jawap

+0

Ваш входной диапазон известен - от 0 до максимального объема (в зависимости от глубины образца?). Вам нужна шкала, которая помещает все низкие образцы близко к основанию, но оставляет много места для больших значений. Скажем val^2, деленное на max_value^2 – radai

ответ

17

Я не думаю, что 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. Чтобы исправить это, запустите с увеличенным размером кучи as described here.

+0

Я думал, что такой сюжет должен визуализировать отличие от других песен, но он просто показывает нам дисперсию вокруг среднего значения ... Мб Я (был/am) не прав? – Ralor

+1

@Ralor Я не понимаю, о чем вы не уверены. См. Мое редактирование для фактического графика усреднения, которое я описал. При таком грубом уровне детализации общая динамика будет единственной заметной разницей между песнями. – Radiodef

+0

+1 для потрясающего пошагового руководства – radai

Смежные вопросы