├── .gitignore ├── CMakeLists.txt ├── Doxyfile ├── LICENSE ├── README.md ├── bin └── spectrogram.exe ├── cmake-modules ├── FindFFTW3.cmake ├── FindLibSamplerate.cmake ├── FindMad.cmake └── FindSndfile.cmake ├── examples ├── blue_danube-hudba.mp3 └── hal_9000-rec.mp3 ├── fft.cpp ├── fft.hpp ├── main.cpp ├── mainwindow.cpp ├── mainwindow.hpp ├── mainwindow.ui ├── palettes ├── fiery.png ├── temperatures.png └── wb.png ├── scripts └── spectrogram-video.py ├── soundfile.cpp ├── soundfile.hpp ├── spectrogram.cpp ├── spectrogram.hpp ├── types.hpp └── viewer ├── cursorlabel.py ├── ui_viewer.py ├── viewer.py └── viewer.ui /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Compiled Dynamic libraries 12 | *.so 13 | *.dylib 14 | *.dll 15 | 16 | # Fortran module files 17 | *.mod 18 | 19 | # Compiled Static libraries 20 | *.lai 21 | *.la 22 | *.a 23 | *.lib 24 | 25 | # Executables 26 | *.out 27 | *.app 28 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | CMAKE_MINIMUM_REQUIRED(VERSION 2.6) 2 | 3 | #SET(CMAKE_VERBOSE_MAKEFILE on) 4 | 5 | PROJECT(spectrogram) 6 | INCLUDE_DIRECTORIES( ${CMAKE_BINARY_DIR} ) 7 | 8 | ### extra find modules for cmake 9 | 10 | LIST(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake-modules) 11 | 12 | ### files 13 | 14 | SET(spectrogram_SOURCES 15 | main.cpp 16 | mainwindow.cpp 17 | spectrogram.cpp 18 | soundfile.cpp 19 | fft.cpp 20 | ) 21 | SET(spectrogram_MOC_HEADERS 22 | mainwindow.hpp 23 | spectrogram.hpp 24 | ) 25 | SET(spectrogram_UIS 26 | mainwindow.ui 27 | ) 28 | SET(spectrogram_RCS 29 | # none (yet?) 30 | ) 31 | 32 | ### compiler options 33 | 34 | # enable warnings 35 | IF (CMAKE_COMPILER_IS_GNUCXX) 36 | ADD_DEFINITIONS("-Wall -Wextra") 37 | ENDIF (CMAKE_COMPILER_IS_GNUCXX) 38 | 39 | ### find and use Qt4 40 | 41 | # SET(QT_USE_QTXML TRUE) # XXX moduly takhle 42 | FIND_PACKAGE(Qt4 REQUIRED) 43 | ## Qt macros 44 | INCLUDE(${QT_USE_FILE}) 45 | QT4_ADD_RESOURCES( spectrogram_RC_SOURCES ${spectrogram_RCS} ) 46 | # this will run uic on .ui files: 47 | QT4_WRAP_UI( spectrogram_UI_HEADERS ${spectrogram_UIS} ) 48 | # this will run moc: 49 | QT4_WRAP_CPP( spectrogram_MOC_SOURCES ${spectrogram_MOC_HEADERS} ) 50 | INCLUDE_DIRECTORIES(${QT_INCLUDE_DIR}) 51 | 52 | ### find libsndfile 53 | 54 | FIND_PACKAGE(Sndfile REQUIRED) 55 | INCLUDE_DIRECTORIES(${SNDFILE_INCLUDE_DIR}) 56 | 57 | ### find libsamplerate 58 | 59 | FIND_PACKAGE(LibSamplerate REQUIRED) 60 | INCLUDE_DIRECTORIES(${SAMPLERATE_INCLUDE_DIR}) 61 | 62 | ### find fftw3 63 | 64 | SET(FFTW3_FIND_QUIETLY TRUE) 65 | FIND_PACKAGE(FFTW3 REQUIRED) 66 | INCLUDE_DIRECTORIES(${FFTW3_INCLUDES}) 67 | 68 | ### find libmad 69 | 70 | FIND_PACKAGE(Mad REQUIRED) 71 | INCLUDE_DIRECTORIES(${MAD_INCLUDE_DIRS}) 72 | 73 | ### included libs 74 | #ADD_SUBDIRECTORY(fftw++) 75 | 76 | #### find boost 77 | #FIND_PACKAGE(Boost COMPONENTS filesystem REQUIRED) 78 | #INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIR}) 79 | 80 | ### debug option 81 | 82 | OPTION(spectrogram_DEBUG "Build the project with debugging code" ON) # XXX ->OFF 83 | IF(spectrogram_DEBUG) 84 | SET(CMAKE_BUILD_TYPE Debug) 85 | SET_SOURCE_FILES_PROPERTIES(${spectrogram_SOURCES} COMPILE_FLAGS -DDEBUG) 86 | ELSE(spectrogram_DEBUG) 87 | SET(CMAKE_BUILD_TYPE Release) 88 | IF(WIN32) 89 | # prevents the console window from opening on windows 90 | SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mwindows") 91 | ENDIF(WIN32) 92 | ENDIF(spectrogram_DEBUG) 93 | 94 | ### finish 95 | 96 | ADD_EXECUTABLE(spectrogram ${spectrogram_SOURCES} ${spectrogram_MOC_SOURCES} ${spectrogram_UI_HEADERS} ${spectrogram_RC_SOURCES}) 97 | 98 | TARGET_LINK_LIBRARIES(spectrogram ${QT_LIBRARIES} ${SNDFILE_LIBRARIES} ${FFTW3_LIBRARIES} ${SAMPLERATE_LIBRARIES} ${MAD_LIBRARIES}) 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | spectrogram 2 | =========== 3 | 4 | This GUI program can generate spectrograms from sound files and synthesize spectrograms back into sound (with large loss of fidelity). 5 | 6 | The program can be built on Linux or you can use the Windows binary (bin/spectrogram.exe). The program was a part of my 2010 bachelor's thesis and there will be no more development. It was inspired heavily by the ARSS program: http://arss.sourceforge.net/ 7 | 8 | ![screenshot](http://krajj7.github.io/spectrogram/spectrogram.png) 9 | 10 | ## Spectrogram examples 11 | 12 | Some example spectrograms of classical music made with this program can be found on my youtube channel: 13 | 14 | https://www.youtube.com/watch?v=Txp-pHU2K6w 15 | 16 | https://www.youtube.com/watch?v=XJ6XXjAOvmY 17 | 18 | https://www.youtube.com/watch?v=bGtup1qCHW0 19 | 20 | There are no demos of the synthesis functionality (sound from spectrogram), but it works a lot like this: http://arss.sourceforge.net/examples.shtml 21 | 22 | ### How to make a video spectrogram like the examples 23 | 24 | To turn the spectrogram image generated by this program into a video there is a quick & dirty script in python (scripts/spectrogram-video.py) to generate each frame as a png file (takes quite long and makes many files) and then you can use this ffmpeg command (on Linux) to put it together into a video with sound (example for 25 fps): 25 | 26 | `ffmpeg -f image2 -r 25 -i ./%06d.png -i soundfile.mp3 -b 5000k -vb 5000k -acodec copy out.mp4` 27 | 28 | ## Documentation 29 | 30 | At http://krajj7.github.io/spectrogram 31 | -------------------------------------------------------------------------------- /bin/spectrogram.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krajj7/spectrogram/f640537617d49ac368b48ea7e3ec372bf44bae10/bin/spectrogram.exe -------------------------------------------------------------------------------- /cmake-modules/FindFFTW3.cmake: -------------------------------------------------------------------------------- 1 | #+-----------------------------------------------------------------------------+ 2 | #| Copyright (C) 2007 | 3 | #| Lars B"ahren (bahren@astron.nl) | 4 | #| | 5 | #| This program is free software; you can redistribute it and/or modify | 6 | #| it under the terms of the GNU General Public License as published by | 7 | #| the Free Software Foundation; either version 2 of the License, or | 8 | #| (at your option) any later version. | 9 | #| | 10 | #| This program is distributed in the hope that it will be useful, | 11 | #| but WITHOUT ANY WARRANTY; without even the implied warranty of | 12 | #| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 13 | #| GNU General Public License for more details. | 14 | #| | 15 | #| You should have received a copy of the GNU General Public License | 16 | #| along with this program; if not, write to the | 17 | #| Free Software Foundation, Inc., | 18 | #| 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | 19 | #+-----------------------------------------------------------------------------+ 20 | # modified version. 21 | 22 | # - Check for the presence of FFTW3 23 | # 24 | # The following variables are set when FFTW3 is found: 25 | # HAVE_FFTW3 = Set to true, if all components of FFTW3 have been found. 26 | # FFTW3_INCLUDES = Include path for the header files of FFTW3 27 | # FFTW3_LIBRARIES = Link these to use FFTW3 28 | 29 | ## ----------------------------------------------------------------------------- 30 | ## Search locations 31 | 32 | #include (CMakeSettings) 33 | 34 | ## ----------------------------------------------------------------------------- 35 | ## Check for the header files 36 | 37 | FIND_PATH (FFTW3_INCLUDES fftw3.h 38 | PATHS 39 | ${include_locations} 40 | /opt/aips++/local/include 41 | ) 42 | 43 | ## ----------------------------------------------------------------------------- 44 | ## Check for the library 45 | 46 | FIND_LIBRARY (FFTW3_LIBRARIES fftw3f fftwf 47 | PATHS 48 | ${lib_locations} 49 | /opt/aips++/local/lib 50 | ) 51 | 52 | ## ----------------------------------------------------------------------------- 53 | ## Actions taken when all components have been found 54 | 55 | IF (FFTW3_INCLUDES AND FFTW3_LIBRARIES) 56 | SET (HAVE_FFTW3 TRUE) 57 | ELSE (FFTW3_INCLUDES AND FFTW3_LIBRARIES) 58 | SET (HAVE_FFTW3 FALSE) 59 | IF (NOT FFTW3_FIND_QUIETLY) 60 | IF (NOT FFTW3_INCLUDES) 61 | MESSAGE (STATUS "Unable to find FFTW3 header files!") 62 | ENDIF (NOT FFTW3_INCLUDES) 63 | IF (NOT FFTW3_LIBRARIES) 64 | MESSAGE (STATUS "Unable to find FFTW3 library files!") 65 | ENDIF (NOT FFTW3_LIBRARIES) 66 | ENDIF (NOT FFTW3_FIND_QUIETLY) 67 | ENDIF (FFTW3_INCLUDES AND FFTW3_LIBRARIES) 68 | 69 | IF (HAVE_FFTW3) 70 | IF (NOT FFTW3_FIND_QUIETLY) 71 | MESSAGE (STATUS "Found components for FFTW3") 72 | MESSAGE (STATUS "FFTW3_INCLUDES = ${FFTW3_INCLUDES}") 73 | MESSAGE (STATUS "FFTW3_LIBRARIES = ${FFTW3_LIBRARIES}") 74 | ENDIF (NOT FFTW3_FIND_QUIETLY) 75 | ELSE (HAVE_FFTW3) 76 | IF (FFTW3_FIND_REQUIRED) 77 | MESSAGE (FATAL_ERROR "Could not find FFTW3!") 78 | ENDIF (FFTW3_FIND_REQUIRED) 79 | ENDIF (HAVE_FFTW3) 80 | 81 | ## ------------------------------------------------------------------------------ 82 | ## Mark as advanced ... 83 | 84 | -------------------------------------------------------------------------------- /cmake-modules/FindLibSamplerate.cmake: -------------------------------------------------------------------------------- 1 | # - Find SAMPLERATE 2 | # Find the native SAMPLERATE includes and libraries 3 | # 4 | # SAMPLERATE_INCLUDE_DIR - where to find SAMPLERATE.h, etc. 5 | # SAMPLERATE_LIBRARIES - List of libraries when using libSAMPLERATE. 6 | # SAMPLERATE_FOUND - True if libSAMPLERATE found. 7 | 8 | if(SAMPLERATE_INCLUDE_DIR) 9 | # Already in cache, be silent 10 | set(SAMPLERATE_FIND_QUIETLY TRUE) 11 | endif(SAMPLERATE_INCLUDE_DIR) 12 | 13 | find_path(SAMPLERATE_INCLUDE_DIR samplerate.h) 14 | 15 | find_library(SAMPLERATE_LIBRARY NAMES samplerate) 16 | 17 | # Handle the QUIETLY and REQUIRED arguments and set SAMPLERATE_FOUND to TRUE if 18 | # all listed variables are TRUE. 19 | include(FindPackageHandleStandardArgs) 20 | find_package_handle_standard_args(SAMPLERATE DEFAULT_MSG 21 | SAMPLERATE_INCLUDE_DIR SAMPLERATE_LIBRARY) 22 | 23 | if(SAMPLERATE_FOUND) 24 | set(SAMPLERATE_LIBRARIES ${SAMPLERATE_LIBRARY}) 25 | else(SAMPLERATE_FOUND) 26 | set(SAMPLERATE_LIBRARIES) 27 | endif(SAMPLERATE_FOUND) 28 | 29 | #mark_as_advanced(SAMPLERATE_INCLUDE_DIR SAMPLERATE_LIBRARY) 30 | -------------------------------------------------------------------------------- /cmake-modules/FindMad.cmake: -------------------------------------------------------------------------------- 1 | # MAD_INCLUDE_DIRS - where to find mad/mad.h, etc. 2 | # MAD_LIBRARIES - List of libraries when using mad. 3 | # MAD_FOUND - True if mad found. 4 | 5 | # Look for the header file. 6 | FIND_PATH(MAD_INCLUDE_DIR NAMES mad.h) 7 | #MARK_AS_ADVANCED(MAD_INCLUDE_DIR) 8 | 9 | # Look for the library. 10 | FIND_LIBRARY(MAD_LIBRARY NAMES mad) 11 | #MARK_AS_ADVANCED(MAD_LIBRARY) 12 | 13 | # Copy the results to the output variables. 14 | IF(MAD_INCLUDE_DIR AND MAD_LIBRARY) 15 | SET(MAD_FOUND 1) 16 | SET(MAD_LIBRARIES ${MAD_LIBRARY}) 17 | SET(MAD_INCLUDE_DIRS ${MAD_INCLUDE_DIR}) 18 | ELSE(MAD_INCLUDE_DIR AND MAD_LIBRARY) 19 | SET(MAD_FOUND 0) 20 | SET(MAD_LIBRARIES) 21 | SET(MAD_INCLUDE_DIRS) 22 | ENDIF(MAD_INCLUDE_DIR AND MAD_LIBRARY) 23 | 24 | # Report the results. 25 | IF(NOT MAD_FOUND) 26 | SET(MAD_DIR_MESSAGE 27 | "libmad was not found. Make sure MAD_LIBRARY and MAD_INCLUDE_DIR are set.") 28 | IF(NOT MAD_FIND_QUIETLY) 29 | MESSAGE(STATUS "${MAD_DIR_MESSAGE}") 30 | ELSE(NOT MAD_FIND_QUIETLY) 31 | IF(MAD_FIND_REQUIRED) 32 | MESSAGE(FATAL_ERROR "${MAD_DIR_MESSAGE}") 33 | ENDIF(MAD_FIND_REQUIRED) 34 | ENDIF(NOT MAD_FIND_QUIETLY) 35 | ENDIF(NOT MAD_FOUND) 36 | 37 | -------------------------------------------------------------------------------- /cmake-modules/FindSndfile.cmake: -------------------------------------------------------------------------------- 1 | ## from the Allegro project 2 | 3 | # - Find sndfile 4 | # Find the native sndfile includes and libraries 5 | # 6 | # SNDFILE_INCLUDE_DIR - where to find sndfile.h, etc. 7 | # SNDFILE_LIBRARIES - List of libraries when using libsndfile. 8 | # SNDFILE_FOUND - True if libsndfile found. 9 | 10 | if(SNDFILE_INCLUDE_DIR) 11 | # Already in cache, be silent 12 | set(SNDFILE_FIND_QUIETLY TRUE) 13 | endif(SNDFILE_INCLUDE_DIR) 14 | 15 | find_path(SNDFILE_INCLUDE_DIR sndfile.h) 16 | 17 | find_library(SNDFILE_LIBRARY NAMES sndfile) 18 | 19 | # Handle the QUIETLY and REQUIRED arguments and set SNDFILE_FOUND to TRUE if 20 | # all listed variables are TRUE. 21 | include(FindPackageHandleStandardArgs) 22 | find_package_handle_standard_args(SNDFILE DEFAULT_MSG 23 | SNDFILE_INCLUDE_DIR SNDFILE_LIBRARY) 24 | 25 | if(SNDFILE_FOUND) 26 | set(SNDFILE_LIBRARIES ${SNDFILE_LIBRARY}) 27 | else(SNDFILE_FOUND) 28 | set(SNDFILE_LIBRARIES) 29 | endif(SNDFILE_FOUND) 30 | 31 | #mark_as_advanced(SNDFILE_INCLUDE_DIR SNDFILE_LIBRARY) 32 | -------------------------------------------------------------------------------- /examples/blue_danube-hudba.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krajj7/spectrogram/f640537617d49ac368b48ea7e3ec372bf44bae10/examples/blue_danube-hudba.mp3 -------------------------------------------------------------------------------- /examples/hal_9000-rec.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krajj7/spectrogram/f640537617d49ac368b48ea7e3ec372bf44bae10/examples/hal_9000-rec.mp3 -------------------------------------------------------------------------------- /fft.cpp: -------------------------------------------------------------------------------- 1 | #include "fft.hpp" 2 | #include 3 | 4 | namespace 5 | { 6 | /// Returns 1 if x is only made of small primes. 7 | int smallprimes(int x) 8 | { 9 | int p[3] = {2, 3, 5}; 10 | for (int i = 0; i < 3; ++i) 11 | while (x%p[i] == 0) 12 | x /= p[i]; 13 | return x; 14 | } 15 | 16 | /// Returns the next integer only made of small primes. 17 | int padded_size(int x) 18 | { 19 | while (smallprimes(x)!=1) 20 | x++; 21 | 22 | return x; 23 | } 24 | } 25 | 26 | complex_vec padded_FFT(real_vec& in) 27 | { 28 | assert(in.size() > 0); 29 | const size_t n = in.size(); 30 | const size_t padded = n > 256 ? padded_size(n) : n; 31 | in.resize(padded); 32 | 33 | complex_vec out(padded/2+1); 34 | 35 | fftwf_plan plan = fftwf_plan_dft_r2c_1d(padded, 36 | &in[0], (fftwf_complex*)&out[0], FFTW_ESTIMATE); 37 | fftwf_execute(plan); 38 | fftwf_destroy_plan(plan); 39 | 40 | in.resize(n); 41 | return out; 42 | } 43 | 44 | real_vec padded_IFFT(complex_vec& in) 45 | { 46 | assert(in.size() > 1); 47 | const size_t n = (in.size()-1)*2; 48 | const size_t padded = n > 256 ? padded_size(n) : n; 49 | in.resize(padded/2+1); 50 | 51 | real_vec out(padded); 52 | 53 | // note: fftw3 destroys the input array for c2r transform 54 | fftwf_plan plan = fftwf_plan_dft_c2r_1d(padded, 55 | (fftwf_complex*)&in[0], &out[0], FFTW_ESTIMATE); 56 | fftwf_execute(plan); 57 | fftwf_destroy_plan(plan); 58 | 59 | in.resize(n/2+1); 60 | return out; 61 | } 62 | -------------------------------------------------------------------------------- /fft.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FFT_HPP 2 | #define FFT_HPP 3 | 4 | /** \file fft.hpp 5 | * \brief Contains utility functions for performing the fast fourier transform and its inverse. 6 | * 7 | * It uses the FFTW3 library to perform the transforms. For better performance, the functions temporarily change the size of the input vector by padding it with zeros to a size that can be expressed as a product of small primes, that is 2^x * 3^y * 5^z. 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include "types.hpp" 14 | 15 | /// Performs a fast fourier transform. 16 | /** The input vector is padded with zeros for better performance and shrunk again to original size when the transform is done. */ 17 | complex_vec padded_FFT(real_vec& in); 18 | /// Performs a fast inverse fourier transform. 19 | /** The input vector is destroyed in the process! */ 20 | real_vec padded_IFFT(complex_vec& in); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | /** \mainpage %Spectrogram -- documentation 2 | * This program can generate spectrograms from sound files and synthesize 3 | * spectrograms back into sound. 4 | * 5 | * The manual is divided into two parts: 6 | * \li \subpage user 7 | * \li \subpage prog 8 | */ 9 | /** \page prog Compilation and code overview 10 | * \section compiling Compiling the program 11 | * The program can be compiled with g++ on Linux or with mingw on Windows. 12 | * 13 | * \subsection deps Dependencies 14 | * The project uses the CMake build system: http://www.cmake.org 15 | * 16 | * The development versions of the following libraries need to be usable 17 | * before compiling: 18 | * \li Qt4 (used for the GUI): http://www.qtsoftware.com/products 19 | * \li FFTW (the single-precision version, used for fast fourier transform): 20 | * http://www.fftw.org 21 | * \li SRC (aka libsamplerate, used for audio resampling): 22 | * http://www.mega-nerd.com/SRC/ 23 | * \li libsndfile (for many audio formats support): 24 | * http://mega-nerd.com/libsndfile 25 | * \li MAD (for mp3 support): http://www.underbit.com/products/mad/ 26 | * 27 | * In Debian for example, you can run the following command to install all the 28 | * dependencies: apt-get install cmake libqt4-dev libfftw3-dev 29 | * libsndfile-dev libsamplerate-dev libmad0-dev 30 | * 31 | * For flac and ogg format support libsndfile version 1.0.18 or higher with 32 | * libflac and libogg plugins built-in has to be used. 33 | * 34 | * \subsection Linux Linux 35 | * To prepare the makefile, go to the \c build directory in the source tree and 36 | * type cmake .. 37 | * 38 | * If there are no errors (like missing libraries), you can build the program 39 | * by typing \c make 40 | * 41 | * The executable \c spectrogram will appear in the \c build directory. 42 | * 43 | * \subsection Windows Windows 44 | * Besides the dependencies you should have MinGW and MSYS installed. 45 | * 46 | * Once you have all the dependencies configured and compiled with mingw, start 47 | * the cmake-gui program and enter the source code directory and use the \c 48 | * build directory to build the binaries in. 49 | * 50 | * Then hit "Configure" and use the "MSYS Makefiles" generator. For each 51 | * library that wasn't found automatically, enter the path manually and 52 | * continue with the configuration process. 53 | * 54 | * Once configuring is done, press "Generate" 55 | * 56 | * A Makefile now appears in the build directory. Navigate to that directory in the MSYS shell and type \c make to compile the program. 57 | * 58 | * \section code Code overview 59 | * The most interesting class of the program is Spectrogram, it holds the 60 | * parameters for a spectrogram and performs analysis (turning sounds to 61 | * images) and synthesis (turning images to sounds). 62 | * 63 | * The MainWindow class handles the GUI. The MainWindow::ui member is used to 64 | * access the widgets as designed in mainwindow.ui created with Qt Designer. 65 | * In the main window the user can specify parameters for the spectrogram and 66 | * supply audio data or a spectrogram to work with. The analysis and synthesis 67 | * itself is then performed in a separate thread to keep the GUI responsive. 68 | * 69 | * The Soundfile class provides abstraction for working with audio files. It 70 | * implements high-level operations like reading a channel of audio data from a 71 | * given file. It aggregates all implementations of SndfileData. 72 | * 73 | * SndfileData is an abstract class whose implementations perform low-level 74 | * format-dependent operations. Different libraries can be used to implement 75 | * the class and thus provide support for multiple audio formats. 76 | * 77 | * For more details, see documentation of these classes. 78 | */ 79 | 80 | /** \page user User documentation 81 | * \section Introduction 82 | * All the functionality of the program is available from the main window. In 83 | * this window you can configure parameters for spectrogram generation or 84 | * synthesis and supply the data. 85 | * 86 | * Progress of long operations is indicated on the right side of the window. 87 | * The \c X button can be used to interrupt an operation that is taking too 88 | * long. 89 | * 90 | * \section analysis Spectrogram analysis 91 | * To turn a sound to a spectrogram, select the sound file in the upper right 92 | * part of the window. Many sound files are stereo, which will appear as two 93 | * channels you can choose from. The "Length" and "Samplerate" indicators are 94 | * purely informative. 95 | * 96 | * Depending on the purpose of the spectrogram and the nature of the supplied 97 | * audio data, different parameters are optimal. The meaning of the main 98 | * parameters is explained below. If you hover your mouse over a parameter 99 | * dial, an explanaition appears as a tooltip. 100 | * 101 | * \li Frequency scale: This setting determines the type of the 102 | * frequency (vertical) axis. Human hearing is logarithmic in nature. For 103 | * music and other audio that contains a high range of frequencies, logarithmic 104 | * frequency scale is a good choice. For speech or artificial sounds, linear 105 | * scale can also be used with good results. 106 | * \li Intensity scale This affects the mapping of sound intensity to 107 | * pixel brightness. The logarithmic setting is better for sounds with high 108 | * range of loundess where a linear setting would make the spectrogram too 109 | * dark. 110 | * \li Base frequency For a logarithmic frequency spectrogram, the 111 | * first band (the row of the spectrogram) will be centered at this frequency. 112 | * For a linear frequency spectrogram, the first band starts at the this 113 | * frequency. 114 | * \li Maximum frequency Sets the top frequency displayed in the 115 | * spectrogram. For speech, value of about 8000 Hz can be sufficient. 116 | * \li Pixels per second Determines the time resolution of the 117 | * spectrogram. The larger the value, the wider the spectrogram. For 118 | * synthesis, 100 or above is recommended. For viewing, 50 can be sufficient 119 | * and make the spectrogram more "compact" with long sound samples. 120 | * \li Brightness correction Some spectrograms can be very dark even 121 | * with logarithmic intensity scale. Using the square root brightness 122 | * correction will make the spectrogram easier to read, but may affect 123 | * synthesis quality. 124 | * \li Bandwidth Each horizontal band of the spectrogram will be as 125 | * wide as set here. Lower value means more detail in the frequency domain, 126 | * but less detail in the time domain. 127 | * \li Window function: Window function is applied to the frequency 128 | * intervals of the given bandwidth to lessen artifacts on their edges. The 129 | * Hann window is a good general choice. 130 | * \li Overlap: Larger overlap gives more detail in the frequency 131 | * domain and makes the spectrogram taller. If no window function is used, it 132 | * can be set to zero, otherwise setting at least 60% overlap is recommended. 133 | * \li %Palette: Shows the colors in which the spectrogram will be 134 | * drawn. You can supply your own palette from an image, in that case the 135 | * first row of pixels of the image is used. For synthesis, the colors in the 136 | * palette shouldn't repeat to make the intensity -> color mapping unambiguous. 137 | * 138 | * Once you are happy with the parameters, click the "Make spectrogram" 139 | * button. A preview will appear and you can save the resulting image. 140 | * 141 | * \section synthesis Spectrogram synthesis 142 | * To turn a spectrogram back into sound, first select the spectrogram image in 143 | * the lower right. 144 | * 145 | * The parameters and palette of the spectrogram should be set to the same 146 | * values as were used for its generation. If the spectrogram was generated by 147 | * this program, the parameters will be loaded automatically from metadata 148 | * saved in the image. You can work with spectrograms from different sources 149 | * too, in which case you need to know or guess the parameters with which they 150 | * have been created. 151 | * 152 | * Two modes of synthesis are provided: 153 | * \li Sine synthesis is fast and produces decent results. 154 | * \li Noise synthesis is slower but may give better results for "busy" 155 | * spectrograms. 156 | * 157 | * When the parameters are set, you can press "Make sound" to synthesize the 158 | * chosen spectrogram. After it's finished, you can save the resulting sound 159 | * file. 160 | * 161 | * \section formats Supported file formats 162 | * The program supports most commonly used sound file formats like mp3, wav, 163 | * flac and ogg. For the last two the build has to be linked with the 164 | * libsndfile library version 1.0.18 or higher with libflac and libogg 165 | * plugins built-in. 166 | * See http://www.mega-nerd.com/libsndfile/#Features for a full list of 167 | * sound file formats supported via libsndfile. MP3 is supported via libmad. 168 | * 169 | * Certain MP3 files with variable bitrate can display the wrong length in the 170 | * GUI. They can still be used to generate spectrograms without problems. 171 | * 172 | * Most commonly used image formats are supported, for example png, bmp, tiff, 173 | * xpm, jpg (read only), gif (read only) and others. 174 | * See http://doc.trolltech.com/4.6/qimage.html#reading-and-writing-image-files 175 | * for a full list of supported image formats. 176 | */ 177 | 178 | /** \file main.cpp 179 | * \brief Code to start up the application. Also contains the main 180 | * documentation. 181 | */ 182 | 183 | #include 184 | #include 185 | #include 186 | 187 | #include "soundfile.hpp" 188 | #include "mainwindow.hpp" 189 | #include "spectrogram.hpp" 190 | 191 | // testing functions 192 | namespace 193 | { 194 | void image_test() 195 | { 196 | //Soundfile file("/home/jan/ads/violin.ogg"); 197 | Soundfile file("/home/jan/music/Windir/1999-Arntor/01-Byrjing.mp3"); 198 | Spectrogram spec; 199 | //spec.palette = Palette("/home/jan/spectrogram/palettes/fiery.png"); 200 | real_vec signal = file.read_channel(0); 201 | QImage out = spec.to_image(signal, file.data().samplerate()); 202 | out.save("out.png"); 203 | } 204 | 205 | void synt_test() 206 | { 207 | QImage img = QImage("/home/jan/spectrogram/out.png"); 208 | //QImage img = QImage("/home/jan/or/vion-xx6.png"); 209 | assert(!img.isNull()); 210 | Spectrogram spec; 211 | real_vec data = spec.synthetize(img, 44100, SYNTHESIS_SINE); 212 | std::cout << "hotovo: "< 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "mainwindow.hpp" 11 | #include 12 | #include 13 | 14 | namespace 15 | { 16 | void setCombo(QComboBox* combo, int value) 17 | { 18 | for (int index = 0; index < combo->count(); ++index) 19 | if (combo->itemData(index) == value) 20 | combo->setCurrentIndex(index); 21 | } 22 | } 23 | 24 | MainWindow::MainWindow() 25 | { 26 | ui.setupUi(this); 27 | 28 | QCompleter* completer = new QCompleter(ui.locationEdit); 29 | completer->setModel(new QDirModel(completer)); 30 | ui.locationEdit->setCompleter(completer); 31 | ui.speclocEdit->setCompleter(completer); 32 | 33 | resetSoundfile(); 34 | connect(ui.locationEdit, SIGNAL(editingFinished()), 35 | this, SLOT(loadSoundfile())); 36 | connect(ui.locationButton, SIGNAL(clicked()), 37 | this, SLOT(chooseSoundfile())); 38 | 39 | connect(ui.paletteButton, SIGNAL(clicked()), this, SLOT(choosePalette())); 40 | 41 | connect(ui.specSaveAsButton, SIGNAL(clicked()), this, SLOT(saveImage())); 42 | connect(ui.speclocButton, SIGNAL(clicked()), this, SLOT(chooseImage())); 43 | connect(ui.makeButton, SIGNAL(clicked()), this, SLOT(makeSpectrogram())); 44 | 45 | //connect(ui.soundSaveAsButton,SIGNAL(clicked()),this, SLOT(saveSoundfile())); 46 | connect(ui.makeSoundButton, SIGNAL(clicked()), this, SLOT(makeSound())); 47 | 48 | ui.intensityCombo->addItem("logarithmic", (int)SCALE_LOGARITHMIC); 49 | ui.intensityCombo->addItem("linear", (int)SCALE_LINEAR); 50 | 51 | ui.frequencyCombo->addItem("logarithmic", (int)SCALE_LOGARITHMIC); 52 | ui.frequencyCombo->addItem("linear", (int)SCALE_LINEAR); 53 | connect(ui.frequencyCombo, SIGNAL(currentIndexChanged(int)), 54 | this, SLOT(setFilterUnits(int))); 55 | 56 | ui.windowCombo->addItem("Hann", (int)WINDOW_HANN); 57 | ui.windowCombo->addItem("Blackman", (int)WINDOW_BLACKMAN); 58 | ui.windowCombo->addItem("Triangular", (int)WINDOW_TRIANGULAR); 59 | ui.windowCombo->addItem("Rectangular (none)", (int)WINDOW_RECTANGULAR); 60 | 61 | ui.syntCombo->addItem("sine", (int)SYNTHESIS_SINE); 62 | ui.syntCombo->addItem("noise", (int)SYNTHESIS_NOISE); 63 | 64 | ui.brightCombo->addItem("none", (int)BRIGHT_NONE); 65 | ui.brightCombo->addItem("square root", (int)BRIGHT_SQRT); 66 | 67 | spectrogram = new Spectrogram(this); 68 | connect(ui.cancelButton, SIGNAL(clicked()), spectrogram, SLOT(cancel())); 69 | connect(spectrogram, SIGNAL(progress(int)), 70 | ui.specProgress, SLOT(setValue(int))); 71 | connect(spectrogram, SIGNAL(status(const QString&)), 72 | ui.specStatus, SLOT(setText(const QString&))); 73 | setValues(); 74 | 75 | image_watcher = new QFutureWatcher(this); 76 | connect(image_watcher, SIGNAL(finished()), this, SLOT(newSpectrogram())); 77 | sound_watcher = new QFutureWatcher(this); 78 | connect(sound_watcher, SIGNAL(finished()), this, SLOT(newSound())); 79 | 80 | ui.lengthEdit->setDisplayFormat("hh:mm:ss"); 81 | 82 | idleState(); 83 | } 84 | 85 | void MainWindow::resetSoundfile() 86 | { 87 | soundfile.reset(); 88 | ui.lengthEdit->setTime(QTime(0,0,0)); 89 | ui.channelsEdit->setText("0"); 90 | ui.channelSpin->setMaximum(0); 91 | ui.samplerateSpin->setValue(0); 92 | } 93 | 94 | void MainWindow::loadSoundfile() 95 | { 96 | const QString& filename = ui.locationEdit->text(); 97 | if (filename.isEmpty()) 98 | return; 99 | soundfile.load(filename); 100 | if (!soundfileOk()) 101 | { 102 | QString error; 103 | QTextStream serror(&error); 104 | serror << "The specified file is not readable or not supported."; 105 | if (!soundfile.error().isEmpty()) 106 | serror << "\n\n" << soundfile.error(); 107 | QMessageBox::warning(this, "Invalid file", error); 108 | return; 109 | } 110 | updateSoundfile(); 111 | } 112 | 113 | void MainWindow::updateSoundfile() 114 | { 115 | if (soundfileOk()) 116 | { 117 | ui.lengthEdit->setTime(QTime().addSecs((int)soundfile.data().length())); 118 | ui.channelSpin->setMinimum(1); 119 | ui.channelSpin->setMaximum(soundfile.data().channels()); 120 | ui.channelsEdit->setText(QString::number(soundfile.data().channels())); 121 | ui.samplerateSpin->setValue(soundfile.data().samplerate()); 122 | } 123 | else 124 | resetSoundfile(); 125 | } 126 | 127 | bool MainWindow::soundfileOk() 128 | { 129 | return soundfile.valid(); 130 | } 131 | 132 | void MainWindow::choosePalette() 133 | { 134 | QString filename = QFileDialog::getOpenFileName(this, 135 | "Choose the palette image", ".", 136 | "Images (*.png *.jpg *.bmp *.gif);;All files (*.*)"); 137 | if (filename == NULL) 138 | return; 139 | QImage img(filename); 140 | if (img.isNull()) 141 | { 142 | QMessageBox::warning(this, "Invalid image", 143 | "The picture format was not recognised."); 144 | return; 145 | } 146 | spectrogram->palette = Palette(img); 147 | updatePalette(); 148 | } 149 | 150 | void MainWindow::chooseSoundfile() 151 | { 152 | QString filename = QFileDialog::getOpenFileName(this, 153 | "Choose the sound file", ".", 154 | "Sound files (*.wav *.mp3 *.ogg *.flac);;All files (*.*)"); 155 | if (filename == NULL) 156 | return; 157 | ui.locationEdit->setText(filename); 158 | loadSoundfile(); 159 | } 160 | 161 | void MainWindow::updatePalette() 162 | { 163 | ui.paletteLabel->setPixmap(spectrogram->palette.preview( 164 | spectrogram->palette.numColors(), ui.paletteLabel->height())); 165 | } 166 | 167 | void MainWindow::saveImage() 168 | { 169 | if (image.isNull()) 170 | { 171 | QMessageBox::warning(this, "Couldn't save file", 172 | "There is nothing to save yet."); 173 | return; 174 | } 175 | QString filename = 176 | QFileDialog::getSaveFileName(this, "Save spectrogram", 177 | "spectrogram.png", "Images (*.png *.xpm)"); 178 | if (filename == NULL) 179 | return; 180 | if (filename.endsWith(".jpg") || filename.endsWith(".JPG")) 181 | { 182 | QMessageBox::warning(this, "Couldn't save file", 183 | "JPG is not supported for writing. As a lossy compression format, it is a poor choice for spectrograms anyway."); 184 | return; 185 | } 186 | if (!filename.contains(".")) 187 | filename.append(".png"); 188 | const bool worked = image.save(filename); 189 | if (!worked) 190 | { 191 | QMessageBox::warning(this, "Couldn't save file", 192 | "The file could not be saved at the specified location, or you specified a not supported format extension."); 193 | return; 194 | } 195 | ui.speclocEdit->setText(filename); 196 | } 197 | 198 | void MainWindow::makeSpectrogram() 199 | { 200 | if (!soundfileOk()) 201 | { 202 | QMessageBox::warning(this, "No sound file", 203 | "Choose a valid sound file first."); 204 | return; 205 | } 206 | 207 | loadValues(); 208 | 209 | if (!checkAnalysisValues()) 210 | return; 211 | 212 | workingState(); 213 | 214 | const int channelidx = ui.channelSpin->value()-1; 215 | ui.specStatus->setText("Loading sound file"); 216 | qApp->processEvents(); 217 | real_vec signal = soundfile.read_channel(channelidx); 218 | if (!signal.size()) 219 | { 220 | QMessageBox::warning(this, "Error", "Error reading sound file."); 221 | idleState(); 222 | return; 223 | } 224 | 225 | QFuture future = QtConcurrent::run(spectrogram, 226 | &Spectrogram::to_image, signal, soundfile.data().samplerate()); 227 | image_watcher->setFuture(future); 228 | } 229 | 230 | void MainWindow::loadValues() 231 | { 232 | spectrogram->bandwidth = ui.bandwidthSpin->value(); 233 | spectrogram->basefreq = ui.basefreqSpin->value(); 234 | spectrogram->maxfreq = ui.maxfreqSpin->value(); 235 | spectrogram->overlap = ui.overlapSpin->value()/100; 236 | spectrogram->pixpersec = ui.ppsSpin->value(); 237 | spectrogram->window = (Window)ui.windowCombo-> 238 | itemData(ui.windowCombo->currentIndex()).toInt(); 239 | spectrogram->frequency_axis = (AxisScale)ui.frequencyCombo-> 240 | itemData(ui.frequencyCombo->currentIndex()).toInt(); 241 | spectrogram->intensity_axis = (AxisScale)ui.intensityCombo-> 242 | itemData(ui.intensityCombo->currentIndex()).toInt(); 243 | spectrogram->correction = (BrightCorrection)ui.brightCombo-> 244 | itemData(ui.brightCombo->currentIndex()).toInt(); 245 | } 246 | 247 | void MainWindow::newSpectrogram() 248 | { 249 | if (!image_watcher->future().result().isNull()) // cancelled? 250 | { 251 | image = image_watcher->future().result(); 252 | ui.speclocEdit->setText("unsaved"); 253 | updateImage(); 254 | } 255 | idleState(); 256 | } 257 | 258 | void MainWindow::workingState() 259 | { 260 | ui.specProgress->setValue(0); 261 | ui.specStatus->setText("Idle"); 262 | ui.cancelButton->setEnabled(true); 263 | ui.makeButton->setEnabled(false); 264 | ui.makeSoundButton->setEnabled(false); 265 | ui.speclocButton->setEnabled(false); 266 | ui.paletteButton->setEnabled(false); 267 | ui.locationButton->setEnabled(false); 268 | } 269 | 270 | void MainWindow::idleState() 271 | { 272 | ui.specProgress->setValue(0); 273 | ui.specStatus->setText("Idle"); 274 | ui.cancelButton->setEnabled(false); 275 | ui.makeButton->setEnabled(true); 276 | ui.makeSoundButton->setEnabled(true); 277 | ui.speclocButton->setEnabled(true); 278 | ui.paletteButton->setEnabled(true); 279 | ui.locationButton->setEnabled(true); 280 | } 281 | 282 | void MainWindow::setFilterUnits(int index) 283 | { 284 | AxisScale scale = (AxisScale)ui.frequencyCombo->itemData(index).toInt(); 285 | switch (scale) 286 | { 287 | case SCALE_LINEAR: 288 | ui.bandwidthSpin->setSuffix(" Hz"); 289 | break; 290 | case SCALE_LOGARITHMIC: 291 | ui.bandwidthSpin->setSuffix(" cents"); 292 | break; 293 | } 294 | } 295 | 296 | void MainWindow::chooseImage() 297 | { 298 | QString filename = QFileDialog::getOpenFileName(this, 299 | "Choose the spectrogram", ".", 300 | "Images (*.png *.jpg *.bmp *.gif);;All files (*.*)"); 301 | if (filename == NULL) 302 | return; 303 | ui.speclocEdit->setText(filename); 304 | loadImage(); 305 | } 306 | 307 | bool MainWindow::imageOk() 308 | { 309 | return !image.isNull(); 310 | } 311 | 312 | void MainWindow::loadImage() 313 | { 314 | QString filename = ui.speclocEdit->text(); 315 | if (filename.isEmpty()) 316 | return; 317 | image.load(filename); 318 | if (!imageOk()) 319 | { 320 | QMessageBox::warning(this, "Invalid file", 321 | "The specified file is not readable or not supported."); 322 | return; 323 | } 324 | QString params = image.text("Spectrogram"); 325 | if (!params.isNull()) 326 | { 327 | spectrogram->deserialize(params); 328 | setValues(); 329 | } 330 | 331 | updateImage(); 332 | } 333 | 334 | void MainWindow::resetImage() 335 | { 336 | ui.speclocEdit->setText(""); 337 | ui.sizeEdit->setText(""); 338 | ui.spectrogramLabel->setText(""); 339 | } 340 | 341 | void MainWindow::updateImage() 342 | { 343 | if (imageOk()) 344 | { 345 | if (image.width() > 30000) 346 | ui.spectrogramLabel->setText("Image too large to preview"); 347 | else 348 | ui.spectrogramLabel->setPixmap(QPixmap::fromImage(image)); 349 | QString sizetext_; 350 | QTextStream sizetext(&sizetext_); 351 | sizetext << image.width() << "x" << image.height() << " px"; 352 | ui.sizeEdit->setText(sizetext_); 353 | } 354 | else 355 | resetImage(); 356 | } 357 | 358 | void MainWindow::saveSoundfile(const real_vec& signal) 359 | { 360 | QString filename; 361 | while (filename.isNull()) 362 | { 363 | filename = QFileDialog::getSaveFileName(this, "Save sound", 364 | "synt.wav", "Sound (*.wav *.ogg *.flac)"); 365 | QMessageBox msg; 366 | msg.setText("If you don't save the sound, it will be discarded."); 367 | msg.setIcon(QMessageBox::Warning); 368 | msg.setStandardButtons(QMessageBox::Discard|QMessageBox::Save); 369 | msg.setDefaultButton(QMessageBox::Discard); 370 | msg.setEscapeButton(QMessageBox::Discard); 371 | if (filename.isNull() && msg.exec() == QMessageBox::Discard) 372 | return; 373 | } 374 | const int samplerate = 44100; 375 | //const int samplerate = ui.samplerateSpin->value(); 376 | Soundfile::writeSound(filename, signal, samplerate); 377 | ui.locationEdit->setText(filename); 378 | } 379 | 380 | void MainWindow::makeSound() 381 | { 382 | if (!imageOk()) 383 | { 384 | QMessageBox::warning(this, "No spectrogram", 385 | "Choose or generate a spectrogram first."); 386 | return; 387 | } 388 | 389 | loadValues(); 390 | if (!checkSynthesisValues()) 391 | return; 392 | 393 | workingState(); 394 | SynthesisType type = (SynthesisType)ui.syntCombo-> 395 | itemData(ui.syntCombo->currentIndex()).toInt(); 396 | //const int samplerate = ui.samplerateSpin->value(); 397 | const int samplerate = 44100; 398 | QFuture future = QtConcurrent::run(spectrogram, 399 | &Spectrogram::synthetize, image, samplerate, type); 400 | sound_watcher->setFuture(future); 401 | } 402 | 403 | void MainWindow::newSound() 404 | { 405 | if (sound_watcher->future().result().size()) // cancelled? 406 | { 407 | saveSoundfile(sound_watcher->future().result()); 408 | loadSoundfile(); 409 | } 410 | 411 | idleState(); 412 | } 413 | 414 | void MainWindow::setValues() 415 | { 416 | ui.bandwidthSpin->setValue((int)spectrogram->bandwidth); 417 | ui.basefreqSpin->setValue(spectrogram->basefreq); 418 | ui.maxfreqSpin->setValue(spectrogram->maxfreq); 419 | ui.overlapSpin->setValue(spectrogram->overlap*100); 420 | ui.ppsSpin->setValue((int)spectrogram->pixpersec); 421 | setCombo(ui.windowCombo, spectrogram->window); 422 | setCombo(ui.intensityCombo, spectrogram->intensity_axis); 423 | setCombo(ui.frequencyCombo, spectrogram->frequency_axis); 424 | setCombo(ui.brightCombo, spectrogram->correction); 425 | updatePalette(); 426 | } 427 | 428 | bool MainWindow::checkAnalysisValues() 429 | { 430 | QStringList errors; 431 | if (spectrogram->maxfreq > soundfile.data().samplerate()/2) 432 | { 433 | errors.append("Maximum frequency of the spectrogram has to be at most half the sampling frequency (aka. Nyquist frequency) of the sound file. It will be changed automatically if you continue."); 434 | spectrogram->maxfreq = soundfile.data().samplerate()/2; 435 | } 436 | if (spectrogram->frequency_axis == SCALE_LOGARITHMIC && 437 | spectrogram->basefreq == 0 ) 438 | { 439 | errors.append("Base frequency of a logarithmic spectrogram has to be larger than zero. It will be set to 27.5 hz."); 440 | spectrogram->basefreq = 27.5; 441 | } 442 | if (spectrogram->window != WINDOW_RECTANGULAR && spectrogram->overlap < 0.4) 443 | { 444 | errors.append("The specified overlap is likely insufficient for use with the selected window function."); 445 | } 446 | const size_t size = soundfile.data().length()*spectrogram->pixpersec; 447 | if (size > 30000) 448 | { 449 | errors.append(QString()); 450 | QTextStream(&errors[errors.size()-1]) << "The resulting spectrogram will be very large (" << size << " px), you may have problems viewing it. Try lowering the Pixels per second value or using a shorter sound."; 451 | } 452 | 453 | return confirmWarnings(errors); 454 | } 455 | 456 | bool MainWindow::confirmWarnings(const QStringList& errors) 457 | { 458 | if (errors.size()) 459 | { 460 | QMessageBox msg; 461 | msg.setWindowTitle("Please note..."); 462 | msg.setIcon(QMessageBox::Warning); 463 | msg.setText(errors.join("\n\n")); 464 | msg.setStandardButtons(QMessageBox::Ok|QMessageBox::Abort); 465 | msg.setDefaultButton(QMessageBox::Ok); 466 | msg.setEscapeButton(QMessageBox::Abort); 467 | int res = msg.exec(); 468 | if (res == QMessageBox::Ok) 469 | setValues(); 470 | else if (res == QMessageBox::Abort) 471 | return false; 472 | } 473 | return true; 474 | } 475 | 476 | bool MainWindow::checkSynthesisValues() 477 | { 478 | QStringList errors; 479 | size_t badcolors = 0; 480 | for (int x = 0; x < image.width(); ++x) 481 | for (int y = 0; y < image.height(); ++y) 482 | if (!spectrogram->palette.has_color(image.pixel(x,y))) 483 | ++badcolors; 484 | if (badcolors) 485 | { 486 | errors.append(QString()); 487 | QTextStream(&errors[errors.size()-1]) << "The spectrogram contains "<< badcolors << (badcolors > 1 ? " pixels":" pixel") <<" whose color is not in the selected palette. Unknown colors are assumed to be zero intensity. Synthesis quality will likely be affected."; 488 | } 489 | 490 | return confirmWarnings(errors); 491 | } 492 | 493 | -------------------------------------------------------------------------------- /mainwindow.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_HPP 2 | #define MAINWINDOW_HPP 3 | 4 | /** \file mainwindow.hpp 5 | * \brief Definitions for everything that has to do with GUI.*/ 6 | 7 | #include 8 | #include "spectrogram.hpp" 9 | #include "ui_mainwindow.h" 10 | 11 | /// Represents the main application window. 12 | class MainWindow : public QMainWindow 13 | { 14 | Q_OBJECT 15 | public: 16 | MainWindow(); 17 | private: 18 | Ui::MainWindow ui; 19 | Soundfile soundfile; 20 | bool soundfileOk(); 21 | QImage image; 22 | bool imageOk(); 23 | Spectrogram* spectrogram; 24 | void setValues(); 25 | void loadValues(); 26 | void updatePalette(); 27 | 28 | void workingState(); 29 | void idleState(); 30 | 31 | QFutureWatcher* image_watcher; 32 | QFutureWatcher* sound_watcher; 33 | private slots: 34 | void setFilterUnits(int scale); 35 | 36 | void makeSpectrogram(); 37 | bool checkAnalysisValues(); 38 | void makeSound(); 39 | bool checkSynthesisValues(); 40 | 41 | void chooseImage(); 42 | void loadImage(); 43 | void updateImage(); 44 | void resetImage(); 45 | void saveImage(); 46 | 47 | void chooseSoundfile(); 48 | void loadSoundfile(); 49 | void updateSoundfile(); 50 | void resetSoundfile(); 51 | void saveSoundfile(const real_vec& signal); 52 | 53 | void choosePalette(); 54 | 55 | //void showHelp(QWidget* parent, const QString& text) const; 56 | bool confirmWarnings(const QStringList& errors); 57 | 58 | void newSpectrogram(); 59 | void newSound(); 60 | signals: 61 | //void makeSound(Spectrogram* spectrogram, QImage image, int samplerate); 62 | void makeSpectrogram(Spectrogram* spectrogram, real_vec signal, 63 | int samplerate); 64 | }; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 652 10 | 539 11 | 12 | 13 | 14 | Spectrogram 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Spectrogram parameters 24 | 25 | 26 | 27 | 5 28 | 29 | 30 | 31 | 32 | QFormLayout::ExpandingFieldsGrow 33 | 34 | 35 | 36 | 37 | Frequency scale 38 | 39 | 40 | frequencyCombo 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 0 49 | 0 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Intensity scale 58 | 59 | 60 | intensityCombo 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 0 69 | 0 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 0 79 | 0 80 | 81 | 82 | 83 | Base frequency 84 | 85 | 86 | basefreqSpin 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 0 95 | 0 96 | 97 | 98 | 99 | <p>The first filter band will be centered at this frequency. 100 | With linear frequency scale, it may be changed to 101 | bandwidth/2 if the band would go below 0 Hz.</p> 102 | 103 | <p>For music, good values values are 27.5&#215;2<span style=" vertical-align:super;">k</span> Hz and logarithmic frequency scale, so that filter bands align nicely with musical notes.</p> 104 | 105 | 106 | Hz 107 | 108 | 109 | 44100.000000000000000 110 | 111 | 112 | 1.000000000000000 113 | 114 | 115 | 55.000000000000000 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 0 124 | 0 125 | 126 | 127 | 128 | Maximum frequency 129 | 130 | 131 | maxfreqSpin 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 0 140 | 0 141 | 142 | 143 | 144 | <p>The maximum frequency that will be displayed in the spectrogram.</p> 145 | 146 | 147 | Hz 148 | 149 | 150 | 2 151 | 152 | 153 | 44100.000000000000000 154 | 155 | 156 | 100.000000000000000 157 | 158 | 159 | 22050.000000000000000 160 | 161 | 162 | 163 | 164 | 165 | 166 | Pixels per second 167 | 168 | 169 | ppsSpin 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 0 178 | 0 179 | 180 | 181 | 182 | <p>This determines the spectrogram width. A good value is double the filter bandwidth. Greater values yield a more descriptive, but longer spectrogram. For synthesis, values above 100 are recommended.</p> 183 | 184 | 185 | 186 | 187 | 188 | 1 189 | 190 | 191 | 10000 192 | 193 | 194 | 5 195 | 196 | 197 | 150 198 | 199 | 200 | 201 | 202 | 203 | 204 | Brightness correction 205 | 206 | 207 | brightCombo 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 0 216 | 0 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Frequency-domain filters 227 | 228 | 229 | 230 | QFormLayout::ExpandingFieldsGrow 231 | 232 | 233 | 5 234 | 235 | 236 | 237 | 238 | Bandwidth 239 | 240 | 241 | bandwidthSpin 242 | 243 | 244 | 245 | 246 | 247 | 248 | Window function 249 | 250 | 251 | windowCombo 252 | 253 | 254 | 255 | 256 | 257 | 258 | Overlap 259 | 260 | 261 | overlapSpin 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 0 270 | 0 271 | 272 | 273 | 274 | <p>Larger filter bandwidth means less horizontal bands and worse frequency resolution.</p> 275 | <p style=" margin-top:10px;">In logarithmic mode, 100 Cents = <span style=" vertical-align:super;">1</span>/<span style=" vertical-align:sub;">12</span> of an octave = one semitone per band.</p> 276 | 277 | 278 | cents 279 | 280 | 281 | 1 282 | 283 | 284 | 10000 285 | 286 | 287 | 10 288 | 289 | 290 | 100 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 0 299 | 0 300 | 301 | 302 | 303 | <p>Windowing prevents artifacts from dividing the frequency domain.</p> 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 0 312 | 0 313 | 314 | 315 | 316 | <p>Windowed filters overlap to give more detail in the frequency domain.</p> 317 | 318 | 319 | % 320 | 321 | 322 | 1 323 | 324 | 325 | 0.000000000000000 326 | 327 | 328 | 99.500000000000000 329 | 330 | 331 | 80.000000000000000 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 0 342 | 343 | 344 | 345 | 346 | Palette 347 | 348 | 349 | 350 | 351 | 352 | 353 | Qt::Horizontal 354 | 355 | 356 | 357 | 10 358 | 20 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 1 368 | 0 369 | 370 | 371 | 372 | 373 | 0 374 | 25 375 | 376 | 377 | 378 | 379 | 16777215 380 | 25 381 | 382 | 383 | 384 | QFrame::StyledPanel 385 | 386 | 387 | QFrame::Sunken 388 | 389 | 390 | [palette] 391 | 392 | 393 | true 394 | 395 | 396 | paletteButton 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 25 405 | 25 406 | 407 | 408 | 409 | ... 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | Sound file 424 | 425 | 426 | 427 | 5 428 | 429 | 430 | 431 | 432 | QFormLayout::ExpandingFieldsGrow 433 | 434 | 435 | 436 | 437 | Location 438 | 439 | 440 | locationEdit 441 | 442 | 443 | 444 | 445 | 446 | 447 | 0 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 22 457 | 22 458 | 459 | 460 | 461 | ... 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | Length 471 | 472 | 473 | lengthEdit 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 0 482 | 0 483 | 484 | 485 | 486 | true 487 | 488 | 489 | QAbstractSpinBox::NoButtons 490 | 491 | 492 | false 493 | 494 | 495 | 496 | 497 | 498 | 499 | Channel 500 | 501 | 502 | channelSpin 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 1 513 | 0 514 | 515 | 516 | 517 | 1 518 | 519 | 520 | 9 521 | 522 | 523 | 524 | 525 | 526 | 527 | of 528 | 529 | 530 | channelsEdit 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 1 539 | 0 540 | 541 | 542 | 543 | 544 | 20 545 | 0 546 | 547 | 548 | 549 | true 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | Samplerate 559 | 560 | 561 | samplerateSpin 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 0 570 | 0 571 | 572 | 573 | 574 | true 575 | 576 | 577 | QAbstractSpinBox::NoButtons 578 | 579 | 580 | Hz 581 | 582 | 583 | 100000 584 | 585 | 586 | 100 587 | 588 | 589 | 44100 590 | 591 | 592 | 593 | 594 | 595 | 596 | Synthesis type 597 | 598 | 599 | syntCombo 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 0 608 | 0 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 170 626 | 0 627 | 0 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 170 637 | 0 638 | 0 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 170 648 | 0 649 | 0 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | Make sound 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | Qt::Vertical 670 | 671 | 672 | 673 | 20 674 | 40 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | Progress 683 | 684 | 685 | 686 | 1 687 | 688 | 689 | 0 690 | 691 | 692 | 5 693 | 694 | 695 | 696 | 697 | 698 | 699 | Idle 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 0 708 | 0 709 | 710 | 711 | 712 | 713 | 25 714 | 25 715 | 716 | 717 | 718 | 719 | 25 720 | 25 721 | 722 | 723 | 724 | Cancel 725 | 726 | 727 | X 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 0 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | Spectrogram 751 | 752 | 753 | 754 | 5 755 | 756 | 757 | 758 | 759 | true 760 | 761 | 762 | 0 763 | 764 | 765 | 766 | 767 | 0 768 | 769 | 770 | 771 | 772 | 773 | 4 774 | 0 775 | 776 | 777 | 778 | QFrame::StyledPanel 779 | 780 | 781 | QFrame::Sunken 782 | 783 | 784 | 1 785 | 786 | 787 | 0 788 | 789 | 790 | [spectrogram preview] 791 | 792 | 793 | true 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | Qt::Vertical 807 | 808 | 809 | 810 | 20 811 | 40 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | QFormLayout::ExpandingFieldsGrow 820 | 821 | 822 | 823 | 824 | Location 825 | 826 | 827 | speclocEdit 828 | 829 | 830 | 831 | 832 | 833 | 834 | 0 835 | 836 | 837 | 838 | 839 | 840 | 0 841 | 0 842 | 843 | 844 | 845 | true 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 22 854 | 22 855 | 856 | 857 | 858 | ... 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | Size 868 | 869 | 870 | sizeEdit 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 170 890 | 0 891 | 0 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 170 901 | 0 902 | 0 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 170 912 | 0 913 | 0 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | Make spectrogram 922 | 923 | 924 | 925 | 926 | 927 | 928 | Save as 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | frequencyCombo 944 | intensityCombo 945 | basefreqSpin 946 | maxfreqSpin 947 | ppsSpin 948 | brightCombo 949 | bandwidthSpin 950 | windowCombo 951 | overlapSpin 952 | paletteButton 953 | locationEdit 954 | locationButton 955 | lengthEdit 956 | channelSpin 957 | channelsEdit 958 | samplerateSpin 959 | syntCombo 960 | makeSoundButton 961 | cancelButton 962 | speclocEdit 963 | speclocButton 964 | sizeEdit 965 | makeButton 966 | specSaveAsButton 967 | 968 | 969 | 970 | 971 | -------------------------------------------------------------------------------- /palettes/fiery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krajj7/spectrogram/f640537617d49ac368b48ea7e3ec372bf44bae10/palettes/fiery.png -------------------------------------------------------------------------------- /palettes/temperatures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krajj7/spectrogram/f640537617d49ac368b48ea7e3ec372bf44bae10/palettes/temperatures.png -------------------------------------------------------------------------------- /palettes/wb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krajj7/spectrogram/f640537617d49ac368b48ea7e3ec372bf44bae10/palettes/wb.png -------------------------------------------------------------------------------- /scripts/spectrogram-video.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import Image, ImageDraw 3 | 4 | # this script takes an RGB picture and cuts it into frames for a scrolling video 5 | 6 | # the spectrogram.png file has to be RGB mode (watch out, the spectrogram program generates grayscale by default) and resized to 720 pixels height in advance. 7 | 8 | # a video from the frames can be generated with ffmpeg: 9 | # ffmpeg -f image2 -r 25 -i ./%06d.png -i soundfile.mp3 -b 5000k -vb 5000k -acodec copy out.mp4 10 | 11 | def main(): 12 | i = Image.open("spectrogram-rgb.png") 13 | res = (1280,720) 14 | # step size 2 means 25 fps for spectrogram with 50 pixels per second 15 | step = 4 16 | for center in xrange(step*0,i.size[0], step): 17 | left = center-res[0]/2 18 | left = max(left, 0) 19 | right = left+res[0] 20 | if right > i.size[0]-1: 21 | right = i.size[0]-1 22 | left = right-res[0] 23 | o = i.crop((left, 0, right, res[1])) 24 | draw = ImageDraw.Draw(o) 25 | xoff = center-left 26 | draw.line((center-left-1, 0, center-left-1, res[1]), (0,0,180)) 27 | draw.line((center-left, 0, center-left, res[1]), (0,0,180)) 28 | draw.line((center-left+1, 0, center-left+1, res[1]), (0,0,180)) 29 | name = str(center/step).rjust(6, "0") 30 | o.save("%s.png"%name) 31 | if center%10 == 0: 32 | print "crop %s.png"%name 33 | 34 | main() 35 | -------------------------------------------------------------------------------- /soundfile.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "soundfile.hpp" 4 | 5 | namespace 6 | { 7 | const size_t MAX_FIXED_T_VAL=std::pow(2.0,(int)(8*sizeof(mad_fixed_t)-1)-1); 8 | } 9 | 10 | QString Soundfile::writeSound(const QString& fname, const real_vec& data, 11 | int samplerate, int format) 12 | { 13 | if (format == -1) 14 | { 15 | format = guessFormat(fname); 16 | if (!format) 17 | return "Unsupported filetype for writing."; 18 | } 19 | SF_INFO check = {0, samplerate, 1, format, 0, 0}; 20 | if (!sf_format_check(&check)) 21 | return "Format didn't pass sf_format_check()"; // shouldn't happen 22 | 23 | //XXX zmeni unicode nazvy 24 | SndfileHandle file(fname.toLocal8Bit(), SFM_WRITE, format, 1, samplerate); 25 | if (!file) 26 | assert(false); 27 | if (file.error()) 28 | return file.strError(); 29 | 30 | file.writef(&data[0], data.size()); 31 | return QString(); 32 | } 33 | 34 | /// Returns the format specification guessed from the specified file extension. 35 | int Soundfile::guessFormat(const QString& filename) 36 | { 37 | assert(!filename.isNull()); 38 | if (filename.endsWith(".wav")) 39 | return SF_FORMAT_WAV|SF_FORMAT_PCM_16; 40 | else if (filename.endsWith(".ogg")) 41 | return SF_FORMAT_OGG|SF_FORMAT_VORBIS; 42 | else if (filename.endsWith(".flac")) 43 | return SF_FORMAT_FLAC|SF_FORMAT_PCM_16; 44 | return 0; 45 | } 46 | 47 | Soundfile::Soundfile() 48 | : data_(NULL) 49 | { 50 | } 51 | 52 | Soundfile::Soundfile(const QString& filename) 53 | { 54 | load(filename); 55 | } 56 | 57 | real_vec Soundfile::read_channel(int channel) 58 | { 59 | return data_->read_channel(channel); 60 | } 61 | 62 | bool Soundfile::valid() const 63 | { 64 | return data_ != NULL; 65 | } 66 | 67 | const QString& Soundfile::error() const 68 | { 69 | return error_; 70 | } 71 | 72 | void Soundfile::load(const QString& filename) 73 | { 74 | if (filename.endsWith(".mp3")) 75 | data_ = new MP3Data(filename); 76 | else 77 | data_ = new SndfileData(filename); 78 | 79 | if (!data_->valid()) 80 | { 81 | error_ = data_->error(); 82 | delete data_; 83 | data_ = NULL; 84 | } 85 | } 86 | 87 | void Soundfile::reset() 88 | { 89 | delete data_; 90 | data_ = NULL; 91 | } 92 | 93 | const SoundfileData& Soundfile::data() const 94 | { 95 | assert(data_ != NULL); 96 | return *data_; 97 | } 98 | 99 | // ---- 100 | 101 | SndfileData::SndfileData(const QString& filename) 102 | { 103 | file_ = SndfileHandle(filename.toLocal8Bit()); 104 | } 105 | 106 | bool SndfileData::valid() const 107 | { 108 | return (file_ && !file_.error()); 109 | } 110 | 111 | QString SndfileData::error() const 112 | { 113 | return file_.strError(); 114 | } 115 | 116 | size_t SndfileData::frames() const 117 | { 118 | return file_.frames(); 119 | } 120 | 121 | double SndfileData::length() const 122 | { 123 | return (double)frames()/samplerate(); 124 | } 125 | 126 | int SndfileData::channels() const 127 | { 128 | return file_.channels(); 129 | } 130 | 131 | int SndfileData::samplerate() const 132 | { 133 | return file_.samplerate(); 134 | } 135 | 136 | real_vec SndfileData::read_channel(int channel) 137 | { 138 | assert(channel < channels()); 139 | real_vec buffer(frames()*channels()); 140 | file_.readf(&buffer[0], frames()); 141 | //size_t frames_read = file_.readf(&buffer[0], frames()); 142 | //assert(frames_read == frames()); 143 | file_.seek(0, SEEK_SET); 144 | 145 | if (channels() > 1) 146 | { 147 | for (size_t i = channel, j = 0; j < frames(); i += channels(), ++j) 148 | buffer[j] = buffer[i]; 149 | buffer.resize(frames()); 150 | } 151 | return buffer; 152 | } 153 | 154 | SndfileData::~SndfileData() 155 | { 156 | } 157 | 158 | // --- 159 | 160 | MP3Data::MP3Data(const QString& fname) 161 | : frames_(0) 162 | , length_(0) 163 | , samplerate_(0) 164 | , channels_(0) 165 | , filename_(fname) 166 | { 167 | get_mp3_stats(); 168 | } 169 | 170 | void MP3Data::get_mp3_stats() 171 | { 172 | mad_stream stream; 173 | mad_header header; 174 | mad_stream_init(&stream); 175 | mad_header_init(&header); 176 | 177 | QFile file(filename_); 178 | if (!file.open(QIODevice::ReadOnly)) 179 | { 180 | error_ = "Error opening file."; 181 | goto cleanup; 182 | } 183 | { 184 | uchar* buf = file.map(0, file.size()); 185 | mad_stream_buffer(&stream, buf, file.size()); 186 | 187 | while (true) 188 | { 189 | if (mad_header_decode(&header, &stream) == -1) 190 | { 191 | if (stream.error == MAD_ERROR_LOSTSYNC) 192 | continue; 193 | else if (stream.error == MAD_ERROR_BUFLEN) 194 | break; 195 | else 196 | { 197 | error_ = "Error decoding mp3 headers!"; 198 | break; 199 | } 200 | } 201 | frames_++; 202 | length_ += header.duration.fraction; 203 | if (!samplerate_) 204 | { 205 | samplerate_ = header.samplerate; 206 | if (header.mode == MAD_MODE_SINGLE_CHANNEL) 207 | channels_ = 1; 208 | else 209 | channels_ = 2; 210 | } 211 | } 212 | length_ /= MAD_TIMER_RESOLUTION; 213 | if (!samplerate_) 214 | error_ = "Invalid mp3 file."; 215 | } 216 | cleanup: 217 | mad_stream_finish(&stream); 218 | mad_header_finish(&header); 219 | } 220 | 221 | QString MP3Data::error() const 222 | { 223 | return error_; 224 | } 225 | 226 | real_vec MP3Data::read_channel(int channel) 227 | { 228 | mad_stream stream; 229 | mad_frame frame; 230 | mad_synth synth; 231 | mad_stream_init(&stream); 232 | mad_frame_init(&frame); 233 | mad_synth_init(&synth); 234 | 235 | real_vec result; 236 | 237 | QFile file(filename_); 238 | if (!file.open(QIODevice::ReadOnly)) 239 | goto cleanup; 240 | { 241 | uchar* buf = file.map(0, file.size()); 242 | mad_stream_buffer(&stream, buf, file.size()); 243 | 244 | while (true) 245 | { 246 | if (mad_frame_decode(&frame, &stream) == -1) 247 | { 248 | if (stream.error == MAD_ERROR_LOSTSYNC) 249 | continue; 250 | else if (stream.error == MAD_ERROR_BUFLEN) 251 | break; 252 | else 253 | { 254 | error_ = "Error decoding mp3 file."; 255 | break; 256 | } 257 | } 258 | mad_synth_frame(&synth, &frame); 259 | mad_fixed_t* samples = synth.pcm.samples[channel]; 260 | for (short i = 0; i < synth.pcm.length; ++i) 261 | result.push_back((double)samples[i]/MAX_FIXED_T_VAL); 262 | } 263 | } 264 | cleanup: 265 | mad_synth_finish(&synth); 266 | mad_frame_finish(&frame); 267 | mad_stream_finish(&stream); 268 | 269 | return result; 270 | } 271 | 272 | size_t MP3Data::frames() const 273 | { 274 | return frames_; 275 | } 276 | 277 | double MP3Data::length() const//in seconds 278 | { 279 | return length_; 280 | } 281 | 282 | int MP3Data::samplerate() const 283 | { 284 | return samplerate_; 285 | } 286 | 287 | int MP3Data::channels() const 288 | { 289 | return channels_; 290 | } 291 | 292 | bool MP3Data::valid() const 293 | { 294 | return error_.isEmpty(); 295 | } 296 | -------------------------------------------------------------------------------- /soundfile.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SOUNDFILE_HPP 2 | #define SOUNDFILE_HPP 3 | 4 | /** \file soundfile.hpp 5 | * \brief Definitions for classes that work with audio file formats. */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "types.hpp" 12 | #include "mad.h" 13 | 14 | /// An abstract interface for decoding sound files. 15 | /** It provides abstraction for all low-level functions used on sound files, implementation can be different for each format. */ 16 | class SoundfileData 17 | { 18 | public: 19 | virtual ~SoundfileData() {}; 20 | /// Used to get details in case of an error. 21 | virtual QString error() const = 0; 22 | /// Loads a specified channel into a real-valued vector. 23 | virtual real_vec read_channel(int channel) = 0; 24 | /// Returns the number of audio frames in each channel. 25 | virtual size_t frames() const = 0; 26 | /// Returns the length of the audio track in seconds. 27 | virtual double length() const = 0; 28 | /// Returns the samplerate of the audio file in Hz. 29 | virtual int samplerate() const = 0; 30 | /// Returns the number of channels. 31 | virtual int channels() const = 0; 32 | /// Checks if the audio file is loaded correctly and ready for use. 33 | virtual bool valid() const = 0; 34 | }; 35 | 36 | /// Implements the SoundfileData interface using libsndfile. 37 | /** This provides support for multiple file formats: wav, ogg, flac and many others. */ 38 | class SndfileData : public SoundfileData 39 | { 40 | public: 41 | SndfileData(const QString& fname); 42 | ~SndfileData(); 43 | QString error() const; 44 | real_vec read_channel(int channel); 45 | size_t frames() const; 46 | double length() const; //in seconds 47 | int samplerate() const; 48 | int channels() const; 49 | bool valid() const; 50 | private: 51 | SndfileHandle file_; 52 | }; 53 | 54 | /// Implements the SoundfileData interface using libmad. 55 | /** This provides support for MP3 files. */ 56 | class MP3Data : public SoundfileData 57 | { 58 | public: 59 | MP3Data(const QString& fname); 60 | QString error() const; 61 | real_vec read_channel(int channel); 62 | size_t frames() const; 63 | double length() const;//in seconds 64 | int samplerate() const; 65 | int channels() const; 66 | bool valid() const; 67 | private: 68 | void get_mp3_stats(); 69 | size_t frames_; 70 | size_t length_; 71 | int samplerate_; 72 | int channels_; 73 | QString filename_; 74 | QString error_; 75 | }; 76 | 77 | /// A format-independent class that provides functionality for sound manipulation, reading and writing. 78 | /** It aggregates all implementations of SndfileData. */ 79 | class Soundfile 80 | { 81 | public: 82 | /// Writes pcm data to an audio file encoded according to the extension. 83 | /** Encoding is performed by libsndfile (no mp3 writing support). 84 | * \return a string error, or null string on success. */ 85 | static QString writeSound(const QString& fname, const real_vec& data, 86 | int samplerate, int format=-1); 87 | static int guessFormat(const QString& filename); 88 | 89 | Soundfile(); 90 | Soundfile(const QString& fname); 91 | /// Forget the loaded file. 92 | void reset(); 93 | /// Loads the specified file. 94 | void load(const QString& fname); 95 | /// If the loaded file isn't valid, this function gives the reason. 96 | const QString& error() const; 97 | /// Used to determine if a file was loaded successfully. 98 | bool valid() const; 99 | /// Read the audio data of the given channel from the loaded file. 100 | /** \return PCM data of the specified audio channel */ 101 | real_vec read_channel(int channel); 102 | /// Allows access to low-level information about the file (eg. samplerate). 103 | const SoundfileData& data() const; 104 | private: 105 | SoundfileData* data_; 106 | QString error_; 107 | }; 108 | 109 | #endif 110 | -------------------------------------------------------------------------------- /spectrogram.cpp: -------------------------------------------------------------------------------- 1 | #include "spectrogram.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "samplerate.h" 15 | 16 | namespace 17 | { 18 | float log10scale(float val) 19 | { 20 | assert(val >= 0 && val <= 1); 21 | return std::log10(1+9*val); 22 | } 23 | 24 | float log10scale_inv(float val) 25 | { 26 | assert(val >= 0 && val <= 1); 27 | return (std::pow(10, val)-1)/9; 28 | } 29 | 30 | // cent = octave/1200 31 | double cent2freq(double cents) 32 | { 33 | return std::pow(2, cents/1200); 34 | } 35 | double freq2cent(double freq) 36 | { 37 | return std::log(freq)/std::log(2)*1200; 38 | } 39 | double cent2oct(double cents) 40 | { 41 | return cents/1200; 42 | } 43 | double oct2cent(double oct) 44 | { 45 | return oct*1200; 46 | } 47 | 48 | void shift90deg(Complex& x) 49 | { 50 | x = std::conj(Complex(x.imag(), x.real())); 51 | } 52 | 53 | /// Uses libsrc to resample the input vector to a given length. 54 | real_vec resample(const real_vec& in, size_t len) 55 | { 56 | assert(len > 0); 57 | //std::cout << "resample(data size: "<= 256) 63 | return resample(resample(in, in.size()*50), len); 64 | else if (ratio <= 1.0/256) 65 | return resample(resample(in, in.size()/50), len); 66 | 67 | real_vec out(len); 68 | 69 | SRC_DATA parms = {const_cast(&in[0]), 70 | &out[0], in.size(), out.size(), 0,0,0, ratio}; 71 | src_simple(&parms, SRC_SINC_FASTEST, 1); 72 | 73 | return out; 74 | } 75 | 76 | /// Envelope detection: http://www.numerix-dsp.com/envelope.html 77 | real_vec get_envelope(complex_vec& band) 78 | { 79 | assert(band.size() > 1); 80 | 81 | // copy + phase shift 82 | complex_vec shifted(band); 83 | std::for_each(shifted.begin(), shifted.end(), shift90deg); 84 | 85 | real_vec envelope = padded_IFFT(band); 86 | real_vec shifted_signal = padded_IFFT(shifted); 87 | 88 | for (size_t i = 0; i < envelope.size(); ++i) 89 | envelope[i] = std::sqrt(envelope[i]*envelope[i] + 90 | shifted_signal[i]*shifted_signal[i]); 91 | 92 | return envelope; 93 | } 94 | 95 | double blackman_window(double x) 96 | { 97 | assert(x >= 0 && x <= 1); 98 | return std::max(0.42 - 0.5*cos(2*PI*x) + 0.08*cos(4*PI*x), 0.0); 99 | } 100 | 101 | double hann_window(double x) 102 | { 103 | assert(x >= 0 && x <= 1); 104 | return 0.5*(1-std::cos(x*2*PI)); 105 | } 106 | 107 | double triangular_window(double x) 108 | { 109 | assert(x >= 0 && x <= 1); 110 | return 1-std::abs(2*(x-0.5)); 111 | } 112 | 113 | double window_coef(double x, Window window) 114 | { 115 | assert(x >= 0 && x <= 1); 116 | if (window == WINDOW_RECTANGULAR) 117 | return 1.0; 118 | switch (window) 119 | { 120 | case WINDOW_HANN: 121 | return hann_window(x); 122 | case WINDOW_BLACKMAN: 123 | return blackman_window(x); 124 | case WINDOW_TRIANGULAR: 125 | return triangular_window(x); 126 | default: 127 | assert(false); 128 | } 129 | } 130 | 131 | float calc_intensity(float val, AxisScale intensity_axis) 132 | { 133 | assert(val >= 0 && val <= 1); 134 | switch (intensity_axis) 135 | { 136 | case SCALE_LOGARITHMIC: 137 | return log10scale(val); 138 | case SCALE_LINEAR: 139 | return val; 140 | default: 141 | assert(false); 142 | } 143 | } 144 | 145 | float calc_intensity_inv(float val, AxisScale intensity_axis) 146 | { 147 | assert(val >= 0 && val <= 1); 148 | switch (intensity_axis) 149 | { 150 | case SCALE_LOGARITHMIC: 151 | return log10scale_inv(val); 152 | case SCALE_LINEAR: 153 | return val; 154 | default: 155 | assert(false); 156 | } 157 | } 158 | 159 | // to <0,1> (cutoff negative) 160 | void normalize_image(std::vector& data) 161 | { 162 | float max = 0.0f; 163 | for (std::vector::iterator it=data.begin(); 164 | it!=data.end(); ++it) 165 | max = std::max(*std::max_element(it->begin(), it->end()), max); 166 | if (max == 0.0f) 167 | return; 168 | for (std::vector::iterator it=data.begin(); 169 | it!=data.end(); ++it) 170 | for (real_vec::iterator i = it->begin(); i != it->end(); ++i) 171 | *i = std::abs(*i)/max; 172 | } 173 | 174 | // to <-1,1> 175 | void normalize_signal(real_vec& vector) 176 | { 177 | float max = 0; 178 | for (real_vec::iterator it = vector.begin(); it != vector.end(); ++it) 179 | max = std::max(max, std::abs(*it)); 180 | //std::cout <<"max: "< 0); 182 | for (real_vec::iterator it = vector.begin(); it != vector.end(); ++it) 183 | *it /= max; 184 | } 185 | 186 | // random number from <0,1> 187 | double random_double() 188 | { 189 | return ((double)rand()/(double)RAND_MAX); 190 | } 191 | 192 | float brightness_correction(float intensity, BrightCorrection correction) 193 | { 194 | switch (correction) 195 | { 196 | case BRIGHT_NONE: 197 | return intensity; 198 | case BRIGHT_SQRT: 199 | return std::sqrt(intensity); 200 | } 201 | assert(false); 202 | } 203 | 204 | /// Creates a random pink noise signal in the frequency domain 205 | /** \param size Desired number of samples in time domain (after IFFT). */ 206 | complex_vec get_pink_noise(size_t size) 207 | { 208 | complex_vec res; 209 | for (size_t i = 0; i < (size+1)/2; ++i) 210 | { 211 | const float mag = std::pow((float) i, -0.5f); 212 | const double phase = (2*random_double()-1) * PI;//+-pi random phase 213 | res.push_back(Complex(mag*std::cos(phase), mag*std::sin(phase))); 214 | } 215 | return res; 216 | } 217 | } 218 | 219 | Spectrogram::Spectrogram(QObject* parent) // defaults 220 | : QObject(parent) 221 | , bandwidth(100) 222 | , basefreq(55) 223 | , maxfreq(22050) 224 | , overlap(0.8) 225 | , pixpersec(100) 226 | , window(WINDOW_HANN) 227 | , intensity_axis(SCALE_LOGARITHMIC) 228 | , frequency_axis(SCALE_LOGARITHMIC) 229 | , cancelled_(false) 230 | { 231 | } 232 | 233 | QImage Spectrogram::to_image(real_vec& signal, int samplerate) const 234 | { 235 | emit status("Transforming input"); 236 | emit progress(0); 237 | const complex_vec spectrum = padded_FFT(signal); 238 | 239 | const size_t width = (spectrum.size()-1)*2*pixpersec/samplerate; 240 | 241 | // transformation of frequency in hz to index in spectrum 242 | const double filterscale = ((double)spectrum.size()*2)/samplerate; 243 | //std::cout << "filterscale: " << filterscale<<"\n"; 244 | 245 | std::auto_ptr filterbank = Filterbank::get_filterbank( 246 | frequency_axis, filterscale, basefreq, bandwidth, overlap); 247 | const int bands = filterbank->num_bands_est(maxfreq); 248 | const int top_index = maxfreq*filterscale; 249 | // maxfreq has to be at most nyquist 250 | assert(top_index <= (int)spectrum.size()); 251 | 252 | std::vector image_data; 253 | for (size_t bandidx = 0;; ++bandidx) 254 | { 255 | if (cancelled()) 256 | return QImage(); 257 | band_progress(bandidx, bands, 5, 93); 258 | 259 | // filtering 260 | intpair range = filterbank->get_band(bandidx); 261 | //std::cout << "-----\n"; 262 | //std::cout << "spectrum size: " << spectrum.size() << "\n"; 263 | //std::cout << "lowidx: "< top_index) 277 | break; 278 | if (range.second > top_index) 279 | std::fill(filterband.begin()+top_index-range.first, 280 | filterband.end(), Complex(0,0)); 281 | 282 | // windowing 283 | apply_window(filterband, range.first, filterscale); 284 | 285 | // envelope detection + resampling 286 | const real_vec envelope = resample(get_envelope(filterband), width); 287 | image_data.push_back(envelope); 288 | } 289 | 290 | normalize_image(image_data); 291 | 292 | emit progress(99); 293 | return make_image(image_data); 294 | } 295 | 296 | /** \param data innermost values from 0 to 1, same sized vectors */ 297 | QImage Spectrogram::make_image(const std::vector& data) const 298 | { 299 | emit status("Generating image"); 300 | const size_t height = data.size(); 301 | const size_t width = data[0].size(); 302 | std::cout << "image size: " << width <<" x "< filterbank = Filterbank::get_filterbank( 360 | frequency_axis, filterscale, basefreq, bandwidth, overlap); 361 | 362 | for (int bandidx = 0; bandidx < image.height(); ++bandidx) 363 | { 364 | if (cancelled()) 365 | return real_vec(); 366 | band_progress(bandidx, image.height()-1); 367 | 368 | real_vec envelope = envelope_from_spectrogram(image, bandidx); 369 | 370 | // random phase between +-pi 371 | const double phase = (2*random_double()-1) * PI; 372 | 373 | real_vec bandsignal(envelope.size()*2); 374 | for (int j = 0; j < 4; ++j) 375 | { 376 | const double sine = std::cos(j*PI/2 + phase); 377 | for (size_t i = j; i < bandsignal.size(); i += 4) 378 | bandsignal[i] = envelope[i/2] * sine; 379 | } 380 | complex_vec filterband = padded_FFT(bandsignal); 381 | 382 | for (size_t i = 0; i < filterband.size(); ++i) 383 | { 384 | const double x = (double)i/(filterband.size()-1); 385 | // normalized blackman window antiderivative 386 | filterband[i] *= x - ((0.5/(2.0*PI))*sin(2.0*PI*x) + 387 | (0.08/(4.0*PI))*sin(4.0*PI*x)/0.42); 388 | } 389 | 390 | //std::cout << "spectrum size: " << spectrum.size() << "\n"; 391 | //std::cout << bandidx << ". filterband size: " << filterband.size() << "; start: " << filterbank->get_band(bandidx).first <<"; end: " << filterbank->get_band(bandidx).second << "\n"; 392 | 393 | const size_t center = filterbank->get_center(bandidx); 394 | const size_t offset = std::max((size_t)0, center - filterband.size()/2); 395 | //std::cout << "offset: " < 0 && offset+i < spectrum.size()) 398 | spectrum[offset+i] += filterband[i]; 399 | } 400 | 401 | real_vec out = padded_IFFT(spectrum); 402 | //std::cout << "samples: " << out.size() << " -> " << samples << "\n"; 403 | normalize_signal(out); 404 | return out; 405 | } 406 | 407 | real_vec Spectrogram::noise_synthesis(const QImage& image, int samplerate) const 408 | { 409 | size_t samples = image.width()*samplerate/pixpersec; 410 | 411 | complex_vec noise = get_pink_noise(samplerate*10); // 10 sec loop 412 | 413 | const double filterscale = ((double)noise.size()*2)/samplerate; 414 | std::auto_ptr filterbank = Filterbank::get_filterbank( 415 | frequency_axis, filterscale, basefreq, bandwidth, overlap); 416 | 417 | const int top_index = maxfreq*filterscale; 418 | 419 | real_vec out(samples); 420 | 421 | for (int bandidx = 0; bandidx < image.height(); ++bandidx) 422 | { 423 | if (cancelled()) 424 | return real_vec(); 425 | band_progress(bandidx, image.height()-1); 426 | 427 | // filter noise 428 | intpair range = filterbank->get_band(bandidx); 429 | //std::cout << bandidx << "/"< from a row of pixels 476 | real_vec Spectrogram::envelope_from_spectrogram(const QImage& image, int row) const 477 | { 478 | real_vec envelope(image.width()); 479 | for (int x = 0; x < image.width(); ++x) 480 | envelope[x] = calc_intensity_inv(palette.get_intensity( 481 | image.pixel(x, image.height()-row-1)), intensity_axis); 482 | return envelope; 483 | } 484 | 485 | void Spectrogram::deserialize(const QString& text) 486 | { 487 | QStringList tokens = text.split(delimiter); 488 | bandwidth = tokens[1].toDouble(); 489 | basefreq = tokens[2].toDouble(); 490 | maxfreq = tokens[3].toDouble(); 491 | overlap = tokens[4].toDouble()/100.0; 492 | pixpersec = tokens[5].toDouble(); 493 | window = (Window)tokens[6].toInt(); 494 | intensity_axis = (AxisScale)tokens[7].toInt(); 495 | frequency_axis = (AxisScale)tokens[8].toInt(); 496 | } 497 | 498 | QString Spectrogram::serialized() const 499 | { 500 | QString out; 501 | QTextStream desc(&out); 502 | desc.setRealNumberPrecision(4); 503 | desc.setRealNumberNotation(QTextStream::FixedNotation); 504 | desc << "Spectrogram:" << delimiter 505 | << bandwidth << delimiter 506 | << basefreq << delimiter 507 | << maxfreq << delimiter 508 | << overlap*100 << delimiter 509 | << pixpersec << delimiter 510 | << (int)window << delimiter 511 | << (int)intensity_axis << delimiter 512 | << (int)frequency_axis << delimiter 513 | ; 514 | //std::cout << "serialized: " << out.toStdString() << "\n"; 515 | return out; 516 | } 517 | 518 | Palette::Palette(const QImage& img) 519 | { 520 | assert(!img.isNull()); 521 | for (int x = 0; x < img.width(); ++x) 522 | colors_.append(img.pixel(x, 0)); 523 | } 524 | 525 | Palette::Palette() 526 | { 527 | QVector colors; 528 | for (int i = 0; i < 256; ++i) 529 | colors.append(qRgb(i, i, i)); 530 | colors_ = colors; 531 | } 532 | 533 | int Palette::get_color(float val) const 534 | { 535 | assert(val >= 0 && val <= 1); 536 | if (indexable()) 537 | // returns the color index 538 | return (colors_.size()-1)*val; 539 | else 540 | // returns the RGB value 541 | return colors_[(colors_.size()-1)*val]; 542 | } 543 | 544 | bool Palette::has_color(QRgb color) const 545 | { 546 | return colors_.indexOf(color) != -1; 547 | } 548 | 549 | // ne moc efektivni 550 | float Palette::get_intensity(QRgb color) const 551 | { 552 | int index = colors_.indexOf(color); 553 | if (index == -1) // shouldn't happen 554 | return 0; 555 | return (float)index/(colors_.size()-1); 556 | } 557 | 558 | QImage Palette::make_canvas(int width, int height) const 559 | { 560 | if (indexable()) 561 | { 562 | QImage out(width, height, QImage::Format_Indexed8); 563 | out.setColorTable(colors_); 564 | out.fill(0); 565 | return out; 566 | } 567 | else 568 | { 569 | QImage out(width, height, QImage::Format_RGB32); 570 | out.fill(colors_[0]); 571 | return out; 572 | } 573 | } 574 | 575 | bool Palette::indexable() const 576 | { 577 | return colors_.size() <= 256; 578 | } 579 | 580 | QPixmap Palette::preview(int width, int height) const 581 | { 582 | QImage out = make_canvas(width, height); 583 | for (int x = 0; x < width; ++x) 584 | out.setPixel(x, 0, get_color((double)x/(width-1))); 585 | int bytes = out.bytesPerLine(); 586 | for (int y = 1; y < height; ++y) 587 | std::memcpy(out.scanLine(y), out.scanLine(0), bytes); 588 | return QPixmap::fromImage(out); 589 | } 590 | 591 | int Palette::numColors() const 592 | { 593 | return colors_.size(); 594 | } 595 | 596 | Filterbank::Filterbank(double scale) 597 | : scale_(scale) 598 | { 599 | } 600 | 601 | Filterbank::~Filterbank() 602 | { 603 | } 604 | 605 | LinearFilterbank::LinearFilterbank(double scale, double base, 606 | double hzbandwidth, double overlap) 607 | : Filterbank(scale) 608 | , bandwidth_(hzbandwidth*scale) 609 | , startidx_(std::max(scale_*base-bandwidth_/2, 0.0)) 610 | , step_((1-overlap)*bandwidth_) 611 | { 612 | //std::cout << "bandwidth: " << bandwidth_ << "\n"; 613 | //std::cout << "step_: " << step_ << " hz\n"; 614 | assert(step_ > 0); 615 | } 616 | 617 | int LinearFilterbank::num_bands_est(double maxfreq) const 618 | { 619 | return (maxfreq*scale_-startidx_)/step_; 620 | } 621 | 622 | intpair LinearFilterbank::get_band(int i) const 623 | { 624 | intpair out; 625 | out.first = startidx_ + i*step_; 626 | out.second = out.first + bandwidth_; 627 | return out; 628 | } 629 | 630 | int LinearFilterbank::get_center(int i) const 631 | { 632 | return startidx_ + i*step_ + bandwidth_/2.0; 633 | } 634 | 635 | LogFilterbank::LogFilterbank(double scale, double base, 636 | double centsperband, double overlap) 637 | : Filterbank(scale) 638 | , centsperband_(centsperband) 639 | , logstart_(freq2cent(base)) 640 | , logstep_((1-overlap)*centsperband_) 641 | { 642 | assert(logstep_ > 0); 643 | //std::cout << "bandwidth: " << centsperband_ << " cpb\n"; 644 | //std::cout << "logstep_: " << logstep_ << " cents\n"; 645 | } 646 | 647 | int LogFilterbank::num_bands_est(double maxfreq) const 648 | { 649 | return (freq2cent(maxfreq)-logstart_)/logstep_+4; 650 | } 651 | 652 | int LogFilterbank::get_center(int i) const 653 | { 654 | const double logcenter = logstart_ + i*logstep_; 655 | return cent2freq(logcenter)*scale_; 656 | } 657 | 658 | intpair LogFilterbank::get_band(int i) const 659 | { 660 | const double logcenter = logstart_ + i*logstep_; 661 | const double loglow = logcenter - centsperband_/2.0; 662 | const double loghigh = loglow + centsperband_; 663 | intpair out; 664 | out.first = cent2freq(loglow)*scale_; 665 | out.second = cent2freq(loghigh)*scale_; 666 | //std::cout << "centerfreq: " << cent2freq(logcenter)<< "\n"; 667 | //std::cout << "lowfreq: " << cent2freq(loglow) << " highfreq: "< Filterbank::get_filterbank(AxisScale type, 672 | double scale, double base, double bandwidth, double overlap) 673 | { 674 | Filterbank* filterbank; 675 | if (type == SCALE_LINEAR) 676 | filterbank=new LinearFilterbank(scale, base, bandwidth, overlap); 677 | else 678 | filterbank=new LogFilterbank(scale, base, bandwidth, overlap); 679 | return std::auto_ptr(filterbank); 680 | } 681 | -------------------------------------------------------------------------------- /spectrogram.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SPECTROGRAM_HPP 2 | #define SPECTROGRAM_HPP 3 | 4 | /** \file spectrogram.hpp 5 | * \brief Definitions for classes used for spectrogram analysis and generation. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "soundfile.hpp" 16 | #include "fft.hpp" 17 | 18 | #include 19 | #include 20 | 21 | /// Represents a palette used to draw a spectrogram. 22 | /** It is basically a mapping of intensity values from the interval <0,1> to a 23 | * set of colors (the palette), where 0 represents zero intensity of the pixel 24 | * and 1 represents maximum intensity. Ideally, the mapping is 1:1 25 | * (bijection), otherwise there will be ambiguity in the synthesis process and 26 | * quality will be affected. Both the Intensity -> Color and Color -> 27 | * Intensity mappings are implemented by the Palette::get_color() and 28 | * Palette::get_intensity() functions respectively. 29 | * 30 | * For optimal image sizes, the palette will be either indexed or RGB, 31 | * depending on how many colors it contains. If there are 256 or less colors 32 | * (eg. in the default grayscale case), the palette will be 8-bit indexed, 33 | * otherwise it's RGB (24-bit, no index). 34 | * 35 | * This is transparent in usage because in both cases the make_canvas() function 36 | * will create a QImage with the appropriate color mode and QImage::setPixel() 37 | * then takes the same parameter type as the Palette::get_color() function 38 | * returns. 39 | */ 40 | class Palette 41 | { 42 | public: 43 | /// Default constructor -- 8-bit grayscale palette. 44 | Palette(); 45 | /// Creates a palette from an image. 46 | /** 47 | * This constructor takes an image and fills the palette with colors 48 | * from the first row of an image. \param img The image used to 49 | * generate the palette. 50 | */ 51 | Palette(const QImage& img); 52 | //const QVector& colors() const; 53 | /// Mapping of intensity values from <0,1> to an index or RGB value. 54 | /** 55 | * \param val A float where 0 <= val <= 1 56 | * \return Index of the color for an indexed palette or RGB value. 57 | */ 58 | int get_color(float val) const; 59 | /// Inverse mapping of color values to intensity, used for spectrogram synthesis. 60 | /** \return Corresponding intensity, a value from <0,1>. */ 61 | float get_intensity(QRgb color) const; 62 | /// Returns true if the palette contains the given color, false otherwise. 63 | bool has_color(QRgb color) const; 64 | /// Creates a QImage with an appropriate color mode and dimensions. 65 | /** The resulting QImage will have the specified dimensions and color 66 | * mode depending on the number of colors in the palette. For 256 or 67 | * less colors, it will be indexed, otherwise RGB. 68 | */ 69 | QImage make_canvas(int width, int height) const; 70 | /// Used to determine if the palette is indexed or RGB. 71 | /** \return \c true if the palette is indexed, otherwise \c false.*/ 72 | bool indexable() const; 73 | /// Generate a preview of the palette suitable for display in a widget. 74 | QPixmap preview(int width, int height) const; 75 | /// Returns the number of colors in the palette. 76 | int numColors() const; 77 | private: 78 | QVector colors_; 79 | }; 80 | 81 | // --- 82 | 83 | /// Represents the window function used for spectrogram generation. 84 | enum Window 85 | { 86 | WINDOW_HANN, /**< See http://en.wikipedia.org/wiki/Hann_window */ 87 | WINDOW_BLACKMAN, /**< See http://en.wikipedia.org/wiki/Window_function#Blackman_windows */ 88 | WINDOW_RECTANGULAR, /**< Doesn't do anything. */ 89 | WINDOW_TRIANGULAR /**< http://en.wikipedia.org/wiki/Triangular_window#Triangular_window_.28non-zero_end-points.29 */ 90 | }; 91 | /// Represents the linear or logarithmic mode for frequency and intensity axes. 92 | enum AxisScale {SCALE_LINEAR, SCALE_LOGARITHMIC}; 93 | /// Represents spectrogram synthesis mode. 94 | enum SynthesisType {SYNTHESIS_SINE, SYNTHESIS_NOISE}; 95 | /// Represents the brightness correction used in spectrogram generation. 96 | enum BrightCorrection {BRIGHT_NONE, BRIGHT_SQRT}; 97 | 98 | /// This class holds the parameters for a spectrogram and implements its synthesis and generation. 99 | class Spectrogram : public QObject 100 | { 101 | Q_OBJECT 102 | public: 103 | Spectrogram(QObject* parent = 0); 104 | /// Generates a spectrogram for the given signal. 105 | QImage to_image(real_vec& signal, int samplerate) const; 106 | /// Synthesizes the given spectrogram to sound. 107 | real_vec synthetize(const QImage& image, int samplerate, 108 | SynthesisType type) const; 109 | /// Serializes the Spectrogram object. 110 | /** The serialized string is saved in image metadata to indicate parameters with which the spectrogram has been generated. */ 111 | QString serialized() const; 112 | /// Loads the serialized parameters into this object. 113 | void deserialize(const QString& serialized); 114 | 115 | /// Bandwidth of the frequency-domain filters. 116 | /** In Hz for linear spectrograms, in cents (cent = octave/1200) for logarithmic spectrograms.*/ 117 | double bandwidth; 118 | /// Base frequency of the spectrogram. 119 | double basefreq; 120 | /// Maximum frequency of the spectrogram. 121 | double maxfreq; 122 | /// Overlap of the frequency-domain filters, 1 = full overlap (useless), 0 = no overlap. 123 | double overlap; 124 | /// Time resolution of the spectrogram, pixels per second. 125 | double pixpersec; 126 | /// Window function used on the frequency-domain intervals. 127 | Window window; 128 | /// Scale type of the intensity axis (linear or logarithmic) 129 | AxisScale intensity_axis; 130 | /// Scale type of the frequency axis (linear or logarithmic) 131 | AxisScale frequency_axis; 132 | /// Brightness correction used in generation of the spectrogram. 133 | BrightCorrection correction; 134 | /// Palette used for drawing the spectrogram. 135 | Palette palette; 136 | private: 137 | /// Performs sine synthesis on the given spectrogram. 138 | real_vec sine_synthesis(const QImage& image, int samplerate) const; 139 | /// Performs noise synthesis on the given spectrogram. 140 | real_vec noise_synthesis(const QImage& image, int samplerate) const; 141 | /// Draws an image from the given image data. 142 | QImage make_image(const std::vector& data) const; 143 | /// Returns intensity values (from <0,1>) from a row of pixels. 144 | real_vec envelope_from_spectrogram(const QImage& image, int row) const; 145 | /// Applies the window function to a frequency-domain interval. 146 | void apply_window(complex_vec& chunk, int lowidx, 147 | double filterscale) const; 148 | /// Delimiter of the serialized data 149 | static const char delimiter = ';'; 150 | void band_progress(int x, int of, int from=0, int to=100) const; 151 | /// Indicates if the computation should be interrupted. 152 | bool cancelled() const; 153 | mutable bool cancelled_; 154 | signals: 155 | /// Signals percentual progress to the main application. 156 | void progress(int value) const; 157 | /// Signals the state of the computation to the main application. 158 | void status(const QString& text) const; 159 | public slots: 160 | /// Informs the working thread of a request to interrupt the computation. 161 | void cancel(); 162 | }; 163 | 164 | // -- 165 | 166 | typedef std::pair intpair; 167 | 168 | /// Used to divide the frequency domain into suitable intervals. 169 | /** Each interval represents a horizontal band in a spectrogram. */ 170 | class Filterbank 171 | { 172 | public: 173 | static std::auto_ptr get_filterbank(AxisScale type, 174 | double scale, double base, double hzbandwidth, double overlap); 175 | 176 | Filterbank(double scale); 177 | /// Returns start-finish indexes for a given filterband. 178 | virtual intpair get_band(int i) const = 0; 179 | /// Returns the index of the filterband's center. 180 | virtual int get_center(int i) const = 0; 181 | /// Estimated total number of intervals. 182 | virtual int num_bands_est(double maxfreq) const = 0; 183 | virtual ~Filterbank(); 184 | protected: 185 | /// The proportion of frequency versus vector indices. 186 | const double scale_; 187 | }; 188 | 189 | /// Divides the frequency domain to intervals of constant bandwidth. 190 | class LinearFilterbank : public Filterbank 191 | { 192 | public: 193 | LinearFilterbank(double scale, double base, double hzbandwidth, 194 | double overlap); 195 | intpair get_band(int i) const; 196 | int get_center(int i) const; 197 | int num_bands_est(double maxfreq) const; 198 | private: 199 | const double bandwidth_; 200 | const int startidx_; 201 | const double step_; 202 | }; 203 | 204 | /// Divides the frequency domain to intervals with variable (logarithmic, constant-Q) bandwidth. 205 | class LogFilterbank : public Filterbank 206 | { 207 | public: 208 | LogFilterbank(double scale, double base, double centsperband, 209 | double overlap); 210 | intpair get_band(int i) const; 211 | int get_center(int i) const; 212 | int num_bands_est(double maxfreq) const; 213 | private: 214 | const double centsperband_; 215 | const double logstart_; 216 | const double logstep_; 217 | }; 218 | 219 | #endif 220 | -------------------------------------------------------------------------------- /types.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_HPP 2 | #define TYPES_HPP 3 | 4 | /** \file types.hpp 5 | * \brief Contains definitions for basic types and constants. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | #define PI 3.1415926535897932384626433832795 12 | 13 | typedef std::complex Complex; 14 | typedef std::vector real_vec; 15 | typedef std::vector complex_vec; 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /viewer/cursorlabel.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt4.QtCore import * 3 | from PyQt4.QtGui import * 4 | 5 | class CursorLabel(QLabel): 6 | def __init__(self, parent=None): 7 | super(CursorLabel, self).__init__(parent) 8 | self.cursor = 0 9 | 10 | def paintEvent(self, event): 11 | QLabel.paintEvent(self, event) 12 | painter = QPainter(self) 13 | pen = QPen(QColor(255,0,0, 128)) 14 | pen.setWidth(3) 15 | painter.setPen(pen) 16 | painter.drawLine(QPoint(self.cursor, 0), 17 | QPoint(self.cursor, self.height())) 18 | painter.end() 19 | 20 | -------------------------------------------------------------------------------- /viewer/ui_viewer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file './viewer.ui' 4 | # 5 | # Created: Sun Aug 23 04:04:27 2009 6 | # by: PyQt4 UI code generator 4.4.2 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PyQt4 import QtCore, QtGui 11 | 12 | class Ui_MainWindow(object): 13 | def setupUi(self, MainWindow): 14 | MainWindow.setObjectName("MainWindow") 15 | MainWindow.resize(566,421) 16 | self.centralwidget = QtGui.QWidget(MainWindow) 17 | self.centralwidget.setObjectName("centralwidget") 18 | self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget) 19 | self.verticalLayout.setObjectName("verticalLayout") 20 | self.horizontalLayout = QtGui.QHBoxLayout() 21 | self.horizontalLayout.setObjectName("horizontalLayout") 22 | self.label = QtGui.QLabel(self.centralwidget) 23 | self.label.setObjectName("label") 24 | self.horizontalLayout.addWidget(self.label) 25 | self.pathEdit = QtGui.QLineEdit(self.centralwidget) 26 | self.pathEdit.setObjectName("pathEdit") 27 | self.horizontalLayout.addWidget(self.pathEdit) 28 | self.browseButton = QtGui.QPushButton(self.centralwidget) 29 | self.browseButton.setObjectName("browseButton") 30 | self.horizontalLayout.addWidget(self.browseButton) 31 | spacerItem = QtGui.QSpacerItem(40,20,QtGui.QSizePolicy.Fixed,QtGui.QSizePolicy.Minimum) 32 | self.horizontalLayout.addItem(spacerItem) 33 | self.label_3 = QtGui.QLabel(self.centralwidget) 34 | self.label_3.setObjectName("label_3") 35 | self.horizontalLayout.addWidget(self.label_3) 36 | self.ppsSpin = QtGui.QSpinBox(self.centralwidget) 37 | self.ppsSpin.setMaximum(10000) 38 | self.ppsSpin.setSingleStep(10) 39 | self.ppsSpin.setProperty("value",QtCore.QVariant(100)) 40 | self.ppsSpin.setObjectName("ppsSpin") 41 | self.horizontalLayout.addWidget(self.ppsSpin) 42 | self.verticalLayout.addLayout(self.horizontalLayout) 43 | self.scrollArea = QtGui.QScrollArea(self.centralwidget) 44 | self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 45 | self.scrollArea.setObjectName("scrollArea") 46 | self.scrollAreaWidgetContents_2 = QtGui.QWidget(self.scrollArea) 47 | self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0,1,513,290)) 48 | self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2") 49 | self.horizontalLayout_3 = QtGui.QHBoxLayout(self.scrollAreaWidgetContents_2) 50 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 51 | self.imageLabel = CursorLabel(self.scrollAreaWidgetContents_2) 52 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) 53 | sizePolicy.setHorizontalStretch(0) 54 | sizePolicy.setVerticalStretch(0) 55 | sizePolicy.setHeightForWidth(self.imageLabel.sizePolicy().hasHeightForWidth()) 56 | self.imageLabel.setSizePolicy(sizePolicy) 57 | self.imageLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 58 | self.imageLabel.setObjectName("imageLabel") 59 | self.horizontalLayout_3.addWidget(self.imageLabel) 60 | self.verticalLayout.addWidget(self.scrollArea) 61 | self.progress = QtGui.QProgressBar(self.centralwidget) 62 | self.progress.setProperty("value",QtCore.QVariant(0)) 63 | self.progress.setTextVisible(False) 64 | self.progress.setObjectName("progress") 65 | self.verticalLayout.addWidget(self.progress) 66 | self.horizontalLayout_2 = QtGui.QHBoxLayout() 67 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 68 | self.commandCheck = QtGui.QCheckBox(self.centralwidget) 69 | self.commandCheck.setObjectName("commandCheck") 70 | self.horizontalLayout_2.addWidget(self.commandCheck) 71 | self.commandEdit = QtGui.QLineEdit(self.centralwidget) 72 | self.commandEdit.setObjectName("commandEdit") 73 | self.horizontalLayout_2.addWidget(self.commandEdit) 74 | spacerItem1 = QtGui.QSpacerItem(20,20,QtGui.QSizePolicy.Fixed,QtGui.QSizePolicy.Minimum) 75 | self.horizontalLayout_2.addItem(spacerItem1) 76 | self.centeredCheck = QtGui.QCheckBox(self.centralwidget) 77 | self.centeredCheck.setChecked(True) 78 | self.centeredCheck.setObjectName("centeredCheck") 79 | self.horizontalLayout_2.addWidget(self.centeredCheck) 80 | spacerItem2 = QtGui.QSpacerItem(20,20,QtGui.QSizePolicy.Fixed,QtGui.QSizePolicy.Minimum) 81 | self.horizontalLayout_2.addItem(spacerItem2) 82 | self.playButton = QtGui.QPushButton(self.centralwidget) 83 | palette = QtGui.QPalette() 84 | brush = QtGui.QBrush(QtGui.QColor(170,0,0)) 85 | brush.setStyle(QtCore.Qt.SolidPattern) 86 | palette.setBrush(QtGui.QPalette.Active,QtGui.QPalette.Button,brush) 87 | brush = QtGui.QBrush(QtGui.QColor(170,0,0)) 88 | brush.setStyle(QtCore.Qt.SolidPattern) 89 | palette.setBrush(QtGui.QPalette.Inactive,QtGui.QPalette.Button,brush) 90 | brush = QtGui.QBrush(QtGui.QColor(170,0,0)) 91 | brush.setStyle(QtCore.Qt.SolidPattern) 92 | palette.setBrush(QtGui.QPalette.Disabled,QtGui.QPalette.Button,brush) 93 | self.playButton.setPalette(palette) 94 | self.playButton.setObjectName("playButton") 95 | self.horizontalLayout_2.addWidget(self.playButton) 96 | self.stopButton = QtGui.QPushButton(self.centralwidget) 97 | self.stopButton.setObjectName("stopButton") 98 | self.horizontalLayout_2.addWidget(self.stopButton) 99 | self.verticalLayout.addLayout(self.horizontalLayout_2) 100 | MainWindow.setCentralWidget(self.centralwidget) 101 | 102 | self.retranslateUi(MainWindow) 103 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 104 | 105 | def retranslateUi(self, MainWindow): 106 | MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) 107 | self.label.setText(QtGui.QApplication.translate("MainWindow", "Spectrogram", None, QtGui.QApplication.UnicodeUTF8)) 108 | self.browseButton.setText(QtGui.QApplication.translate("MainWindow", "Browse", None, QtGui.QApplication.UnicodeUTF8)) 109 | self.label_3.setText(QtGui.QApplication.translate("MainWindow", "Pixels per second", None, QtGui.QApplication.UnicodeUTF8)) 110 | self.imageLabel.setText(QtGui.QApplication.translate("MainWindow", "[image]", None, QtGui.QApplication.UnicodeUTF8)) 111 | self.commandCheck.setText(QtGui.QApplication.translate("MainWindow", "Command", None, QtGui.QApplication.UnicodeUTF8)) 112 | self.commandEdit.setText(QtGui.QApplication.translate("MainWindow", "xmms -p", None, QtGui.QApplication.UnicodeUTF8)) 113 | self.centeredCheck.setText(QtGui.QApplication.translate("MainWindow", "Centered", None, QtGui.QApplication.UnicodeUTF8)) 114 | self.playButton.setText(QtGui.QApplication.translate("MainWindow", "Play", None, QtGui.QApplication.UnicodeUTF8)) 115 | self.stopButton.setText(QtGui.QApplication.translate("MainWindow", "Stop", None, QtGui.QApplication.UnicodeUTF8)) 116 | 117 | from cursorlabel import CursorLabel 118 | -------------------------------------------------------------------------------- /viewer/viewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys, os, time 4 | from PyQt4.QtCore import * 5 | from PyQt4.QtGui import * 6 | import ui_viewer 7 | 8 | class ViewerWindow(QMainWindow): 9 | def __init__(self, parent=None): 10 | super(ViewerWindow, self).__init__(parent) 11 | self.ui = ui_viewer.Ui_MainWindow() 12 | self.ui.setupUi(self) 13 | self.timer = QTimer(self) 14 | self.connect(self.timer, SIGNAL("timeout()"), self.update) 15 | self.image = QImage() 16 | self.time = 0 17 | self.lpos = 0 18 | self.paused = False 19 | self.interval = 100 20 | self.ui.scrollArea.setWidget(self.ui.imageLabel) 21 | #self.ui.scrollArea.setWidgetResizable(True) 22 | self.connect(self.ui.browseButton, SIGNAL("clicked()"), self.browse) 23 | self.connect(self.ui.playButton, SIGNAL("clicked()"), self.play) 24 | self.connect(self.ui.stopButton, SIGNAL("clicked()"), self.stop) 25 | 26 | def update(self): 27 | # center on pos 28 | self.time += self.interval 29 | pos = int(self.time/self.len*self.image.width()) 30 | if pos > self.image.width(): 31 | pos = 0 32 | self.stop() 33 | self.ui.imageLabel.cursor = pos 34 | #print "---" 35 | #print "cas:", self.time/1000.0, "s", "/", self.len/1000.0 36 | #print "pozice:", pos, "/", self.image.width() 37 | self.ui.progress.setValue(self.time) 38 | self.ui.imageLabel.update() 39 | if self.ui.centeredCheck.isChecked(): 40 | margin = self.ui.scrollArea.width()//2-10 41 | self.ui.scrollArea.ensureVisible(pos,self.image.height()//2, margin) 42 | else: 43 | width = self.ui.scrollArea.width() 44 | margin = 3*width/4.0 45 | if pos - self.lpos > margin: 46 | self.ui.scrollArea.ensureVisible(pos+margin, 47 | self.image.height()//2, width-margin) 48 | self.lpos = pos 49 | 50 | def browse(self): 51 | filename = QFileDialog.getOpenFileName(self, "Choose spectrogram", 52 | ".", "Images (*.png *.jpg *.bmp *.xpm);;All files (*.*)") 53 | if not filename: 54 | return 55 | self.ui.pathEdit.setText(filename) 56 | self.image = QImage(filename) 57 | if self.image.isNull(): 58 | QMessageBox.information(self, "error", "Couldn't read that file") 59 | return 60 | self.ui.imageLabel.setPixmap(QPixmap.fromImage(self.image)) 61 | self.ui.imageLabel.adjustSize() 62 | 63 | def play(self): 64 | if self.paused: 65 | self.timer.start(self.interval) 66 | self.paused = False 67 | return 68 | if self.timer.isActive(): 69 | self.timer.stop() 70 | self.paused = True 71 | return 72 | 73 | if self.image.isNull(): 74 | return 75 | cmd = str(self.ui.commandEdit.text()) 76 | if self.ui.commandCheck.isChecked() and str: 77 | os.system(cmd) 78 | #for i in xrange(0,5): 79 | # print i 80 | # time.sleep(1) 81 | pps = self.ui.ppsSpin.value() 82 | self.interval = 40 83 | self.len = self.image.width()*1000.0/pps 84 | self.ui.progress.setMaximum(int(self.len)) 85 | if not self.timer.isActive(): 86 | self.timer.start(self.interval) 87 | 88 | def stop(self): 89 | self.timer.stop() 90 | self.time = 0 91 | self.paused = False 92 | 93 | def main(): 94 | app = QApplication(sys.argv) 95 | form = ViewerWindow() 96 | form.show() 97 | app.exec_() 98 | 99 | if __name__ == "__main__": 100 | main() 101 | -------------------------------------------------------------------------------- /viewer/viewer.ui: -------------------------------------------------------------------------------- 1 | 2 | MainWindow 3 | 4 | 5 | 6 | 0 7 | 0 8 | 566 9 | 421 10 | 11 | 12 | 13 | MainWindow 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Spectrogram 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Browse 33 | 34 | 35 | 36 | 37 | 38 | 39 | Qt::Horizontal 40 | 41 | 42 | QSizePolicy::Fixed 43 | 44 | 45 | 46 | 40 47 | 20 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Pixels per second 56 | 57 | 58 | 59 | 60 | 61 | 62 | 10000 63 | 64 | 65 | 10 66 | 67 | 68 | 100 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 78 | 79 | 80 | 81 | 82 | 0 83 | 1 84 | 513 85 | 290 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 0 94 | 0 95 | 96 | 97 | 98 | [image] 99 | 100 | 101 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 0 113 | 114 | 115 | false 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Command 125 | 126 | 127 | 128 | 129 | 130 | 131 | xmms -p 132 | 133 | 134 | 135 | 136 | 137 | 138 | Qt::Horizontal 139 | 140 | 141 | QSizePolicy::Fixed 142 | 143 | 144 | 145 | 20 146 | 20 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | Centered 155 | 156 | 157 | true 158 | 159 | 160 | 161 | 162 | 163 | 164 | Qt::Horizontal 165 | 166 | 167 | QSizePolicy::Fixed 168 | 169 | 170 | 171 | 20 172 | 20 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 170 186 | 0 187 | 0 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 170 197 | 0 198 | 0 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 170 208 | 0 209 | 0 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | Play 218 | 219 | 220 | 221 | 222 | 223 | 224 | Stop 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | CursorLabel 236 | QLabel 237 |
cursorlabel
238 |
239 |
240 | 241 | 242 |
243 | --------------------------------------------------------------------------------