Как избавиться от вертикальных полос в моей спектрограмме?

В рамках статьи, которую я пишу в старшей школе, я решил создать свой собственный конвертер аудио-файла в спектрограмму с нуля, чтобы создать пейзажи из этих спектрограмм.

У меня уже есть моя реализация БПФ и использования ее для создания карты высот, спектрограммы. Но я часто получаю странные артефакты в виде вертикальных полос, когда частоты становятся плотными, как вы можете видеть на рисунке ниже.

Спектрограмма Реквиема Моцарта, Лакримоза

Пример в самом начале с длиной окна 2048 и логарифмическим масштабом ^2. БПФ, который я использую, безупречен, я уже сравнил его с другими, и они дают тот же результат.

Это функция, которая преобразует амплитуды в частоты и сохраняет их в 2D-массиве:

private void transform(int from, int until) {
    double val, step;
    for(int i=from; i<until; i++) {
        for(int j=0; j<n; j++)
            chunk[j] = data[0][i*n+j+start];

        fft.realForward(chunk);

        for(int j=0; j<height; j++) {
            val = Math.sqrt(chunk[2*j]*chunk[2*j] + chunk[2*j+1]*chunk[2*j+1]);
            map[i][j] = val;
        }
    }
}

Теперь мой вопрос: откуда взялись эти вертикальные полосы и как от них избавиться?

В настоящее время я не использую оконную функцию, и все вычисления связаны друг с другом, что означает, что нет перекрытия. Это самый простой способ составить спектрограмму. Может ли это помочь ввести оконную функцию или выполнить каждый расчет независимо от того, был ли фрейм уже вовлечен в предыдущий расчет, то есть перекрывать фрейм-окна?

Кроме того, какие еще способы улучшить мой базовый подход, чтобы получить лучший результат?

Это целый класс. Я передаю ему данные и всю необходимую информацию из аудиофайла:

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;

import javax.imageio.ImageIO;
import javax.swing.*;

import org.jtransforms.fft.DoubleFFT_1D;

public class Heightmap extends JFrame implements WindowListener{
    public static final int LOG_SCALE = 0;
    public static final int LOG_SQUARE_SCALE = 1;
    public static final int SQUARE_SCALE = 2;
    public static final int LINEAR_SCALE = 3;

    private BufferedImage heightmap;
    private FileDialog chooser; 

    private JMenuBar menuBar;
    private JMenu fileMenu;
    private JMenuItem save, close;

    private DoubleFFT_1D fft;
    private int[][] data;
    private double[][] map;
    private double[] chunk;
    private int width, height, n, start, scale;
    private String name;

    private boolean inactive;

    public Heightmap(int[][] data, int resolution, int start,
            int width, int height, int scale, String name) {

        this.data = data;
        this.n = resolution;
        this.start = start;
        this.width = width;
        this.height = height;
        this.scale = scale;
        this.name = name;

        fft = new DoubleFFT_1D(n);
        map = new double[width][height];
        heightmap = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        chunk = new double[n];      

        System.out.println("Starting transformation...");

        long time;
        time = System.currentTimeMillis();
        transform();
        time = System.currentTimeMillis() - time;
        System.out.println("Time taken for calculation: "+time+" ms");

        time = System.currentTimeMillis();
        makeHeightmap();
        initComponents();
        time = System.currentTimeMillis() - time;
        System.out.println("Time taken for drawing heightmap: "+time+" ms");

    }

    private void initComponents() {
        this.setSize(width, height);
        this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        this.setResizable(false);
        this.setLocationRelativeTo(null);
        this.setTitle(name);

        createMenuBar();
        chooser = new FileDialog(this, "Choose a directory", FileDialog.SAVE);
        chooser.setDirectory("/Users/<user>/Desktop");

        this.addMouseListener(new HeightmapMouseListener());
        this.addKeyListener(new HeightmapKeyListener());
        this.addWindowListener(this);

        this.setVisible(true);

    }

    private void createMenuBar() {
        menuBar = new JMenuBar();
        fileMenu = new JMenu();

        fileMenu.setText("File");

        save = new JMenuItem("Save...", KeyEvent.VK_S);
        save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.META_DOWN_MASK));
        save.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                chooser.setVisible(true);

                String fileName = chooser.getFile();
                String dir = chooser.getDirectory();
                chooser.setDirectory(dir);

                if(fileName != null) {
                    try {
                        File outputfile = new File(dir + fileName + ".png");
                        ImageIO.write(heightmap, "png", outputfile);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Saved "+fileName+".png to "+dir);
                }
            }
        });

        close = new JMenuItem("Close", KeyEvent.VK_C);
        close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.META_DOWN_MASK));
        close.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                setVisible(false);
                dispose();
            }
        });

        fileMenu.add(save);
        fileMenu.addSeparator();
        fileMenu.add(close);

        menuBar.add(fileMenu);
        this.setJMenuBar(menuBar);
    }

    public void paint(Graphics g) {
        g.drawImage(heightmap, 0, 0, null);
    }

    private void transform() {
        transform(0, width);
    }
    private void transform(int from, int until) {
        double max = Double.MIN_VALUE;
        double min = Double.MAX_VALUE;
        double val, step;
        for(int i=from; i<until; i++) {
            for(int j=0; j<n; j++) {
                chunk[j] = data[0][i*n+j+start];
            }
            fft.realForward(chunk);

            for(int j=0; j<height; j++) {
                val = Math.sqrt(chunk[2*j]*chunk[2*j] + chunk[2*j+1]*chunk[2*j+1]);

                if(val > max)
                    max = val;
                if(val < min)
                    min = val;
                map[i][j] = val;
            }

            if(min != 0) {
                step = max/(max-min);
                for(int j=0; j<height; j++)
                    map[i][j] = (map[i][j]-min)*step;
            }
        }
    }

    /*
     * Paints heightmap into the BufferedImage
     */
    private void makeHeightmap() {
        double max = 0;
        switch(scale) {
        case LOG_SCALE: max = Math.log(findMax(map)+1); break;
        case LOG_SQUARE_SCALE: max = Math.pow(Math.log(findMax(map)+1), 2); break;
        case SQUARE_SCALE: max = Math.sqrt(findMax(map)); break;
        case LINEAR_SCALE: max = findMax(map); break;
        default: max = Math.pow(Math.log(findMax(map)+1), 2); break;
        }
        double stepsize = 255.0/max;
        int val, rgb;

        for(int x=0; x<width; x++)
            for(int y=0; y<height; y++) {
                switch(scale) {
                case LOG_SCALE: val = (int) (Math.log(map[x][y]+1)*stepsize); break;
                case LOG_SQUARE_SCALE: val = (int) (Math.log(map[x][y]+1)*stepsize); val *= val; break;
                case SQUARE_SCALE: val = (int) (Math.sqrt(map[x][y])*stepsize); break;
                case LINEAR_SCALE: val = (int) (map[x][y]*stepsize); break;
                default: val = (int) (Math.log(map[x][y]+1)*stepsize); val *= val; break;
                }
                rgb = 255<<24 | val<<16 | val<<8 | val;
                heightmap.setRGB(x, height-y-1, rgb);
            }

    }

    private double findMax(double[][] data) {
        double max = 0;
        for(double[] val1: data)
            for(double d: val1)
                if(d > max)
                    max = d;
        return max;
    }

    private class HeightmapKeyListener implements KeyListener {
        boolean busy = false;

        public void keyPressed(KeyEvent e) {

            if(e.getKeyCode() == KeyEvent.VK_RIGHT && !busy && start < data[0].length-width*n) {
                busy = true;

                for(int x=0; x<width-1; x++)
                    map[x] = map[x+1].clone();

                start += n;
                transform(width-1, width);              
                makeHeightmap();
                repaint();
                busy = false;
            }
            else if(e.getKeyCode() == KeyEvent.VK_LEFT && !busy && start > 0) {
                busy = true;

                for(int x=width-1; x>0; x--)
                    map[x] = map[x-1];

                start -= n;
                transform(0, 1);
                makeHeightmap();
                repaint();
                busy = false;
            }
        }

        public void keyReleased(KeyEvent e) {   }
        public void keyTyped(KeyEvent e) {  }
    }

    private class HeightmapMouseListener implements MouseListener {


        public void mouseClicked(MouseEvent e) {
            if(inactive) {
                inactive = false;
                return;
            }

            long time = System.currentTimeMillis();

            int posX = e.getX();
            int diff = posX - width/2;  //difference between old and new center in pixels
            int oldStart = start;

            start = start + diff*n;
            if(start < 0) start = 0;
            int maxFrame = data[0].length-width*n;
            if(start > maxFrame) start = maxFrame;
            if(start == oldStart) return;

            System.out.println("Changing center...");

            int absDiff = Math.abs(diff);
            if(start < oldStart) {  //shift the start backward, recalculate the start
                for(int x=width-1; x>=absDiff; x--)
                    map[x] = map[x-absDiff].clone();
                transform(0, absDiff);
            }
            else if(start > oldStart) { //shift the back forward, recalculate the back
                for(int x=0; x<width-absDiff; x++)
                    map[x] = map[x+absDiff].clone();
                transform(width-absDiff, width);
            }

            makeHeightmap();
            repaint();
            System.out.println("Time taken: "+(System.currentTimeMillis()-time)+" ms");

        }

        public void mousePressed(MouseEvent e) {    }
        public void mouseReleased(MouseEvent e) {   }
        public void mouseEntered(MouseEvent e) {    }
        public void mouseExited(MouseEvent e) { }
    }

    public void windowActivated(WindowEvent arg0) { }
    public void windowClosed(WindowEvent arg0) {    }
    public void windowClosing(WindowEvent arg0) {   }
    public void windowDeactivated(WindowEvent arg0) {
        inactive = true;
    }
    public void windowDeiconified(WindowEvent arg0) {   }
    public void windowIconified(WindowEvent arg0) { }
    public void windowOpened(WindowEvent arg0) {    }

}

РЕДАКТИРОВАТЬ: реализация оконной функции значительно улучшила результат. Я действительно не понимал, что будет делать оконная функция, и поэтому недооценил ее эффект.

Однако после этого я попытался нанести на карту косинусоидальную волну с частотой 10 кГц, которая (опять же) вызвала некоторые странные артефакты:

Что может быть причиной этого? Я реализовал защиту от переполнения, обрезав все от 0 до 0 и от 255 до 255 без каких-либо изменений.

1 ответ

Этот тип артефакта может быть вызван переполнением или иным образом превышением границ параметров до или в вашей функции цветового отображения, или, возможно, с помощью некоторой функции (log?), Возвращающей значения NaN. Вы могли бы найти это, вставив некоторые утверждения для значений, выходящих за пределы допустимого или недопустимых значений.

Другие вопросы по тегам